feat: unify admin order keyword search
This commit is contained in:
@@ -10,8 +10,12 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
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.modules.order.module.constant.OrderConstant.OrderStatus;
|
||||
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
|
||||
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
|
||||
import com.starry.admin.modules.withdraw.enums.EarningsType;
|
||||
import com.starry.admin.modules.withdraw.service.IEarningsService;
|
||||
import com.starry.admin.utils.SecurityUtils;
|
||||
import com.starry.common.context.CustomSecurityContextHolder;
|
||||
import com.starry.common.utils.IdUtils;
|
||||
@@ -38,12 +42,20 @@ class PlayOrderInfoControllerApiTest extends AbstractApiTest {
|
||||
@Autowired
|
||||
private IPlayOrderInfoService orderInfoService;
|
||||
|
||||
@Autowired
|
||||
private IEarningsService earningsService;
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final List<String> orderIdsToCleanup = new ArrayList<>();
|
||||
private final List<String> earningsLineIdsToCleanup = new ArrayList<>();
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
ensureTenantContext();
|
||||
if (!earningsLineIdsToCleanup.isEmpty()) {
|
||||
earningsService.removeByIds(earningsLineIdsToCleanup);
|
||||
earningsLineIdsToCleanup.clear();
|
||||
}
|
||||
if (!orderIdsToCleanup.isEmpty()) {
|
||||
orderInfoService.removeByIds(orderIdsToCleanup);
|
||||
orderIdsToCleanup.clear();
|
||||
@@ -183,6 +195,165 @@ class PlayOrderInfoControllerApiTest extends AbstractApiTest {
|
||||
assertFilterMatches(combinedPayload, matching.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void listByPage_keywordFiltersByOrderNoOrClerkName() throws Exception {
|
||||
ensureTenantContext();
|
||||
String marker = ("KW" + IdUtils.getUuid().replace("-", "").substring(0, 4)).toUpperCase();
|
||||
LocalDateTime reference = LocalDateTime.now().minusHours(2);
|
||||
|
||||
PlayOrderInfoEntity orderByNo = persistOrder(marker, "ord", reference, order -> {
|
||||
order.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||
});
|
||||
PlayOrderInfoEntity orderByClerk = persistOrder(marker, "clk", reference.minusMinutes(10), order -> {
|
||||
order.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||
});
|
||||
|
||||
ObjectNode orderNoPayload = baseQuery();
|
||||
orderNoPayload.put("keyword", orderByNo.getOrderNo());
|
||||
assertFilterMatches(orderNoPayload, orderByNo.getId());
|
||||
|
||||
ObjectNode clerkKeywordPayload = baseQuery();
|
||||
clerkKeywordPayload.put("keyword", "小测官");
|
||||
RecordsResponse clerkResponse = executeList(clerkKeywordPayload);
|
||||
JsonNode clerkRecords = clerkResponse.records;
|
||||
assertThat(clerkRecords.size()).isGreaterThanOrEqualTo(2);
|
||||
List<String> matchedIds = new ArrayList<>();
|
||||
clerkRecords.forEach(node -> matchedIds.add(node.path("id").asText()));
|
||||
assertThat(matchedIds).contains(orderByNo.getId(), orderByClerk.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void listByPage_keywordRespectsAdditionalFilters() throws Exception {
|
||||
ensureTenantContext();
|
||||
String marker = ("KWFLT" + IdUtils.getUuid().replace("-", "").substring(0, 4)).toUpperCase();
|
||||
LocalDateTime reference = LocalDateTime.now().minusHours(3);
|
||||
|
||||
PlayOrderInfoEntity assignedOrder = persistOrder(marker, "assigned", reference, order -> {
|
||||
order.setOrderStatus("3");
|
||||
order.setPlaceType("0");
|
||||
});
|
||||
|
||||
persistOrder(marker, "random", reference.minusMinutes(20), order -> {
|
||||
order.setOrderStatus("3");
|
||||
order.setPlaceType("1");
|
||||
});
|
||||
|
||||
ObjectNode keywordAndFilterPayload = baseQuery();
|
||||
keywordAndFilterPayload.put("keyword", "小测官");
|
||||
keywordAndFilterPayload.put("placeType", "0");
|
||||
|
||||
RecordsResponse filteredResponse = executeList(keywordAndFilterPayload);
|
||||
JsonNode records = filteredResponse.records;
|
||||
assertThat(records.size()).isEqualTo(1);
|
||||
assertThat(records.get(0).path("id").asText()).isEqualTo(assignedOrder.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void revokeCompletedOrder_keepEarningsIgnoresLockedLines() throws Exception {
|
||||
ensureTenantContext();
|
||||
LocalDateTime reference = LocalDateTime.now().minusHours(1);
|
||||
PlayOrderInfoEntity order = persistOrder("RVK", "keep", reference, entity -> {
|
||||
entity.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||
entity.setFinalAmount(new BigDecimal("166.00"));
|
||||
});
|
||||
seedLockedEarningLine(order.getId(), new BigDecimal("80.00"), "withdrawn");
|
||||
|
||||
ObjectNode payload = objectMapper.createObjectNode();
|
||||
payload.put("orderId", order.getId());
|
||||
payload.put("refundToCustomer", false);
|
||||
payload.put("refundAmount", BigDecimal.ZERO);
|
||||
payload.put("refundReason", "API撤销-保留收益");
|
||||
payload.put("earningsStrategy", "NONE");
|
||||
|
||||
mockMvc.perform(post("/order/order/revokeCompleted")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||
.header(USER_HEADER, DEFAULT_USER)
|
||||
.content(payload.toString()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200))
|
||||
.andExpect(jsonPath("$.message").value("撤销成功"));
|
||||
|
||||
PlayOrderInfoEntity updated = orderInfoService.getById(order.getId());
|
||||
assertThat(updated.getOrderStatus()).isEqualTo(OrderStatus.REVOKED.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void revokeCompletedOrder_reverseClerkBlockedWhenLocked() throws Exception {
|
||||
ensureTenantContext();
|
||||
LocalDateTime reference = LocalDateTime.now().minusHours(2);
|
||||
PlayOrderInfoEntity order = persistOrder("RVK", "reverse", reference, entity -> {
|
||||
entity.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||
entity.setFinalAmount(new BigDecimal("210.00"));
|
||||
});
|
||||
seedLockedEarningLine(order.getId(), new BigDecimal("120.00"), "withdrawn");
|
||||
|
||||
ObjectNode payload = objectMapper.createObjectNode();
|
||||
payload.put("orderId", order.getId());
|
||||
payload.put("refundToCustomer", false);
|
||||
payload.put("refundAmount", new BigDecimal("20.00"));
|
||||
payload.put("refundReason", "API撤销-冲销收益");
|
||||
payload.put("earningsStrategy", "REVERSE_CLERK");
|
||||
|
||||
mockMvc.perform(post("/order/order/revokeCompleted")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||
.header(USER_HEADER, DEFAULT_USER)
|
||||
.content(payload.toString()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(500))
|
||||
.andExpect(jsonPath("$.message").value("收益已提现或处理中,无法撤销"));
|
||||
|
||||
PlayOrderInfoEntity latest = orderInfoService.getById(order.getId());
|
||||
assertThat(latest.getOrderStatus()).isEqualTo(OrderStatus.COMPLETED.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void revokeCompletedOrder_counterStrategyCreatesNegativeLineAfterWithdrawal() throws Exception {
|
||||
ensureTenantContext();
|
||||
LocalDateTime reference = LocalDateTime.now().minusMinutes(90);
|
||||
PlayOrderInfoEntity order = persistOrder("RVK", "counter", reference, entity -> {
|
||||
entity.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||
entity.setFinalAmount(new BigDecimal("230.00"));
|
||||
entity.setEstimatedRevenue(new BigDecimal("150.00"));
|
||||
});
|
||||
seedLockedEarningLine(order.getId(), new BigDecimal("140.00"), "withdrawn");
|
||||
|
||||
ObjectNode payload = objectMapper.createObjectNode();
|
||||
payload.put("orderId", order.getId());
|
||||
payload.put("refundToCustomer", false);
|
||||
payload.put("refundAmount", new BigDecimal("50.00"));
|
||||
payload.put("refundReason", "API撤销-转待扣");
|
||||
payload.put("earningsStrategy", "COUNTER_TO_PEIPEI");
|
||||
payload.put("counterClerkId", ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||
|
||||
mockMvc.perform(post("/order/order/revokeCompleted")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||
.header(USER_HEADER, DEFAULT_USER)
|
||||
.content(payload.toString()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200))
|
||||
.andExpect(jsonPath("$.message").value("撤销成功"));
|
||||
|
||||
PlayOrderInfoEntity updated = orderInfoService.getById(order.getId());
|
||||
assertThat(updated.getOrderStatus()).isEqualTo(OrderStatus.REVOKED.getCode());
|
||||
|
||||
List<EarningsLineEntity> lines = earningsService.lambdaQuery()
|
||||
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||
.list();
|
||||
assertThat(lines).hasSize(2);
|
||||
|
||||
EarningsLineEntity counterLine = lines.stream()
|
||||
.filter(line -> line.getAmount().compareTo(BigDecimal.ZERO) < 0)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new AssertionError("未生成负收益行"));
|
||||
assertThat(counterLine.getAmount()).isEqualByComparingTo(new BigDecimal("-50.00"));
|
||||
assertThat(counterLine.getStatus()).isEqualTo("available");
|
||||
assertThat(counterLine.getClerkId()).isEqualTo(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||
earningsLineIdsToCleanup.add(counterLine.getId());
|
||||
}
|
||||
|
||||
private PlayOrderInfoEntity persistOrder(
|
||||
String marker,
|
||||
String token,
|
||||
@@ -272,6 +443,29 @@ class PlayOrderInfoControllerApiTest extends AbstractApiTest {
|
||||
return array;
|
||||
}
|
||||
|
||||
private String seedLockedEarningLine(String orderId, BigDecimal amount, String status) {
|
||||
EarningsLineEntity entity = new EarningsLineEntity();
|
||||
String id = "earn-revoke-" + IdUtils.getUuid();
|
||||
entity.setId(id);
|
||||
entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||
entity.setClerkId(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||
entity.setOrderId(orderId);
|
||||
entity.setAmount(amount);
|
||||
entity.setStatus(status);
|
||||
entity.setEarningType(EarningsType.ORDER);
|
||||
entity.setUnlockTime(LocalDateTime.now().minusHours(2));
|
||||
entity.setWithdrawalId("withdraw-" + IdUtils.getUuid());
|
||||
Date nowDate = toDate(LocalDateTime.now());
|
||||
entity.setCreatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
|
||||
entity.setCreatedTime(nowDate);
|
||||
entity.setUpdatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
|
||||
entity.setUpdatedTime(nowDate);
|
||||
entity.setDeleted(false);
|
||||
earningsService.save(entity);
|
||||
earningsLineIdsToCleanup.add(id);
|
||||
return id;
|
||||
}
|
||||
|
||||
private void assertFilterMatches(ObjectNode payload, String expectedOrderId) throws Exception {
|
||||
RecordsResponse response = executeList(payload);
|
||||
JsonNode records = response.records;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.starry.admin.modules.order.service.impl;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
@@ -204,6 +205,34 @@ class OrderLifecycleServiceImplTest {
|
||||
verify(applicationEventPublisher, never()).publishEvent(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void revokeCompletedOrder_counterStrategyAllowedAfterWithdrawal() {
|
||||
String orderId = UUID.randomUUID().toString();
|
||||
PlayOrderInfoEntity completed = buildOrder(orderId, OrderStatus.COMPLETED.getCode());
|
||||
completed.setRefundType(OrderRefundFlag.NOT_REFUNDED.getCode());
|
||||
completed.setEstimatedRevenue(BigDecimal.valueOf(120));
|
||||
PlayOrderInfoEntity revoked = buildOrder(orderId, OrderStatus.REVOKED.getCode());
|
||||
|
||||
when(orderInfoMapper.selectById(orderId)).thenReturn(completed, revoked);
|
||||
lenient().when(orderInfoMapper.update(isNull(), any())).thenReturn(1);
|
||||
when(earningsService.hasLockedLines(orderId)).thenReturn(true);
|
||||
|
||||
OrderRevocationContext context = new OrderRevocationContext();
|
||||
context.setOrderId(orderId);
|
||||
context.setOperatorId("admin-counter");
|
||||
context.setOperatorType(OperatorType.ADMIN.getCode());
|
||||
context.setRefundToCustomer(false);
|
||||
context.setRefundAmount(BigDecimal.valueOf(50));
|
||||
context.setRefundReason("撤销并转待扣");
|
||||
context.setEarningsStrategy(EarningsAdjustStrategy.COUNTER_TO_PEIPEI);
|
||||
context.setCounterClerkId("clerk-negative");
|
||||
|
||||
assertDoesNotThrow(() -> lifecycleService.revokeCompletedOrder(context));
|
||||
|
||||
verify(orderInfoMapper).update(isNull(), any());
|
||||
verify(applicationEventPublisher).publishEvent(any(OrderRevocationEvent.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void placeOrder_throwsWhenCommandNull() {
|
||||
assertThrows(CustomException.class, () -> lifecycleService.placeOrder(null));
|
||||
|
||||
@@ -6,6 +6,7 @@ import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant;
|
||||
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
|
||||
import com.starry.admin.modules.withdraw.mapper.EarningsLineMapper;
|
||||
@@ -71,6 +72,17 @@ class EarningsServiceImplTest {
|
||||
verify(baseMapper, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createFromOrder_skipsBlindBoxPurchaseOrders() {
|
||||
PlayOrderInfoEntity order = baselineOrder();
|
||||
order.setEstimatedRevenue(BigDecimal.valueOf(66));
|
||||
order.setOrderType(OrderConstant.OrderType.BLIND_BOX_PURCHASE.getCode());
|
||||
|
||||
earningsService.createFromOrder(order);
|
||||
|
||||
verify(baseMapper, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createCounterLine_persistsNegativeAvailableLine() {
|
||||
when(baseMapper.insert(any())).thenReturn(1);
|
||||
|
||||
Reference in New Issue
Block a user