From 7315b9ef11a9e46796b7526be6aef783dc2e8627 Mon Sep 17 00:00:00 2001 From: tanglong <842690096@qq.com> Date: Tue, 28 Jul 2026 15:52:23 +0800 Subject: [PATCH] =?UTF-8?q?=E6=98=8E=E7=BB=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/semantic-routing.md | 5 +- .../report/ExcelReportService.java | 143 +----------------- .../report/TeachingAlertPushService.java | 38 ++--- .../report/ExcelReportServiceTest.java | 28 ++-- .../report/TeachingAlertPushServiceTest.java | 5 +- 5 files changed, 45 insertions(+), 174 deletions(-) diff --git a/docs/semantic-routing.md b/docs/semantic-routing.md index d9090aa..e061504 100644 --- a/docs/semantic-routing.md +++ b/docs/semantic-routing.md @@ -31,13 +31,14 @@ - 指标:外呼总量、接通率、有效通话与时长、录音覆盖率、AI 质检、敏感词预警。 - 人员链路:企业微信 UserID → 本地学邦账号映射 → EC 员工账号;管理层再按企业微信授权组织匹配 EC 部门。 - 明细权限:管理员、区域总和城市总按运动馆汇总;馆长按本馆人员展示;顾问和教练仅展示本人。 -- 企业微信返回:先返回日报摘要,再向当前查询人发送 Excel;管理员附件为“运动馆明细”,馆长附件为“人员明细”。 +- 企业微信返回:日报汇总指标全部通过消息回复,Excel 不再包含“日报汇总”页;管理员附件只保留“运动馆明细”,馆长附件只保留“人员明细”。 - 账号风控暂未启用。 ### 学员预警名单附件 - 课时预警和高频缺勤名单超过聊天展示上限时,统一发送 `.xlsx`,不再发送 `.txt`。 -- Excel 按运动馆、学员和预警类型逐行展示,并保留日期、接收人、剩余课时、排课及缺勤次数。 +- 日期、接收人、预警数量和运动馆数量通过企业微信消息展示。 +- Excel 只保留详细名单,按运动馆、学员和预警类型逐行展示剩余课时、排课及缺勤次数。 ## 扩展新意图 diff --git a/src/main/java/com/company/agentfoundation/report/ExcelReportService.java b/src/main/java/com/company/agentfoundation/report/ExcelReportService.java index 3b97910..af08070 100644 --- a/src/main/java/com/company/agentfoundation/report/ExcelReportService.java +++ b/src/main/java/com/company/agentfoundation/report/ExcelReportService.java @@ -17,7 +17,6 @@ import org.springframework.stereotype.Service; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.time.LocalDate; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; @@ -26,10 +25,6 @@ import java.util.Map; @Service public class ExcelReportService { public byte[] teachingWarnings( - String title, - LocalDate startDate, - LocalDate endDate, - String recipient, List> alerts ) { SXSSFWorkbook workbook = new SXSSFWorkbook(200); @@ -38,16 +33,12 @@ public class ExcelReportService { Styles styles = styles(workbook); Sheet summary = workbook.createSheet("预警名单"); summary.setDisplayGridlines(false); - title(summary, styles, title, 7); - metadata(summary, styles, 1, "日期", dateRange(startDate, endDate), "接收人", recipient); - metadata(summary, styles, 2, "预警记录", alerts.size(), "运动馆数量", - alerts.stream().map(this::campusName).distinct().count()); String[] headers = {"序号", "运动馆", "学员", "预警类型", "剩余课时", "排课次数", "缺勤次数", "顾问/教练"}; - Row header = summary.createRow(4); + Row header = summary.createRow(0); for (int index = 0; index < headers.length; index++) cell(header, index, headers[index], styles.header()); - int rowIndex = 5; + int rowIndex = 1; for (int index = 0; index < alerts.size(); index++) { Map alert = alerts.get(index); Row row = summary.createRow(rowIndex++); @@ -60,7 +51,7 @@ public class ExcelReportService { nullableNumber(row, 6, alert.get("absenceCount"), styles.integer()); cell(row, 7, first(text(alert, "advisorName"), text(alert, "coachName")), styles.body()); } - configureTable(summary, 4, Math.max(rowIndex - 1, 4), headers.length, styles); + configureTable(summary, 0, Math.max(rowIndex - 1, 0), headers.length, styles); widths(summary, 8, 10, 26, 16, 16, 12, 12, 12, 18); return bytes(workbook); } catch (IOException exception) { @@ -73,43 +64,10 @@ public class ExcelReportService { public byte[] ecCallCompliance(Map report) { try (XSSFWorkbook workbook = new XSSFWorkbook()) { Styles styles = styles(workbook); - Sheet summary = workbook.createSheet("日报汇总"); - summary.setDisplayGridlines(false); - title(summary, styles, "EC电话合规监控日报", 5); - Map window = map(report.get("reportWindow")); - Map scope = map(report.get("scope")); - metadata(summary, styles, 1, "日期", - dateRange(date(window.get("startDate")), date(window.get("endDate"))), - "权限范围", text(scope.get("label"))); - - Map calls = map(report.get("callMetrics")); - Map recording = map(report.get("recordingCompliance")); - Map quality = map(report.get("qualityCompliance")); - Map sensitive = map(report.get("sensitiveWordRisk")); - String[] headers = {"指标", "数值", "指标", "数值"}; - Row header = summary.createRow(3); - for (int index = 0; index < headers.length; index++) cell(header, index, headers[index], styles.header()); - metricRow(summary, styles, 4, "外呼总量", calls.get("outboundTotal"), - "接通次数", calls.get("connectedCount")); - metricRow(summary, styles, 5, "接通率", percent(calls.get("connectionRate")), - "平均通话时长(秒)", calls.get("averageDurationSeconds")); - metricRow(summary, styles, 6, "有效通话", calls.get("effectiveCallCount"), - "有效通话时长(秒)", calls.get("effectiveDurationSeconds")); - metricRow(summary, styles, 7, "录音覆盖率", percent(recording.get("recordingCoverageRate")), - "已录音/应录音", text(recording.get("recordedCallCount")) + "/" + text(recording.get("eligibleCallCount"))); - metricRow(summary, styles, 8, "AI已分析", quality.get("analyzedCount"), - "质检问题", quality.get("issueCount")); - metricRow(summary, styles, 9, "敏感词命中", sensitive.get("hitCount"), - "涉及通话", sensitive.get("hitCallCount")); - widths(summary, 4, 22, 18, 24, 18); - - List> breakdown = listOfMaps(report.get("venueBreakdown")); - String sheetName = "运动馆明细"; - if (breakdown.isEmpty()) { - breakdown = listOfMaps(report.get("personBreakdown")); - sheetName = "人员明细"; - } - if (!breakdown.isEmpty()) writeEcBreakdown(workbook, styles, sheetName, breakdown); + boolean venueView = report.containsKey("venueBreakdown"); + List> breakdown = listOfMaps( + report.get(venueView ? "venueBreakdown" : "personBreakdown")); + writeEcBreakdown(workbook, styles, venueView ? "运动馆明细" : "人员明细", breakdown); return bytes(workbook); } catch (IOException exception) { throw new IllegalStateException("生成EC电话合规日报Excel失败", exception); @@ -149,57 +107,6 @@ public class ExcelReportService { widths(sheet, 13, 28, 16, 12, 12, 12, 16, 12, 16, 12, 16, 12, 12, 14); } - private void title(Sheet sheet, Styles styles, String value, int columns) { - sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, columns - 1)); - Row row = sheet.createRow(0); - row.setHeightInPoints(28); - cell(row, 0, value, styles.title()); - } - - private void metadata( - Sheet sheet, - Styles styles, - int rowIndex, - String firstLabel, - Object firstValue, - String secondLabel, - Object secondValue - ) { - Row row = sheet.createRow(rowIndex); - cell(row, 0, firstLabel, styles.label()); - cell(row, 1, text(firstValue), styles.body()); - cell(row, 3, secondLabel, styles.label()); - cell(row, 4, text(secondValue), styles.body()); - } - - private void metricRow( - Sheet sheet, - Styles styles, - int rowIndex, - String firstLabel, - Object firstValue, - String secondLabel, - Object secondValue - ) { - Row row = sheet.createRow(rowIndex); - cell(row, 0, firstLabel, styles.label()); - if (firstValue instanceof PercentValue percentage) { - number(row, 1, percentage.value(), styles.percent()); - } else if (firstValue instanceof Number number) { - number(row, 1, number.doubleValue(), styles.decimal()); - } else { - cell(row, 1, text(firstValue), styles.body()); - } - cell(row, 2, secondLabel, styles.label()); - if (secondValue instanceof PercentValue percentage) { - number(row, 3, percentage.value(), styles.percent()); - } else if (secondValue instanceof Number number) { - number(row, 3, number.doubleValue(), styles.decimal()); - } else { - cell(row, 3, text(secondValue), styles.body()); - } - } - private void configureTable(Sheet sheet, int headerRow, int lastRow, int columns, Styles styles) { sheet.createFreezePane(0, headerRow + 1); if (lastRow >= headerRow) sheet.setAutoFilter(new CellRangeAddress(headerRow, lastRow, 0, columns - 1)); @@ -216,13 +123,6 @@ public class ExcelReportService { } private Styles styles(Workbook workbook) { - Font titleFont = workbook.createFont(); - titleFont.setBold(true); titleFont.setFontHeightInPoints((short) 16); titleFont.setColor(IndexedColors.WHITE.getIndex()); - CellStyle title = workbook.createCellStyle(); - title.setFillForegroundColor(IndexedColors.DARK_BLUE.getIndex()); - title.setFillPattern(FillPatternType.SOLID_FOREGROUND); - title.setFont(titleFont); title.setAlignment(HorizontalAlignment.LEFT); - Font headerFont = workbook.createFont(); headerFont.setBold(true); headerFont.setColor(IndexedColors.WHITE.getIndex()); CellStyle header = workbook.createCellStyle(); @@ -231,12 +131,6 @@ public class ExcelReportService { header.setFont(headerFont); header.setAlignment(HorizontalAlignment.CENTER); header.setBorderBottom(BorderStyle.THIN); - Font labelFont = workbook.createFont(); - labelFont.setBold(true); labelFont.setColor(IndexedColors.DARK_BLUE.getIndex()); - CellStyle label = workbook.createCellStyle(); - label.setFont(labelFont); label.setFillForegroundColor(IndexedColors.LIGHT_CORNFLOWER_BLUE.getIndex()); - label.setFillPattern(FillPatternType.SOLID_FOREGROUND); - CellStyle body = workbook.createCellStyle(); body.setVerticalAlignment(org.apache.poi.ss.usermodel.VerticalAlignment.CENTER); CellStyle alternate = workbook.createCellStyle(); @@ -247,7 +141,7 @@ public class ExcelReportService { CellStyle integer = workbook.createCellStyle(); integer.setDataFormat(workbook.createDataFormat().getFormat("#,##0")); CellStyle decimal = workbook.createCellStyle(); decimal.setDataFormat(workbook.createDataFormat().getFormat("#,##0.0")); CellStyle percent = workbook.createCellStyle(); percent.setDataFormat(workbook.createDataFormat().getFormat("0.0%")); - return new Styles(title, header, label, body, alternate, integer, decimal, percent); + return new Styles(header, body, alternate, integer, decimal, percent); } private void cell(Row row, int column, String value, CellStyle style) { @@ -291,30 +185,12 @@ public class ExcelReportService { return first(text(alert, "campusName"), "未匹配运动馆"); } - private String dateRange(LocalDate startDate, LocalDate endDate) { - if (startDate == null) return ""; - return startDate.equals(endDate) ? startDate.toString() : startDate + " 至 " + endDate; - } - - private LocalDate date(Object value) { - if (value instanceof LocalDate date) return date; - return LocalDate.parse(text(value)); - } - - private PercentValue percent(Object value) { - return new PercentValue(numberValue(value)); - } - private double numberValue(Object value) { if (value instanceof Number number) return number.doubleValue(); try { return Double.parseDouble(text(value)); } catch (NumberFormatException ignored) { return 0D; } } - private Map map(Object value) { - return value instanceof Map map ? map : Map.of(); - } - private List> listOfMaps(Object value) { if (!(value instanceof List list)) return List.of(); List> result = new ArrayList<>(); @@ -339,11 +215,8 @@ public class ExcelReportService { return first == null || first.isBlank() ? second : first; } - private record PercentValue(double value) {} private record Styles( - CellStyle title, CellStyle header, - CellStyle label, CellStyle body, CellStyle alternate, CellStyle integer, diff --git a/src/main/java/com/company/agentfoundation/teaching/report/TeachingAlertPushService.java b/src/main/java/com/company/agentfoundation/teaching/report/TeachingAlertPushService.java index e904635..47fe101 100644 --- a/src/main/java/com/company/agentfoundation/teaching/report/TeachingAlertPushService.java +++ b/src/main/java/com/company/agentfoundation/teaching/report/TeachingAlertPushService.java @@ -13,7 +13,6 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; -import java.time.LocalDate; @Service class TeachingAlertPushService { @@ -90,15 +89,9 @@ class TeachingAlertPushService { .toList(); if (ordered.size() > LARGE_LIST_FILE_THRESHOLD) { try { - Map first = ordered.get(0); - LocalDate startDate = parseDate(text(first, "warningStartDate")); - LocalDate endDate = parseDate(text(first, "warningEndDate")); - byte[] workbook = excelReportService.teachingWarnings( - "学员" + warningTitle(ordered) + "名单", - startDate, - endDate, - batch.name().isEmpty() ? batch.userId() : batch.name(), - ordered); + wecom.sendMarkdown(APP_KEY, batch.userId(), formatFileSummary(batch, ordered)); + sentMessages++; + byte[] workbook = excelReportService.teachingWarnings(ordered); wecom.sendExcelFile(APP_KEY, batch.userId(), fileName(ordered), workbook); sentMessages++; } catch (RuntimeException exception) { @@ -125,6 +118,23 @@ class TeachingAlertPushService { return new PushResult(alerts.size(), targetRecipientCount, sentMessages, unmatched, failedRecipients); } + private String formatFileSummary(RecipientBatch batch, List> alerts) { + Map first = alerts.get(0); + String start = text(first, "warningStartDate"); + String end = text(first, "warningEndDate"); + String period = start.isEmpty() ? "当前查询范围" + : start.equals(end) ? start : start + " 至 " + end; + long venueCount = alerts.stream().map(TeachingAlertPushService::campusName).distinct().count(); + return new StringBuilder("### ⚠️ 学员运营预警\n") + .append("> 类型:**").append(warningTitle(alerts)).append("**\n") + .append("> 日期:").append(period).append("\n") + .append("> 接收人:").append(batch.name().isEmpty() ? batch.userId() : batch.name()).append("\n") + .append("> 预警记录:").append(alerts.size()).append("条") + .append(" | 运动馆:").append(venueCount).append("个\n\n") + .append("详细名单、剩余课时及排课/缺勤次数请查看 Excel 附件。") + .toString(); + } + private List alertCopyUserIds() { FoundationProperties.Wecom.App app = properties.getWecom().getApps().get(APP_KEY); if (app == null || app.getAlertCopyUserIds() == null) return List.of(); @@ -211,14 +221,6 @@ class TeachingAlertPushService { Object value = row.get(key); return value == null ? "" : String.valueOf(value).trim(); } - private static LocalDate parseDate(String value) { - try { - return value == null || value.isBlank() ? LocalDate.now() : LocalDate.parse(value); - } catch (RuntimeException ignored) { - return LocalDate.now(); - } - } - record PushResult(long alertCount, int recipientCount, int sentMessageCount, long unmatchedAlertCount, int failedRecipientCount) {} diff --git a/src/test/java/com/company/agentfoundation/report/ExcelReportServiceTest.java b/src/test/java/com/company/agentfoundation/report/ExcelReportServiceTest.java index 65e3368..6af02a8 100644 --- a/src/test/java/com/company/agentfoundation/report/ExcelReportServiceTest.java +++ b/src/test/java/com/company/agentfoundation/report/ExcelReportServiceTest.java @@ -5,7 +5,6 @@ import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.junit.jupiter.api.Test; import java.io.ByteArrayInputStream; -import java.time.LocalDate; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -18,10 +17,6 @@ class ExcelReportServiceTest { @Test void createsTeachingWarningWorkbookThatCanBeOpened() throws Exception { byte[] content = service.teachingWarnings( - "课时预警", - LocalDate.of(2026, 7, 1), - LocalDate.of(2026, 7, 28), - "浦东金桥馆长", List.of(Map.of( "campusName", "浦东金桥-跃动运动馆", "studentName", "测试学员", @@ -34,11 +29,11 @@ class ExcelReportServiceTest { try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(content))) { assertThat(workbook.getNumberOfSheets()).isEqualTo(1); assertThat(workbook.getSheetName(0)).isEqualTo("预警名单"); - assertThat(workbook.getSheetAt(0).getRow(4).getCell(1).getStringCellValue()).isEqualTo("运动馆"); - assertThat(workbook.getSheetAt(0).getRow(5).getCell(1).getStringCellValue()) + assertThat(workbook.getSheetAt(0).getRow(0).getCell(1).getStringCellValue()).isEqualTo("运动馆"); + assertThat(workbook.getSheetAt(0).getRow(1).getCell(1).getStringCellValue()) .isEqualTo("浦东金桥-跃动运动馆"); - assertThat(workbook.getSheetAt(0).getRow(5).getCell(4).getCellType()).isEqualTo(CellType.NUMERIC); - assertThat(workbook.getSheetAt(0).getRow(5).getCell(4).getNumericCellValue()).isEqualTo(8.5D); + assertThat(workbook.getSheetAt(0).getRow(1).getCell(4).getCellType()).isEqualTo(CellType.NUMERIC); + assertThat(workbook.getSheetAt(0).getRow(1).getCell(4).getNumericCellValue()).isEqualTo(8.5D); } } @@ -50,11 +45,11 @@ class ExcelReportServiceTest { byte[] content = service.ecCallCompliance(report); try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(content))) { - assertThat(workbook.getSheetName(0)).isEqualTo("日报汇总"); - assertThat(workbook.getSheetName(1)).isEqualTo("运动馆明细"); - assertThat(workbook.getSheetAt(1).getRow(1).getCell(0).getStringCellValue()) + assertThat(workbook.getNumberOfSheets()).isEqualTo(1); + assertThat(workbook.getSheetName(0)).isEqualTo("运动馆明细"); + assertThat(workbook.getSheetAt(0).getRow(1).getCell(0).getStringCellValue()) .isEqualTo("浦东金桥-跃动运动馆"); - assertThat(workbook.getSheetAt(1).getRow(1).getCell(2).getNumericCellValue()).isEqualTo(12D); + assertThat(workbook.getSheetAt(0).getRow(1).getCell(2).getNumericCellValue()).isEqualTo(12D); } } @@ -68,9 +63,10 @@ class ExcelReportServiceTest { byte[] content = service.ecCallCompliance(report); try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(content))) { - assertThat(workbook.getSheetName(1)).isEqualTo("人员明细"); - assertThat(workbook.getSheetAt(1).getRow(1).getCell(1).getStringCellValue()).isEqualTo("张三"); - assertThat(workbook.getSheetAt(1).getRow(2).getCell(1).getStringCellValue()).isEqualTo("李四"); + assertThat(workbook.getNumberOfSheets()).isEqualTo(1); + assertThat(workbook.getSheetName(0)).isEqualTo("人员明细"); + assertThat(workbook.getSheetAt(0).getRow(1).getCell(1).getStringCellValue()).isEqualTo("张三"); + assertThat(workbook.getSheetAt(0).getRow(2).getCell(1).getStringCellValue()).isEqualTo("李四"); } } diff --git a/src/test/java/com/company/agentfoundation/teaching/report/TeachingAlertPushServiceTest.java b/src/test/java/com/company/agentfoundation/teaching/report/TeachingAlertPushServiceTest.java index d6da606..a509b2b 100644 --- a/src/test/java/com/company/agentfoundation/teaching/report/TeachingAlertPushServiceTest.java +++ b/src/test/java/com/company/agentfoundation/teaching/report/TeachingAlertPushServiceTest.java @@ -13,9 +13,7 @@ import static org.assertj.core.api.Assertions.assertThat; import org.mockito.ArgumentCaptor; import static org.mockito.ArgumentMatchers.contains; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; class TeachingAlertPushServiceTest { @@ -94,7 +92,8 @@ class TeachingAlertPushServiceTest { verify(wecom).sendExcelFile(org.mockito.ArgumentMatchers.eq("sports-venue"), org.mockito.ArgumentMatchers.eq("manager1"), contains("课时预警名单.xlsx"), any(byte[].class)); - verify(wecom, never()).sendMarkdown(anyString(), anyString(), anyString()); + verify(wecom).sendMarkdown(org.mockito.ArgumentMatchers.eq("sports-venue"), + org.mockito.ArgumentMatchers.eq("manager1"), contains("详细名单")); } private Map alert(String id, String name, String campus, String hours) {