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

Merge branch 'fix/报告柱状图辅助线' of jyx/dcjxb.microservice into master

金逸霄 2 долоо хоног өмнө
parent
commit
ea34856ccb

+ 19 - 0
abilities/exam-sprint/infrastructure/src/main/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/rendering/outlook/ClasspathOutlookExamSprintReportRenderer.java

@@ -302,6 +302,7 @@ public class ClasspathOutlookExamSprintReportRenderer implements ExamSprintRepor
                 .append("<div class='chart-box'>")
                 .append("<svg class='past-paper-column-chart'").append(SVG_CJK_FONT_FAMILY)
                 .append(" xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 220' role='img' aria-label='真题试卷词汇掌握情况'>")
+                .append(renderYAxisGridLines(320, axisMax, yAxisTickStep))
                 .append(renderChartAxes(320))
                 .append(renderYAxisTicks(axisMax, yAxisTickStep))
                 .append(renderXAxisTickMarks(112, 208))
@@ -343,6 +344,7 @@ public class ClasspathOutlookExamSprintReportRenderer implements ExamSprintRepor
                 .append("<div class='chart-box'>")
                 .append("<svg class='high-frequency-column-chart'").append(SVG_CJK_FONT_FAMILY)
                 .append(" xmlns='http://www.w3.org/2000/svg' viewBox='0 0 360 220' role='img' aria-label='常考词汇掌握情况'>")
+                .append(renderYAxisGridLines(360, axisMax, 20))
                 .append(renderChartAxes(360))
                 .append(renderYAxisTicks(axisMax, 20))
                 .append(renderXAxisTickMarks(97, 187, 277))
@@ -386,6 +388,7 @@ public class ClasspathOutlookExamSprintReportRenderer implements ExamSprintRepor
                 .append("<div class='chart-box'>")
                 .append("<svg class='frequency-band-column-chart'").append(SVG_CJK_FONT_FAMILY)
                 .append(" xmlns='http://www.w3.org/2000/svg' viewBox='0 0 360 220' role='img' aria-label='词频区间掌握度'>")
+                .append(renderYAxisGridLines(360, axisMax, 50))
                 .append(renderChartAxes(360))
                 .append(renderYAxisTicks(axisMax, 50))
                 .append(renderXAxisTickMarks(97, 187, 277));
@@ -558,6 +561,22 @@ public class ClasspathOutlookExamSprintReportRenderer implements ExamSprintRepor
                 .toString();
     }
 
