Эх сурвалжийг харах

Merge branch 'release_lutong_bug' into sit

lutong 2 сар өмнө
parent
commit
4866d59ec7

+ 10 - 0
alien-entity/src/main/java/shop/alien/entity/store/StoreOperationalStatisticsHistory.java

@@ -39,6 +39,16 @@ public class StoreOperationalStatisticsHistory {
     @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
     private Date endTime;
 
+    @ApiModelProperty(value = "上期开始时间")
+    @TableField("previous_start_time")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private Date previousStartTime;
+
+    @ApiModelProperty(value = "上期结束时间")
+    @TableField("previous_end_time")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private Date previousEndTime;
+
     @ApiModelProperty(value = "统计数据(JSON格式,包含所有统计字段)")
     @TableField("statistics_data")
     private String statisticsData;

+ 60 - 0
alien-store/src/main/java/shop/alien/store/controller/StoreOperationalStatisticsController.java

@@ -272,4 +272,64 @@ public class StoreOperationalStatisticsController {
             return R.fail("更新PDF URL失败: " + e.getMessage());
         }
     }
+
+    @ApiOperation("对比数据后绘制图片并生成PDF报告上传到OSS")
+    @ApiOperationSupport(order = 7)
+    @ApiImplicitParams({
+            @ApiImplicitParam(
+                    name = "storeId",
+                    value = "店铺ID",
+                    dataType = "Integer",
+                    paramType = "query",
+                    required = true
+            ),
+            @ApiImplicitParam(
+                    name = "currentStartTime",
+                    value = "当期开始时间(格式:yyyy-MM-dd)",
+                    dataType = "String",
+                    paramType = "query",
+                    required = true
+            ),
+            @ApiImplicitParam(
+                    name = "currentEndTime",
+                    value = "当期结束时间(格式:yyyy-MM-dd)",
+                    dataType = "String",
+                    paramType = "query",
+                    required = true
+            ),
+            @ApiImplicitParam(
+                    name = "previousStartTime",
+                    value = "上期开始时间(格式:yyyy-MM-dd)",
+                    dataType = "String",
+                    paramType = "query",
+                    required = true
+            ),
+            @ApiImplicitParam(
+                    name = "previousEndTime",
+                    value = "上期结束时间(格式:yyyy-MM-dd)",
+                    dataType = "String",
+                    paramType = "query",
+                    required = true
+            )
+    })
+    @GetMapping("/generateStatisticsComparisonPdf")
+    public R<String> generateStatisticsComparisonPdf(
+            @RequestParam("storeId") Integer storeId,
+            @RequestParam("currentStartTime") String currentStartTime,
+            @RequestParam("currentEndTime") String currentEndTime,
+            @RequestParam("previousStartTime") String previousStartTime,
+            @RequestParam("previousEndTime") String previousEndTime) {
+        log.info("StoreOperationalStatisticsController.generateStatisticsComparisonPdf - storeId={}, currentStartTime={}, currentEndTime={}, previousStartTime={}, previousEndTime={}",
+                storeId, currentStartTime, currentEndTime, previousStartTime, previousEndTime);
+        try {
+            String pdfUrl = storeOperationalStatisticsService.generateStatisticsComparisonPdf(
+                    storeId, currentStartTime, currentEndTime, previousStartTime, previousEndTime);
+            return R.data(pdfUrl);
+        } catch (Exception e) {
+            log.error("生成统计数据对比PDF报告失败 - storeId={}, error={}", storeId, e.getMessage(), e);
+            return R.fail("生成PDF报告失败: " + e.getMessage());
+        }
+    }
+
+
 }

+ 13 - 0
alien-store/src/main/java/shop/alien/store/service/StoreOperationalStatisticsService.java

@@ -80,4 +80,17 @@ public interface StoreOperationalStatisticsService {
      * @return 是否成功
      */
     boolean updateHistoryPdfUrl(Integer historyId, String pdfUrl);
+
+    /**
+     * 生成统计数据对比PDF报告并上传到OSS
+     *
+     * @param storeId           店铺ID
+     * @param currentStartTime  当期开始时间(格式:yyyy-MM-dd)
+     * @param currentEndTime    当期结束时间(格式:yyyy-MM-dd)
+     * @param previousStartTime 上期开始时间(格式:yyyy-MM-dd)
+     * @param previousEndTime   上期结束时间(格式:yyyy-MM-dd)
+     * @return PDF的OSS URL
+     */
+    String generateStatisticsComparisonPdf(Integer storeId, String currentStartTime, String currentEndTime,
+                                           String previousStartTime, String previousEndTime);
 }

+ 70 - 13
alien-store/src/main/java/shop/alien/store/service/impl/StoreOperationalStatisticsServiceImpl.java

@@ -11,6 +11,10 @@ import shop.alien.entity.store.vo.StoreOperationalStatisticsComparisonVo;
 import shop.alien.entity.store.vo.StoreOperationalStatisticsVo;
 import shop.alien.mapper.*;
 import shop.alien.store.service.StoreOperationalStatisticsService;
+import shop.alien.store.util.StatisticsComparisonImageUtil;
+import shop.alien.util.ali.AliOSSUtil;
+import shop.alien.util.common.RandomCreateUtil;
+import shop.alien.util.pdf.ImageToPdfUtil;
 
 import java.math.BigDecimal;
 import java.math.RoundingMode;
@@ -49,6 +53,7 @@ public class StoreOperationalStatisticsServiceImpl implements StoreOperationalSt
     private final StoreUserMapper storeUserMapper;
     private final StoreOperationalStatisticsHistoryMapper statisticsHistoryMapper;
     private final StoreTrackStatisticsMapper storeTrackStatisticsMapper;
