From ee0fc4d1f69023cc09b2a38425ae4ae84ff6b096 Mon Sep 17 00:00:00 2001 From: irving Date: Thu, 13 Nov 2025 13:56:52 -0500 Subject: [PATCH] feat: unify admin order keyword search --- .../order/module/vo/PlayOrderInfoQueryVo.java | 6 + .../impl/OrderLifecycleServiceImpl.java | 7 +- .../impl/PlayOrderInfoServiceImpl.java | 17 +- .../service/impl/EarningsServiceImpl.java | 4 + .../api/PlayOrderInfoControllerApiTest.java | 194 ++++++++++++++++++ .../impl/OrderLifecycleServiceImplTest.java | 29 +++ .../service/impl/EarningsServiceImplTest.java | 12 ++ 7 files changed, 262 insertions(+), 7 deletions(-) diff --git a/play-admin/src/main/java/com/starry/admin/modules/order/module/vo/PlayOrderInfoQueryVo.java b/play-admin/src/main/java/com/starry/admin/modules/order/module/vo/PlayOrderInfoQueryVo.java index f8d4a05..cd99721 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/order/module/vo/PlayOrderInfoQueryVo.java +++ b/play-admin/src/main/java/com/starry/admin/modules/order/module/vo/PlayOrderInfoQueryVo.java @@ -30,6 +30,12 @@ public class PlayOrderInfoQueryVo extends BasePageEntity { @ApiModelProperty(value = "订单编号", example = "ORDER20240320001", notes = "订单的编号,支持模糊查询") private String orderNo; + /** + * 统一关键字(订单号或店员昵称) + */ + @ApiModelProperty(value = "关键词", example = "ORDER20240320001", notes = "支持订单号或店员昵称模糊查询") + private String keyword; + /** * 订单状态【0:1:2:3:4】 0:已下单(待接单) 1:已接单(待开始) 2:已开始(服务中) 3:已完成 4:已取消 */ diff --git a/play-admin/src/main/java/com/starry/admin/modules/order/service/impl/OrderLifecycleServiceImpl.java b/play-admin/src/main/java/com/starry/admin/modules/order/service/impl/OrderLifecycleServiceImpl.java index fa329a3..681ea2f 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/order/service/impl/OrderLifecycleServiceImpl.java +++ b/play-admin/src/main/java/com/starry/admin/modules/order/service/impl/OrderLifecycleServiceImpl.java @@ -632,7 +632,8 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService { OrderRevocationContext.EarningsAdjustStrategy strategy = context.getEarningsStrategy() != null ? context.getEarningsStrategy() : OrderRevocationContext.EarningsAdjustStrategy.NONE; - if (strategy != OrderRevocationContext.EarningsAdjustStrategy.NONE && earningsService.hasLockedLines(order.getId())) { + if (strategy == OrderRevocationContext.EarningsAdjustStrategy.REVERSE_CLERK + && earningsService.hasLockedLines(order.getId())) { throw new CustomException("收益已提现或处理中,无法撤销"); } @@ -857,6 +858,10 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService { } private boolean ensureEarnings(PlayOrderInfoEntity order, OrderTriggerSource source) { + if (OrderConstant.OrderType.BLIND_BOX_PURCHASE.getCode().equals(order.getOrderType())) { + log.debug("Skip earnings creation for blind box purchase order {}", order.getId()); + return false; + } Long existing = earningsService.lambdaQuery() .eq(EarningsLineEntity::getTenantId, order.getTenantId()) .eq(EarningsLineEntity::getOrderId, order.getId()) diff --git a/play-admin/src/main/java/com/starry/admin/modules/order/service/impl/PlayOrderInfoServiceImpl.java b/play-admin/src/main/java/com/starry/admin/modules/order/service/impl/PlayOrderInfoServiceImpl.java index 5a131b7..183fc2c 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/order/service/impl/PlayOrderInfoServiceImpl.java +++ b/play-admin/src/main/java/com/starry/admin/modules/order/service/impl/PlayOrderInfoServiceImpl.java @@ -402,7 +402,7 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl selectOrderInfoPage(PlayOrderInfoQueryVo vo) { MPJLambdaWrapper lambdaQueryWrapper = getCommonOrderQueryVo( - ConvertUtil.entityToVo(vo, PlayOrderInfoEntity.class)); + ConvertUtil.entityToVo(vo, PlayOrderInfoEntity.class), vo.getKeyword()); lambdaQueryWrapper.in(PlayOrderInfoEntity::getPlaceType, "0", "1", "2"); if (StringUtils.isNotBlank(vo.getGroupId())) { lambdaQueryWrapper.eq(PlayOrderInfoEntity::getGroupId, vo.getGroupId()); @@ -454,7 +454,7 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl lambdaQueryWrapper = getCommonOrderQueryVo(entity); + MPJLambdaWrapper lambdaQueryWrapper = getCommonOrderQueryVo(entity, null); // 拼接用户等级 lambdaQueryWrapper.selectAs(PlayCustomLevelInfoEntity::getId, "customLevelId") .selectAs(PlayCustomLevelInfoEntity::getName, "customLevelName"); @@ -505,7 +505,7 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl clerkSelectOrderInfoByPage(PlayClerkOrderInfoQueryVo vo) { MPJLambdaWrapper lambdaQueryWrapper = getCommonOrderQueryVo( - ConvertUtil.entityToVo(vo, PlayOrderInfoEntity.class)); + ConvertUtil.entityToVo(vo, PlayOrderInfoEntity.class), null); // 拼接用户等级 lambdaQueryWrapper.selectAs(PlayCustomLevelInfoEntity::getId, "customLevelId") .selectAs(PlayCustomLevelInfoEntity::getName, "customLevelName"); @@ -520,7 +520,7 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl lambdaQueryWrapper = getCommonOrderQueryVo(entity); + MPJLambdaWrapper lambdaQueryWrapper = getCommonOrderQueryVo(entity, null); PlayCustomOrderDetailsReturnVo returnVo = this.baseMapper.selectJoinOne(PlayCustomOrderDetailsReturnVo.class, lambdaQueryWrapper); // 如果订单状态为退款,查询订单退款原因 @@ -546,7 +546,7 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl customSelectOrderInfoByPage(PlayCustomOrderInfoQueryVo vo) { MPJLambdaWrapper lambdaQueryWrapper = getCommonOrderQueryVo( - ConvertUtil.entityToVo(vo, PlayOrderInfoEntity.class)); + ConvertUtil.entityToVo(vo, PlayOrderInfoEntity.class), null); if (StringUtils.isBlank(vo.getOrderType())) { lambdaQueryWrapper.notIn(PlayOrderInfoEntity::getOrderType, OrderConstant.OrderType.RECHARGE.getCode(), @@ -711,7 +711,7 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl */ - public MPJLambdaWrapper getCommonOrderQueryVo(PlayOrderInfoEntity entity) { + public MPJLambdaWrapper getCommonOrderQueryVo(PlayOrderInfoEntity entity, String keyword) { MPJLambdaWrapper lambdaQueryWrapper = new MPJLambdaWrapper<>(); // 查询主表全部字段 lambdaQueryWrapper.selectAll(PlayOrderInfoEntity.class); @@ -748,6 +748,11 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl w.like(PlayOrderInfoEntity::getOrderNo, keyword) + .or() + .like(PlayClerkUserInfoEntity::getNickname, keyword)); + } lambdaQueryWrapper.orderByDesc(PlayOrderInfoEntity::getCreatedTime); return lambdaQueryWrapper; diff --git a/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/impl/EarningsServiceImpl.java b/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/impl/EarningsServiceImpl.java index 3d622dc..4abfb65 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/impl/EarningsServiceImpl.java +++ b/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/impl/EarningsServiceImpl.java @@ -4,6 +4,7 @@ import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +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.enums.EarningsType; @@ -29,6 +30,9 @@ public class EarningsServiceImpl extends ServiceImpl orderIdsToCleanup = new ArrayList<>(); + private final List 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 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 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; diff --git a/play-admin/src/test/java/com/starry/admin/modules/order/service/impl/OrderLifecycleServiceImplTest.java b/play-admin/src/test/java/com/starry/admin/modules/order/service/impl/OrderLifecycleServiceImplTest.java index 32177e2..af7adc8 100644 --- a/play-admin/src/test/java/com/starry/admin/modules/order/service/impl/OrderLifecycleServiceImplTest.java +++ b/play-admin/src/test/java/com/starry/admin/modules/order/service/impl/OrderLifecycleServiceImplTest.java @@ -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)); diff --git a/play-admin/src/test/java/com/starry/admin/modules/withdraw/service/impl/EarningsServiceImplTest.java b/play-admin/src/test/java/com/starry/admin/modules/withdraw/service/impl/EarningsServiceImplTest.java index 6437cf5..a00a286 100644 --- a/play-admin/src/test/java/com/starry/admin/modules/withdraw/service/impl/EarningsServiceImplTest.java +++ b/play-admin/src/test/java/com/starry/admin/modules/withdraw/service/impl/EarningsServiceImplTest.java @@ -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);