feat:打印机使用"套餐名"字段
This commit is contained in:
1 parent
1ffd466ba6
commit
54f784c16d
9 files changed
+425
-7
No files matched your search
@@ -0,0 +1,20 @@
|
||||
-- 每日菜谱菜品表补充套餐名列:发票/兑换券打印商品名优先展示套餐名(comboName),为空降级商品名(name)。
|
||||
-- 该列由 2026-09-21 代码提交(实体 CanteenSpuEntity.comboName)引入但当时未附迁移;若 DBA 已手工加列,
|
||||
-- 本脚本通过 information_schema 判断自动跳过,可反复执行。
|
||||
-- 注意:canteen_spu.name 列长度请以目标库 SHOW CREATE TABLE 为准,如不一致请先对齐再执行。
|
||||
|
||||
SET @ddl = (SELECT IF(COUNT(*) = 0,
|
||||
'ALTER TABLE `pjcloudx_canteen`.`canteen_spu` ADD COLUMN `combo_name` varchar(128) DEFAULT NULL COMMENT ''套餐名(仅spuType=2套餐使用),发票/兑换券打印商品名优先展示,为空降级商品名'' AFTER `name_en`',
|
||||
'SELECT 1')
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = 'pjcloudx_canteen' AND TABLE_NAME = 'canteen_spu' AND COLUMN_NAME = 'combo_name');
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 【可选诊断】确认列已添加。
|
||||
SELECT COLUMN_NAME, COLUMN_TYPE, COLUMN_COMMENT
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = 'pjcloudx_canteen'
|
||||
AND TABLE_NAME = 'canteen_spu'
|
||||
AND COLUMN_NAME = 'combo_name';
|
||||
+14
@@ -23,6 +23,7 @@ package com.bsd.pjcloud.canteen.api.feign;
|
||||
import com.bsd.pjcloud.canteen.dto.CanteenSpuUpdateStockReqDTO;
|
||||
import com.bsd.pjcloud.canteen.entity.CanteenOnlineSpuEntity;
|
||||
import com.bsd.pjcloud.canteen.entity.CanteenSpuEntity;
|
||||
import com.bsd.pjcloud.canteen.vo.SpuPrintNameVo;
|
||||
import com.bsd.pjcloud.canteen.vo.app.OnlineSpuComboItemVO;
|
||||
import com.bsd.pjcloud.common.core.constant.ServiceNameConstants;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
@@ -102,6 +103,19 @@ public interface RemoteCanteenSpuService {
|
||||
@NoToken
|
||||
R<List<CanteenSpuEntity>> getInnerSpuListByIds(@RequestBody List<Long> ids);
|
||||
|
||||
/**
|
||||
* 供 Hardware 打印发票/兑换券查询商品名称:套餐名优先、商品名兜底。
|
||||
*
|
||||
* <p>入参 ID 兼容 canteen_spu.id 和 canteen_online_spu.id(经 linkSpuId 关联),
|
||||
* 未命中的 ID 不出现在返回结果中,调用方自行降级购物车快照名。</p>
|
||||
*
|
||||
* @param productIds 购物车商品 ID 列表
|
||||
* @return 命中商品的打印名称列表
|
||||
*/
|
||||
@PostMapping("/canteenSpu/inner/getSpuPrintNames")
|
||||
@NoToken
|
||||
R<List<SpuPrintNameVo>> getInnerSpuPrintNames(@RequestBody List<Long> productIds);
|
||||
|
||||
/**
|
||||
* 按套餐 SPU ID 批量查询套餐组成,用于跨服务补充只读展示数据。
|
||||
*
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.bsd.pjcloud.canteen.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 打印凭证商品名称视图对象。
|
||||
*
|
||||
* <p>Hardware 打印发票/兑换券时按购物车商品 ID 实时查询,返回套餐名优先、商品名兜底的展示名称;
|
||||
* {@code productId} 回传调用方原始入参 ID,兼容直接命中 canteen_spu 和经 canteen_online_spu.linkSpuId
|
||||
* 关联命中的两种来源。</p>
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "打印凭证商品名称")
|
||||
public class SpuPrintNameVo {
|
||||
|
||||
/**
|
||||
* 调用方传入的商品 ID,可能是 canteen_spu.id 或 canteen_online_spu.id。
|
||||
*/
|
||||
@Schema(description = "商品ID,回传原始入参")
|
||||
private Long productId;
|
||||
|
||||
/**
|
||||
* 套餐名(仅 spuType=2 套餐有值),打印凭证优先展示。
|
||||
*/
|
||||
@Schema(description = "套餐名")
|
||||
private String comboName;
|
||||
|
||||
/**
|
||||
* 商品名称,套餐名为空时打印凭证降级使用。
|
||||
*/
|
||||
@Schema(description = "商品名称")
|
||||
private String name;
|
||||
}
|
||||
+16
@@ -21,6 +21,7 @@ import com.bsd.pjcloud.canteen.excel.CanteenSpuTemplateMergeHandler;
|
||||
import com.bsd.pjcloud.canteen.service.impl.CanteenSpuImportHelper;
|
||||
import com.bsd.pjcloud.canteen.util.CanteenMerchantScopeHelper;
|
||||
import com.bsd.pjcloud.canteen.vo.ADCanteenSpuListVO;
|
||||
import com.bsd.pjcloud.canteen.vo.SpuPrintNameVo;
|
||||
import com.bsd.pjcloud.canteen.vo.app.OnlineSpuComboItemVO;
|
||||
import com.bsd.pjcloud.canteen.vo.erp.CanteenSpuSimpleVO;
|
||||
import com.bsd.pjcloud.canteen.vo.erp.CanteenSpuStockVO;
|
||||
@@ -430,6 +431,21 @@ public class CanteenSpuController {
|
||||
return querySpuByIds(ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为 Hardware 打印发票/兑换券提供商品名称查询:套餐名(comboName)优先、商品名(name)兜底。
|
||||
*
|
||||
* <p>入参 ID 兼容 canteen_spu.id(线下/每日菜谱单)和 canteen_online_spu.id(App 线上预约单,
|
||||
* 经 linkSpuId 关联回基础菜品);未命中的 ID 不出现在返回结果中,调用方自行降级购物车快照名。</p>
|
||||
*
|
||||
* @param productIds 购物车商品 ID 列表
|
||||
* @return 命中商品的打印名称列表
|
||||
*/
|
||||
@Inner
|
||||
@PostMapping("/inner/getSpuPrintNames")
|
||||
public R<List<SpuPrintNameVo>> getInnerSpuPrintNames(@RequestBody List<Long> productIds) {
|
||||
return R.ok(canteenSpuService.loadSpuPrintNames(productIds));
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一执行菜品 SPU 批量查询,避免管理端和内部端点产生不同的数据筛选口径。
|
||||
*
|
||||
|
||||
+12
@@ -9,6 +9,7 @@ import com.bsd.pjcloud.canteen.entity.CanteenSpuEntity;
|
||||
import com.bsd.pjcloud.canteen.vo.erp.CanteenSpuSimpleVO;
|
||||
import com.bsd.pjcloud.canteen.vo.erp.CanteenSpuStockVO;
|
||||
import com.bsd.pjcloud.canteen.vo.ADCanteenSpuListVO;
|
||||
import com.bsd.pjcloud.canteen.vo.SpuPrintNameVo;
|
||||
import com.bsd.pjcloud.canteen.vo.app.OnlineSpuComboItemVO;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
|
||||
@@ -127,4 +128,15 @@ public interface CanteenSpuService extends IService<CanteenSpuEntity> {
|
||||
* 批量加载套餐组成(App/每日菜谱展示)
|
||||
*/
|
||||
Map<Long, List<OnlineSpuComboItemVO>> loadComboItemVos(List<Long> comboSpuIds);
|
||||
|
||||
/**
|
||||
* 按购物车商品 ID 批量加载打印凭证名称(套餐名优先、商品名兜底)。
|
||||
*
|
||||
* <p>ID 兼容两种来源:直接命中 canteen_spu(线下/每日菜谱单),或经 canteen_online_spu.linkSpuId
|
||||
* 关联命中 canteen_spu(App 线上预约单);返回的 productId 回传原始入参 ID。</p>
|
||||
*
|
||||
* @param productIds 商品 ID 列表;空列表返回空列表
|
||||
* @return 命中的打印名称列表;未命中的 ID 不出现在结果中
|
||||
*/
|
||||
List<SpuPrintNameVo> loadSpuPrintNames(List<Long> productIds);
|
||||
}
|
||||
+64
@@ -18,6 +18,7 @@ import com.bsd.pjcloud.canteen.entity.CanteenStockEntity;
|
||||
import com.bsd.pjcloud.canteen.mapper.*;
|
||||
import com.bsd.pjcloud.canteen.service.CanteenSpuService;
|
||||
import com.bsd.pjcloud.canteen.vo.app.OnlineSpuComboItemVO;
|
||||
import com.bsd.pjcloud.canteen.vo.SpuPrintNameVo;
|
||||
import com.bsd.pjcloud.canteen.vo.erp.CanteenSpuSimpleVO;
|
||||
import com.bsd.pjcloud.canteen.vo.erp.CanteenSpuStockVO;
|
||||
import com.bsd.pjcloud.canteen.vo.ADCanteenSpuListVO;
|
||||
@@ -769,6 +770,69 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SpuPrintNameVo> loadSpuPrintNames(List<Long> productIds) {
|
||||
if (CollUtil.isEmpty(productIds)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<Long> distinctIds = productIds.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList());
|
||||
if (distinctIds.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
// 第一步:直接命中 canteen_spu(线下单、每日菜谱单的商品 ID)。
|
||||
List<CanteenSpuEntity> directHits = list(new LambdaQueryWrapper<CanteenSpuEntity>()
|
||||
.in(CanteenSpuEntity::getId, distinctIds));
|
||||
Map<Long, CanteenSpuEntity> spuById = new LinkedHashMap<>();
|
||||
for (CanteenSpuEntity spu : directHits) {
|
||||
if (spu != null && spu.getId() != null) {
|
||||
spuById.putIfAbsent(spu.getId(), spu);
|
||||
}
|
||||
}
|
||||
List<SpuPrintNameVo> result = new ArrayList<>(spuById.size());
|
||||
for (Map.Entry<Long, CanteenSpuEntity> entry : spuById.entrySet()) {
|
||||
result.add(buildPrintNameVo(entry.getKey(), entry.getValue()));
|
||||
}
|
||||
// 第二步: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));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private SpuPrintNameVo buildPrintNameVo(Long productId, CanteenSpuEntity spu) {
|
||||
return SpuPrintNameVo.builder()
|
||||
.productId(productId)
|
||||
.comboName(spu.getComboName())
|
||||
.name(spu.getName())
|
||||
.build();
|
||||
}
|
||||
|
||||
private void normalizeSpuType(CanteenSpuEntity entity) {
|
||||
if (entity.getSpuType() == null || (entity.getSpuType() != 1 && entity.getSpuType() != 2)) {
|
||||
entity.setSpuType(1);
|
||||
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
package com.bsd.pjcloud.canteen.service.impl;
|
||||
|
||||
import com.bsd.pjcloud.canteen.entity.CanteenOnlineSpuEntity;
|
||||
import com.bsd.pjcloud.canteen.entity.CanteenSpuEntity;
|
||||
import com.bsd.pjcloud.canteen.mapper.CanteenOnlineSpuMapper;
|
||||
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.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* 打印凭证商品名称查询测试:线下/每日菜谱商品直接命中,App 线上预约商品经 linkSpuId 关联取名。
|
||||
*/
|
||||
class CanteenSpuServiceImplPrintNameTest {
|
||||
|
||||
/** 被测服务。 */
|
||||
private CanteenSpuServiceImpl service;
|
||||
|
||||
/** 基础菜品 Mapper,模拟 canteen_spu 查询。 */
|
||||
private com.bsd.pjcloud.canteen.mapper.CanteenSpuMapper spuMapper;
|
||||
|
||||
/** 在线点餐 Mapper,模拟 canteen_online_spu 查询。 */
|
||||
private CanteenOnlineSpuMapper onlineSpuMapper;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
spuMapper = mock(com.bsd.pjcloud.canteen.mapper.CanteenSpuMapper.class);
|
||||
onlineSpuMapper = mock(CanteenOnlineSpuMapper.class);
|
||||
CanteenSpuServiceImpl target = new CanteenSpuServiceImpl(mock(StringRedisTemplate.class),
|
||||
mock(com.bsd.pjcloud.canteen.mapper.CanteenDailyMenuMapper.class), null, null,
|
||||
onlineSpuMapper, null, mock(CanteenMerchantScopeHelper.class),
|
||||
mock(CanteenDailyMenuStockSupport.class));
|
||||
ReflectionTestUtils.setField(target, "baseMapper", spuMapper);
|
||||
service = target;
|
||||
}
|
||||
|
||||
/** 线下/每日菜谱单的商品 ID 直接命中 canteen_spu,返回套餐名与商品名原始口径。 */
|
||||
@Test
|
||||
void directSpuHitReturnsPrintName() {
|
||||
when(spuMapper.selectList(any())).thenReturn(List.of(spu(501L, "A套餐", "A套餐-红烧肉")));
|
||||
|
||||
List<SpuPrintNameVo> names = service.loadSpuPrintNames(List.of(501L));
|
||||
|
||||
assertEquals(1, names.size());
|
||||
assertEquals(501L, names.get(0).getProductId());
|
||||
assertEquals("A套餐", names.get(0).getComboName());
|
||||
assertEquals("A套餐-红烧肉", names.get(0).getName());
|
||||
verify(onlineSpuMapper, never()).selectList(any());
|
||||
}
|
||||
|
||||
/** App 线上预约单的商品 ID 是 canteen_online_spu.id,必须经 linkSpuId 关联回基础菜品取名并回传原始 ID。 */
|
||||
@Test
|
||||
void onlineSpuIdResolvesThroughLinkSpuId() {
|
||||
when(spuMapper.selectList(any())).thenReturn(List.of()).thenReturn(List.of(spu(501L, "B套餐", "B套餐-主菜")));
|
||||
when(onlineSpuMapper.selectList(any())).thenReturn(List.of(onlineSpu(601L, 501L)));
|
||||
|
||||
List<SpuPrintNameVo> names = service.loadSpuPrintNames(List.of(601L));
|
||||
|
||||
assertEquals(1, names.size());
|
||||
assertEquals(601L, names.get(0).getProductId());
|
||||
assertEquals("B套餐", names.get(0).getComboName());
|
||||
assertEquals("B套餐-主菜", names.get(0).getName());
|
||||
}
|
||||
|
||||
/** 混合入参:直接命中、经 linkSpuId 命中与完全未命中(已删除/非食堂商品)互不影响。 */
|
||||
@Test
|
||||
void mixedIdsKeepHitsAndSkipUnknown() {
|
||||
when(spuMapper.selectList(any())).thenReturn(List.of(spu(501L, "A套餐", "A套餐-红烧肉")))
|
||||
.thenReturn(List.of(spu(502L, null, "糖醋排骨")));
|
||||
when(onlineSpuMapper.selectList(any())).thenReturn(List.of(onlineSpu(601L, 502L)));
|
||||
|
||||
List<SpuPrintNameVo> names = service.loadSpuPrintNames(List.of(501L, 601L, 999L));
|
||||
|
||||
assertEquals(2, names.size());
|
||||
SpuPrintNameVo direct = names.stream().filter(vo -> vo.getProductId() == 501L).findFirst().orElseThrow();
|
||||
assertEquals("A套餐", direct.getComboName());
|
||||
SpuPrintNameVo linked = names.stream().filter(vo -> vo.getProductId() == 601L).findFirst().orElseThrow();
|
||||
assertEquals("糖醋排骨", linked.getName());
|
||||
assertNull(linked.getComboName());
|
||||
}
|
||||
|
||||
/** 空入参直接返回空列表且不访问数据库。 */
|
||||
@Test
|
||||
void emptyInputSkipsQueries() {
|
||||
assertTrue(service.loadSpuPrintNames(List.of()).isEmpty());
|
||||
verify(spuMapper, never()).selectList(any());
|
||||
}
|
||||
|
||||
/** 构造基础菜品实体。 */
|
||||
private CanteenSpuEntity spu(Long id, String comboName, String name) {
|
||||
CanteenSpuEntity entity = new CanteenSpuEntity();
|
||||
entity.setId(id);
|
||||
entity.setName(name);
|
||||
entity.setComboName(comboName);
|
||||
return entity;
|
||||
}
|
||||
|
||||
/** 构造在线点餐实体,linkSpuId 指向基础菜品。 */
|
||||
private CanteenOnlineSpuEntity onlineSpu(Long id, Long linkSpuId) {
|
||||
CanteenOnlineSpuEntity entity = new CanteenOnlineSpuEntity();
|
||||
entity.setId(id);
|
||||
entity.setLinkSpuId(linkSpuId);
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
+66
-7
@@ -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.vo.SpuPrintNameVo;
|
||||
import com.bsd.pjcloud.canteen.vo.app.OnlineSpuComboItemVO;
|
||||
import com.bsd.pjcloud.common.core.util.BeanUtils;
|
||||
import com.bsd.pjcloud.common.core.util.CollectionUtils;
|
||||
@@ -290,7 +291,8 @@ public class HardInvoiceServiceImpl extends ServiceImpl<HardInvoiceMapper, HardI
|
||||
if (StrUtil.isNotBlank(walletTypeName)) renderer.keyValue("钱包类型", walletTypeName);
|
||||
appendVoucherUser(renderer, user);
|
||||
appendInvoiceOrderBlock(renderer, order);
|
||||
appendInvoiceProducts(renderer, carList, metadata, showComboItems);
|
||||
Map<Long, String> printNames = loadPrintProductNames(carList);
|
||||
appendInvoiceProducts(renderer, carList, metadata, showComboItems, printNames);
|
||||
if (order.getExtraPrice() != null && order.getExtraPrice() != 0) {
|
||||
renderer.keyValue("额外支付金额", "¥" + money(order.getExtraPrice().longValue()));
|
||||
}
|
||||
@@ -326,15 +328,18 @@ public class HardInvoiceServiceImpl extends ServiceImpl<HardInvoiceMapper, HardI
|
||||
/**
|
||||
* 按兑换券商品卡片版式渲染发票商品明细,数量支持份数与称重重量。
|
||||
*
|
||||
* <p>发票是支付凭证,小计使用下单时的商品快照总额;套餐标签与组合内容沿用兑换券元数据和显示开关。</p>
|
||||
* <p>发票是支付凭证,小计使用下单时的商品快照总额;商品名称打印时实时按套餐名优先解析,
|
||||
* 套餐标签与组合内容沿用兑换券元数据和显示开关。</p>
|
||||
*
|
||||
* @param renderer 当前渲染器
|
||||
* @param cars 订单商品快照
|
||||
* @param metadata 按商品 ID 索引的套餐类型和组成项
|
||||
* @param showComboItems 是否展示套餐组成内容
|
||||
* @param printNames 按商品 ID 索引的实时打印名称;未命中的商品降级购物车快照名
|
||||
*/
|
||||
private static void appendInvoiceProducts(ReceiptRenderer renderer, List<HardCarVo> cars,
|
||||
Map<Long, ProductMetadata> metadata, boolean showComboItems) {
|
||||
Map<Long, ProductMetadata> metadata, boolean showComboItems,
|
||||
Map<Long, String> printNames) {
|
||||
renderer.divider().sectionTitle("商品明细");
|
||||
boolean hasProduct = false;
|
||||
for (HardCarVo car : cars) {
|
||||
@@ -344,7 +349,7 @@ public class HardInvoiceServiceImpl extends ServiceImpl<HardInvoiceMapper, HardI
|
||||
boolean combo = product != null && product.combo();
|
||||
List<String> comboItems = combo && showComboItems
|
||||
? componentItemTexts(product.items(), invoiceComboQuantity(car)) : List.of();
|
||||
renderer.voucherProduct(car.getProductName(), invoiceQuantityText(car), money(voucherUnitPrice(car)),
|
||||
renderer.voucherProduct(resolvePrintName(car, printNames), invoiceQuantityText(car), money(voucherUnitPrice(car)),
|
||||
money(car.getTotalPrice()), combo, comboItems);
|
||||
}
|
||||
if (!hasProduct) renderer.item("无商品明细", "x0", false, false);
|
||||
@@ -906,16 +911,28 @@ public class HardInvoiceServiceImpl extends ServiceImpl<HardInvoiceMapper, HardI
|
||||
/**
|
||||
* 加载批量兑换券的订单商品快照及套餐元数据,供两种票据版式复用。
|
||||
*
|
||||
* <p>商品名称打印时实时按套餐名优先解析:先集中收集全部订单的商品快照一次性查询打印名称,
|
||||
* 避免多订单兑换券逐单发起远程调用;未命中实时名称的商品降级购物车快照名。</p>
|
||||
*
|
||||
* @param voucherOrders 同一次核销的有序订单
|
||||
* @param showComboItems 是否加载并显示套餐组成内容
|
||||
* @return 与订单顺序一致的商品快照分组
|
||||
*/
|
||||
private List<VoucherOrderProducts> loadVoucherOrderProducts(List<HardOrderEntity> voucherOrders,
|
||||
boolean showComboItems) {
|
||||
List<VoucherOrderProducts> result = new ArrayList<>();
|
||||
List<List<HardCarVo>> carsPerOrder = new ArrayList<>(voucherOrders.size());
|
||||
List<HardCarVo> allCars = new ArrayList<>();
|
||||
for (HardOrderEntity voucherOrder : voucherOrders) {
|
||||
List<HardCarVo> cars = Optional.ofNullable(carService.getByOrderId(voucherOrder.getId()))
|
||||
.orElseGet(List::of);
|
||||
carsPerOrder.add(cars);
|
||||
allCars.addAll(cars);
|
||||
}
|
||||
Map<Long, String> printNames = loadPrintProductNames(allCars);
|
||||
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);
|
||||
List<VoucherProductLine> products = new ArrayList<>();
|
||||
for (HardCarVo car : cars) {
|
||||
@@ -924,7 +941,7 @@ public class HardInvoiceServiceImpl extends ServiceImpl<HardInvoiceMapper, HardI
|
||||
// 历史购物车允许商品 ID 为空,Map.of() 不接受空键查询,因此空 ID 直接按单品降级。
|
||||
ProductMetadata product = car.getProductId() == null ? null : metadata.get(car.getProductId());
|
||||
boolean combo = product != null && product.combo();
|
||||
products.add(new VoucherProductLine(car.getProductId(), car.getProductName(), count,
|
||||
products.add(new VoucherProductLine(car.getProductId(), resolvePrintName(car, printNames), count,
|
||||
voucherUnitPrice(car), voucherSubtotal(car), combo,
|
||||
combo && showComboItems ? product.items() : List.of()));
|
||||
}
|
||||
@@ -1169,6 +1186,48 @@ public class HardInvoiceServiceImpl extends ServiceImpl<HardInvoiceMapper, HardI
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量加载打印凭证商品名称:食堂商品套餐名(comboName)优先、商品名(name)兜底。
|
||||
*
|
||||
* <p>商品 ID 兼容 canteen_spu.id(线下/每日菜谱单)和 canteen_online_spu.id(App 线上预约单,
|
||||
* 服务端经 linkSpuId 关联取名);非食堂商品、已删除商品或名称全为空的记录不进入结果,
|
||||
* 调用方降级购物车快照名。打印是核销后的关键闭环,查询失败时记录警告并整体降级,不阻断出票。</p>
|
||||
*
|
||||
* @param cars 票据涉及的商品快照
|
||||
* @return 按商品 ID 索引的打印名称;查询失败时返回空 Map
|
||||
*/
|
||||
private Map<Long, String> loadPrintProductNames(List<HardCarVo> cars) {
|
||||
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);
|
||||
if (result == null || !result.isOk() || result.getData() == null) return Map.of();
|
||||
Map<Long, String> names = new LinkedHashMap<>();
|
||||
for (SpuPrintNameVo vo : result.getData()) {
|
||||
if (vo == null || vo.getProductId() == null) continue;
|
||||
String comboName = StrUtil.trimToNull(vo.getComboName());
|
||||
String printName = comboName != null ? comboName : StrUtil.trimToNull(vo.getName());
|
||||
if (printName != null) names.put(vo.getProductId(), printName);
|
||||
}
|
||||
return names;
|
||||
} catch (Exception exception) {
|
||||
log.warn("打印商品名称加载失败,按购物车快照名降级,productIds:{}", productIds, exception);
|
||||
return Map.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析单个购物车行的打印名称:命中实时名称时使用套餐名优先的结果,否则降级下单快照名。
|
||||
*
|
||||
* @param car 订单商品快照;历史数据允许商品 ID 为空
|
||||
* @param printNames 按商品 ID 索引的实时打印名称
|
||||
* @return 该商品打印在票据上的名称
|
||||
*/
|
||||
private static String resolvePrintName(HardCarVo car, Map<Long, String> printNames) {
|
||||
if (car.getProductId() == null) return car.getProductName();
|
||||
return printNames.getOrDefault(car.getProductId(), car.getProductName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验订单当前允许补建或重打兑换券。
|
||||
*/
|
||||
@@ -1402,7 +1461,7 @@ public class HardInvoiceServiceImpl extends ServiceImpl<HardInvoiceMapper, HardI
|
||||
* 可直接绘制或参与多订单聚合的兑换券商品行。
|
||||
*
|
||||
* @param productId 商品主键;历史快照允许为空
|
||||
* @param productName 下单时商品名称快照
|
||||
* @param productName 打印名称:套餐名实时优先,未命中时为下单商品名称快照
|
||||
* @param quantity 剩余可履约数量
|
||||
* @param unitPrice 下单时单价快照,单位分
|
||||
* @param subtotal 当前商品行小计,单位分
|
||||
|
||||
+75
@@ -10,6 +10,7 @@ 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.entity.CanteenSpuEntity;
|
||||
import com.bsd.pjcloud.canteen.vo.SpuPrintNameVo;
|
||||
import com.bsd.pjcloud.canteen.vo.app.OnlineSpuComboItemVO;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.hard.api.constant.HardPrintStatusEnum;
|
||||
@@ -428,6 +429,80 @@ class HardInvoiceServiceImplTest {
|
||||
assertEquals(1300L, merged.get(2).unitPrice());
|
||||
}
|
||||
|
||||
/** 打印商品名称必须套餐名优先,且两侧空白被去除后才参与展示。 */
|
||||
@Test
|
||||
void printProductNamesPreferTrimmedComboNameOverName() {
|
||||
HardCarVo car = new HardCarVo();
|
||||
car.setProductId(501L);
|
||||
when(remoteCanteenSpuService.getInnerSpuPrintNames(List.of(501L))).thenReturn(R.ok(List.of(
|
||||
SpuPrintNameVo.builder().productId(501L).comboName(" 15元套餐 ").name("15元套餐-红烧肉").build())));
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<Long, String> names = ReflectionTestUtils.invokeMethod(service, "loadPrintProductNames", List.of(car));
|
||||
|
||||
assertEquals(Map.of(501L, "15元套餐"), names);
|
||||
}
|
||||
|
||||
/** 套餐名为空串(非套餐商品保存时被清空)时必须降级商品名。 */
|
||||
@Test
|
||||
void printProductNamesFallBackToNameWhenComboBlank() {
|
||||
HardCarVo car = new HardCarVo();
|
||||
car.setProductId(502L);
|
||||
when(remoteCanteenSpuService.getInnerSpuPrintNames(List.of(502L))).thenReturn(R.ok(List.of(
|
||||
SpuPrintNameVo.builder().productId(502L).comboName("").name("糖醋排骨").build())));
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<Long, String> names = ReflectionTestUtils.invokeMethod(service, "loadPrintProductNames", List.of(car));
|
||||
|
||||
assertEquals(Map.of(502L, "糖醋排骨"), names);
|
||||
}
|
||||
|
||||
/** 商品已删除或非食堂商品时不在返回结果中,渲染层由快照名兜底。 */
|
||||
@Test
|
||||
void printProductNamesSkipMissingProductAndBlankNames() {
|
||||
HardCarVo car = new HardCarVo();
|
||||
car.setProductId(503L);
|
||||
when(remoteCanteenSpuService.getInnerSpuPrintNames(List.of(503L))).thenReturn(R.ok(List.of(
|
||||
SpuPrintNameVo.builder().productId(504L).comboName("B套餐").name("B套餐-主菜").build())));
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<Long, String> names = ReflectionTestUtils.invokeMethod(service, "loadPrintProductNames", List.of(car));
|
||||
|
||||
assertFalse(names.containsKey(503L));
|
||||
}
|
||||
|
||||
/** 打印是核销后的关键闭环:名称查询失败必须整体降级为快照名而不是阻断出票。 */
|
||||
@Test
|
||||
void printProductNamesFallBackToEmptyWhenQueryFails() {
|
||||
HardCarVo car = new HardCarVo();
|
||||
car.setProductId(501L);
|
||||
when(remoteCanteenSpuService.getInnerSpuPrintNames(List.of(501L)))
|
||||
.thenThrow(new RuntimeException("canteen service unavailable"));
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<Long, String> names = ReflectionTestUtils.invokeMethod(service, "loadPrintProductNames", List.of(car));
|
||||
|
||||
assertTrue(names.isEmpty());
|
||||
}
|
||||
|
||||
/** 未命中实时名称或历史购物车商品 ID 为空时,打印名必须回落到下单快照名。 */
|
||||
@Test
|
||||
void resolvePrintNameFallsBackToSnapshot() {
|
||||
HardCarVo named = new HardCarVo();
|
||||
named.setProductId(501L);
|
||||
named.setProductName("下单快照名");
|
||||
HardCarVo legacy = new HardCarVo();
|
||||
legacy.setProductId(null);
|
||||
legacy.setProductName("历史无ID商品");
|
||||
|
||||
assertEquals("实时套餐名", ReflectionTestUtils.invokeMethod(service, "resolvePrintName",
|
||||
named, Map.of(501L, "实时套餐名")));
|
||||
assertEquals("下单快照名", ReflectionTestUtils.invokeMethod(service, "resolvePrintName",
|
||||
named, Map.of()));
|
||||
assertEquals("历史无ID商品", ReflectionTestUtils.invokeMethod(service, "resolvePrintName",
|
||||
legacy, Map.of()));
|
||||
}
|
||||
|
||||
private static HardOrderEntity order() {
|
||||
return HardOrderEntity.builder().id(1L).hardId(10L).merchantId(30L).deptId(40L).tenantId(50L).build();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user