From 0d0613e810d4baade2265d5860403aba8f4277db Mon Sep 17 00:00:00 2001 From: caojia <1301869380@qq.com> Date: Thu, 12 Nov 2026 16:35:01 +0800 Subject: [PATCH] =?UTF-8?q?:zap:=20=E7=A7=BB=E9=99=A4=E5=BA=94=E6=94=B6?= =?UTF-8?q?=E8=B4=A6=E5=8D=95=E7=A1=AE=E8=AE=A4=E6=97=B6=E7=9A=84=E6=98=8E?= =?UTF-8?q?=E7=BB=86=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/bms/receivable/ArBillConfirmTest.java | 122 ++++++++++++++++++ .../receivable/mapper/ArBillDetailMapper.java | 5 - .../service/impl/ArBillServiceImpl.java | 27 +--- .../mapper/receivable/ArBillDetailMapper.xml | 43 ------ 4 files changed, 123 insertions(+), 74 deletions(-) create mode 100644 bms-back/bms-admin/src/test/java/com/bms/receivable/ArBillConfirmTest.java diff --git a/bms-back/bms-admin/src/test/java/com/bms/receivable/ArBillConfirmTest.java b/bms-back/bms-admin/src/test/java/com/bms/receivable/ArBillConfirmTest.java new file mode 100644 index 00000000..5a956f77 --- /dev/null +++ b/bms-back/bms-admin/src/test/java/com/bms/receivable/ArBillConfirmTest.java @@ -0,0 +1,122 @@ +package com.bms.receivable; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Set; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.util.ReflectionTestUtils; + +import com.bms.common.core.domain.entity.SysUser; +import com.bms.common.core.domain.model.LoginUser; +import com.bms.common.exception.ServiceException; +import com.bms.receivable.domain.ArBill; +import com.bms.receivable.domain.ArBillLog; +import com.bms.receivable.mapper.ArBillDetailMapper; +import com.bms.receivable.mapper.ArBillExportMapper; +import com.bms.receivable.mapper.ArBillMapper; +import com.bms.receivable.security.ArBillScopeResolver; +import com.bms.receivable.service.IArBillLogService; +import com.bms.receivable.service.impl.ArBillServiceImpl; + +class ArBillConfirmTest { + private final ArBillMapper billMapper = mock(ArBillMapper.class); + private final ArBillDetailMapper detailMapper = mock(ArBillDetailMapper.class); + private final ArBillExportMapper exportMapper = mock(ArBillExportMapper.class); + private final ArBillScopeResolver scope = mock(ArBillScopeResolver.class); + private final IArBillLogService logService = mock(IArBillLogService.class); + private final ArBillServiceImpl service = new ArBillServiceImpl(); + + @BeforeEach + void setUp() { + ReflectionTestUtils.setField(service, "billMapper", billMapper); + ReflectionTestUtils.setField(service, "detailMapper", detailMapper); + ReflectionTestUtils.setField(service, "exportMapper", exportMapper); + ReflectionTestUtils.setField(service, "billScope", scope); + ReflectionTestUtils.setField(service, "logService", logService); + SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken( + new LoginUser(7L, 1L, new SysUser(), Set.of()), null)); + } + + @AfterEach + void clearSecurity() { + SecurityContextHolder.clearContext(); + } + + @Test + void readyDraftCanBeConfirmedWithoutReadingDetails() { + ArBill draft = bill("DRAFT", "READY"); + draft.setTotalAmount(new BigDecimal("123.45")); + ArBill confirmed = bill("CONFIRMED", "READY"); + when(billMapper.selectArBillByIdForUpdate(11L)).thenReturn(draft); + when(billMapper.updateArBillStatus(11L, "CONFIRMED", null, "finance", "DRAFT")).thenReturn(1); + when(billMapper.selectArBillByIdScoped(11L, null)).thenReturn(confirmed); + when(exportMapper.selectByBillId(11L)).thenReturn(List.of()); + + assertSame(confirmed, service.confirmBill(11L, null, "finance", "request-1")); + verify(scope).requireBillAccessible(11L); + verifyNoInteractions(detailMapper); + verify(logService).record(any(ArBillLog.class)); + } + + @Test + void unfinishedOrNonDraftBillStillCannotBeConfirmed() { + when(billMapper.selectArBillByIdForUpdate(11L)).thenReturn( + bill("DRAFT", "GENERATING"), bill("CONFIRMED", "READY")); + + ServiceException unfinished = assertThrows(ServiceException.class, + () -> service.confirmBill(11L, null, "finance", "request-1")); + assertEquals("账单尚未生成完成,暂不能确认", unfinished.getMessage()); + ServiceException notDraft = assertThrows(ServiceException.class, + () -> service.confirmBill(11L, null, "finance", "request-2")); + assertEquals("只有草稿账单可以确认,当前状态为 CONFIRMED", notDraft.getMessage()); + verify(billMapper, never()).updateArBillStatus(eq(11L), any(), nullable(String.class), any(), any()); + verifyNoInteractions(detailMapper); + } + + @Test + void inaccessibleBillStillCannotBeConfirmed() { + when(billMapper.selectArBillByIdForUpdate(11L)).thenReturn(bill("DRAFT", "READY")); + doThrow(new ServiceException("账单不存在或无权访问")).when(scope).requireBillAccessible(11L); + + assertThrows(ServiceException.class, () -> service.confirmBill(11L, null, "finance", "request-1")); + verify(billMapper, never()).updateArBillStatus(eq(11L), any(), nullable(String.class), any(), any()); + verifyNoInteractions(detailMapper); + } + + @Test + void concurrentStatusChangeStillPreventsConfirmation() { + when(billMapper.selectArBillByIdForUpdate(11L)).thenReturn(bill("DRAFT", "READY")); + + ServiceException error = assertThrows(ServiceException.class, + () -> service.confirmBill(11L, null, "finance", "request-1")); + assertEquals("账单状态已发生变化,请刷新后重试", error.getMessage()); + verify(logService, never()).record(any(ArBillLog.class)); + verifyNoInteractions(detailMapper); + } + + private static ArBill bill(String status, String generationStatus) { + ArBill bill = new ArBill(); + bill.setArBillId(11L); + bill.setStatus(status); + bill.setGenerationStatus(generationStatus); + return bill; + } +} diff --git a/bms-back/bms-system/src/main/java/com/bms/receivable/mapper/ArBillDetailMapper.java b/bms-back/bms-system/src/main/java/com/bms/receivable/mapper/ArBillDetailMapper.java index f361e703..b6a39366 100644 --- a/bms-back/bms-system/src/main/java/com/bms/receivable/mapper/ArBillDetailMapper.java +++ b/bms-back/bms-system/src/main/java/com/bms/receivable/mapper/ArBillDetailMapper.java @@ -16,11 +16,6 @@ public interface ArBillDetailMapper { List selectByBillIdAndSourceIds(@Param("arBillId") Long arBillId, @Param("sourceBillIds") List sourceBillIds); - /** - * 查询账单明细及费用明细,避免详情页逐层查询。 - */ - List selectByBillIdWithFees(Long arBillId); - List selectExportPage(@Param("arBillId") Long arBillId, @Param("lastDetailNo") Integer lastDetailNo, @Param("pageSize") Integer pageSize); diff --git a/bms-back/bms-system/src/main/java/com/bms/receivable/service/impl/ArBillServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/receivable/service/impl/ArBillServiceImpl.java index 8e27d0be..808b8933 100644 --- a/bms-back/bms-system/src/main/java/com/bms/receivable/service/impl/ArBillServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/receivable/service/impl/ArBillServiceImpl.java @@ -985,7 +985,7 @@ public class ArBillServiceImpl implements IArBillService { } /** - * 确认已完成生成且明细金额校验通过的草稿账单。 + * 确认已完成生成的草稿账单。 */ @Override @Transactional(rollbackFor = Exception.class) @@ -1007,11 +1007,6 @@ public class ArBillServiceImpl implements IArBillService { bill.getStatus(), "只有草稿账单可以确认,当前状态为 " + bill.getStatus(), operator, requestId); } - if (!validateDetails(arBillId, bill.getTotalAmount())) { - throw failure(arBillId, ArLogObjectType.BILL, String.valueOf(arBillId), ArLogOperationType.CONFIRM, - bill.getStatus(), - "账单费用明细汇总与订单或账单总额不一致", operator, requestId); - } if (billMapper.updateArBillStatus(arBillId, ArBillStatus.CONFIRMED.getCode(), StringUtils.isEmpty(request == null ? null : request.getRemark()) ? bill.getRemark() : request.getRemark(), operator, ArBillStatus.DRAFT.getCode()) != 1) { @@ -1313,26 +1308,6 @@ public class ArBillServiceImpl implements IArBillService { return message.length() > 500 ? message.substring(0, 500) : message; } - private boolean validateDetails(Long arBillId, BigDecimal billTotal) { - List details = detailMapper.selectByBillIdWithFees(arBillId); - BigDecimal detailTotal = BigDecimal.ZERO; - for (ArBillDetail detail : details) { - BigDecimal feeTotal = BigDecimal.ZERO; - List fees = detail.getFees(); - if (fees == null) { - fees = new ArrayList(); - } - for (ArBillDetailFee fee : fees) { - feeTotal = feeTotal.add(nvl(fee.getAmount())); - } - if (feeTotal.compareTo(nvl(detail.getTotalAmount())) != 0) { - return false; - } - detailTotal = detailTotal.add(nvl(detail.getTotalAmount())); - } - return !details.isEmpty() && detailTotal.compareTo(nvl(billTotal)) == 0; - } - private ArBillDetail toDetail(ArBill bill, PrcOrderBill source, PrcIncomeBill sourceIncomeBill, int detailNo, String operator) { ArBillDetail detail = new ArBillDetail(); diff --git a/bms-back/bms-system/src/main/resources/mapper/receivable/ArBillDetailMapper.xml b/bms-back/bms-system/src/main/resources/mapper/receivable/ArBillDetailMapper.xml index b54acb31..710ffbaf 100644 --- a/bms-back/bms-system/src/main/resources/mapper/receivable/ArBillDetailMapper.xml +++ b/bms-back/bms-system/src/main/resources/mapper/receivable/ArBillDetailMapper.xml @@ -69,49 +69,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" order by detail_no - - - - - - - - - - - - - - - - - - - - - - - - - -