From 496c93167892599e09d115bc08c78b86404c5c55 Mon Sep 17 00:00:00 2001 From: gong <1157756119@qq.com> Date: Fri, 30 Jan 2026 14:13:02 +0800 Subject: [PATCH] =?UTF-8?q?=E5=95=86=E5=93=81=E5=AF=BC=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/czg/product/dto/ProductExportDTO.java | 107 ++--------- .../product/dto/ProductPackageExportDTO.java | 101 ++++++++++ .../java/com/czg/excel/ExcelExportUtil.java | 63 ++++--- .../main/java/com/czg/excel/SheetData.java | 21 +++ .../service/impl/ProductServiceImpl.java | 174 ++++++++++++++---- 5 files changed, 318 insertions(+), 148 deletions(-) create mode 100644 cash-common/cash-common-service/src/main/java/com/czg/product/dto/ProductPackageExportDTO.java create mode 100644 cash-common/cash-common-tools/src/main/java/com/czg/excel/SheetData.java diff --git a/cash-common/cash-common-service/src/main/java/com/czg/product/dto/ProductExportDTO.java b/cash-common/cash-common-service/src/main/java/com/czg/product/dto/ProductExportDTO.java index 94563c686..a86269586 100644 --- a/cash-common/cash-common-service/src/main/java/com/czg/product/dto/ProductExportDTO.java +++ b/cash-common/cash-common-service/src/main/java/com/czg/product/dto/ProductExportDTO.java @@ -2,12 +2,11 @@ package com.czg.product.dto; import com.alibaba.excel.annotation.ExcelIgnore; import com.alibaba.excel.annotation.ExcelProperty; +import com.alibaba.excel.annotation.write.style.ColumnWidth; import lombok.Data; import java.math.BigDecimal; import java.time.LocalTime; -import java.util.ArrayList; -import java.util.List; /** * 商品导出 @@ -17,112 +16,61 @@ import java.util.List; @Data public class ProductExportDTO { - @Data - public static class ProductSkuExportDTO { - - @ExcelProperty("条形码") - private String barCode; - - @ExcelProperty("原价") - private BigDecimal originPrice; - - @ExcelProperty("成本价") - private BigDecimal costPrice; - - @ExcelProperty("会员价") - private BigDecimal memberPrice; - - @ExcelProperty("售价") - private BigDecimal salePrice; - - @ExcelProperty("起售数量") - private Integer suitNum; - - @ExcelProperty("规格详情") - private String specInfo; - - @ExcelProperty("是否上架") - private Integer isGrounding; - - - @ExcelProperty("规格名称") - private String name; - } - - @Data - public static class ProductGroupExportDTO { - @ExcelProperty("套餐内商品总数") - private Integer count; - - @ExcelProperty("套餐选几") - private Integer number; - - @ExcelProperty("套餐名称") - private String title; - - @ExcelProperty("套餐内商品列表") - private List goods = new ArrayList<>(); - - @Data - public static class FoodExportDTO { - - @ExcelProperty("商品名称") - private String proName; - - @ExcelProperty("规格名称") - private String skuName; - } - } - @ExcelProperty("商品名称") + @ColumnWidth(20) private String name; - @ExcelProperty("商品分类名称") + @ExcelProperty("商品分类") + @ColumnWidth(15) private String categoryName; @ExcelProperty("条码") + @ColumnWidth(20) private String barCode; - @ExcelProperty("商品规格名称") + @ExcelProperty("商品规格") + @ColumnWidth(20) private String specFullName; @ExcelProperty("售价") + @ColumnWidth(10) private BigDecimal price; @ExcelProperty("会员价") + @ColumnWidth(10) private BigDecimal memberPrice; + @ExcelProperty("成本价") + @ColumnWidth(10) + private BigDecimal costPrice; - @ExcelProperty("商品单位名称") + @ExcelProperty("商品单位") + @ColumnWidth(10) private String unitName; /** * 商品类型 single-单规格商品 sku-多规格商品 package-套餐商品 weight-称重商品 coupon-团购券 */ @ExcelProperty("商品类型") + @ColumnWidth(15) private String type; - /** - * 套餐类型 0 固定套餐 1可选套餐 - */ - @ExcelIgnore - private Integer groupType; - @ExcelProperty("套餐类型") - private String groupTypeRemark; - /** * 可用开始时间 */ @ExcelProperty("可用开始时间") + @ColumnWidth(16) private LocalTime startTime; /** * 可用结束时间 */ @ExcelProperty("可用结束时间") + @ColumnWidth(16) private LocalTime endTime; /** * 商品级库存数量 */ @ExcelProperty("库存数量") + @ColumnWidth(10) private Integer stockNumber; /** @@ -131,16 +79,10 @@ public class ProductExportDTO { @ExcelIgnore private Integer isSale; @ExcelProperty("是否上架") + @ColumnWidth(10) private String isSaleRemark; - @ExcelIgnore - private List skuList; - - @ExcelIgnore - private List proGroupVo; - - public String getType() { return switch (type) { case "single" -> "单规格商品"; @@ -152,17 +94,6 @@ public class ProductExportDTO { }; } - public String getGroupTypeRemark() { - if (!"package".equals(type)) { - return ""; - } - return switch (groupType) { - case 0 -> "固定套餐"; - case 1 -> "可选套餐"; - case null, default -> "未知类型"; - }; - } - public String getIsSaleRemark() { return switch (isSale) { case 0 -> "下架"; diff --git a/cash-common/cash-common-service/src/main/java/com/czg/product/dto/ProductPackageExportDTO.java b/cash-common/cash-common-service/src/main/java/com/czg/product/dto/ProductPackageExportDTO.java new file mode 100644 index 000000000..9036e6c61 --- /dev/null +++ b/cash-common/cash-common-service/src/main/java/com/czg/product/dto/ProductPackageExportDTO.java @@ -0,0 +1,101 @@ +package com.czg.product.dto; + +import com.alibaba.excel.annotation.ExcelIgnore; +import com.alibaba.excel.annotation.ExcelProperty; +import com.alibaba.excel.annotation.write.style.ColumnWidth; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; + +/** + * 套餐商品导出 + * @author yjjie + * @date 2026/1/30 10:26 + */ +@Data +@Accessors(chain = true) +public class ProductPackageExportDTO { + + @ExcelProperty("套餐名称") + @ColumnWidth(20) + private String name; + + @ExcelProperty("套餐分类") + @ColumnWidth(15) + private String categoryName; + + @ExcelProperty("售价") + @ColumnWidth(10) + private BigDecimal price; + @ExcelProperty("会员价") + @ColumnWidth(10) + private BigDecimal memberPrice; + + /** + * 商品类型 single-单规格商品 sku-多规格商品 package-套餐商品 weight-称重商品 coupon-团购券 + */ + @ExcelIgnore() + private String type; + + /** + * 套餐类型 0 固定套餐 1可选套餐 + */ + @ExcelIgnore + private Integer groupType; + @ExcelProperty("套餐类型") + @ColumnWidth(15) + private String groupTypeRemark; + + @ExcelProperty("组名称") + @ColumnWidth(15) + private String groupTitleName; + + @ExcelProperty("商品名称") + @ColumnWidth(21) + private String groupProductName; + + @ExcelProperty("商品单位") + @ColumnWidth(10) + private String unitName; + + @ExcelProperty("套餐内选择数量") + @ColumnWidth(10) + private String groupProductNumber; + + /** + * 商品级库存数量 + */ + @ExcelProperty("库存数量") + @ColumnWidth(10) + private Integer stockNumber; + + /** + * 是否上架 + */ + @ExcelIgnore + private Integer isSale; + @ExcelProperty("是否上架") + @ColumnWidth(10) + private String isSaleRemark; + + public String getGroupTypeRemark() { + if (!"package".equals(type)) { + return ""; + } + return switch (groupType) { + case 0 -> "固定套餐"; + case 1 -> "可选套餐"; + case null, default -> "未知类型"; + }; + } + + public String getIsSaleRemark() { + return switch (isSale) { + case 0 -> "下架"; + case 1 -> "上架"; + case null, default -> "未知状态"; + }; + } + +} diff --git a/cash-common/cash-common-tools/src/main/java/com/czg/excel/ExcelExportUtil.java b/cash-common/cash-common-tools/src/main/java/com/czg/excel/ExcelExportUtil.java index b5030ddd4..fffa512d7 100644 --- a/cash-common/cash-common-tools/src/main/java/com/czg/excel/ExcelExportUtil.java +++ b/cash-common/cash-common-tools/src/main/java/com/czg/excel/ExcelExportUtil.java @@ -1,9 +1,12 @@ package com.czg.excel; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; import com.alibaba.excel.EasyExcel; import com.alibaba.excel.ExcelWriter; import com.alibaba.excel.annotation.ExcelProperty; import com.alibaba.excel.write.builder.ExcelWriterBuilder; +import com.alibaba.excel.write.builder.ExcelWriterSheetBuilder; import com.alibaba.excel.write.handler.SheetWriteHandler; import com.alibaba.excel.write.metadata.WriteSheet; import com.alibaba.excel.write.metadata.style.WriteCellStyle; @@ -248,54 +251,66 @@ public class ExcelExportUtil { } /** - * 带合并单元格的商品导出到Response + * 带合并单元格的导出到Response + * 多sheet导出到response * - * @param data 数据列表 + * @param sheetDataList 数据列表 * @param fileName 文件名 * @param response HttpServletResponse - * @param 数据类型 */ - public static void exportProductWithMergeToResponse(List data, Class clazz, - String fileName, - HttpServletResponse response, List handlers) { - if (data == null) { - data = Collections.emptyList(); + public static void exportMultipleSheetsToResponse(List sheetDataList, + String fileName, + HttpServletResponse response) { + if (CollectionUtil.isEmpty(sheetDataList)) { + throw new CzgException("数据列表不能为空"); } setResponseHeader(response, fileName, DEFAULT_CONFIG); try (OutputStream outputStream = response.getOutputStream()) { - // 创建样式策略 - 设置表头和内容都居中 + // 创建样式策略 WriteCellStyle headStyle = new WriteCellStyle(); - headStyle.setHorizontalAlignment(HorizontalAlignment.CENTER); + headStyle.setHorizontalAlignment(HorizontalAlignment.LEFT); + headStyle.setVerticalAlignment(VerticalAlignment.CENTER); WriteCellStyle contentStyle = new WriteCellStyle(); - contentStyle.setHorizontalAlignment(HorizontalAlignment.CENTER); + contentStyle.setHorizontalAlignment(HorizontalAlignment.LEFT); + contentStyle.setVerticalAlignment(VerticalAlignment.CENTER); HorizontalCellStyleStrategy styleStrategy = new HorizontalCellStyleStrategy(headStyle, contentStyle); - // 创建写入器 - 必须指定clazz - ExcelWriterBuilder builder = EasyExcel.write(outputStream, clazz) + // 创建ExcelWriter + ExcelWriterBuilder builder = EasyExcel.write(outputStream) .autoCloseStream(true) .registerConverter(new LocalTimeConverter()) .registerWriteHandler(styleStrategy); - if (handlers != null && !handlers.isEmpty()) { - for (SheetWriteHandler h : handlers) { - builder.registerWriteHandler(h); - } - } - ExcelWriter excelWriter = builder.build(); - WriteSheet writeSheet = EasyExcel.writerSheet(DEFAULT_CONFIG.getDefaultSheetName()).build(); + // 逐个写入sheet + for (int i = 0; i < sheetDataList.size(); i++) { + SheetData sheetData = sheetDataList.get(i); + String sheetName = StrUtil.isNotBlank(sheetData.getSheetName()) + ? sheetData.getSheetName() + : DEFAULT_CONFIG.getDefaultSheetName() + (i + 1); + + ExcelWriterSheetBuilder sheetBuilder = EasyExcel.writerSheet(sheetName); + + // 注册该sheet的合并处理器 + if (sheetData.getHandlers() != null && !sheetData.getHandlers().isEmpty()) { + for (SheetWriteHandler handler : sheetData.getHandlers()) { + sheetBuilder.registerWriteHandler(handler); + } + } + + WriteSheet writeSheet = sheetBuilder.head(sheetData.getClazz()).build(); + excelWriter.write(sheetData.getData(), writeSheet); + } - excelWriter.write(data, writeSheet); excelWriter.finish(); - - log.info("带合并单元格的商品Excel导出成功,文件名:{},数据量:{}", fileName, data.size()); + log.info("多sheet商品Excel导出成功,文件名:{},共{}个sheet", fileName, sheetDataList.size()); } catch (IOException e) { - log.error("带合并单元格的商品Excel导出失败", e); + log.error("多sheet商品Excel导出失败", e); throw new CzgException("Excel导出失败", e); } } diff --git a/cash-common/cash-common-tools/src/main/java/com/czg/excel/SheetData.java b/cash-common/cash-common-tools/src/main/java/com/czg/excel/SheetData.java new file mode 100644 index 000000000..806bbc7c9 --- /dev/null +++ b/cash-common/cash-common-tools/src/main/java/com/czg/excel/SheetData.java @@ -0,0 +1,21 @@ +package com.czg.excel; + +import com.alibaba.excel.write.handler.SheetWriteHandler; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.util.List; + +/** + * 多sheet导出数据封装类 + * @author yjjie + * @date 2026/1/30 10:53 + */ +@Data +@Accessors(chain = true) +public class SheetData { + private List data; + private Class clazz; + private String sheetName; + private List handlers; +} diff --git a/cash-service/product-service/src/main/java/com/czg/service/product/service/impl/ProductServiceImpl.java b/cash-service/product-service/src/main/java/com/czg/service/product/service/impl/ProductServiceImpl.java index fe58280ae..d80c65797 100644 --- a/cash-service/product-service/src/main/java/com/czg/service/product/service/impl/ProductServiceImpl.java +++ b/cash-service/product-service/src/main/java/com/czg/service/product/service/impl/ProductServiceImpl.java @@ -13,12 +13,14 @@ import com.alibaba.fastjson2.JSONWriter; import com.czg.constant.CacheConstant; import com.czg.constants.SystemConstants; import com.czg.excel.ExcelExportUtil; +import com.czg.excel.SheetData; import com.czg.exception.CzgException; import com.czg.product.dto.*; import com.czg.product.entity.*; import com.czg.product.enums.*; import com.czg.product.param.*; import com.czg.product.service.*; +import com.czg.product.vo.ProductGroupVo; import com.czg.product.vo.ProductStatisticsVo; import com.czg.sa.StpKit; import com.czg.service.RedisService; @@ -47,6 +49,7 @@ import java.time.LocalDate; import java.time.LocalTime; import java.time.format.TextStyle; import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; import static com.czg.constant.CacheConstant.ADMIN_CLIENT_PRODUCT_LIST; @@ -141,6 +144,7 @@ public class ProductServiceImpl extends ServiceImpl impl private void buildProductExtInfo(List records) { records.forEach(record -> { record.setIsSaleTime(calcIsSaleTime(record.getDays(), record.getStartTime(), record.getEndTime())); + record.setProGroupVo(JSONArray.parseArray(record.getGroupSnap().toString(), ProductGroupVo.class)); List skuList = prodSkuMapper.selectListByQueryAs(query().eq(ProdSku::getProductId, record.getId()).eq(ProdSku::getIsDel, SystemConstants.OneZero.ZERO), ProdSkuDTO.class); if (CollUtil.isNotEmpty(skuList)) { Optional lowPriceIsPresent = skuList.stream().map(obj -> NumberUtil.nullToZero(obj.getSalePrice())).min(BigDecimal::compareTo); @@ -180,53 +184,151 @@ public class ProductServiceImpl extends ServiceImpl impl @Override public void exportProductList(ProductDTO param, HttpServletResponse response) { + // 1. 查询并构建完整数据 QueryWrapper queryWrapper = buildFullQueryWrapper(param); List records = super.listAs(queryWrapper, ProductDTO.class); buildProductExtInfo(records); + // 2. 分别处理普通商品和套餐商品 + SheetData normalSheet = buildNormalProductSheet(records); + SheetData packageSheet = buildPackageProductSheet(records); + + // 3. 导出 + List dataList = List.of(normalSheet, packageSheet); + ExcelExportUtil.exportMultipleSheetsToResponse(dataList, "商品列表", response); + } + + // ----------------------------- + // 普通商品处理 + // ----------------------------- + private SheetData buildNormalProductSheet(List records) { List handlers = new ArrayList<>(); - List dataList = new ArrayList<>(); - records.forEach(exportDTO -> { - int first = dataList.size() + 1; - if (exportDTO.getSkuList() != null && !exportDTO.getSkuList().isEmpty()) { - exportDTO.getSkuList().forEach(sku -> { - ProductExportDTO dto = new ProductExportDTO(); - BeanUtil.copyProperties(exportDTO, dto); - dto.setSpecFullName(sku.getSpecInfo()); - dto.setPrice(sku.getSalePrice()); - dto.setMemberPrice(sku.getMemberPrice()); - dto.setIsSale(sku.getIsGrounding()); - dto.setBarCode(sku.getBarCode()); - dataList.add(dto); - }); - if (exportDTO.getSkuList().size() > 1) { - OnceAbsoluteMergeStrategy name = new OnceAbsoluteMergeStrategy(first, first + exportDTO.getSkuList().size() - 1, 0, 0); - handlers.add(name); - OnceAbsoluteMergeStrategy category = new OnceAbsoluteMergeStrategy(first, first + exportDTO.getSkuList().size() - 1, 1, 1); - handlers.add(category); - OnceAbsoluteMergeStrategy unit = new OnceAbsoluteMergeStrategy(first, first + exportDTO.getSkuList().size() - 1, 6, 6); - handlers.add(unit); - OnceAbsoluteMergeStrategy type = new OnceAbsoluteMergeStrategy(first, first + exportDTO.getSkuList().size() - 1, 7, 7); - handlers.add(type); - OnceAbsoluteMergeStrategy groupType = new OnceAbsoluteMergeStrategy(first, first + exportDTO.getSkuList().size() - 1, 8, 8); - handlers.add(groupType); - OnceAbsoluteMergeStrategy startTime = new OnceAbsoluteMergeStrategy(first, first + exportDTO.getSkuList().size() - 1, 9, 9); - handlers.add(startTime); - OnceAbsoluteMergeStrategy endTime = new OnceAbsoluteMergeStrategy(first, first + exportDTO.getSkuList().size() - 1, 10, 10); - handlers.add(endTime); - OnceAbsoluteMergeStrategy stockNumber = new OnceAbsoluteMergeStrategy(first, first + exportDTO.getSkuList().size() - 1, 11, 11); - handlers.add(stockNumber); - OnceAbsoluteMergeStrategy createTime = new OnceAbsoluteMergeStrategy(first, first + exportDTO.getSkuList().size() - 1, 12, 12); - handlers.add(createTime); + for (ProductDTO dto : records) { + if ("package".equals(dto.getType())) continue; // 跳过套餐 + + int firstRow = dataList.size() + 1; + + if (dto.getSkuList() != null && !dto.getSkuList().isEmpty()) { + for (ProdSkuDTO sku : dto.getSkuList()) { + ProductExportDTO exportDto = new ProductExportDTO(); + BeanUtil.copyProperties(dto, exportDto); + exportDto.setSpecFullName(sku.getSpecInfo()); + exportDto.setPrice(sku.getSalePrice()); + exportDto.setMemberPrice(sku.getMemberPrice()); + exportDto.setIsSale(sku.getIsGrounding()); + exportDto.setBarCode(sku.getBarCode()); + exportDto.setCostPrice(sku.getCostPrice()); + dataList.add(exportDto); + } + + int skuCount = dto.getSkuList().size(); + if (skuCount > 1) { + mergeColumns(handlers, firstRow, firstRow + skuCount - 1, + 0, 1, 7, 8, 9, 10, 11); // 多列合并 } } else { - dataList.add(BeanUtil.copyProperties(exportDTO, ProductExportDTO.class)); + dataList.add(BeanUtil.copyProperties(dto, ProductExportDTO.class)); } - }); + } - ExcelExportUtil.exportProductWithMergeToResponse(dataList, ProductExportDTO.class, "商品列表", response, handlers); + return new SheetData() + .setSheetName("普通商品") + .setData(dataList) + .setClazz(ProductExportDTO.class) + .setHandlers(handlers); + } + + // ----------------------------- + // 套餐商品处理 + // ----------------------------- + private SheetData buildPackageProductSheet(List records) { + List handlers = new ArrayList<>(); + List dataList = new ArrayList<>(); + + for (ProductDTO exportDTO : records) { + if (!"package".equals(exportDTO.getType())) continue; + + if (exportDTO.getProGroupVo() == null || exportDTO.getProGroupVo().isEmpty()) { + dataList.add(BeanUtil.copyProperties(exportDTO, ProductPackageExportDTO.class)); + continue; + } + + int sheetFirstRow = dataList.size() + 1; + boolean needOuterMerge = exportDTO.getProGroupVo().size() > 1; + + for (ProductGroupVo proGroupDTO : exportDTO.getProGroupVo()) { + int groupFirstRow = dataList.size() + 1; + List goods = proGroupDTO.getGoods(); + int groupSize = goods.size(); + + // 添加每条商品记录 + for (ProductGroupVo.Food good : goods) { + ProductPackageExportDTO pkgDto = new ProductPackageExportDTO() + .setName(exportDTO.getName()) + .setCategoryName(exportDTO.getCategoryName()) + .setUnitName(exportDTO.getUnitName()) + .setPrice(getMainSkuPrice(exportDTO)) + .setMemberPrice(getMainSkuMemberPrice(exportDTO)) + .setType(exportDTO.getType()) + .setGroupType(exportDTO.getGroupType()) + .setStockNumber(exportDTO.getStockNumber()) + .setIsSale(getMainSkuIsSale(exportDTO)) + .setGroupTitleName(proGroupDTO.getTitle()) + .setGroupProductNumber(Optional.ofNullable(proGroupDTO.getNumber()).map(String::valueOf).orElse("")) + .setGroupProductName(good.getProName() + " " + good.getSkuName()); + dataList.add(pkgDto); + } + + // 组内合并:如果该组有多个商品 + if (groupSize > 1) { + needOuterMerge = true; + mergeColumns(handlers, groupFirstRow, groupFirstRow + groupSize - 1, 5, 8); + } + } + + // 外层合并:整个套餐的信息(名称、分类等) + if (needOuterMerge) { + int lastRow = dataList.size(); + mergeColumns(handlers, sheetFirstRow, lastRow, 0, 1, 2, 3, 4); + } + } + + return new SheetData() + .setSheetName("套餐商品") + .setData(dataList) + .setClazz(ProductPackageExportDTO.class) + .setHandlers(handlers); + } + + // ----------------------------- + // 辅助方法:提取主 SKU 信息(避免重复 getFirst()) + // ----------------------------- + private BigDecimal getMainSkuPrice(ProductDTO dto) { + return dto.getSkuList().isEmpty() ? null : dto.getSkuList().getFirst().getSalePrice(); + } + + private BigDecimal getMainSkuMemberPrice(ProductDTO dto) { + return dto.getSkuList().isEmpty() ? null : dto.getSkuList().getFirst().getMemberPrice(); + } + + private Integer getMainSkuIsSale(ProductDTO dto) { + return dto.getSkuList().isEmpty() ? null : dto.getSkuList().getFirst().getIsSale(); + } + + // ----------------------------- + // 合并工具方法:支持多列合并 + // ----------------------------- + private void mergeColumns(List handlers, int firstRow, int lastRow, int... columns) { + for (int col : columns) { + addMergeHandler(handlers, firstRow, lastRow, col, col); + } + } + + private void addMergeHandler(List handlers, int firstRow, int lastRow, int firstCol, int lastCol) { + OnceAbsoluteMergeStrategy strategy = new OnceAbsoluteMergeStrategy(firstRow, lastRow, firstCol, lastCol); + handlers.add(strategy); } @Override