+    private final AliOSSUtil aliOSSUtil;
 
     private static final String DATE_FORMAT = "yyyy-MM-dd";
     private static final String STAT_TYPE_DAILY = "DAILY";
@@ -183,13 +188,7 @@ public class StoreOperationalStatisticsServiceImpl implements StoreOperationalSt
         comparison.setVoucherData(buildVoucherDataComparison(currentStatistics.getVoucherData(), previousStatistics.getVoucherData()));
         comparison.setServiceQualityData(buildServiceQualityDataComparison(currentStatistics.getServiceQualityData(), previousStatistics.getServiceQualityData()));
 
-        // 保存对比数据到历史表,获取历史记录ID
-        Integer historyId = saveStatisticsHistory(storeId, currentStartTime, currentEndTime, comparison);
-        
-        // 将历史记录ID设置到返回对象中,方便前端后续更新PDF URL
-        if (historyId != null) {
-            comparison.setHistoryId(historyId);
-        }
+        // 不再在此处保存历史记录,历史记录只在 generateStatisticsComparisonPdf 接口中保存
 
         return comparison;
     }
@@ -836,32 +835,49 @@ public class StoreOperationalStatisticsServiceImpl implements StoreOperationalSt
     
     /**
      * 保存对比统计数据到历史表
-     * 仅在调用 getStatisticsComparison 接口时触发
+     * @param storeId 店铺ID
+     * @param currentStartTime 当期开始时间
+     * @param currentEndTime 当期结束时间
+     * @param previousStartTime 上期开始时间
+     * @param previousEndTime 上期结束时间
+     * @param comparison 对比数据
+     * @param pdfUrl PDF文件URL(可选)
      * @return 历史记录ID,保存失败返回null
      */
-    private Integer saveStatisticsHistory(Integer storeId, String currentStartTime, String currentEndTime, StoreOperationalStatisticsComparisonVo comparison) {
+    private Integer saveStatisticsHistory(Integer storeId, String currentStartTime, String currentEndTime,
+                                          String previousStartTime, String previousEndTime,
+                                          StoreOperationalStatisticsComparisonVo comparison, String pdfUrl) {
         try {
             SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT);
             Date startDate = sdf.parse(currentStartTime);
             Date endDate = sdf.parse(currentEndTime);
+            Date previousStartDate = sdf.parse(previousStartTime);
+            Date previousEndDate = sdf.parse(previousEndTime);
 
             StoreOperationalStatisticsHistory history = new StoreOperationalStatisticsHistory();
             history.setStoreId(storeId);
             history.setStartTime(startDate);
             history.setEndTime(endDate);
+            history.setPreviousStartTime(previousStartDate);
+            history.setPreviousEndTime(previousEndDate);
             history.setQueryTime(new Date());
             
             // 将对比统计数据序列化为JSON字符串(包含当期、上期和变化率等完整对比数据)
             String statisticsJson = JSON.toJSONString(comparison);
             history.setStatisticsData(statisticsJson);
+            
+            // 如果提供了PDF URL,则保存
+            if (pdfUrl != null && !pdfUrl.trim().isEmpty()) {
+                history.setPdfUrl(pdfUrl);
+            }
 
             statisticsHistoryMapper.insert(history);
-            log.info("保存对比统计数据到历史表成功 - storeId={}, currentStartTime={}, currentEndTime={}, historyId={}", 
-                    storeId, currentStartTime, currentEndTime, history.getId());
+            log.info("保存对比统计数据到历史表成功 - storeId={}, currentStartTime={}, currentEndTime={}, previousStartTime={}, previousEndTime={}, historyId={}", 
+                    storeId, currentStartTime, currentEndTime, previousStartTime, previousEndTime, history.getId());
             return history.getId();
         } catch (Exception e) {
-            log.error("保存对比统计数据到历史表失败 - storeId={}, currentStartTime={}, currentEndTime={}, error={}", 
-                    storeId, currentStartTime, currentEndTime, e.getMessage(), e);
+            log.error("保存对比统计数据到历史表失败 - storeId={}, currentStartTime={}, currentEndTime={}, previousStartTime={}, previousEndTime={}, error={}", 
+                    storeId, currentStartTime, currentEndTime, previousStartTime, previousEndTime, e.getMessage(), e);
             // 保存失败不影响主流程,只记录日志
             return null;
         }
@@ -1423,4 +1439,45 @@ public class StoreOperationalStatisticsServiceImpl implements StoreOperationalSt
             return BigDecimal.ZERO;
         }
     }
