feat:别名打印问题
This commit is contained in:
1 parent
0ff9fb9342
commit
66be2c3ad2
9 files changed
+359
-34
No files matched your search
+15
@@ -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<List<SpuPrintNameVo>> getInnerSpuPrintNames(@RequestBody List<Long> productIds);
|
||||
|
||||
/**
|
||||
* 供 Hardware 打印发票/兑换券查询商品名称,带排菜上下文时当日菜谱展示名优先作为套餐名。
|
||||
*
|
||||
* <p>在 {@link #getInnerSpuPrintNames(List)} 的解析口径之上,按商户+用餐日期+餐段匹配
|
||||
* canteen_daily_menu 行,非空 display_name 覆盖套餐名,保证打印主行与排菜展示一致;
|
||||
* 上下文不齐全时返回结果与原接口完全一致。</p>
|
||||
*
|
||||
* @param query 商品 ID 列表与可选排菜上下文
|
||||
* @return 命中商品的打印名称列表
|
||||
*/
|
||||
@PostMapping("/canteenSpu/inner/getSpuPrintNamesWithMenu")
|
||||
@NoToken
|
||||
R<List<SpuPrintNameVo>> getInnerSpuPrintNamesWithMenu(@RequestBody SpuPrintNameQueryDTO query);
|
||||
|
||||
/**
|
||||
* 按套餐 SPU ID 批量查询套餐组成,用于跨服务补充只读展示数据。
|
||||
*
|
||||
|
||||
+49
@@ -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 加可选的排菜上下文。
|
||||
*
|
||||
* <p>排菜上下文(商户 + 用餐日期 + 餐段)齐全时,先按既有口径解析 canteen_spu 套餐名/商品名,
|
||||
* 再用当日菜谱行的展示名(canteen_daily_menu.display_name)覆盖套餐名,使打印主行与排菜展示一致;
|
||||
* 上下文缺失时行为与仅传商品 ID 完全相同。</p>
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "打印凭证商品名称查询请求")
|
||||
public class SpuPrintNameQueryDTO {
|
||||
|
||||
/**
|
||||
* 购物车商品 ID 列表,兼容 canteen_spu.id 和 canteen_online_spu.id。
|
||||
*/
|
||||
@Schema(description = "商品ID列表")
|
||||
private List<Long> 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;
|
||||
}
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+17
@@ -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 打印发票/兑换券提供带排菜上下文的商品名称查询。
|
||||
*
|
||||
* <p>在 {@link #getInnerSpuPrintNames(List)} 的解析口径之上,按商户+用餐日期+餐段匹配
|
||||
* canteen_daily_menu 行,非空展示名(display_name)覆盖套餐名,使打印主行与当日排菜展示一致;
|
||||
* 上下文不齐全时返回结果与原接口完全一致。</p>
|
||||
*
|
||||
* @param query 商品 ID 列表与可选排菜上下文
|
||||
* @return 命中商品的打印名称列表
|
||||
*/
|
||||
@Inner
|
||||
@PostMapping("/inner/getSpuPrintNamesWithMenu")
|
||||
public R<List<SpuPrintNameVo>> getInnerSpuPrintNamesWithMenu(@RequestBody SpuPrintNameQueryDTO query) {
|
||||
return R.ok(canteenSpuService.loadSpuPrintNames(query));
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一执行菜品 SPU 批量查询,避免管理端和内部端点产生不同的数据筛选口径。
|
||||
*
|
||||
|
||||
+13
@@ -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<CanteenSpuEntity> {
|
||||
* @return 命中的打印名称列表;未命中的 ID 不出现在结果中
|
||||
*/
|
||||
List<SpuPrintNameVo> loadSpuPrintNames(List<Long> productIds);
|
||||
|
||||
/**
|
||||
* 按购物车商品 ID 批量加载打印凭证名称,排菜上下文齐全时叠加当日菜谱展示名。
|
||||
*
|
||||
* <p>解析口径与 {@link #loadSpuPrintNames(List)} 一致,另按商户+用餐日期+餐段匹配
|
||||
* canteen_daily_menu 行,非空 display_name 覆盖套餐名,使打印主行与排菜展示一致;
|
||||
* 上下文不齐全时退化为无上下文行为,不额外查询菜谱。</p>
|
||||
*
|
||||
* @param query 商品 ID 列表与可选排菜上下文
|
||||
* @return 命中的打印名称列表;未命中的 ID 不出现在结果中
|
||||
*/
|
||||
List<SpuPrintNameVo> loadSpuPrintNames(SpuPrintNameQueryDTO query);
|
||||
}
|
||||
+91
-28
@@ -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<CanteenSpuMapper, Canteen
|
||||
|
||||
@Override
|
||||
public List<SpuPrintNameVo> loadSpuPrintNames(List<Long> productIds) {
|
||||
if (CollUtil.isEmpty(productIds)) {
|
||||
return loadSpuPrintNames(SpuPrintNameQueryDTO.builder().productIds(productIds).build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SpuPrintNameVo> loadSpuPrintNames(SpuPrintNameQueryDTO query) {
|
||||
if (query == null || CollUtil.isEmpty(query.getProductIds())) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<Long> distinctIds = productIds.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList());
|
||||
List<Long> 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<CanteenSpuMapper, Canteen
|
||||
}
|
||||
}
|
||||
List<SpuPrintNameVo> result = new ArrayList<>(spuById.size());
|
||||
// 打印名回传调用方原始商品 ID;菜谱展示名按底层 SPU ID 匹配,这里同步记录两套 ID 的映射。
|
||||
Map<Long, Long> spuIdByProductId = new LinkedHashMap<>();
|
||||
for (Map.Entry<Long, CanteenSpuEntity> entry : spuById.entrySet()) {
|
||||
result.add(buildPrintNameVo(entry.getKey(), entry.getValue()));
|
||||
spuIdByProductId.put(entry.getKey(), entry.getKey());
|
||||
}
|
||||
// 第二步:App 线上预约单的商品 ID 是 canteen_online_spu.id,经 linkSpuId 关联回基础菜品取名。
|
||||
List<Long> missIds = distinctIds.stream().filter(id -> !spuById.containsKey(id)).collect(Collectors.toList());
|
||||
if (missIds.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
List<CanteenOnlineSpuEntity> onlineHits = canteenOnlineSpuMapper.selectList(
|
||||
new LambdaQueryWrapper<CanteenOnlineSpuEntity>()
|
||||
.in(CanteenOnlineSpuEntity::getId, missIds)
|
||||
.isNotNull(CanteenOnlineSpuEntity::getLinkSpuId));
|
||||
if (CollUtil.isEmpty(onlineHits)) {
|
||||
return result;
|
||||
}
|
||||
List<Long> linkSpuIds = onlineHits.stream()
|
||||
.map(CanteenOnlineSpuEntity::getLinkSpuId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
Map<Long, CanteenSpuEntity> linkedById = new LinkedHashMap<>();
|
||||
for (CanteenSpuEntity spu : list(new LambdaQueryWrapper<CanteenSpuEntity>()
|
||||
.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<CanteenOnlineSpuEntity> onlineHits = canteenOnlineSpuMapper.selectList(
|
||||
new LambdaQueryWrapper<CanteenOnlineSpuEntity>()
|
||||
.in(CanteenOnlineSpuEntity::getId, missIds)
|
||||
.isNotNull(CanteenOnlineSpuEntity::getLinkSpuId));
|
||||
if (CollUtil.isNotEmpty(onlineHits)) {
|
||||
List<Long> linkSpuIds = onlineHits.stream()
|
||||
.map(CanteenOnlineSpuEntity::getLinkSpuId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
Map<Long, CanteenSpuEntity> linkedById = new LinkedHashMap<>();
|
||||
for (CanteenSpuEntity spu : list(new LambdaQueryWrapper<CanteenSpuEntity>()
|
||||
.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)覆盖打印套餐名。
|
||||
*
|
||||
* <p>单品菜品的套餐名在保存时会被清空,商户在排菜行上配置的展示名只挂菜谱、不写回菜品,
|
||||
* 因此打印查名若只看 canteen_spu 会漏掉展示名。这里按商户+用餐日期+餐段精确定位菜谱行,
|
||||
* 非空展示名覆盖套餐名(含真套餐的原套餐名,以当日排菜展示为准);上下文不齐全、
|
||||
* 菜谱行缺失或展示名为空时保持原解析结果不变。</p>
|
||||
*
|
||||
* @param result 已按商品 ID 解析出的打印名称,允许原地覆盖
|
||||
* @param spuIdByProductId 打印名商品 ID 到底层 canteen_spu ID 的映射
|
||||
* @param query 含排菜上下文的原始查询
|
||||
*/
|
||||
private void overlayDailyMenuDisplayNames(List<SpuPrintNameVo> result, Map<Long, Long> spuIdByProductId,
|
||||
SpuPrintNameQueryDTO query) {
|
||||
if (result.isEmpty() || query.getMerchantId() == null || query.getMealDate() == null
|
||||
|| query.getHourType() == null) {
|
||||
return;
|
||||
}
|
||||
List<Long> spuIds = spuIdByProductId.values().stream().filter(Objects::nonNull).distinct()
|
||||
.collect(Collectors.toList());
|
||||
if (spuIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
// 同一 SPU 当日同餐段正常只有一条菜谱行;万一存在重复,先出现的非空展示名优先,结果保持稳定。
|
||||
Map<Long, String> displayNameBySpuId = new LinkedHashMap<>();
|
||||
for (CanteenDailyMenuEntity menu : canteenDailyMenuMapper.selectList(
|
||||
Wrappers.<CanteenDailyMenuEntity>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)
|
||||
|
||||
+77
-2
@@ -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<SpuPrintNameVo> 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<SpuPrintNameVo> 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<SpuPrintNameVo> 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<SpuPrintNameVo> 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;
|
||||
}
|
||||
}
|
||||
+51
-3
@@ -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<HardInvoiceMapper, HardI
|
||||
if (StrUtil.isNotBlank(walletTypeName)) renderer.keyValue("钱包类型", walletTypeName);
|
||||
appendVoucherUser(renderer, user);
|
||||
appendInvoiceOrderBlock(renderer, order);
|
||||
Map<Long, PrintProductName> printNames = loadPrintProductNames(carList);
|
||||
// 商品名称打印时实时解析为套餐名主行加商品名小字副行;携带商户+用餐日期+餐段时
|
||||
// 套餐名优先取当日菜谱展示名,与商户排菜配置保持一致。
|
||||
Map<Long, PrintProductName> 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<HardInvoiceMapper, HardI
|
||||
carsPerOrder.add(cars);
|
||||
allCars.addAll(cars);
|
||||
}
|
||||
Map<Long, PrintProductName> printNames = loadPrintProductNames(allCars);
|
||||
// 按商户+用餐日期+餐段缓存查名结果:同一批核销的订单通常同餐段,避免逐单发起远程调用。
|
||||
Map<String, Map<Long, PrintProductName>> printNamesByContext = new LinkedHashMap<>();
|
||||
List<VoucherOrderProducts> result = new ArrayList<>(voucherOrders.size());
|
||||
for (int index = 0; index < voucherOrders.size(); index++) {
|
||||
HardOrderEntity voucherOrder = voucherOrders.get(index);
|
||||
List<HardCarVo> cars = carsPerOrder.get(index);
|
||||
Map<Long, ProductMetadata> metadata = loadProductMetadata(cars, showComboItems);
|
||||
Integer hourType = parseHourType(voucherOrder.getHourType());
|
||||
String contextKey = voucherOrder.getMerchantId() + "|" + voucherOrder.getApptUseDate() + "|" + hourType;
|
||||
Map<Long, PrintProductName> printNames = printNamesByContext.computeIfAbsent(contextKey,
|
||||
ignored -> loadPrintProductNames(allCars, voucherOrder.getMerchantId(),
|
||||
voucherOrder.getApptUseDate(), hourType));
|
||||
List<VoucherProductLine> products = new ArrayList<>();
|
||||
for (HardCarVo car : cars) {
|
||||
int count = effectiveCount(car);
|
||||
@@ -1244,10 +1255,32 @@ public class HardInvoiceServiceImpl extends ServiceImpl<HardInvoiceMapper, HardI
|
||||
* @return 按商品 ID 索引的双名结构;查询失败时返回空 Map
|
||||
*/
|
||||
private Map<Long, PrintProductName> loadPrintProductNames(List<HardCarVo> cars) {
|
||||
return loadPrintProductNames(cars, null, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量加载打印凭证商品名称,排菜上下文齐全时叠加当日菜谱展示名。
|
||||
*
|
||||
* <p>在仅按商品 ID 解析的口径之上,携带商户、用餐日期和餐段调用带菜谱上下文的内部接口,
|
||||
* canteen 侧会用当日菜谱行的展示名覆盖套餐名,使打印主行与商户排菜展示一致;上下文缺失时
|
||||
* 自动退回无上下文接口,行为与历史版本完全一致。</p>
|
||||
*
|
||||
* @param cars 票据涉及的商品快照
|
||||
* @param merchantId 下单商户 ID;用于定位当日菜谱行
|
||||
* @param mealDate 用餐日期;用于定位当日菜谱行
|
||||
* @param hourType 餐段;用于定位当日菜谱行
|
||||
* @return 按商品 ID 索引的双名结构;查询失败时返回空 Map
|
||||
*/
|
||||
private Map<Long, PrintProductName> loadPrintProductNames(List<HardCarVo> cars, Long merchantId,
|
||||
LocalDate mealDate, Integer hourType) {
|
||||
List<Long> productIds = cars.stream().map(HardCarVo::getProductId).filter(Objects::nonNull).distinct().toList();
|
||||
if (productIds.isEmpty()) return Map.of();
|
||||
try {
|
||||
R<List<SpuPrintNameVo>> result = remoteCanteenSpuService.getInnerSpuPrintNames(productIds);
|
||||
boolean withMenuContext = merchantId != null && mealDate != null && hourType != null;
|
||||
R<List<SpuPrintNameVo>> 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<Long, PrintProductName> names = new LinkedHashMap<>();
|
||||
for (SpuPrintNameVo vo : result.getData()) {
|
||||
@@ -1263,6 +1296,21 @@ public class HardInvoiceServiceImpl extends ServiceImpl<HardInvoiceMapper, HardI
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析订单上的餐段文本为餐段编号;历史订单可能存非数字文本,无法解析时返回 null 放弃菜谱匹配。
|
||||
*
|
||||
* @param hourType 订单餐段文本
|
||||
* @return 餐段编号;空或非数字时返回 null
|
||||
*/
|
||||
private static Integer parseHourType(String hourType) {
|
||||
if (StrUtil.isBlank(hourType)) return null;
|
||||
try {
|
||||
return Integer.valueOf(hourType.trim());
|
||||
} catch (NumberFormatException exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析单个购物车行的打印名称:命中实时名称时保留套餐名/商品名双字段,否则降级下单快照名。
|
||||
*
|
||||
|
||||
+45
@@ -9,6 +9,7 @@ import com.bsd.pjcloud.admin.api.dto.user.SysUserTeacherDto;
|
||||
import com.bsd.pjcloud.admin.api.feign.RemoteParamService;
|
||||
import com.bsd.pjcloud.admin.api.vo.UserVO;
|
||||
import com.bsd.pjcloud.canteen.api.feign.RemoteCanteenSpuService;
|
||||
import com.bsd.pjcloud.canteen.dto.SpuPrintNameQueryDTO;
|
||||
import com.bsd.pjcloud.canteen.entity.CanteenSpuEntity;
|
||||
import com.bsd.pjcloud.canteen.vo.SpuPrintNameVo;
|
||||
import com.bsd.pjcloud.canteen.vo.app.OnlineSpuComboItemVO;
|
||||
@@ -37,6 +38,7 @@ import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -538,6 +540,49 @@ class HardInvoiceServiceImplTest {
|
||||
assertTrue(names.isEmpty());
|
||||
}
|
||||
|
||||
/** 排菜上下文齐全时必须携带商户+用餐日期+餐段调用带菜谱的接口,菜谱展示名进入套餐名主行。 */
|
||||
@Test
|
||||
void printProductNamesUseMenuContextWhenAvailable() {
|
||||
HardCarVo car = new HardCarVo();
|
||||
car.setProductId(501L);
|
||||
when(remoteCanteenSpuService.getInnerSpuPrintNamesWithMenu(any(SpuPrintNameQueryDTO.class)))
|
||||
.thenReturn(R.ok(List.of(SpuPrintNameVo.builder().productId(501L)
|
||||
.comboName("榨菜肉丝面123123").name("榨菜肉丝面").build())));
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<Long, Object> names = ReflectionTestUtils.invokeMethod(service, "loadPrintProductNames",
|
||||
List.of(car), 30L, LocalDate.of(2026, 10, 14), 2);
|
||||
|
||||
ArgumentCaptor<SpuPrintNameQueryDTO> 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<Long, Object> 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() {
|
||||
|
||||
Reference in new issue
Block a user