+    private String renderYAxisGridLines(int width, int axisMax, int tickStep) {
+        if (axisMax <= 0 || tickStep <= 0) {
+            return "";
+        }
+
+        StringBuilder builder = new StringBuilder();
+        for (int tickValue = 0; tickValue <= axisMax; tickValue += tickStep) {
+            int y = CHART_AXIS_BOTTOM - (int) Math.round((tickValue / (double) axisMax) * CHART_AXIS_HEIGHT);
+            builder.append("<line class='chart-grid-line chart-grid-line-y' x1='")
+                    .append(CHART_AXIS_LEFT).append("' y1='").append(y)
+                    .append("' x2='").append(width - 24).append("' y2='").append(y)
+                    .append("' stroke='#edf2f7' stroke-width='1'/>");
+        }
+        return builder.toString();
+    }
+
     private String renderYAxisTicks(int axisMax, int tickStep) {
         if (axisMax <= 0 || tickStep <= 0) {
             return "";

+ 30 - 0
abilities/exam-sprint/infrastructure/src/test/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/rendering/achievement/AchievementExamSprintReportSvgChartBuilderTest.java

@@ -11,6 +11,9 @@ class AchievementExamSprintReportSvgChartBuilderTest {
 
     private static final Pattern SELF_CLOSING_RECT_PATTERN = Pattern.compile("<rect\\b[^>]*/>");
 
+    /**
+     * 覆盖成果报告柱状图正值极小但非零时,应按坐标轴比例绘制且不使用兜底最小柱高。
+     */
     @Test
     void comparisonBarChartKeepsTinyPositiveBarsProportionalToAxisScale() {
         AchievementExamSprintReportSvgChartBuilder builder = new AchievementExamSprintReportSvgChartBuilder();
@@ -39,6 +42,9 @@ class AchievementExamSprintReportSvgChartBuilderTest {
                 .contains("fill='#448aff'");
     }
 
+    /**
+     * 覆盖成果报告柱状图渲染内联 SVG 时,应声明 Batik 可识别的 CJK 字体族。
+     */
     @Test
     void comparisonBarChartDeclaresBatikCjkFontFamilyOnInlineSvg() {
         AchievementExamSprintReportSvgChartBuilder builder = new AchievementExamSprintReportSvgChartBuilder();
@@ -57,6 +63,30 @@ class AchievementExamSprintReportSvgChartBuilderTest {
         assertThat(svg).contains("<svg class='achievement-bar-chart vocabulary-growth-chart' font-family=\"'MiSans VF', MiSans, ReportFont, sans-serif\"");
     }
 
+    /**
+     * 覆盖成果报告柱状图渲染 y 轴刻度时,应输出横向辅助网格线便于对比训练前后数据。
+     */
+    @Test
+    void comparisonBarChartRendersYAxisHorizontalGridLines() {
+        AchievementExamSprintReportSvgChartBuilder builder = new AchievementExamSprintReportSvgChartBuilder();
+
+        String svg = builder.comparisonBarChart(
+                "vocabulary-growth-chart",
+                "词汇量对比",
+                "训练前",
+                50,
+                "50 词",
+                "训练后",
+                100,
+                "100 词",
+                "#448aff");
+
+        assertThat(svg)
+                .contains("<line class='chart-grid-line' x1='58' y1='36' x2='324' y2='36' stroke='#edf2f7' stroke-width='1'/>")
+                .contains("<line class='chart-grid-line' x1='58' y1='108' x2='324' y2='108' stroke='#edf2f7' stroke-width='1'/>")
+                .contains("<line class='chart-grid-line' x1='58' y1='180' x2='324' y2='180' stroke='#edf2f7' stroke-width='1'/>");
+    }
+
     private String extractRect(String svgOrHtml, String rectClass) {
         Matcher rectMatcher = SELF_CLOSING_RECT_PATTERN.matcher(svgOrHtml);
         String singleQuotedClass = "class='" + rectClass + "'";

+ 52 - 0
abilities/exam-sprint/infrastructure/src/test/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/rendering/outlook/ClasspathOutlookExamSprintReportRendererTest.java

@@ -169,6 +169,39 @@ class ClasspathOutlookExamSprintReportRendererTest {
                 .containsPattern("<text class='chart-axis-tick-label chart-axis-tick-label-y' x='26' y='54' text-anchor='end' fill='#7f8b97' font-size='11'>5</text>");
     }
 
+    /**
+     * 覆盖展望报告三组柱状图渲染时,应在 y 轴刻度位置输出横向辅助网格线便于读数。
+     */
+    @Test
+    void renderAddsYAxisHorizontalGridLinesToOutlookBarCharts() throws Exception {
+        ClasspathOutlookExamSprintReportRenderer renderer = new ClasspathOutlookExamSprintReportRenderer(OBJECT_MAPPER);
+
+        String html = renderer.render(unmodeledOutlookContent(callerVocabularyPayload()), Instant.parse("2026-01-03T08:00:00Z"));
+
+        String pastPaperSvg = extractSvgByClass(html, "past-paper-column-chart");
+        assertThat(gridLineElements(pastPaperSvg))
+                .containsExactly(
+                        "<line class='chart-grid-line chart-grid-line-y' x1='34' y1='180' x2='296' y2='180' stroke='#edf2f7' stroke-width='1'/>",
+                        "<line class='chart-grid-line chart-grid-line-y' x1='34' y1='50' x2='296' y2='50' stroke='#edf2f7' stroke-width='1'/>");
+
+        String highFrequencySvg = extractSvgByClass(html, "high-frequency-column-chart");
+        assertThat(gridLineElements(highFrequencySvg))
+                .containsExactly(
+                        "<line class='chart-grid-line chart-grid-line-y' x1='34' y1='180' x2='336' y2='180' stroke='#edf2f7' stroke-width='1'/>",
+                        "<line class='chart-grid-line chart-grid-line-y' x1='34' y1='154' x2='336' y2='154' stroke='#edf2f7' stroke-width='1'/>",
+                        "<line class='chart-grid-line chart-grid-line-y' x1='34' y1='128' x2='336' y2='128' stroke='#edf2f7' stroke-width='1'/>",
+                        "<line class='chart-grid-line chart-grid-line-y' x1='34' y1='102' x2='336' y2='102' stroke='#edf2f7' stroke-width='1'/>",
+                        "<line class='chart-grid-line chart-grid-line-y' x1='34' y1='76' x2='336' y2='76' stroke='#edf2f7' stroke-width='1'/>",
+                        "<line class='chart-grid-line chart-grid-line-y' x1='34' y1='50' x2='336' y2='50' stroke='#edf2f7' stroke-width='1'/>");
+
+        String frequencyBandSvg = extractSvgByClass(html, "frequency-band-column-chart");
+        assertThat(gridLineElements(frequencyBandSvg))
+                .containsExactly(
+                        "<line class='chart-grid-line chart-grid-line-y' x1='34' y1='180' x2='336' y2='180' stroke='#edf2f7' stroke-width='1'/>",
+                        "<line class='chart-grid-line chart-grid-line-y' x1='34' y1='115' x2='336' y2='115' stroke='#edf2f7' stroke-width='1'/>",
+                        "<line class='chart-grid-line chart-grid-line-y' x1='34' y1='50' x2='336' y2='50' stroke='#edf2f7' stroke-width='1'/>");
+    }
+
     /**
      * 覆盖科学备考建议频率卡片的线性进度条时,应使用 PDF 友好的 SVG 几何图形,避免 CSS 圆角裁剪在 PDF 中变尖。
      */
@@ -426,6 +459,25 @@ class ClasspathOutlookExamSprintReportRendererTest {
         return svgStartTags;
     }
 
+    private String extractSvgByClass(String html, String cssClass) {
+        Pattern svgPattern = Pattern.compile("<svg\\b(?=[^>]*class='[^']*" + Pattern.quote(cssClass) + "[^']*')[\\s\\S]*?</svg>");
+        Matcher matcher = svgPattern.matcher(html);
+        if (matcher.find()) {
+            return matcher.group();
+        }
+        throw new AssertionError("svg should exist for class '" + cssClass + "' in HTML fragment:\n" + html);
+    }
+
+    private List<String> gridLineElements(String svg) {
+        Pattern gridLinePattern = Pattern.compile("<line class='chart-grid-line chart-grid-line-y'[^>]*/>");
+        Matcher matcher = gridLinePattern.matcher(svg);
+        List<String> gridLines = new ArrayList<>();
+        while (matcher.find()) {
+            gridLines.add(matcher.group());
+        }
+        return gridLines;
+    }
+
     private JsonNode payloadWithCallerControlledTextSamples() throws Exception {
         ObjectNode payload = (ObjectNode) callerVocabularyPayload();
         payload.put("StudentName", "注入学生<script>alert(1)</script>");