From 9b9b1024c8e09625d510831dcf52906ce822211c Mon Sep 17 00:00:00 2001 From: irving Date: Mon, 19 Jan 2026 00:19:02 -0500 Subject: [PATCH] fix(deduction): make apply idempotent --- .../EarningsDeductionBatchServiceImpl.java | 6 +- ...ServiceImplIdempotencyIntegrationTest.java | 130 ++++++++++++++++++ 2 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 play-admin/src/test/java/com/starry/admin/modules/withdraw/service/impl/EarningsDeductionBatchServiceImplIdempotencyIntegrationTest.java diff --git a/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/impl/EarningsDeductionBatchServiceImpl.java b/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/impl/EarningsDeductionBatchServiceImpl.java index b042de5..631a0fd 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/impl/EarningsDeductionBatchServiceImpl.java +++ b/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/impl/EarningsDeductionBatchServiceImpl.java @@ -228,6 +228,10 @@ public class EarningsDeductionBatchServiceImpl if (item == null || !StringUtils.hasText(item.getClerkId())) { continue; } + if (StringUtils.hasText(item.getAdjustmentId())) { + adjustmentService.triggerApplyAsync(item.getAdjustmentId()); + continue; + } BigDecimal amount = item.getApplyAmount() == null ? BigDecimal.ZERO : item.getApplyAmount(); if (amount.compareTo(BigDecimal.ZERO) == 0) { markItemFailed(item.getId(), "applyAmount=0"); @@ -241,7 +245,7 @@ public class EarningsDeductionBatchServiceImpl EarningsAdjustmentReasonType.MANUAL, batch.getReasonDescription(), idempotencyKey, - LocalDateTime.now()); + null); if (!StringUtils.hasText(item.getAdjustmentId())) { EarningsDeductionItemEntity patch = new EarningsDeductionItemEntity(); diff --git a/play-admin/src/test/java/com/starry/admin/modules/withdraw/service/impl/EarningsDeductionBatchServiceImplIdempotencyIntegrationTest.java b/play-admin/src/test/java/com/starry/admin/modules/withdraw/service/impl/EarningsDeductionBatchServiceImplIdempotencyIntegrationTest.java new file mode 100644 index 0000000..6b5140b --- /dev/null +++ b/play-admin/src/test/java/com/starry/admin/modules/withdraw/service/impl/EarningsDeductionBatchServiceImplIdempotencyIntegrationTest.java @@ -0,0 +1,130 @@ +package com.starry.admin.modules.withdraw.service.impl; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.starry.admin.api.AbstractApiTest; +import com.starry.admin.common.apitest.ApiTestDataSeeder; +import com.starry.admin.modules.withdraw.entity.EarningsDeductionBatchEntity; +import com.starry.admin.modules.withdraw.entity.EarningsDeductionBatchLogEntity; +import com.starry.admin.modules.withdraw.entity.EarningsDeductionItemEntity; +import com.starry.admin.modules.withdraw.entity.EarningsLineEntity; +import com.starry.admin.modules.withdraw.enums.EarningsDeductionOperationType; +import com.starry.admin.modules.withdraw.enums.EarningsDeductionRuleType; +import com.starry.admin.modules.withdraw.enums.EarningsSourceType; +import com.starry.admin.modules.withdraw.mapper.EarningsDeductionBatchLogMapper; +import com.starry.admin.modules.withdraw.mapper.EarningsDeductionItemMapper; +import com.starry.admin.modules.withdraw.service.IEarningsAdjustmentService; +import com.starry.admin.modules.withdraw.service.IEarningsService; +import com.starry.admin.utils.SecurityUtils; +import com.starry.common.utils.IdUtils; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class EarningsDeductionBatchServiceImplIdempotencyIntegrationTest extends AbstractApiTest { + + @Autowired + private EarningsDeductionBatchServiceImpl batchService; + + @Autowired + private EarningsDeductionItemMapper itemMapper; + + @Autowired + private EarningsDeductionBatchLogMapper logMapper; + + @Autowired + private IEarningsAdjustmentService adjustmentService; + + @Autowired + private IEarningsService earningsService; + + private String batchId; + private String clerkId; + + @BeforeEach + void setUp() { + SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID); + clerkId = IdUtils.getUuid(); + } + + @AfterEach + void tearDown() { + SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID); + if (batchId == null) { + return; + } + + List items = itemMapper.selectList(Wrappers.lambdaQuery(EarningsDeductionItemEntity.class) + .eq(EarningsDeductionItemEntity::getTenantId, ApiTestDataSeeder.DEFAULT_TENANT_ID) + .eq(EarningsDeductionItemEntity::getBatchId, batchId) + .eq(EarningsDeductionItemEntity::getDeleted, false)); + + List adjustmentIds = items.stream() + .map(EarningsDeductionItemEntity::getAdjustmentId) + .filter(id -> id != null && !id.isBlank()) + .distinct() + .collect(Collectors.toList()); + + if (!adjustmentIds.isEmpty()) { + earningsService.lambdaUpdate() + .eq(EarningsLineEntity::getTenantId, ApiTestDataSeeder.DEFAULT_TENANT_ID) + .eq(EarningsLineEntity::getClerkId, clerkId) + .eq(EarningsLineEntity::getSourceType, EarningsSourceType.ADJUSTMENT) + .in(EarningsLineEntity::getSourceId, adjustmentIds) + .remove(); + adjustmentService.removeByIds(adjustmentIds); + } + + itemMapper.delete(Wrappers.lambdaQuery(EarningsDeductionItemEntity.class) + .eq(EarningsDeductionItemEntity::getTenantId, ApiTestDataSeeder.DEFAULT_TENANT_ID) + .eq(EarningsDeductionItemEntity::getBatchId, batchId)); + + logMapper.delete(Wrappers.lambdaQuery(EarningsDeductionBatchLogEntity.class) + .eq(EarningsDeductionBatchLogEntity::getTenantId, ApiTestDataSeeder.DEFAULT_TENANT_ID) + .eq(EarningsDeductionBatchLogEntity::getBatchId, batchId)); + + batchService.removeById(batchId); + } + + @Test + void applyOnce_canBeTriggeredTwiceWithoutBreakingIdempotency() { + String tenantId = ApiTestDataSeeder.DEFAULT_TENANT_ID; + LocalDateTime begin = LocalDateTime.now().minusDays(1); + LocalDateTime end = LocalDateTime.now(); + String idempotencyKey = "e2e-deduct-idem-" + UUID.randomUUID(); + + EarningsDeductionBatchEntity batch = batchService.createOrGetProcessing( + tenantId, + List.of(clerkId), + begin, + end, + EarningsDeductionRuleType.FIXED, + new BigDecimal("1.00"), + EarningsDeductionOperationType.PUNISHMENT, + "idempotency test", + idempotencyKey); + assertNotNull(batch); + batchId = batch.getId(); + + batchService.applyOnce(batchId); + + EarningsDeductionItemEntity item = itemMapper.selectOne(Wrappers.lambdaQuery(EarningsDeductionItemEntity.class) + .eq(EarningsDeductionItemEntity::getTenantId, tenantId) + .eq(EarningsDeductionItemEntity::getBatchId, batchId) + .eq(EarningsDeductionItemEntity::getClerkId, clerkId) + .last("limit 1")); + assertNotNull(item); + assertTrue(item.getAdjustmentId() != null && !item.getAdjustmentId().isBlank()); + + assertDoesNotThrow(() -> batchService.applyOnce(batchId)); + } +}