From 66be2c3ad249a7d567805d17f8c27e39e2f9b453 Mon Sep 17 00:00:00 2001 From: dzc <2388224167@qq.com> Date: Wed, 14 Oct 2026 14:57:07 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E5=88=AB=E5=90=8D=E6=89=93?= =?UTF-8?q?=E5=8D=B0=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/feign/RemoteCanteenSpuService.java | 15 +++ .../canteen/dto/SpuPrintNameQueryDTO.java | 49 ++++++++ .../pjcloud/canteen/vo/SpuPrintNameVo.java | 2 +- .../controller/CanteenSpuController.java | 17 +++ .../canteen/service/CanteenSpuService.java | 13 ++ .../service/impl/CanteenSpuServiceImpl.java | 119 +++++++++++++----- .../CanteenSpuServiceImplPrintNameTest.java | 79 +++++++++++- .../service/impl/HardInvoiceServiceImpl.java | 54 +++++++- .../impl/HardInvoiceServiceImplTest.java | 45 +++++++ 9 files changed, 359 insertions(+), 34 deletions(-) create mode 100644 pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/SpuPrintNameQueryDTO.java diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/api/feign/RemoteCanteenSpuService.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/api/feign/RemoteCanteenSpuService.java index cdded4d08..e12629873 100644 --- a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/api/feign/RemoteCanteenSpuService.java +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/api/feign/RemoteCanteenSpuService.java @@ -21,6 +21,7 @@ package com.bsd.pjcloud.canteen.api.feign; import com.bsd.pjcloud.canteen.dto.CanteenSpuUpdateStockReqDTO; +import com.bsd.pjcloud.canteen.dto.SpuPrintNameQueryDTO; import com.bsd.pjcloud.canteen.entity.CanteenOnlineSpuEntity; import com.bsd.pjcloud.canteen.entity.CanteenSpuEntity; import com.bsd.pjcloud.canteen.vo.SpuPrintNameVo; @@ -116,6 +117,20 @@ public interface RemoteCanteenSpuService { @NoToken R> getInnerSpuPrintNames(@RequestBody List productIds); + /** + * 供 Hardware 打印发票/兑换券查询商品名称,带排菜上下文时当日菜谱展示名优先作为套餐名。 + * + *

在 {@link #getInnerSpuPrintNames(List)} 的解析口径之上,按商户+用餐日期+餐段匹配 + * canteen_daily_menu 行,非空 display_name 覆盖套餐名,保证打印主行与排菜展示一致; + * 上下文不齐全时返回结果与原接口完全一致。

+ * + * @param query 商品 ID 列表与可选排菜上下文 + * @return 命中商品的打印名称列表 + */ + @PostMapping("/canteenSpu/inner/getSpuPrintNamesWithMenu") + @NoToken + R> getInnerSpuPrintNamesWithMenu(@RequestBody SpuPrintNameQueryDTO query); + /** * 按套餐 SPU ID 批量查询套餐组成,用于跨服务补充只读展示数据。 * diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/SpuPrintNameQueryDTO.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/SpuPrintNameQueryDTO.java new file mode 100644 index 000000000..bb983886c --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/SpuPrintNameQueryDTO.java @@ -0,0 +1,49 @@ +package com.bsd.pjcloud.canteen.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; +import java.util.List; + +/** + * 打印凭证商品名称查询请求:商品 ID 加可选的排菜上下文。 + * + *

排菜上下文(商户 + 用餐日期 + 餐段)齐全时,先按既有口径解析 canteen_spu 套餐名/商品名, + * 再用当日菜谱行的展示名(canteen_daily_menu.display_name)覆盖套餐名,使打印主行与排菜展示一致; + * 上下文缺失时行为与仅传商品 ID 完全相同。

+ */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "打印凭证商品名称查询请求") +public class SpuPrintNameQueryDTO { + + /** + * 购物车商品 ID 列表,兼容 canteen_spu.id 和 canteen_online_spu.id。 + */ + @Schema(description = "商品ID列表") + private List productIds; + + /** + * 商户 ID;与用餐日期、餐段共同定位当日菜谱行。 + */ + @Schema(description = "商户ID") + private Long merchantId; + + /** + * 用餐日期;对应 canteen_daily_menu.start_time。 + */ + @Schema(description = "用餐日期") + private LocalDate mealDate; + + /** + * 餐段:1 早餐 2 午餐 3 晚餐;对应 canteen_daily_menu.hour_type。 + */ + @Schema(description = "餐段") + private Integer hourType; +} diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/SpuPrintNameVo.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/SpuPrintNameVo.java index 3ecdc186f..cbf6d7cdb 100644 --- a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/SpuPrintNameVo.java +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/SpuPrintNameVo.java @@ -27,7 +27,7 @@ public class SpuPrintNameVo { private Long productId; /** - * 套餐名(canteen_spu.combo_name);每日排菜别名见 display_name,打印若需排菜名需另查菜谱。 + * 套餐名(canteen_spu.combo_name);携带排菜上下文查询时已被当日菜谱展示名(display_name)覆盖。 */ @Schema(description = "套餐名") private String comboName; diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenSpuController.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenSpuController.java index 259a0cb4c..2de6f515f 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenSpuController.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenSpuController.java @@ -13,6 +13,7 @@ import com.bsd.pjcloud.canteen.dto.CanteenSpuBatchPicDTO; import com.bsd.pjcloud.canteen.dto.CanteenSpuExportQuery; import com.bsd.pjcloud.canteen.dto.CanteenSpuImportExcel; import com.bsd.pjcloud.canteen.dto.CanteenSpuUpdateStockReqDTO; +import com.bsd.pjcloud.canteen.dto.SpuPrintNameQueryDTO; import com.bsd.pjcloud.canteen.entity.CanteenErpMaterialEntity; import com.bsd.pjcloud.canteen.entity.CanteenOnlineSpuEntity; import com.bsd.pjcloud.canteen.entity.CanteenStockEntity; @@ -446,6 +447,22 @@ public class CanteenSpuController { return R.ok(canteenSpuService.loadSpuPrintNames(productIds)); } + /** + * 为 Hardware 打印发票/兑换券提供带排菜上下文的商品名称查询。 + * + *

在 {@link #getInnerSpuPrintNames(List)} 的解析口径之上,按商户+用餐日期+餐段匹配 + * canteen_daily_menu 行,非空展示名(display_name)覆盖套餐名,使打印主行与当日排菜展示一致; + * 上下文不齐全时返回结果与原接口完全一致。

+ * + * @param query 商品 ID 列表与可选排菜上下文 + * @return 命中商品的打印名称列表 + */ + @Inner + @PostMapping("/inner/getSpuPrintNamesWithMenu") + public R> getInnerSpuPrintNamesWithMenu(@RequestBody SpuPrintNameQueryDTO query) { + return R.ok(canteenSpuService.loadSpuPrintNames(query)); + } + /** * 统一执行菜品 SPU 批量查询,避免管理端和内部端点产生不同的数据筛选口径。 * diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenSpuService.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenSpuService.java index 4b977e80d..81f482d98 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenSpuService.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenSpuService.java @@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.IService; import com.bsd.pjcloud.canteen.dto.CanteenDailySpuDTO; import com.bsd.pjcloud.canteen.dto.CanteenSpuUpdateStockReqDTO; +import com.bsd.pjcloud.canteen.dto.SpuPrintNameQueryDTO; import com.bsd.pjcloud.canteen.entity.CanteenSpuEntity; import com.bsd.pjcloud.canteen.vo.erp.CanteenSpuSimpleVO; import com.bsd.pjcloud.canteen.vo.erp.CanteenSpuStockVO; @@ -147,4 +148,16 @@ public interface CanteenSpuService extends IService { * @return 命中的打印名称列表;未命中的 ID 不出现在结果中 */ List loadSpuPrintNames(List productIds); + + /** + * 按购物车商品 ID 批量加载打印凭证名称,排菜上下文齐全时叠加当日菜谱展示名。 + * + *

解析口径与 {@link #loadSpuPrintNames(List)} 一致,另按商户+用餐日期+餐段匹配 + * canteen_daily_menu 行,非空 display_name 覆盖套餐名,使打印主行与排菜展示一致; + * 上下文不齐全时退化为无上下文行为,不额外查询菜谱。

+ * + * @param query 商品 ID 列表与可选排菜上下文 + * @return 命中的打印名称列表;未命中的 ID 不出现在结果中 + */ + List loadSpuPrintNames(SpuPrintNameQueryDTO query); } 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 708e97822..dafe6ee11 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 @@ -9,6 +9,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.bsd.pjcloud.canteen.dto.CanteenDailySpuDTO; import com.bsd.pjcloud.canteen.dto.CanteenSpuUpdateStockReqDTO; +import com.bsd.pjcloud.canteen.dto.SpuPrintNameQueryDTO; import com.bsd.pjcloud.canteen.entity.CanteenCategoryEntity; import com.bsd.pjcloud.canteen.entity.CanteenDailyMenuEntity; import com.bsd.pjcloud.canteen.entity.CanteenOnlineSpuEntity; @@ -920,10 +921,15 @@ public class CanteenSpuServiceImpl extends ServiceImpl loadSpuPrintNames(List productIds) { - if (CollUtil.isEmpty(productIds)) { + return loadSpuPrintNames(SpuPrintNameQueryDTO.builder().productIds(productIds).build()); + } + + @Override + public List loadSpuPrintNames(SpuPrintNameQueryDTO query) { + if (query == null || CollUtil.isEmpty(query.getProductIds())) { return Collections.emptyList(); } - List distinctIds = productIds.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList()); + List distinctIds = query.getProductIds().stream().filter(Objects::nonNull).distinct().collect(Collectors.toList()); if (distinctIds.isEmpty()) { return Collections.emptyList(); } @@ -937,42 +943,99 @@ public class CanteenSpuServiceImpl extends ServiceImpl result = new ArrayList<>(spuById.size()); + // 打印名回传调用方原始商品 ID;菜谱展示名按底层 SPU ID 匹配,这里同步记录两套 ID 的映射。 + Map spuIdByProductId = new LinkedHashMap<>(); for (Map.Entry entry : spuById.entrySet()) { result.add(buildPrintNameVo(entry.getKey(), entry.getValue())); + spuIdByProductId.put(entry.getKey(), entry.getKey()); } // 第二步:App 线上预约单的商品 ID 是 canteen_online_spu.id,经 linkSpuId 关联回基础菜品取名。 List missIds = distinctIds.stream().filter(id -> !spuById.containsKey(id)).collect(Collectors.toList()); - if (missIds.isEmpty()) { - return result; - } - List onlineHits = canteenOnlineSpuMapper.selectList( - new LambdaQueryWrapper() - .in(CanteenOnlineSpuEntity::getId, missIds) - .isNotNull(CanteenOnlineSpuEntity::getLinkSpuId)); - if (CollUtil.isEmpty(onlineHits)) { - return result; - } - List linkSpuIds = onlineHits.stream() - .map(CanteenOnlineSpuEntity::getLinkSpuId) - .filter(Objects::nonNull) - .distinct() - .collect(Collectors.toList()); - Map linkedById = new LinkedHashMap<>(); - for (CanteenSpuEntity spu : list(new LambdaQueryWrapper() - .in(CanteenSpuEntity::getId, linkSpuIds))) { - if (spu != null && spu.getId() != null) { - linkedById.putIfAbsent(spu.getId(), spu); - } - } - for (CanteenOnlineSpuEntity online : onlineHits) { - CanteenSpuEntity linked = online.getLinkSpuId() == null ? null : linkedById.get(online.getLinkSpuId()); - if (linked != null) { - result.add(buildPrintNameVo(online.getId(), linked)); + if (!missIds.isEmpty()) { + List onlineHits = canteenOnlineSpuMapper.selectList( + new LambdaQueryWrapper() + .in(CanteenOnlineSpuEntity::getId, missIds) + .isNotNull(CanteenOnlineSpuEntity::getLinkSpuId)); + if (CollUtil.isNotEmpty(onlineHits)) { + List linkSpuIds = onlineHits.stream() + .map(CanteenOnlineSpuEntity::getLinkSpuId) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + Map linkedById = new LinkedHashMap<>(); + for (CanteenSpuEntity spu : list(new LambdaQueryWrapper() + .in(CanteenSpuEntity::getId, linkSpuIds))) { + if (spu != null && spu.getId() != null) { + linkedById.putIfAbsent(spu.getId(), spu); + } + } + for (CanteenOnlineSpuEntity online : onlineHits) { + CanteenSpuEntity linked = online.getLinkSpuId() == null ? null : linkedById.get(online.getLinkSpuId()); + if (linked != null) { + result.add(buildPrintNameVo(online.getId(), linked)); + spuIdByProductId.put(online.getId(), online.getLinkSpuId()); + } + } } } + // 第三步:排菜上下文齐全时,用当日菜谱展示名覆盖套餐名,保证打印主行与排菜展示一致。 + overlayDailyMenuDisplayNames(result, spuIdByProductId, query); return result; } + /** + * 用当日菜谱行的展示名(canteen_daily_menu.display_name)覆盖打印套餐名。 + * + *

单品菜品的套餐名在保存时会被清空,商户在排菜行上配置的展示名只挂菜谱、不写回菜品, + * 因此打印查名若只看 canteen_spu 会漏掉展示名。这里按商户+用餐日期+餐段精确定位菜谱行, + * 非空展示名覆盖套餐名(含真套餐的原套餐名,以当日排菜展示为准);上下文不齐全、 + * 菜谱行缺失或展示名为空时保持原解析结果不变。

+ * + * @param result 已按商品 ID 解析出的打印名称,允许原地覆盖 + * @param spuIdByProductId 打印名商品 ID 到底层 canteen_spu ID 的映射 + * @param query 含排菜上下文的原始查询 + */ + private void overlayDailyMenuDisplayNames(List result, Map spuIdByProductId, + SpuPrintNameQueryDTO query) { + if (result.isEmpty() || query.getMerchantId() == null || query.getMealDate() == null + || query.getHourType() == null) { + return; + } + List spuIds = spuIdByProductId.values().stream().filter(Objects::nonNull).distinct() + .collect(Collectors.toList()); + if (spuIds.isEmpty()) { + return; + } + // 同一 SPU 当日同餐段正常只有一条菜谱行;万一存在重复,先出现的非空展示名优先,结果保持稳定。 + Map displayNameBySpuId = new LinkedHashMap<>(); + for (CanteenDailyMenuEntity menu : canteenDailyMenuMapper.selectList( + Wrappers.lambdaQuery() + .select(CanteenDailyMenuEntity::getSpuId, CanteenDailyMenuEntity::getDisplayName) + .eq(CanteenDailyMenuEntity::getMerchantId, query.getMerchantId()) + .eq(CanteenDailyMenuEntity::getStartTime, query.getMealDate()) + .eq(CanteenDailyMenuEntity::getHourType, query.getHourType()) + .in(CanteenDailyMenuEntity::getSpuId, spuIds))) { + if (menu == null || menu.getSpuId() == null) { + continue; + } + String displayName = StrUtil.trimToNull(menu.getDisplayName()); + if (displayName != null) { + displayNameBySpuId.putIfAbsent(menu.getSpuId(), displayName); + } + } + if (displayNameBySpuId.isEmpty()) { + return; + } + for (int index = 0; index < result.size(); index++) { + SpuPrintNameVo vo = result.get(index); + String displayName = displayNameBySpuId.get(spuIdByProductId.get(vo.getProductId())); + if (displayName != null) { + result.set(index, SpuPrintNameVo.builder() + .productId(vo.getProductId()).comboName(displayName).name(vo.getName()).build()); + } + } + } + private SpuPrintNameVo buildPrintNameVo(Long productId, CanteenSpuEntity spu) { return SpuPrintNameVo.builder() .productId(productId) diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/test/java/com/bsd/pjcloud/canteen/service/impl/CanteenSpuServiceImplPrintNameTest.java b/pjcloud-canteen/pjcloud-canteen-biz/src/test/java/com/bsd/pjcloud/canteen/service/impl/CanteenSpuServiceImplPrintNameTest.java index 2a32bbe0a..11ea3d5fc 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/test/java/com/bsd/pjcloud/canteen/service/impl/CanteenSpuServiceImplPrintNameTest.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/test/java/com/bsd/pjcloud/canteen/service/impl/CanteenSpuServiceImplPrintNameTest.java @@ -1,5 +1,9 @@ package com.bsd.pjcloud.canteen.service.impl; +import com.baomidou.mybatisplus.core.MybatisConfiguration; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import com.bsd.pjcloud.canteen.dto.SpuPrintNameQueryDTO; +import com.bsd.pjcloud.canteen.entity.CanteenDailyMenuEntity; import com.bsd.pjcloud.canteen.entity.CanteenOnlineSpuEntity; import com.bsd.pjcloud.canteen.entity.CanteenSpuEntity; import com.bsd.pjcloud.canteen.mapper.CanteenOnlineSpuMapper; @@ -7,9 +11,11 @@ import com.bsd.pjcloud.canteen.util.CanteenMerchantScopeHelper; import com.bsd.pjcloud.canteen.vo.SpuPrintNameVo; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.apache.ibatis.builder.MapperBuilderAssistant; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.test.util.ReflectionTestUtils; +import java.time.LocalDate; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -22,7 +28,8 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; /** - * 打印凭证商品名称查询测试:线下/每日菜谱商品直接命中,App 线上预约商品经 linkSpuId 关联取名。 + * 打印凭证商品名称查询测试:线下/每日菜谱商品直接命中,App 线上预约商品经 linkSpuId 关联取名; + * 带排菜上下文时当日菜谱展示名覆盖套餐名。 */ class CanteenSpuServiceImplPrintNameTest { @@ -35,12 +42,19 @@ class CanteenSpuServiceImplPrintNameTest { /** 在线点餐 Mapper,模拟 canteen_online_spu 查询。 */ private CanteenOnlineSpuMapper onlineSpuMapper; + /** 每日菜谱 Mapper,模拟 canteen_daily_menu 查询。 */ + private com.bsd.pjcloud.canteen.mapper.CanteenDailyMenuMapper dailyMenuMapper; + @BeforeEach void setUp() { + // 菜谱展示名覆盖使用 select 指定列,lambda 列名在单测环境需要预注册实体元数据。 + TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), "test"), + CanteenDailyMenuEntity.class); spuMapper = mock(com.bsd.pjcloud.canteen.mapper.CanteenSpuMapper.class); onlineSpuMapper = mock(CanteenOnlineSpuMapper.class); + dailyMenuMapper = mock(com.bsd.pjcloud.canteen.mapper.CanteenDailyMenuMapper.class); CanteenSpuServiceImpl target = new CanteenSpuServiceImpl(mock(StringRedisTemplate.class), - mock(com.bsd.pjcloud.canteen.mapper.CanteenDailyMenuMapper.class), null, null, + dailyMenuMapper, null, null, onlineSpuMapper, null, mock(CanteenMerchantScopeHelper.class), mock(CanteenDailyMenuStockSupport.class)); ReflectionTestUtils.setField(target, "baseMapper", spuMapper); @@ -99,6 +113,59 @@ class CanteenSpuServiceImplPrintNameTest { verify(spuMapper, never()).selectList(any()); } + /** 排菜上下文齐全时当日菜谱展示名覆盖套餐名:单品套餐名为空也能借展示名获得打印主行。 */ + @Test + void menuDisplayNameOverridesComboNameForDirectHit() { + when(spuMapper.selectList(any())).thenReturn(List.of(spu(501L, null, "榨菜肉丝面"))); + when(dailyMenuMapper.selectList(any())) + .thenReturn(List.of(menu(501L, "榨菜肉丝面123123"))); + + List names = service.loadSpuPrintNames(SpuPrintNameQueryDTO.builder() + .productIds(List.of(501L)).merchantId(30L).mealDate(LocalDate.of(2026, 10, 14)) + .hourType(2).build()); + + assertEquals(1, names.size()); + assertEquals(501L, names.get(0).getProductId()); + assertEquals("榨菜肉丝面123123", names.get(0).getComboName()); + assertEquals("榨菜肉丝面", names.get(0).getName()); + } + + /** 线上预约商品按 linkSpuId 反查菜谱行:展示名覆盖的是关联基础菜品的菜谱,不是原始入参 ID。 */ + @Test + void menuDisplayNameAppliesToOnlineLinkedProduct() { + when(spuMapper.selectList(any())).thenReturn(List.of()).thenReturn(List.of(spu(501L, "B套餐", "B套餐-主菜"))); + when(onlineSpuMapper.selectList(any())).thenReturn(List.of(onlineSpu(601L, 501L))); + when(dailyMenuMapper.selectList(any())).thenReturn(List.of(menu(501L, "今日B套餐"))); + + List names = service.loadSpuPrintNames(SpuPrintNameQueryDTO.builder() + .productIds(List.of(601L)).merchantId(30L).mealDate(LocalDate.of(2026, 10, 14)) + .hourType(2).build()); + + assertEquals(1, names.size()); + assertEquals(601L, names.get(0).getProductId()); + assertEquals("今日B套餐", names.get(0).getComboName()); + assertEquals("B套餐-主菜", names.get(0).getName()); + } + + /** 菜谱展示名为空白时保持 canteen_spu 原套餐名;上下文不齐全时不查询菜谱。 */ + @Test + void blankMenuDisplayNameOrMissingContextKeepsSpuComboName() { + when(spuMapper.selectList(any())).thenReturn(List.of(spu(501L, "A套餐", "A套餐-红烧肉"))); + when(dailyMenuMapper.selectList(any())).thenReturn(List.of(menu(501L, " "))); + + // 缺少餐段时无法定位菜谱行,必须完全退化为无上下文行为。 + List noHour = service.loadSpuPrintNames(SpuPrintNameQueryDTO.builder() + .productIds(List.of(501L)).merchantId(30L).mealDate(LocalDate.of(2026, 10, 14)).build()); + assertEquals("A套餐", noHour.get(0).getComboName()); + verify(dailyMenuMapper, never()).selectList(any()); + + // 菜谱展示名为空白时不得覆盖原套餐名。 + List names = service.loadSpuPrintNames(SpuPrintNameQueryDTO.builder() + .productIds(List.of(501L)).merchantId(30L).mealDate(LocalDate.of(2026, 10, 14)) + .hourType(2).build()); + assertEquals("A套餐", names.get(0).getComboName()); + } + /** 构造基础菜品实体。 */ private CanteenSpuEntity spu(Long id, String comboName, String name) { CanteenSpuEntity entity = new CanteenSpuEntity(); @@ -115,4 +182,12 @@ class CanteenSpuServiceImplPrintNameTest { entity.setLinkSpuId(linkSpuId); return entity; } + + /** 构造每日菜谱实体,仅填充展示名覆盖逻辑依赖的字段。 */ + private CanteenDailyMenuEntity menu(Long spuId, String displayName) { + CanteenDailyMenuEntity entity = new CanteenDailyMenuEntity(); + entity.setSpuId(spuId); + entity.setDisplayName(displayName); + return entity; + } } diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/HardInvoiceServiceImpl.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/HardInvoiceServiceImpl.java index bec7e8749..5284122ea 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/HardInvoiceServiceImpl.java +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/HardInvoiceServiceImpl.java @@ -17,6 +17,7 @@ import com.bsd.pjcloud.admin.api.utils.DataUtil; import com.bsd.pjcloud.admin.api.vo.UserVO; import com.bsd.pjcloud.canteen.api.feign.RemoteCanteenSpuService; import com.bsd.pjcloud.canteen.entity.CanteenSpuEntity; +import com.bsd.pjcloud.canteen.dto.SpuPrintNameQueryDTO; import com.bsd.pjcloud.canteen.vo.SpuPrintNameVo; import com.bsd.pjcloud.canteen.vo.app.OnlineSpuComboItemVO; import com.bsd.pjcloud.common.core.util.BeanUtils; @@ -76,6 +77,7 @@ import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.math.BigDecimal; import java.math.RoundingMode; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Comparator; @@ -293,7 +295,10 @@ public class HardInvoiceServiceImpl extends ServiceImpl printNames = loadPrintProductNames(carList); + // 商品名称打印时实时解析为套餐名主行加商品名小字副行;携带商户+用餐日期+餐段时 + // 套餐名优先取当日菜谱展示名,与商户排菜配置保持一致。 + Map printNames = loadPrintProductNames(carList, order.getMerchantId(), + order.getApptUseDate(), parseHourType(order.getHourType())); appendInvoiceProducts(renderer, carList, metadata, showComboItems, printNames); if (order.getExtraPrice() != null && order.getExtraPrice() != 0) { renderer.keyValue("额外支付金额", "¥" + money(order.getExtraPrice().longValue())); @@ -952,12 +957,18 @@ public class HardInvoiceServiceImpl extends ServiceImpl printNames = loadPrintProductNames(allCars); + // 按商户+用餐日期+餐段缓存查名结果:同一批核销的订单通常同餐段,避免逐单发起远程调用。 + Map> printNamesByContext = new LinkedHashMap<>(); List result = new ArrayList<>(voucherOrders.size()); for (int index = 0; index < voucherOrders.size(); index++) { HardOrderEntity voucherOrder = voucherOrders.get(index); List cars = carsPerOrder.get(index); Map metadata = loadProductMetadata(cars, showComboItems); + Integer hourType = parseHourType(voucherOrder.getHourType()); + String contextKey = voucherOrder.getMerchantId() + "|" + voucherOrder.getApptUseDate() + "|" + hourType; + Map printNames = printNamesByContext.computeIfAbsent(contextKey, + ignored -> loadPrintProductNames(allCars, voucherOrder.getMerchantId(), + voucherOrder.getApptUseDate(), hourType)); List products = new ArrayList<>(); for (HardCarVo car : cars) { int count = effectiveCount(car); @@ -1244,10 +1255,32 @@ public class HardInvoiceServiceImpl extends ServiceImpl loadPrintProductNames(List cars) { + return loadPrintProductNames(cars, null, null, null); + } + + /** + * 批量加载打印凭证商品名称,排菜上下文齐全时叠加当日菜谱展示名。 + * + *

在仅按商品 ID 解析的口径之上,携带商户、用餐日期和餐段调用带菜谱上下文的内部接口, + * canteen 侧会用当日菜谱行的展示名覆盖套餐名,使打印主行与商户排菜展示一致;上下文缺失时 + * 自动退回无上下文接口,行为与历史版本完全一致。

+ * + * @param cars 票据涉及的商品快照 + * @param merchantId 下单商户 ID;用于定位当日菜谱行 + * @param mealDate 用餐日期;用于定位当日菜谱行 + * @param hourType 餐段;用于定位当日菜谱行 + * @return 按商品 ID 索引的双名结构;查询失败时返回空 Map + */ + private Map loadPrintProductNames(List cars, Long merchantId, + LocalDate mealDate, Integer hourType) { List productIds = cars.stream().map(HardCarVo::getProductId).filter(Objects::nonNull).distinct().toList(); if (productIds.isEmpty()) return Map.of(); try { - R> result = remoteCanteenSpuService.getInnerSpuPrintNames(productIds); + boolean withMenuContext = merchantId != null && mealDate != null && hourType != null; + R> result = withMenuContext + ? remoteCanteenSpuService.getInnerSpuPrintNamesWithMenu(SpuPrintNameQueryDTO.builder() + .productIds(productIds).merchantId(merchantId).mealDate(mealDate).hourType(hourType).build()) + : remoteCanteenSpuService.getInnerSpuPrintNames(productIds); if (result == null || !result.isOk() || result.getData() == null) return Map.of(); Map names = new LinkedHashMap<>(); for (SpuPrintNameVo vo : result.getData()) { @@ -1263,6 +1296,21 @@ public class HardInvoiceServiceImpl extends ServiceImpl names = ReflectionTestUtils.invokeMethod(service, "loadPrintProductNames", + List.of(car), 30L, LocalDate.of(2026, 10, 14), 2); + + ArgumentCaptor captor = ArgumentCaptor.forClass(SpuPrintNameQueryDTO.class); + verify(remoteCanteenSpuService).getInnerSpuPrintNamesWithMenu(captor.capture()); + verify(remoteCanteenSpuService, never()).getInnerSpuPrintNames(any()); + assertEquals(List.of(501L), captor.getValue().getProductIds()); + assertEquals(30L, captor.getValue().getMerchantId()); + assertEquals(LocalDate.of(2026, 10, 14), captor.getValue().getMealDate()); + assertEquals(2, captor.getValue().getHourType()); + Object printName = names.get(501L); + assertEquals("榨菜肉丝面123123", ReflectionTestUtils.invokeMethod(printName, "mainName")); + assertEquals("榨菜肉丝面", ReflectionTestUtils.invokeMethod(printName, "subName")); + } + + /** 排菜上下文缺失(如历史订单餐段不可解析)时必须退回无上下文接口,行为与历史版本一致。 */ + @Test + void printProductNamesFallBackToPlainQueryWithoutFullContext() { + HardCarVo car = new HardCarVo(); + car.setProductId(501L); + when(remoteCanteenSpuService.getInnerSpuPrintNames(List.of(501L))).thenReturn(R.ok(List.of( + SpuPrintNameVo.builder().productId(501L).comboName("A套餐").name("A套餐-红烧肉").build()))); + + @SuppressWarnings("unchecked") + Map names = ReflectionTestUtils.invokeMethod(service, "loadPrintProductNames", + List.of(car), 30L, LocalDate.of(2026, 10, 14), null); + + verify(remoteCanteenSpuService).getInnerSpuPrintNames(List.of(501L)); + verify(remoteCanteenSpuService, never()).getInnerSpuPrintNamesWithMenu(any(SpuPrintNameQueryDTO.class)); + Object printName = names.get(501L); + assertEquals("A套餐", ReflectionTestUtils.invokeMethod(printName, "mainName")); + } + /** 未命中实时名称或历史购物车商品 ID 为空时,打印名必须回落到下单快照名并保持大字主行。 */ @Test void resolvePrintNameFallsBackToSnapshot() {