refactor(clerk-performance):make better performance analysis
Some checks failed
Build and Push Backend / docker (push) Failing after 6s

This commit is contained in:
irving
2025-10-27 23:35:17 -04:00
17 changed files with 1391 additions and 12 deletions

View File

@@ -0,0 +1,159 @@
package com.starry.admin.modules.statistics.controller;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceDetailCompositionVo;
import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceDetailQueryVo;
import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceDetailResponseVo;
import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceOverviewQueryVo;
import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceOverviewResponseVo;
import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceOverviewSummaryVo;
import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceProfileVo;
import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceSnapshotVo;
import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceTrendPointVo;
import com.starry.admin.modules.statistics.service.IPlayClerkPerformanceService;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.Collections;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
/**
* MockMvc tests for {@link PlayClerkPerformanceController} verifying new overview/detail endpoints.
*/
@ExtendWith(MockitoExtension.class)
class PlayClerkPerformanceControllerTest {
@Mock
private IPlayClerkPerformanceService playClerkPerformanceService;
@InjectMocks
private PlayClerkPerformanceController controller;
private MockMvc mockMvc;
private ObjectMapper objectMapper;
@BeforeEach
void setUp() {
objectMapper = new ObjectMapper();
objectMapper.findAndRegisterModules();
mockMvc = MockMvcBuilders.standaloneSetup(controller)
.setMessageConverters(new MappingJackson2HttpMessageConverter(objectMapper))
.build();
}
@Test
@DisplayName("POST /statistics/performance/overview should delegate and wrap service response")
void overviewEndpointReturnsAggregatedData() throws Exception {
ClerkPerformanceSnapshotVo snapshot = new ClerkPerformanceSnapshotVo();
snapshot.setClerkId("c1");
snapshot.setClerkNickname("Alice");
snapshot.setGmv(new BigDecimal("300.00"));
snapshot.setOrderCount(5);
snapshot.setContinuedRate(new BigDecimal("60.00"));
ClerkPerformanceOverviewSummaryVo summary = new ClerkPerformanceOverviewSummaryVo();
summary.setTotalGmv(new BigDecimal("300.00"));
summary.setTotalOrderCount(5);
summary.setContinuedRate(new BigDecimal("60.00"));
ClerkPerformanceOverviewResponseVo responseVo = new ClerkPerformanceOverviewResponseVo();
responseVo.setSummary(summary);
responseVo.setRankings(Collections.singletonList(snapshot));
responseVo.setTotal(1);
when(playClerkPerformanceService.queryOverview(any())).thenReturn(responseVo);
String payload = "{\"endOrderTime\":[\"2024-08-01 00:00:00\",\"2024-08-07 23:59:59\"],\"limit\":3}";
mockMvc.perform(MockMvcRequestBuilders.post("/statistics/performance/overview")
.contentType(MediaType.APPLICATION_JSON)
.content(payload))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.success").value(true))
.andExpect(MockMvcResultMatchers.jsonPath("$.data.rankings", hasSize(1)))
.andExpect(MockMvcResultMatchers.jsonPath("$.data.rankings[0].clerkId").value("c1"))
.andExpect(MockMvcResultMatchers.jsonPath("$.data.summary.totalGmv").value(300.00))
.andExpect(MockMvcResultMatchers.jsonPath("$.data.total").value(1));
ArgumentCaptor<ClerkPerformanceOverviewQueryVo> captor = ArgumentCaptor
.forClass(ClerkPerformanceOverviewQueryVo.class);
verify(playClerkPerformanceService).queryOverview(captor.capture());
ClerkPerformanceOverviewQueryVo captured = captor.getValue();
assertEquals(Integer.valueOf(3), captured.getLimit());
assertEquals(Arrays.asList("2024-08-01 00:00:00", "2024-08-07 23:59:59"), captured.getEndOrderTime());
}
@Test
@DisplayName("POST /statistics/performance/detail should return snapshot and trend data")
void detailEndpointReturnsSnapshot() throws Exception {
ClerkPerformanceProfileVo profile = new ClerkPerformanceProfileVo();
profile.setClerkId("c1");
profile.setNickname("Alice");
profile.setGroupName("一组");
ClerkPerformanceSnapshotVo snapshotVo = new ClerkPerformanceSnapshotVo();
snapshotVo.setClerkId("c1");
snapshotVo.setGmv(new BigDecimal("260.00"));
snapshotVo.setOrderCount(3);
snapshotVo.setContinuedRate(new BigDecimal("66.67"));
ClerkPerformanceDetailCompositionVo composition = new ClerkPerformanceDetailCompositionVo();
ClerkPerformanceDetailCompositionVo.CompositionSlice slice = new ClerkPerformanceDetailCompositionVo.CompositionSlice();
slice.setKey("FIRST_ORDER");
slice.setLabel("首单");
slice.setCount(1);
slice.setCountRatio(new BigDecimal("33.33"));
composition.setOrderComposition(Collections.singletonList(slice));
ClerkPerformanceTrendPointVo trendPoint = new ClerkPerformanceTrendPointVo();
trendPoint.setDate(LocalDate.of(2024, 8, 1));
trendPoint.setGmv(new BigDecimal("120.00"));
ClerkPerformanceDetailResponseVo detailResponse = new ClerkPerformanceDetailResponseVo();
detailResponse.setProfile(profile);
detailResponse.setSnapshot(snapshotVo);
detailResponse.setComposition(composition);
detailResponse.setTrend(Collections.singletonList(trendPoint));
detailResponse.setTrendDimension("DAY");
when(playClerkPerformanceService.queryDetail(any())).thenReturn(detailResponse);
String payload = "{\"clerkId\":\"c1\",\"endOrderTime\":[\"2024-08-01 00:00:00\",\"2024-08-03 23:59:59\"],\"includeTrend\":true}";
mockMvc.perform(MockMvcRequestBuilders.post("/statistics/performance/detail")
.contentType(MediaType.APPLICATION_JSON)
.content(payload))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.success").value(true))
.andExpect(MockMvcResultMatchers.jsonPath("$.data.profile.clerkId").value("c1"))
.andExpect(MockMvcResultMatchers.jsonPath("$.data.snapshot.gmv").value(260.00))
.andExpect(MockMvcResultMatchers.jsonPath("$.data.trend[0].gmv").value(120.00));
ArgumentCaptor<ClerkPerformanceDetailQueryVo> captor = ArgumentCaptor
.forClass(ClerkPerformanceDetailQueryVo.class);
verify(playClerkPerformanceService).queryDetail(captor.capture());
ClerkPerformanceDetailQueryVo captured = captor.getValue();
assertTrue(captured.getIncludeTrend());
assertEquals("c1", captured.getClerkId());
assertEquals(Arrays.asList("2024-08-01 00:00:00", "2024-08-03 23:59:59"), captured.getEndOrderTime());
}
}