+
+    @Override
+    public String generateStatisticsComparisonPdf(Integer storeId, String currentStartTime, String currentEndTime,
+                                                   String previousStartTime, String previousEndTime) {
+        log.info("StoreOperationalStatisticsServiceImpl.generateStatisticsComparisonPdf - storeId={}, currentStartTime={}, currentEndTime={}, previousStartTime={}, previousEndTime={}",
+                storeId, currentStartTime, currentEndTime, previousStartTime, previousEndTime);
+        
+        try {
+            // 1. 获取统计数据对比
+            StoreOperationalStatisticsComparisonVo comparison = getStatisticsComparison(
+                    storeId, currentStartTime, currentEndTime, previousStartTime, previousEndTime);
+            
+            // 2. 生成图片
+            byte[] imageBytes = StatisticsComparisonImageUtil.generateImage(comparison);
+            
+            // 3. 将图片转换为PDF
+            java.io.ByteArrayInputStream imageInputStream = new java.io.ByteArrayInputStream(imageBytes);
+            byte[] pdfBytes = ImageToPdfUtil.imageToPdfBytes(imageInputStream);
+            
+            if (pdfBytes == null || pdfBytes.length == 0) {
+                throw new RuntimeException("图片转PDF失败");
+            }
+            
+            // 4. 上传PDF到OSS
+            String ossFilePath = "statistics/comparison_" + storeId + "_" + RandomCreateUtil.getRandomNum(8) + ".pdf";
+            java.io.ByteArrayInputStream pdfInputStream = new java.io.ByteArrayInputStream(pdfBytes);
+            String pdfUrl = aliOSSUtil.uploadFile(pdfInputStream, ossFilePath);
+            
+            log.info("统计数据对比图片转PDF并上传成功 - storeId={}, pdfUrl={}", storeId, pdfUrl);
+            
+            // 5. 保存对比数据到历史表(包含所有入参和PDF URL)
+            saveStatisticsHistory(storeId, currentStartTime, currentEndTime, 
+                    previousStartTime, previousEndTime, comparison, pdfUrl);
+
+            return pdfUrl;
+        } catch (Exception e) {
+            log.error("生成统计数据对比图片并转PDF上传失败 - storeId={}, error={}", storeId, e.getMessage(), e);
+            throw new RuntimeException("生成图片并转PDF上传失败: " + e.getMessage(), e);
+        }
+    }
+
 }

+ 665 - 0
alien-store/src/main/java/shop/alien/store/util/StatisticsComparisonImageUtil.java

