From 6470e222d1ebf2a2f396312d6d69dff3efff791b Mon Sep 17 00:00:00 2001 From: dzc <2388224167@qq.com> Date: Mon, 14 Sep 2026 22:56:22 +0800 Subject: [PATCH] =?UTF-8?q?fix=EF=BC=9A=E5=BA=93=E5=AD=98=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E5=A4=B1=E8=B4=A5=EF=BC=8C=E8=AF=B7=E9=87=8D=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/impl/CanteenSpuServiceImpl.java | 5 +- .../mapper/CanteenDailyMenuMapper.xml | 20 ++++++- .../impl/CanteenSpuServiceImplTest.java | 59 ++++++++++++++++++- 3 files changed, 78 insertions(+), 6 deletions(-) diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenSpuServiceImpl.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenSpuServiceImpl.java index 6e166fc78..58bec30fb 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenSpuServiceImpl.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenSpuServiceImpl.java @@ -318,8 +318,9 @@ public class CanteenSpuServiceImpl extends ServiceImpl 0 ? null : item.getBeforeStock(); updated = offline diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/resources/mapper/CanteenDailyMenuMapper.xml b/pjcloud-canteen/pjcloud-canteen-biz/src/main/resources/mapper/CanteenDailyMenuMapper.xml index 6b81757e7..82626312c 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/resources/mapper/CanteenDailyMenuMapper.xml +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/resources/mapper/CanteenDailyMenuMapper.xml @@ -324,17 +324,26 @@ delta 与旧接口一致:下单扣减传负数、售后回传正数。 实际改写销量:sold = sold - delta(扣减时 sold 增加)。 expected 为操作前「剩余」快照(配额 - 销量)。 + 正增量恢复把销量钳制到 0 为止:菜谱重导或按订单重算销量后 sold 可能小于恢复量, + 退款/补偿不能被该统计口径阻断;负增量扣减方向恒增,GREATEST 不改变原语义。 --> UPDATE canteen_daily_menu - SET online_sold = IFNULL(online_sold, 0) - #{delta} + SET online_sold = GREATEST(IFNULL(online_sold, 0) - #{delta}, 0) WHERE del_flag = '0' AND merchant_id = #{merchantId} AND start_time = #{mealDate} AND hour_type = #{hourType} AND spu_id = #{spuId} + + + AND GREATEST(IFNULL(online_sold, 0) - #{delta}, 0) <= IFNULL(online_stock, 600) + + AND IFNULL(online_sold, 0) - #{delta} >= 0 AND IFNULL(online_sold, 0) - #{delta} <= IFNULL(online_stock, 600) + + AND (IFNULL(online_stock, 600) - IFNULL(online_sold, 0)) = #{expected} @@ -342,14 +351,21 @@ UPDATE canteen_daily_menu - SET offline_sold = IFNULL(offline_sold, 0) - #{delta} + SET offline_sold = GREATEST(IFNULL(offline_sold, 0) - #{delta}, 0) WHERE del_flag = '0' AND merchant_id = #{merchantId} AND start_time = #{mealDate} AND hour_type = #{hourType} AND spu_id = #{spuId} + + + AND GREATEST(IFNULL(offline_sold, 0) - #{delta}, 0) <= IFNULL(offline_stock, 600) + + AND IFNULL(offline_sold, 0) - #{delta} >= 0 AND IFNULL(offline_sold, 0) - #{delta} <= IFNULL(offline_stock, 600) + + AND (IFNULL(offline_stock, 600) - IFNULL(offline_sold, 0)) = #{expected} diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/test/java/com/bsd/pjcloud/canteen/service/impl/CanteenSpuServiceImplTest.java b/pjcloud-canteen/pjcloud-canteen-biz/src/test/java/com/bsd/pjcloud/canteen/service/impl/CanteenSpuServiceImplTest.java index 6ce609ed2..d4045f9b4 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/test/java/com/bsd/pjcloud/canteen/service/impl/CanteenSpuServiceImplTest.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/test/java/com/bsd/pjcloud/canteen/service/impl/CanteenSpuServiceImplTest.java @@ -1,7 +1,9 @@ package com.bsd.pjcloud.canteen.service.impl; import com.bsd.pjcloud.canteen.dto.CanteenSpuUpdateStockReqDTO; +import com.bsd.pjcloud.canteen.entity.CanteenDailyMenuEntity; import com.bsd.pjcloud.canteen.entity.CanteenSpuEntity; +import com.bsd.pjcloud.canteen.mapper.CanteenDailyMenuMapper; import com.bsd.pjcloud.canteen.mapper.CanteenSpuMapper; import com.bsd.pjcloud.canteen.service.CanteenSpuService; import com.bsd.pjcloud.canteen.util.CanteenMerchantScopeHelper; @@ -18,6 +20,7 @@ import org.springframework.test.util.ReflectionTestUtils; import org.springframework.transaction.support.TransactionSynchronizationManager; import java.time.Duration; +import java.time.LocalDate; import java.util.List; import java.util.Locale; @@ -43,6 +46,9 @@ class CanteenSpuServiceImplTest { /** 基础 SPU Mapper,用于验证库存增量是否真实执行。 */ private CanteenSpuMapper spuMapper; + /** 每日菜谱 Mapper,用于验证分池正增量恢复的剩余匹配条件。 */ + private CanteenDailyMenuMapper dailyMenuMapper; + /** Redis 字符串操作,用于模拟和验证库存业务状态迁移。 */ private ValueOperations valueOperations; @@ -52,11 +58,12 @@ class CanteenSpuServiceImplTest { @BeforeEach void setUp() { spuMapper = mock(CanteenSpuMapper.class); + dailyMenuMapper = mock(CanteenDailyMenuMapper.class); StringRedisTemplate redisTemplate = mock(StringRedisTemplate.class); valueOperations = mock(ValueOperations.class); when(redisTemplate.opsForValue()).thenReturn(valueOperations); - CanteenSpuServiceImpl target = new CanteenSpuServiceImpl(redisTemplate, null, null, null, null, null, - mock(CanteenMerchantScopeHelper.class), mock(CanteenDailyMenuStockSupport.class)); + CanteenSpuServiceImpl target = new CanteenSpuServiceImpl(redisTemplate, dailyMenuMapper, null, null, null, + null, mock(CanteenMerchantScopeHelper.class), mock(CanteenDailyMenuStockSupport.class)); ReflectionTestUtils.setField(target, "baseMapper", spuMapper); service = target; TransactionSynchronizationManager.initSynchronization(); @@ -122,6 +129,54 @@ class CanteenSpuServiceImplTest { verify(spuMapper, never()).updateStockIfMatch(anyLong(), anyInt(), anyInt()); } + /** + * 每日菜谱分池正增量恢复:当前剩余与快照不一致(菜谱重导/销量重算后)也不得阻断退款, + * 且传给 SQL 的剩余匹配条件为 null(销量回退由 SQL 钳制到 0 为止)。 + */ + @Test + void dailyRestoreApplyShouldSkipRemainMatch() { + LocalDate mealDate = LocalDate.of(2026, 9, 14); + CanteenSpuUpdateStockReqDTO.Item item = new CanteenSpuUpdateStockReqDTO.Item(); + item.setId(100L); + item.setIncrCount(2); + item.setBeforeStock(5); + item.setMealDate(mealDate); + item.setHourType(1); + CanteenSpuUpdateStockReqDTO dto = new CanteenSpuUpdateStockReqDTO(); + dto.setBizId("HARD_DAILY_MENU_AFTER_SALE:9"); + dto.setOperation("APPLY"); + dto.setStockChannel("ONLINE"); + dto.setMerchantId(1L); + dto.setItems(List.of(item)); + + // 配额 10、已售 0:剩余 10 与快照 5 不一致,正增量恢复仍应放行 + when(dailyMenuMapper.selectOne(any())).thenReturn(dailyMenu(10, 0)); + when(dailyMenuMapper.updateOnlineStockDelta(1L, mealDate, 1, 100L, 2, null)).thenReturn(1); + + assertTrue(service.updateSkuStockIdempotent(dto).isOk()); + + verify(dailyMenuMapper).updateOnlineStockDelta(1L, mealDate, 1, 100L, 2, null); + verify(valueOperations).set("canteen:spu:order-stock:daily:ONLINE:HARD_DAILY_MENU_AFTER_SALE:9", + "APPLIED", Duration.ofDays(7)); + } + + /** + * 构造每日菜谱菜谱行实体。 + * + * @param onlineStock 线上配额 + * @param onlineSold 线上已售 + * @return 仅包含本用例所需字段的实体 + */ + private CanteenDailyMenuEntity dailyMenu(int onlineStock, int onlineSold) { + CanteenDailyMenuEntity menu = new CanteenDailyMenuEntity(); + menu.setMerchantId(1L); + menu.setSpuId(100L); + menu.setHourType(1); + menu.setOnlineStock(onlineStock); + menu.setOnlineSold(onlineSold); + return menu; + } + /** * 已执行 APPLY 的回滚必须标记为可重试补偿态,区别于阻断网络延迟 APPLY 的未确认回滚态。 */