View File

@@ -0,0 +1,250 @@
package com.starry.admin.modules.statistics.service;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.starry.admin.common.domain.LoginUser;
import com.starry.admin.common.exception.ServiceException;
import com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
import com.starry.admin.modules.clerk.service.IPlayClerkLevelInfoService;
import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
import com.starry.admin.modules.personnel.module.entity.PlayPersonnelGroupInfoEntity;
import com.starry.admin.modules.personnel.service.IPlayPersonnelGroupInfoService;
import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceDetailQueryVo;
import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceDetailResponseVo;
import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceOverviewQueryVo;
import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceOverviewResponseVo;
import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceOverviewSummaryVo;
import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceSnapshotVo;
import com.starry.admin.modules.statistics.service.impl.PlayClerkPerformanceServiceImpl;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.Month;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.DisplayName;
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 org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
/**
* Unit tests for {@link PlayClerkPerformanceServiceImpl} covering aggregation, ranking and detail logic.
*/
@ExtendWith(MockitoExtension.class)
class PlayClerkPerformanceServiceImplTest {
@Mock
private IPlayClerkUserInfoService clerkUserInfoService;
@Mock
private IPlayOrderInfoService playOrderInfoService;
@Mock
private IPlayClerkLevelInfoService playClerkLevelInfoService;
@Mock
private IPlayPersonnelGroupInfoService playPersonnelGroupInfoService;
@InjectMocks
private PlayClerkPerformanceServiceImpl service;
@Test
@DisplayName("queryOverview should aggregate metrics and sort clerks by GMV")
void queryOverviewAggregatesAndSorts() {
ClerkPerformanceOverviewQueryVo vo = new ClerkPerformanceOverviewQueryVo();
vo.setEndOrderTime(Arrays.asList("2024-08-01 00:00:00", "2024-08-07 23:59:59"));
vo.setIncludeSummary(true);
vo.setIncludeRankings(true);
PlayClerkUserInfoEntity clerkA = buildClerk("c1", "Alice", "g1", "l1");
PlayClerkUserInfoEntity clerkB = buildClerk("c2", "Bob", "g2", "l2");
when(playPersonnelGroupInfoService.getValidClerkIdList(any(), any())).thenReturn(Arrays.asList("c1", "c2"));
when(clerkUserInfoService.list((Wrapper<PlayClerkUserInfoEntity>) any())).thenReturn(Arrays.asList(clerkA, clerkB));
when(playClerkLevelInfoService.selectAll()).thenReturn(Arrays.asList(level("l1", "白银"), level("l2", "黄金")));
when(playPersonnelGroupInfoService.selectAll())
.thenReturn(Arrays.asList(group("g1", "一组"), group("g2", "二组")));
List<PlayOrderInfoEntity> ordersA = Arrays.asList(
order("c1", "userA", "1", "0", "0", new BigDecimal("100.00"), new BigDecimal("60.00"),
LocalDateTime.of(2024, Month.AUGUST, 1, 10, 0)),
order("c1", "userA", "0", "2", "0", new BigDecimal("150.00"), new BigDecimal("90.00"),
LocalDateTime.of(2024, Month.AUGUST, 2, 14, 0)),
withRefund(order("c1", "userB", "0", "0", "1", new BigDecimal("50.00"), new BigDecimal("30.00"),
LocalDateTime.of(2024, Month.AUGUST, 3, 9, 0)), new BigDecimal("30.00")));
List<PlayOrderInfoEntity> ordersB = Collections.singletonList(
order("c2", "userC", "1", "0", "0", new BigDecimal("80.00"), new BigDecimal("50.00"),
LocalDateTime.of(2024, Month.AUGUST, 1, 12, 0)));
when(playOrderInfoService.clerkSelectOrderInfoList(eq("c1"), anyString(), anyString())).thenReturn(ordersA);
when(playOrderInfoService.clerkSelectOrderInfoList(eq("c2"), anyString(), anyString())).thenReturn(ordersB);
setAuthentication();
try {
ClerkPerformanceOverviewResponseVo response = service.queryOverview(vo);
assertNotNull(response.getSummary(), "summary should be present when includeSummary=true");
assertEquals(2, response.getTotal());
assertEquals(2, response.getRankings().size());
ClerkPerformanceSnapshotVo top = response.getRankings().get(0);
assertEquals("c1", top.getClerkId(), "Highest GMV clerk should rank first");
assertEquals(new BigDecimal("300.00"), top.getGmv());
assertEquals(new BigDecimal("100.00"), top.getFirstOrderAmount());
assertEquals(new BigDecimal("200.00"), top.getContinuedOrderAmount());
assertEquals(new BigDecimal("150.00"), top.getRewardAmount());
assertEquals(new BigDecimal("30.00"), top.getRefundAmount());
assertEquals(3, top.getOrderCount());
assertEquals(2, top.getContinuedOrderCount());
assertEquals(new BigDecimal("66.67"), top.getContinuedRate());
assertEquals(new BigDecimal("100.00"), top.getAverageTicketPrice());
ClerkPerformanceOverviewSummaryVo summary = response.getSummary();
assertEquals(new BigDecimal("380.00"), summary.getTotalGmv());
assertEquals(4, summary.getTotalOrderCount());
assertEquals(2, summary.getTotalFirstOrderCount());
assertEquals(2, summary.getTotalContinuedOrderCount());
assertEquals(new BigDecimal("50.00"), summary.getContinuedRate());
assertEquals(new BigDecimal("95.00"), summary.getAverageTicketPrice());
} finally {
clearAuthentication();
}
}
@Test
@DisplayName("queryDetail should build snapshot, composition and trend for accessible clerk")
void queryDetailBuildsSnapshotCompositionAndTrend() {
ClerkPerformanceDetailQueryVo vo = new ClerkPerformanceDetailQueryVo();
vo.setClerkId("c1");
vo.setEndOrderTime(Arrays.asList("2024-08-01 00:00:00", "2024-08-03 23:59:59"));
vo.setIncludeTrend(true);
vo.setTrendDays(3);
PlayClerkUserInfoEntity clerk = buildClerk("c1", "Alice", "g1", "l1");
when(playPersonnelGroupInfoService.getValidClerkIdList(any(), any())).thenReturn(Collections.singletonList("c1"));
when(clerkUserInfoService.getById("c1")).thenReturn(clerk);
when(playClerkLevelInfoService.selectAll()).thenReturn(Collections.singletonList(level("l1", "白银")));
when(playPersonnelGroupInfoService.selectAll()).thenReturn(Collections.singletonList(group("g1", "一组")));
List<PlayOrderInfoEntity> orders = Arrays.asList(
order("c1", "userA", "1", "0", "0", new BigDecimal("120.00"), new BigDecimal("70.00"),
LocalDateTime.of(2024, Month.AUGUST, 1, 9, 0)),
order("c1", "userA", "0", "0", "0", new BigDecimal("80.00"), new BigDecimal("40.00"),
LocalDateTime.of(2024, Month.AUGUST, 2, 10, 0)),
withRefund(order("c1", "userB", "0", "2", "1", new BigDecimal("60.00"), new BigDecimal("30.00"),
LocalDateTime.of(2024, Month.AUGUST, 2, 18, 0)), new BigDecimal("20.00")));
when(playOrderInfoService.clerkSelectOrderInfoList(eq("c1"), anyString(), anyString())).thenReturn(orders);
setAuthentication();
try {
ClerkPerformanceDetailResponseVo response = service.queryDetail(vo);
assertNotNull(response.getProfile());
assertEquals("Alice", response.getProfile().getNickname());
assertEquals("一组", response.getProfile().getGroupName());
assertNotNull(response.getSnapshot());
assertEquals(new BigDecimal("260.00"), response.getSnapshot().getGmv());
assertEquals(3, response.getSnapshot().getOrderCount());
assertEquals(new BigDecimal("66.67"), response.getSnapshot().getContinuedRate());
assertEquals(new BigDecimal("86.67"), response.getSnapshot().getAverageTicketPrice());
assertNotNull(response.getComposition());
assertEquals(4, response.getComposition().getOrderComposition().size());
assertEquals(new BigDecimal("33.33"), response.getComposition().getOrderComposition().get(0).getCountRatio());
assertEquals(3, response.getTrend().size());
assertEquals("DAY", response.getTrendDimension());
assertEquals(new BigDecimal("120.00"), response.getTrend().get(0).getGmv());
assertEquals(new BigDecimal("140.00"), response.getTrend().get(1).getGmv());
} finally {
clearAuthentication();
}
}
@Test
@DisplayName("queryDetail should reject access when clerk not under current user")
void queryDetailShouldEnforcePermissions() {
ClerkPerformanceDetailQueryVo vo = new ClerkPerformanceDetailQueryVo();
vo.setClerkId("c99");
vo.setEndOrderTime(Arrays.asList("2024-08-01 00:00:00", "2024-08-02 23:59:59"));
when(playPersonnelGroupInfoService.getValidClerkIdList(any(), any())).thenReturn(Collections.singletonList("c1"));
when(clerkUserInfoService.getById("c99")).thenReturn(buildClerk("c99", "Ghost", "g1", "l1"));
setAuthentication();
try {
ServiceException ex = assertThrows(ServiceException.class, () -> service.queryDetail(vo));
assertTrue(ex.getMessage().contains("无权"));
} finally {
clearAuthentication();
}
}
private PlayClerkUserInfoEntity buildClerk(String id, String name, String groupId, String levelId) {
PlayClerkUserInfoEntity entity = new PlayClerkUserInfoEntity();
entity.setId(id);
entity.setNickname(name);
entity.setGroupId(groupId);
entity.setLevelId(levelId);
entity.setSex("1");
entity.setListingState("1");
entity.setOnlineState("1");
return entity;
}
private PlayClerkLevelInfoEntity level(String id, String name) {
PlayClerkLevelInfoEntity level = new PlayClerkLevelInfoEntity();
level.setId(id);
level.setName(name);
return level;
}
private PlayPersonnelGroupInfoEntity group(String id, String name) {
PlayPersonnelGroupInfoEntity entity = new PlayPersonnelGroupInfoEntity();
entity.setId(id);
entity.setGroupName(name);
return entity;
}
private PlayOrderInfoEntity order(String clerkId, String purchaser, String firstOrder, String placeType,
String refundType, BigDecimal finalAmount, BigDecimal estimatedRevenue, LocalDateTime purchaserTime) {
PlayOrderInfoEntity order = new PlayOrderInfoEntity();
order.setAcceptBy(clerkId);
order.setPurchaserBy(purchaser);
order.setFirstOrder(firstOrder);
order.setPlaceType(placeType);
order.setRefundType(refundType);
order.setFinalAmount(finalAmount);
order.setEstimatedRevenue(estimatedRevenue);
order.setOrdersExpiredState("1".equals(refundType) ? "1" : "0");
order.setPurchaserTime(purchaserTime);
return order;
}
private void setAuthentication() {
LoginUser loginUser = new LoginUser();
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(loginUser, null, Collections.emptyList());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
private void clearAuthentication() {
SecurityContextHolder.clearContext();
}
private PlayOrderInfoEntity withRefund(PlayOrderInfoEntity order, BigDecimal refundAmount) {
order.setRefundAmount(refundAmount);
return order;
}
}