@@ -0,0 +1,665 @@
+package shop.alien.store.util;
+
+import lombok.extern.slf4j.Slf4j;
+import shop.alien.entity.store.vo.StoreOperationalStatisticsComparisonVo;
+
+import javax.imageio.ImageIO;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayOutputStream;
+import java.math.BigDecimal;
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 经营统计数据对比图片生成工具类
+ * 将 StoreOperationalStatisticsComparisonVo 数据转换为图片
+ *
+ * @author system
+ * @since 2026-01-05
+ */
+@Slf4j
+public class StatisticsComparisonImageUtil {
+
+    // 颜色定义
+    private static final Color BACKGROUND_COLOR = Color.WHITE;
+    private static final Color TEXT_COLOR = new Color(51, 51, 51); // #333333
+    private static final Color HEADER_BG_COLOR = new Color(245, 245, 245); // #F5F5F5
+    private static final Color POSITIVE_COLOR = new Color(76, 175, 80); // 绿色 #4CAF50
+    private static final Color NEGATIVE_COLOR = new Color(244, 67, 54); // 红色 #F44336
+    private static final Color SECTION_TITLE_COLOR = new Color(33, 33, 33); // #212121
+    private static final Color BORDER_COLOR = new Color(224, 224, 224); // #E0E0E0
+
+    // 字体定义
+    private static final String FONT_NAME = "Microsoft YaHei"; // 微软雅黑,如果系统没有则使用默认字体
+    private static final int TITLE_FONT_SIZE = 24;
+    private static final int SECTION_TITLE_FONT_SIZE = 18;
+    private static final int DATA_FONT_SIZE = 14;
+    private static final int LABEL_FONT_SIZE = 12;
+
+    // 尺寸定义
+    private static final int IMAGE_WIDTH = 800;
+    private static final int PADDING = 20;
+    private static final int SECTION_SPACING = 30;
+    private static final int ROW_HEIGHT = 35;
+    private static final int HEADER_HEIGHT = 50;
+
+    private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#,##0.00");
+    private static final DecimalFormat PERCENT_FORMAT = new DecimalFormat("#,##0.00%");
+
+    /**
+     * 将统计数据对比转换为图片字节数组
+     *
+     * @param comparison 统计数据对比对象
+     * @return 图片字节数组
+     */
+    public static byte[] generateImage(StoreOperationalStatisticsComparisonVo comparison) {
+        if (comparison == null) {
+            throw new IllegalArgumentException("统计数据对比对象不能为空");
+        }
+
+        try {
+            // 计算图片高度
+            int totalHeight = calculateImageHeight(comparison);
+            
+            // 创建图片
+            BufferedImage image = new BufferedImage(IMAGE_WIDTH, totalHeight, BufferedImage.TYPE_INT_RGB);
+            Graphics2D g2d = image.createGraphics();
+            
+            // 设置抗锯齿
+            g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+            g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+            
+            // 填充背景
+            g2d.setColor(BACKGROUND_COLOR);
+            g2d.fillRect(0, 0, IMAGE_WIDTH, totalHeight);
+            
+            int currentY = PADDING;
+            
+            // 绘制标题
+            currentY = drawTitle(g2d, currentY, comparison);
+            
+            // 绘制日期范围
+            currentY = drawDateRange(g2d, currentY, comparison);
+            
+            currentY += SECTION_SPACING;
+            
+            // 绘制流量数据
+            if (comparison.getTrafficData() != null) {
+                currentY = drawSection(g2d, currentY, "流量数据", 
+                    buildTrafficDataRows(comparison.getTrafficData()));
+            }
+            
+            // 绘制互动数据
+            if (comparison.getInteractionData() != null) {
+                currentY = drawSection(g2d, currentY, "互动数据", 
+                    buildInteractionDataRows(comparison.getInteractionData()));
+            }
+            
+            // 绘制优惠券数据
+            if (comparison.getCouponData() != null) {
+                currentY = drawSection(g2d, currentY, "优惠券", 
+                    buildCouponDataRows(comparison.getCouponData()));
+            }
+            
+            // 绘制代金券数据
+            if (comparison.getVoucherData() != null) {
+                currentY = drawSection(g2d, currentY, "代金券", 
+                    buildVoucherDataRows(comparison.getVoucherData()));
+            }
+            
+            // 绘制服务质量数据
+            if (comparison.getServiceQualityData() != null) {
+                currentY = drawSection(g2d, currentY, "服务质量", 
+                    buildServiceQualityDataRows(comparison.getServiceQualityData()));
+            }
+            
+            g2d.dispose();
+            
+            // 转换为字节数组
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            ImageIO.write(image, "PNG", baos);
+            return baos.toByteArray();
+            
+        } catch (Exception e) {
+            log.error("生成统计数据对比图片失败", e);
+            throw new RuntimeException("生成图片失败: " + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 计算图片高度
+     */
+    private static int calculateImageHeight(StoreOperationalStatisticsComparisonVo comparison) {
+        int height = PADDING * 2;
+        height += HEADER_HEIGHT; // 标题
+        height += 40; // 日期范围
+        height += SECTION_SPACING;
+        
+        int rowCount = 0;
+        if (comparison.getTrafficData() != null) {
+            rowCount += 6; // 流量数据行数(6个字段)
+        }
+        if (comparison.getInteractionData() != null) {
+            rowCount += 13; // 互动数据行数(13个字段)
+        }
+        if (comparison.getCouponData() != null) {
+            rowCount += 10; // 优惠券数据行数(10个字段)
+        }
+        if (comparison.getVoucherData() != null) {
+            rowCount += 10; // 代金券数据行数(10个字段)
+        }
+        if (comparison.getServiceQualityData() != null) {
+            rowCount += 12; // 服务质量数据行数(12个字段)
+        }
+        
+        height += rowCount * ROW_HEIGHT;
+        height += (rowCount / 6 + 4) * SECTION_SPACING; // 区块间距
+        
+        return height;
+    }
+
+    /**
+     * 绘制标题
+     */
+    private static int drawTitle(Graphics2D g2d, int y, StoreOperationalStatisticsComparisonVo comparison) {
+        Font titleFont = new Font(FONT_NAME, Font.BOLD, TITLE_FONT_SIZE);
+        g2d.setFont(titleFont);
+        g2d.setColor(SECTION_TITLE_COLOR);
+        
+        String title = "经营数据";
+        FontMetrics fm = g2d.getFontMetrics();
+        int titleWidth = fm.stringWidth(title);
+        int titleX = (IMAGE_WIDTH - titleWidth) / 2;
+        
+        g2d.drawString(title, titleX, y + TITLE_FONT_SIZE);
+        return y + HEADER_HEIGHT;
+    }
+
+    /**
+     * 绘制日期范围
+     */
+    private static int drawDateRange(Graphics2D g2d, int y, StoreOperationalStatisticsComparisonVo comparison) {
+        Font dateFont = new Font(FONT_NAME, Font.PLAIN, DATA_FONT_SIZE);
+        g2d.setFont(dateFont);
+        g2d.setColor(TEXT_COLOR);
+        
+        String currentDate = formatDate(comparison.getCurrentStartTime()) + "-" + formatDate(comparison.getCurrentEndTime());
+        String previousDate = formatDate(comparison.getPreviousStartTime()) + "-" + formatDate(comparison.getPreviousEndTime());
+        
+        int dateX = PADDING;
+        g2d.drawString(currentDate, dateX, y);
+        g2d.drawString(previousDate, dateX, y + 20);
+        
+        return y + 40;
+    }
+
+    /**
+     * 格式化日期
+     */
+    private static String formatDate(String date) {
+        if (date == null || date.length() < 10) {
+            return date;
+        }
+        // 将 yyyy-MM-dd 转换为 yyyy/MM/dd
+        return date.replace("-", "/");
+    }
+
+    /**
+     * 绘制数据区块
+     */
+    private static int drawSection(Graphics2D g2d, int y, String sectionTitle, List<DataRow> rows) {
+        if (rows == null || rows.isEmpty()) {
+            return y;
+        }
+        
+        // 绘制区块标题
+        Font sectionFont = new Font(FONT_NAME, Font.BOLD, SECTION_TITLE_FONT_SIZE);
+        g2d.setFont(sectionFont);
+        g2d.setColor(SECTION_TITLE_COLOR);
+        g2d.drawString(sectionTitle, PADDING, y);
+        y += 30;
+        
+        // 绘制表头
+        y = drawTableHeader(g2d, y);
+        
+        // 绘制数据行
+        Font dataFont = new Font(FONT_NAME, Font.PLAIN, DATA_FONT_SIZE);
+        Font labelFont = new Font(FONT_NAME, Font.PLAIN, LABEL_FONT_SIZE);
+        
+        for (DataRow row : rows) {
+            y = drawDataRow(g2d, y, row, dataFont, labelFont);
+        }
+        
+        return y + SECTION_SPACING;
+    }
+
+    /**
+     * 绘制表头
+     */
+    private static int drawTableHeader(Graphics2D g2d, int y) {
+        // 绘制表头背景
+        g2d.setColor(HEADER_BG_COLOR);
+        g2d.fillRect(PADDING, y, IMAGE_WIDTH - PADDING * 2, ROW_HEIGHT);
+        
+        // 绘制表头文字
+        Font headerFont = new Font(FONT_NAME, Font.BOLD, LABEL_FONT_SIZE);
+        g2d.setFont(headerFont);
+        g2d.setColor(TEXT_COLOR);
+        
+        int col1X = PADDING + 10;
+        int col2X = IMAGE_WIDTH / 2 - 80;
+        int col3X = IMAGE_WIDTH / 2 + 20;
+        int col4X = IMAGE_WIDTH - PADDING - 100;
+        
+        g2d.drawString("指标", col1X, y + 20);
+        g2d.drawString("当期", col2X, y + 20);
+        g2d.drawString("上期", col3X, y + 20);
+        g2d.drawString("变化率", col4X, y + 20);
+        
+        // 绘制边框
+        g2d.setColor(BORDER_COLOR);
+        g2d.drawRect(PADDING, y, IMAGE_WIDTH - PADDING * 2, ROW_HEIGHT);
+        
+        return y + ROW_HEIGHT;
+    }
+
+    /**
+     * 绘制数据行
+     */
+    private static int drawDataRow(Graphics2D g2d, int y, DataRow row, Font dataFont, Font labelFont) {
+        // 绘制行背景(交替颜色)
+        if ((y / ROW_HEIGHT) % 2 == 0) {
+            g2d.setColor(new Color(250, 250, 250));
+            g2d.fillRect(PADDING, y, IMAGE_WIDTH - PADDING * 2, ROW_HEIGHT);
+        }
+        
+        // 绘制指标名称
+        g2d.setFont(labelFont);
+        g2d.setColor(TEXT_COLOR);
+        g2d.drawString(row.getLabel(), PADDING + 10, y + 22);
+        
+        // 绘制当期值
+        g2d.setFont(dataFont);
+        String currentValue = formatValue(row.getCurrent());
+        g2d.drawString(currentValue, IMAGE_WIDTH / 2 - 80, y + 22);
+        
+        // 绘制上期值
+        String previousValue = formatValue(row.getPrevious());
+        g2d.drawString(previousValue, IMAGE_WIDTH / 2 + 20, y + 22);
+        
+        // 绘制变化率(带颜色)
+        String changeRate = formatChangeRate(row.getChangeRate());
+        Color changeColor = row.getChangeRate() != null && row.getChangeRate().compareTo(BigDecimal.ZERO) >= 0 
+            ? POSITIVE_COLOR : NEGATIVE_COLOR;
+        g2d.setColor(changeColor);
+        g2d.drawString(changeRate, IMAGE_WIDTH - PADDING - 100, y + 22);
+        
+        // 绘制边框
+        g2d.setColor(BORDER_COLOR);
+        g2d.drawLine(PADDING, y + ROW_HEIGHT, IMAGE_WIDTH - PADDING, y + ROW_HEIGHT);
+        
+        return y + ROW_HEIGHT;
+    }
+
+    /**
+     * 格式化数值
+     */
+    private static String formatValue(Object value) {
+        if (value == null) {
+            return "0";
+        }
+        if (value instanceof BigDecimal) {
+            BigDecimal bd = (BigDecimal) value;
+            if (bd.scale() > 0) {
+                return DECIMAL_FORMAT.format(bd);
+            } else {
+                return String.valueOf(bd.longValue());
+            }
+        }
+        if (value instanceof Number) {
+            return DECIMAL_FORMAT.format(((Number) value).doubleValue());
+        }
+        return String.valueOf(value);
+    }
+
+    /**
+     * 格式化变化率
+     */
+    private static String formatChangeRate(BigDecimal changeRate) {
+        if (changeRate == null) {
+            return "0.00%";
+        }
+        String sign = changeRate.compareTo(BigDecimal.ZERO) >= 0 ? "+" : "";
+        return sign + PERCENT_FORMAT.format(changeRate.divide(new BigDecimal(100), 4, BigDecimal.ROUND_HALF_UP));
+    }
+
+    /**
+     * 构建流量数据行(按照实体类字段顺序)
+     */
+    private static List<DataRow> buildTrafficDataRows(StoreOperationalStatisticsComparisonVo.TrafficDataComparison data) {
+        List<DataRow> rows = new ArrayList<>();
+        // 1. 店铺搜索量对比
+        if (data.getStoreSearchVolume() != null) {
+            rows.add(new DataRow("店铺搜索量对比", data.getStoreSearchVolume().getCurrent(),
+                data.getStoreSearchVolume().getPrevious(), data.getStoreSearchVolume().getChangeRate()));
+        }
+        // 2. 浏览量对比
+        if (data.getPageViews() != null) {
+            rows.add(new DataRow("浏览量对比", data.getPageViews().getCurrent(),
+                data.getPageViews().getPrevious(), data.getPageViews().getChangeRate()));
+        }
+        // 3. 访客数对比
+        if (data.getVisitors() != null) {
+            rows.add(new DataRow("访客数对比", data.getVisitors().getCurrent(),
+                data.getVisitors().getPrevious(), data.getVisitors().getChangeRate()));
+        }
+        // 4. 新增访客数对比
+        if (data.getNewVisitors() != null) {
+            rows.add(new DataRow("新增访客数对比", data.getNewVisitors().getCurrent(),
+                data.getNewVisitors().getPrevious(), data.getNewVisitors().getChangeRate()));
+        }
+        // 5. 访问时长对比
+        if (data.getVisitDuration() != null) {
+            rows.add(new DataRow("访问时长对比", data.getVisitDuration().getCurrent(),
+                data.getVisitDuration().getPrevious(), data.getVisitDuration().getChangeRate()));
+        }
+        // 6. 平均访问时长对比
+        if (data.getAvgVisitDuration() != null) {
+            rows.add(new DataRow("平均访问时长对比", data.getAvgVisitDuration().getCurrent(),
+                data.getAvgVisitDuration().getPrevious(), data.getAvgVisitDuration().getChangeRate()));
+        }
+        return rows;
+    }
+
+    /**
+     * 构建互动数据行(按照实体类字段顺序)
+     */
+    private static List<DataRow> buildInteractionDataRows(StoreOperationalStatisticsComparisonVo.InteractionDataComparison data) {
+        List<DataRow> rows = new ArrayList<>();
+        // 1. 店铺收藏次数对比
+        if (data.getStoreCollectionCount() != null) {
+            rows.add(new DataRow("店铺收藏次数对比", data.getStoreCollectionCount().getCurrent(),
+                data.getStoreCollectionCount().getPrevious(), data.getStoreCollectionCount().getChangeRate()));
+        }
+        // 2. 店铺分享次数对比
+        if (data.getStoreShareCount() != null) {
+            rows.add(new DataRow("店铺分享次数对比", data.getStoreShareCount().getCurrent(),
+                data.getStoreShareCount().getPrevious(), data.getStoreShareCount().getChangeRate()));
+        }
+        // 3. 店铺打卡次数对比
+        if (data.getStoreCheckInCount() != null) {
+            rows.add(new DataRow("店铺打卡次数对比", data.getStoreCheckInCount().getCurrent(),
+                data.getStoreCheckInCount().getPrevious(), data.getStoreCheckInCount().getChangeRate()));
+        }
+        // 4. 咨询商家次数对比
+        if (data.getConsultMerchantCount() != null) {
+            rows.add(new DataRow("咨询商家次数对比", data.getConsultMerchantCount().getCurrent(),
+                data.getConsultMerchantCount().getPrevious(), data.getConsultMerchantCount().getChangeRate()));
+        }
+        // 5. 好友数量对比
+        if (data.getFriendsCount() != null) {
+            rows.add(new DataRow("好友数量对比", data.getFriendsCount().getCurrent(),
+                data.getFriendsCount().getPrevious(), data.getFriendsCount().getChangeRate()));
+        }
+        // 6. 关注数量对比
+        if (data.getFollowCount() != null) {
+            rows.add(new DataRow("关注数量对比", data.getFollowCount().getCurrent(),
+                data.getFollowCount().getPrevious(), data.getFollowCount().getChangeRate()));
+        }
+        // 7. 粉丝数量对比
+        if (data.getFansCount() != null) {
+            rows.add(new DataRow("粉丝数量对比", data.getFansCount().getCurrent(),
+                data.getFansCount().getPrevious(), data.getFansCount().getChangeRate()));
+        }
+        // 8. 发布动态数量对比
+        if (data.getPostsPublishedCount() != null) {
+            rows.add(new DataRow("发布动态数量对比", data.getPostsPublishedCount().getCurrent(),
+                data.getPostsPublishedCount().getPrevious(), data.getPostsPublishedCount().getChangeRate()));
+        }
+        // 9. 动态点赞数量对比
+        if (data.getPostLikesCount() != null) {
+            rows.add(new DataRow("动态点赞数量对比", data.getPostLikesCount().getCurrent(),
+                data.getPostLikesCount().getPrevious(), data.getPostLikesCount().getChangeRate()));
+        }
+        // 10. 动态评论数量对比
+        if (data.getPostCommentsCount() != null) {
+            rows.add(new DataRow("动态评论数量对比", data.getPostCommentsCount().getCurrent(),
+                data.getPostCommentsCount().getPrevious(), data.getPostCommentsCount().getChangeRate()));
+        }
+        // 11. 动态转发数量对比
+        if (data.getPostSharesCount() != null) {
+            rows.add(new DataRow("动态转发数量对比", data.getPostSharesCount().getCurrent(),
+                data.getPostSharesCount().getPrevious(), data.getPostSharesCount().getChangeRate()));
+        }
+        // 12. 被举报次数对比
+        if (data.getReportedCount() != null) {
+            rows.add(new DataRow("被举报次数对比", data.getReportedCount().getCurrent(),
+                data.getReportedCount().getPrevious(), data.getReportedCount().getChangeRate()));
+        }
+        // 13. 被拉黑次数对比
+        if (data.getBlockedCount() != null) {
+            rows.add(new DataRow("被拉黑次数对比", data.getBlockedCount().getCurrent(),
+                data.getBlockedCount().getPrevious(), data.getBlockedCount().getChangeRate()));
+        }
+        return rows;
+    }
+
+    /**
+     * 构建优惠券数据行(按照实体类字段顺序)
+     */
+    private static List<DataRow> buildCouponDataRows(StoreOperationalStatisticsComparisonVo.CouponDataComparison data) {
+        List<DataRow> rows = new ArrayList<>();
+        // 1. 赠送好友数量对比
+        if (data.getGiftToFriendsCount() != null) {
+            rows.add(new DataRow("赠送好友数量对比", data.getGiftToFriendsCount().getCurrent(), 
+                data.getGiftToFriendsCount().getPrevious(), data.getGiftToFriendsCount().getChangeRate()));
+        }
+        // 2. 赠送好友金额合计对比
+        if (data.getGiftToFriendsAmount() != null) {
+            rows.add(new DataRow("赠送好友金额合计对比", data.getGiftToFriendsAmount().getCurrent(), 
+                data.getGiftToFriendsAmount().getPrevious(), data.getGiftToFriendsAmount().getChangeRate()));
+        }
+        // 3. 赠送好友使用数量对比
+        if (data.getGiftToFriendsUsedCount() != null) {
+            rows.add(new DataRow("赠送好友使用数量对比", data.getGiftToFriendsUsedCount().getCurrent(), 
+                data.getGiftToFriendsUsedCount().getPrevious(), data.getGiftToFriendsUsedCount().getChangeRate()));
+        }
+        // 4. 赠送好友使用金额合计对比
+        if (data.getGiftToFriendsUsedAmount() != null) {
+            rows.add(new DataRow("赠送好友使用金额合计对比", data.getGiftToFriendsUsedAmount().getCurrent(), 
+                data.getGiftToFriendsUsedAmount().getPrevious(), data.getGiftToFriendsUsedAmount().getChangeRate()));
+        }
+        // 5. 赠送好友使用金额占比对比
+        if (data.getGiftToFriendsUsedAmountRatio() != null) {
+            rows.add(new DataRow("赠送好友使用金额占比对比", data.getGiftToFriendsUsedAmountRatio().getCurrent(), 
+                data.getGiftToFriendsUsedAmountRatio().getPrevious(), data.getGiftToFriendsUsedAmountRatio().getChangeRate()));
+        }
+        // 6. 好友赠送数量对比
+        if (data.getFriendsGiftCount() != null) {
+            rows.add(new DataRow("好友赠送数量对比", data.getFriendsGiftCount().getCurrent(), 
+                data.getFriendsGiftCount().getPrevious(), data.getFriendsGiftCount().getChangeRate()));
+        }
+        // 7. 好友赠送金额合计对比
+        if (data.getFriendsGiftAmount() != null) {
+            rows.add(new DataRow("好友赠送金额合计对比", data.getFriendsGiftAmount().getCurrent(), 
+                data.getFriendsGiftAmount().getPrevious(), data.getFriendsGiftAmount().getChangeRate()));
+        }
+        // 8. 好友赠送使用数量对比
+        if (data.getFriendsGiftUsedCount() != null) {
+            rows.add(new DataRow("好友赠送使用数量对比", data.getFriendsGiftUsedCount().getCurrent(), 
+                data.getFriendsGiftUsedCount().getPrevious(), data.getFriendsGiftUsedCount().getChangeRate()));
+        }
+        // 9. 好友赠送使用金额合计对比
+        if (data.getFriendsGiftUsedAmount() != null) {
+            rows.add(new DataRow("好友赠送使用金额合计对比", data.getFriendsGiftUsedAmount().getCurrent(), 
+                data.getFriendsGiftUsedAmount().getPrevious(), data.getFriendsGiftUsedAmount().getChangeRate()));
+        }
+        // 10. 好友赠送使用金额占比对比
+        if (data.getFriendsGiftUsedAmountRatio() != null) {
+            rows.add(new DataRow("好友赠送使用金额占比对比", data.getFriendsGiftUsedAmountRatio().getCurrent(), 
+                data.getFriendsGiftUsedAmountRatio().getPrevious(), data.getFriendsGiftUsedAmountRatio().getChangeRate()));
+        }
+        return rows;
+    }
+
+    /**
+     * 构建代金券数据行(按照实体类字段顺序)
+     */
+    private static List<DataRow> buildVoucherDataRows(StoreOperationalStatisticsComparisonVo.VoucherDataComparison data) {
+        List<DataRow> rows = new ArrayList<>();
+        // 1. 赠送好友数量对比
+        if (data.getGiftToFriendsCount() != null) {
+            rows.add(new DataRow("赠送好友数量对比", data.getGiftToFriendsCount().getCurrent(), 
+                data.getGiftToFriendsCount().getPrevious(), data.getGiftToFriendsCount().getChangeRate()));
+        }
+        // 2. 赠送好友金额合计对比
+        if (data.getGiftToFriendsAmount() != null) {
+            rows.add(new DataRow("赠送好友金额合计对比", data.getGiftToFriendsAmount().getCurrent(), 
+                data.getGiftToFriendsAmount().getPrevious(), data.getGiftToFriendsAmount().getChangeRate()));
+        }
+        // 3. 赠送好友使用数量对比
+        if (data.getGiftToFriendsUsedCount() != null) {
+            rows.add(new DataRow("赠送好友使用数量对比", data.getGiftToFriendsUsedCount().getCurrent(), 
+                data.getGiftToFriendsUsedCount().getPrevious(), data.getGiftToFriendsUsedCount().getChangeRate()));
+        }
+        // 4. 赠送好友使用金额合计对比
+        if (data.getGiftToFriendsUsedAmount() != null) {
+            rows.add(new DataRow("赠送好友使用金额合计对比", data.getGiftToFriendsUsedAmount().getCurrent(), 
+                data.getGiftToFriendsUsedAmount().getPrevious(), data.getGiftToFriendsUsedAmount().getChangeRate()));
+        }
+        // 5. 赠送好友使用金额占比对比
+        if (data.getGiftToFriendsUsedAmountRatio() != null) {
+            rows.add(new DataRow("赠送好友使用金额占比对比", data.getGiftToFriendsUsedAmountRatio().getCurrent(), 
+                data.getGiftToFriendsUsedAmountRatio().getPrevious(), data.getGiftToFriendsUsedAmountRatio().getChangeRate()));
+        }
+        // 6. 好友赠送数量对比
+        if (data.getFriendsGiftCount() != null) {
+            rows.add(new DataRow("好友赠送数量对比", data.getFriendsGiftCount().getCurrent(), 
+                data.getFriendsGiftCount().getPrevious(), data.getFriendsGiftCount().getChangeRate()));
+        }
+        // 7. 好友赠送金额合计对比
+        if (data.getFriendsGiftAmount() != null) {
+            rows.add(new DataRow("好友赠送金额合计对比", data.getFriendsGiftAmount().getCurrent(), 
+                data.getFriendsGiftAmount().getPrevious(), data.getFriendsGiftAmount().getChangeRate()));
+        }
+        // 8. 好友赠送使用数量对比
+        if (data.getFriendsGiftUsedCount() != null) {
+            rows.add(new DataRow("好友赠送使用数量对比", data.getFriendsGiftUsedCount().getCurrent(), 
+                data.getFriendsGiftUsedCount().getPrevious(), data.getFriendsGiftUsedCount().getChangeRate()));
+        }
+        // 9. 好友赠送使用金额合计对比
+        if (data.getFriendsGiftUsedAmount() != null) {
+            rows.add(new DataRow("好友赠送使用金额合计对比", data.getFriendsGiftUsedAmount().getCurrent(), 
+                data.getFriendsGiftUsedAmount().getPrevious(), data.getFriendsGiftUsedAmount().getChangeRate()));
+        }
+        // 10. 好友赠送使用金额占比对比
+        if (data.getFriendsGiftUsedAmountRatio() != null) {
+            rows.add(new DataRow("好友赠送使用金额占比对比", data.getFriendsGiftUsedAmountRatio().getCurrent(), 
+                data.getFriendsGiftUsedAmountRatio().getPrevious(), data.getFriendsGiftUsedAmountRatio().getChangeRate()));
+        }
+        return rows;
+    }
+
+    /**
+     * 构建服务质量数据行(按照实体类字段顺序)
+     */
+    private static List<DataRow> buildServiceQualityDataRows(StoreOperationalStatisticsComparisonVo.ServiceQualityDataComparison data) {
+        List<DataRow> rows = new ArrayList<>();
+        // 1. 店铺评分对比
+        if (data.getStoreRating() != null) {
+            rows.add(new DataRow("店铺评分对比", data.getStoreRating().getCurrent(), 
+                data.getStoreRating().getPrevious(), data.getStoreRating().getChangeRate()));
+        }
+        // 2. 口味评分对比
+        if (data.getTasteRating() != null) {
+            rows.add(new DataRow("口味评分对比", data.getTasteRating().getCurrent(), 
+                data.getTasteRating().getPrevious(), data.getTasteRating().getChangeRate()));
+        }
+        // 3. 环境评分对比
+        if (data.getEnvironmentRating() != null) {
+            rows.add(new DataRow("环境评分对比", data.getEnvironmentRating().getCurrent(), 
+                data.getEnvironmentRating().getPrevious(), data.getEnvironmentRating().getChangeRate()));
+        }
+        // 4. 服务评分对比
+        if (data.getServiceRating() != null) {
+            rows.add(new DataRow("服务评分对比", data.getServiceRating().getCurrent(), 
+                data.getServiceRating().getPrevious(), data.getServiceRating().getChangeRate()));
+        }
+        // 5. 评价数量对比
+        if (data.getTotalReviews() != null) {
+            rows.add(new DataRow("评价数量对比", data.getTotalReviews().getCurrent(), 
+                data.getTotalReviews().getPrevious(), data.getTotalReviews().getChangeRate()));
+        }
+        // 6. 好评数量对比
+        if (data.getPositiveReviews() != null) {
+            rows.add(new DataRow("好评数量对比", data.getPositiveReviews().getCurrent(), 
+                data.getPositiveReviews().getPrevious(), data.getPositiveReviews().getChangeRate()));
+        }
+        // 7. 中评数量对比
+        if (data.getNeutralReviews() != null) {
+            rows.add(new DataRow("中评数量对比", data.getNeutralReviews().getCurrent(), 
+                data.getNeutralReviews().getPrevious(), data.getNeutralReviews().getChangeRate()));
+        }
+        // 8. 差评数量对比
+        if (data.getNegativeReviews() != null) {
+            rows.add(new DataRow("差评数量对比", data.getNegativeReviews().getCurrent(), 
+                data.getNegativeReviews().getPrevious(), data.getNegativeReviews().getChangeRate()));
+        }
+        // 9. 差评占比对比
+        if (data.getNegativeReviewRatio() != null) {
+            rows.add(new DataRow("差评占比对比", data.getNegativeReviewRatio().getCurrent(), 
+                data.getNegativeReviewRatio().getPrevious(), data.getNegativeReviewRatio().getChangeRate()));
+        }
+        // 10. 差评申诉次数对比
+        if (data.getNegativeReviewAppealsCount() != null) {
+            rows.add(new DataRow("差评申诉次数对比", data.getNegativeReviewAppealsCount().getCurrent(), 
+                data.getNegativeReviewAppealsCount().getPrevious(), data.getNegativeReviewAppealsCount().getChangeRate()));
+        }
+        // 11. 差评申诉成功次数对比
+        if (data.getNegativeReviewAppealsSuccessCount() != null) {
+            rows.add(new DataRow("差评申诉成功次数对比", data.getNegativeReviewAppealsSuccessCount().getCurrent(), 
+                data.getNegativeReviewAppealsSuccessCount().getPrevious(), data.getNegativeReviewAppealsSuccessCount().getChangeRate()));
+        }
+        // 12. 差评申诉成功占比对比
+        if (data.getNegativeReviewAppealsSuccessRatio() != null) {
+            rows.add(new DataRow("差评申诉成功占比对比", data.getNegativeReviewAppealsSuccessRatio().getCurrent(), 
+                data.getNegativeReviewAppealsSuccessRatio().getPrevious(), data.getNegativeReviewAppealsSuccessRatio().getChangeRate()));
+        }
+        return rows;
+    }
+
+    /**
+     * 数据行内部类
+     */
+    private static class DataRow {
+        private String label;
+        private Object current;
+        private Object previous;
+        private BigDecimal changeRate;
+
+        public DataRow(String label, Object current, Object previous, BigDecimal changeRate) {
+            this.label = label;
+            this.current = current;
+            this.previous = previous;
+            this.changeRate = changeRate;
+        }
+
+        public String getLabel() {
+            return label;
+        }
+
+        public Object getCurrent() {
+            return current;
+        }
+
+        public Object getPrevious() {
+            return previous;
+        }
+
+        public BigDecimal getChangeRate() {
+            return changeRate;
+        }
+    }
+}