diff --git a/src/main/java/com/company/agentfoundation/config/FoundationProperties.java b/src/main/java/com/company/agentfoundation/config/FoundationProperties.java index c314484..522b199 100644 --- a/src/main/java/com/company/agentfoundation/config/FoundationProperties.java +++ b/src/main/java/com/company/agentfoundation/config/FoundationProperties.java @@ -132,6 +132,7 @@ public class FoundationProperties { private String secret; private String callbackPath; private List adminUserIds = new ArrayList<>(); + private List alertCopyUserIds = new ArrayList<>(); public String getName() { return name; } public void setName(String name) { this.name = name; } @@ -143,6 +144,8 @@ public class FoundationProperties { public void setCallbackPath(String callbackPath) { this.callbackPath = callbackPath; } public List getAdminUserIds() { return adminUserIds; } public void setAdminUserIds(List adminUserIds) { this.adminUserIds = adminUserIds; } + public List getAlertCopyUserIds() { return alertCopyUserIds; } + public void setAlertCopyUserIds(List alertCopyUserIds) { this.alertCopyUserIds = alertCopyUserIds; } } } diff --git a/src/main/java/com/company/agentfoundation/integration/wecom/controller/WecomCallbackController.java b/src/main/java/com/company/agentfoundation/integration/wecom/controller/WecomCallbackController.java index 1d6cf0c..0f0b0ec 100644 --- a/src/main/java/com/company/agentfoundation/integration/wecom/controller/WecomCallbackController.java +++ b/src/main/java/com/company/agentfoundation/integration/wecom/controller/WecomCallbackController.java @@ -169,10 +169,10 @@ public class WecomCallbackController { if (!reportLabel.isEmpty()) { String running = runningReports.putIfAbsent(taskKey, reportLabel); if (running != null) { - sendProgressMessage(appKey, fromUser, "正在查询[" + running + "]数据,请等待查询完成后再获取其他日报。"); + sendProgressMessage(appKey, fromUser, busyMessage(running)); return "success"; } - sendProgressMessage(appKey, fromUser, "正在查询[" + reportLabel + "]数据......"); + sendProgressMessage(appKey, fromUser, startedMessage(reportLabel)); } try { wecomTaskExecutor.execute(() -> processAndReply( @@ -224,16 +224,31 @@ public class WecomCallbackController { private void sendProgressMessage(String appKey, String userId, String content) { try { - wecomClient.sendText(appKey, userId, content); + wecomClient.sendMarkdown(appKey, userId, content); } catch (Exception exception) { log.warn("wecom report progress message failed appKey={} userId={} err={}", appKey, userId, exception.getMessage()); } } + private String startedMessage(String reportLabel) { + return "### ⏳ 正在查询\n" + + "> **" + reportLabel + "**\n" + + "> 正在获取最新数据,请稍候……\n\n" + + "查询完成后将自动发送结果,请勿重复提交日报指令。"; + } + + private String busyMessage(String reportLabel) { + return "### ⏳ 查询任务进行中\n" + + "> **" + reportLabel + "** 尚未完成\n\n" + + "请等待当前任务完成后,再查询其他日报。"; + } + private String reportLabel(String userText) { String text = userText == null ? "" : userText.trim(); - if (text.contains("推送") && text.contains("低课时")) return "低课时预警名单"; + if (text.contains("推送") && (text.contains("低课时") || text.contains("预警"))) { + return "学员运营预警数据"; + } if (text.contains("客资")) return "客资日报"; if (text.contains("学员") || text.contains("教学") || text.contains("出勤")) return "学员教学出勤日报"; if (text.contains("企业微信") || text.contains("客户运营")) return "企业微信客户运营日报"; diff --git a/src/main/java/com/company/agentfoundation/semantic/SemanticRecognitionService.java b/src/main/java/com/company/agentfoundation/semantic/SemanticRecognitionService.java index 5d95c79..8d370d5 100644 --- a/src/main/java/com/company/agentfoundation/semantic/SemanticRecognitionService.java +++ b/src/main/java/com/company/agentfoundation/semantic/SemanticRecognitionService.java @@ -44,8 +44,8 @@ public class SemanticRecognitionService { public SemanticRecognition recognize(String userText, LocalDate today) { String command = userText == null ? "" : userText; - if (command.contains("推送") && command.contains("低课时") && command.contains("名单")) { - return new SemanticRecognition("PUSH_LOW_HOURS_WARNING_LIST", 0.99D, + if (isTeachingWarningPushCommand(command)) { + return new SemanticRecognition("PUSH_TEACHING_WARNING_DATA", 0.99D, Map.of(), "DETERMINISTIC_COMMAND"); } try { @@ -90,8 +90,8 @@ public class SemanticRecognitionService { private SemanticRecognition fallback(String userText) { String text = userText == null ? "" : userText; - if (text.contains("推送") && text.contains("低课时") && text.contains("名单")) { - return new SemanticRecognition("PUSH_LOW_HOURS_WARNING_LIST", 0.99D, + if (isTeachingWarningPushCommand(text)) { + return new SemanticRecognition("PUSH_TEACHING_WARNING_DATA", 0.99D, Map.of(), "KEYWORD_FALLBACK"); } boolean teachingAttendanceReport = text.contains("教学出勤") || text.contains("学员出勤") @@ -113,6 +113,13 @@ public class SemanticRecognitionService { return SemanticRecognition.unknown("FALLBACK"); } + private boolean isTeachingWarningPushCommand(String text) { + if (text == null || !text.contains("推送")) return false; + return (text.contains("低课时") && (text.contains("名单") || text.contains("预警"))) + || (text.contains("预警") && (text.contains("数据") || text.contains("名单") + || text.contains("学员") || text.contains("教学") || text.contains("出勤"))); + } + private String systemPrompt(LocalDate today) { String catalog = handlers.stream() .map(handler -> "- %s:%s;示例:%s".formatted(handler.intent(), handler.description(), handler.example())) diff --git a/src/main/java/com/company/agentfoundation/semantic/handler/LowHoursWarningPushIntentHandler.java b/src/main/java/com/company/agentfoundation/semantic/handler/LowHoursWarningPushIntentHandler.java index c5b4872..414ff37 100644 --- a/src/main/java/com/company/agentfoundation/semantic/handler/LowHoursWarningPushIntentHandler.java +++ b/src/main/java/com/company/agentfoundation/semantic/handler/LowHoursWarningPushIntentHandler.java @@ -1,6 +1,7 @@ package com.company.agentfoundation.semantic.handler; import com.company.agentfoundation.semantic.SemanticIntentHandler; +import com.company.agentfoundation.semantic.SemanticDateRangeResolver; import com.company.agentfoundation.semantic.SemanticRecognition; import com.company.agentfoundation.semantic.SemanticRequestContext; import com.company.agentfoundation.teaching.report.LowHoursWarningPushService; @@ -13,35 +14,45 @@ import org.springframework.web.server.ResponseStatusException; public class LowHoursWarningPushIntentHandler implements SemanticIntentHandler { private static final Logger log = LoggerFactory.getLogger(LowHoursWarningPushIntentHandler.class); private final LowHoursWarningPushService service; + private final SemanticDateRangeResolver dateRangeResolver; - public LowHoursWarningPushIntentHandler(LowHoursWarningPushService service) { + public LowHoursWarningPushIntentHandler( + LowHoursWarningPushService service, + SemanticDateRangeResolver dateRangeResolver + ) { this.service = service; + this.dateRangeResolver = dateRangeResolver; } - @Override public String intent() { return "PUSH_LOW_HOURS_WARNING_LIST"; } - @Override public String description() { return "生成低于10课时的学员名单,并推送给权限范围内各场馆馆长"; } - @Override public String example() { return "推送低课时名单"; } + @Override public String intent() { return "PUSH_TEACHING_WARNING_DATA"; } + @Override public String description() { return "生成低课时和高频缺勤预警名单,并推送给权限范围内各场馆馆长"; } + @Override public String example() { return "推送本月预警数据"; } @Override public String handle(SemanticRequestContext context, SemanticRecognition recognition) { try { - LowHoursWarningPushService.PushSummary result = service.push(context.wecomUserId()); - return "### ✅ 低课时名单推送完成\n" + SemanticDateRangeResolver.DateRange range = dateRangeResolver.resolve(context, recognition); + LowHoursWarningPushService.PushSummary result = service.push( + context.wecomUserId(), range.startDate(), range.endDate()); + return "### ✅ 学员运营预警推送完成\n" + + "> 日期:**" + range.startDate() + + (range.startDate().equals(range.endDate()) ? "" : " 至 " + range.endDate()) + "**\n" + "> 范围:**" + result.scopeLabel() + "**\n\n" - + "低于10课时:" + result.warningStudentCount() + "人" + + "低于10课时:" + result.lowHoursStudentCount() + "人" + + " | 高频缺勤:" + result.frequentAbsenceStudentCount() + "人" + " | 匹配馆长:" + result.managerCount() + "人" + " | 成功消息:" + result.sentMessageCount() + "条" + "\n\n未匹配馆长的学员:" + result.unmatchedStudentCount() + "人" + " | 发送失败馆长:" + result.failedManagerCount() + "人"; } catch (ResponseStatusException exception) { if (exception.getStatusCode().value() == 403) { - return exception.getReason() == null ? "无权限执行低课时名单推送" : exception.getReason(); + return exception.getReason() == null ? "无权限执行学员预警推送" : exception.getReason(); } throw exception; } catch (Exception exception) { - log.error("low hours warning push failed wecomUserId={} err={}", + log.error("teaching warning push failed wecomUserId={} err={}", context.wecomUserId(), exception.getMessage(), exception); - return "低课时名单推送失败,请稍后重试。"; + return "学员运营预警推送失败,请稍后重试。"; } } } diff --git a/src/main/java/com/company/agentfoundation/semantic/handler/StudentTeachingAttendanceReportIntentHandler.java b/src/main/java/com/company/agentfoundation/semantic/handler/StudentTeachingAttendanceReportIntentHandler.java index af90286..8d17cf9 100644 --- a/src/main/java/com/company/agentfoundation/semantic/handler/StudentTeachingAttendanceReportIntentHandler.java +++ b/src/main/java/com/company/agentfoundation/semantic/handler/StudentTeachingAttendanceReportIntentHandler.java @@ -97,13 +97,13 @@ public class StudentTeachingAttendanceReportIntentHandler implements SemanticInt } reply.append("\n\n**高频缺勤** 排课≥3次且缺勤≥2次:") .append(absence.path("studentCount").asInt()).append("人") - .append("\n\n**预警推送** 已匹配教练/顾问/馆长:") + .append("\n\n**可推送对象** 已匹配教练/顾问/馆长:") .append(delivery.path("resolvedRecipientCount").asInt()).append("人"); appendWarningStudents(reply, "低课时", lowHours.path("students"), true); appendWarningStudents(reply, "高频缺勤", absence.path("students"), false); appendRecipients(reply, delivery.path("resolvedRecipients")); - if (!delivery.path("readyForAutomaticPush").asBoolean(false) + if (!delivery.path("readyForManualPush").asBoolean(false) && ((lowHours.path("detailsAvailable").asBoolean(false) && lowHours.path("studentCount").asInt() > 0) || absence.path("studentCount").asInt() > 0)) { reply.append("\n\n部分预警未匹配企业微信 UserID,请补全人员映射。"); @@ -113,7 +113,7 @@ public class StudentTeachingAttendanceReportIntentHandler implements SemanticInt private void appendRecipients(StringBuilder reply, JsonNode recipients) { if (!recipients.isArray() || recipients.isEmpty()) return; - reply.append("\n\n**预警推送名单(前10)**"); + reply.append("\n\n**可推送人员(前10)**"); int limit = Math.min(10, recipients.size()); for (int index = 0; index < limit; index++) { JsonNode recipient = recipients.get(index); diff --git a/src/main/java/com/company/agentfoundation/teaching/report/LowHoursWarningPushService.java b/src/main/java/com/company/agentfoundation/teaching/report/LowHoursWarningPushService.java index c1b9ed0..84b3dfc 100644 --- a/src/main/java/com/company/agentfoundation/teaching/report/LowHoursWarningPushService.java +++ b/src/main/java/com/company/agentfoundation/teaching/report/LowHoursWarningPushService.java @@ -12,7 +12,6 @@ import org.springframework.web.server.ResponseStatusException; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; -import java.time.ZoneId; import java.util.ArrayList; import java.util.Comparator; import java.util.LinkedHashMap; @@ -22,7 +21,6 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.Executor; -import java.util.concurrent.ConcurrentHashMap; import java.util.function.Supplier; @Service @@ -33,35 +31,56 @@ public class LowHoursWarningPushService { private final XuebangApiClient xuebang; private final ReportDataScopeService scopeService; + private final XuebangTeachingReportDataService realtimeData; private final TeachingAlertRecipientService recipientService; private final TeachingAlertPushService pushService; private final Executor executor; - private final Set automaticDailyTasks = ConcurrentHashMap.newKeySet(); public LowHoursWarningPushService( XuebangApiClient xuebang, ReportDataScopeService scopeService, + XuebangTeachingReportDataService realtimeData, TeachingAlertRecipientService recipientService, TeachingAlertPushService pushService, @Qualifier("xuebangReportExecutor") Executor executor ) { this.xuebang = xuebang; this.scopeService = scopeService; + this.realtimeData = realtimeData; this.recipientService = recipientService; this.pushService = pushService; this.executor = executor; } - public PushSummary push(String wecomUserId) { - return push(wecomUserId, true); - } - - private PushSummary push(String wecomUserId, boolean force) { + public PushSummary push(String wecomUserId, LocalDate startDate, LocalDate endDate) { + if (wecomUserId == null || wecomUserId.isBlank()) { + throw new IllegalArgumentException("wecomUserId is required"); + } + LocalDate today = LocalDate.now(java.time.ZoneId.of("Asia/Shanghai")); + LocalDate effectiveStart = startDate == null ? today : startDate; + LocalDate effectiveEnd = endDate == null ? effectiveStart : endDate; + if (effectiveEnd.isBefore(effectiveStart) || effectiveStart.plusYears(1).isBefore(effectiveEnd)) { + throw new IllegalArgumentException("invalid warning date range"); + } Scope scope = scopeService.resolveSportsVenueScope(wecomUserId); if (scope.self()) { - throw new ResponseStatusException(HttpStatus.FORBIDDEN, "仅管理员、区域总、城市总或馆长可以推送低课时名单"); + throw new ResponseStatusException(HttpStatus.FORBIDDEN, + "仅管理员、区域总、城市总或馆长可以推送学员运营预警"); } + XuebangTeachingReportDataService.Snapshot snapshot = realtimeData.load(effectiveStart, effectiveEnd, scope); + List> lowHoursAlerts = loadLowHoursAlerts(scope); + List> alerts = new ArrayList<>( + lowHoursAlerts.size() + snapshot.frequentAbsenceStudents().size()); + alerts.addAll(lowHoursAlerts); + alerts.addAll(snapshot.frequentAbsenceStudents()); + recipientService.enrich(alerts); + TeachingAlertPushService.PushResult pushed = pushService.pushToVenueManagers(alerts); + return new PushSummary(scope.label(), lowHoursAlerts.size(), snapshot.frequentAbsenceCount(), + pushed.recipientCount(), pushed.sentMessageCount(), pushed.unmatchedAlertCount(), + pushed.failedRecipientCount()); + } + private List> loadLowHoursAlerts(Scope scope) { Map studentParameters = new LinkedHashMap<>(); studentParameters.put("stuStatus", 1); studentParameters.put("studentContractType", "OFFICIAL_STUDENT"); @@ -86,28 +105,10 @@ public class LowHoursWarningPushService { balance.add(product); } - List> alerts = balances.values().stream() + return balances.values().stream() .filter(value -> value.remaining.signum() >= 0 && value.remaining.compareTo(BigDecimal.TEN) < 0) .sorted(Comparator.comparing(Balance::remaining)) .map(Balance::toAlert).toList(); - recipientService.enrich(alerts); - TeachingAlertPushService.PushResult pushed = force - ? pushService.pushToVenueManagers(alerts) - : pushService.pushToVenueManagersOnceDaily(alerts); - return new PushSummary(scope.label(), alerts.size(), pushed.recipientCount(), pushed.sentMessageCount(), - pushed.unmatchedAlertCount(), pushed.failedRecipientCount()); - } - - public void pushOnceDailyAsync(String wecomUserId) { - String taskKey = LocalDate.now(ZoneId.of("Asia/Shanghai")) + "|" + wecomUserId.toLowerCase(); - if (!automaticDailyTasks.add(taskKey)) return; - executor.execute(() -> { - try { - push(wecomUserId, false); - } catch (RuntimeException exception) { - automaticDailyTasks.remove(taskKey); - } - }); } private List fetchScoped(String path, Map parameters, String campusField, Scope scope) { @@ -162,7 +163,8 @@ public class LowHoursWarningPushService { catch (NumberFormatException ignored) { return BigDecimal.ZERO; } } - public record PushSummary(String scopeLabel, long warningStudentCount, int managerCount, + public record PushSummary(String scopeLabel, long lowHoursStudentCount, + long frequentAbsenceStudentCount, int managerCount, int sentMessageCount, long unmatchedStudentCount, int failedManagerCount) {} private static final class Balance { diff --git a/src/main/java/com/company/agentfoundation/teaching/report/StudentTeachingAttendanceReportController.java b/src/main/java/com/company/agentfoundation/teaching/report/StudentTeachingAttendanceReportController.java index bd5edd3..cc494f0 100644 --- a/src/main/java/com/company/agentfoundation/teaching/report/StudentTeachingAttendanceReportController.java +++ b/src/main/java/com/company/agentfoundation/teaching/report/StudentTeachingAttendanceReportController.java @@ -11,13 +11,24 @@ import java.util.Map; @RequestMapping("/api/teaching/student-attendance") public class StudentTeachingAttendanceReportController { private final StudentTeachingAttendanceReportService reportService; + private final LowHoursWarningPushService warningPushService; - public StudentTeachingAttendanceReportController(StudentTeachingAttendanceReportService reportService) { + public StudentTeachingAttendanceReportController( + StudentTeachingAttendanceReportService reportService, + LowHoursWarningPushService warningPushService + ) { this.reportService = reportService; + this.warningPushService = warningPushService; } @PostMapping("/daily-report") public Map dailyReport(@RequestBody StudentTeachingAttendanceReportRequest request) { return reportService.generate(request); } + + @PostMapping("/warning-push") + public LowHoursWarningPushService.PushSummary pushWarnings( + @RequestBody TeachingWarningPushRequest request) { + return warningPushService.push(request.wecomUserId(), request.startDate(), request.endDate()); + } } diff --git a/src/main/java/com/company/agentfoundation/teaching/report/StudentTeachingAttendanceReportService.java b/src/main/java/com/company/agentfoundation/teaching/report/StudentTeachingAttendanceReportService.java index 3a9f372..dcf3950 100644 --- a/src/main/java/com/company/agentfoundation/teaching/report/StudentTeachingAttendanceReportService.java +++ b/src/main/java/com/company/agentfoundation/teaching/report/StudentTeachingAttendanceReportService.java @@ -8,7 +8,6 @@ import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; import java.time.ZoneId; -import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -26,21 +25,15 @@ public class StudentTeachingAttendanceReportService { private final ReportDataScopeService scopeService; private final XuebangTeachingReportDataService realtimeData; private final TeachingAlertRecipientService recipientService; - private final TeachingAlertPushService alertPushService; - private final LowHoursWarningPushService lowHoursPushService; public StudentTeachingAttendanceReportService( ReportDataScopeService scopeService, XuebangTeachingReportDataService realtimeData, - TeachingAlertRecipientService recipientService, - TeachingAlertPushService alertPushService, - LowHoursWarningPushService lowHoursPushService + TeachingAlertRecipientService recipientService ) { this.scopeService = scopeService; this.realtimeData = realtimeData; this.recipientService = recipientService; - this.alertPushService = alertPushService; - this.lowHoursPushService = lowHoursPushService; } public Map generate(StudentTeachingAttendanceReportRequest request) { @@ -58,14 +51,14 @@ public class StudentTeachingAttendanceReportService { XuebangTeachingReportDataService.Snapshot snapshot = realtimeData.load(startDate, endDate, scope); List> lowHoursAlerts = snapshot.lowHoursStudents(); List> absenceAlerts = snapshot.frequentAbsenceStudents(); - List> allAlerts = new ArrayList<>(lowHoursAlerts.size() + absenceAlerts.size()); - allAlerts.addAll(lowHoursAlerts); - allAlerts.addAll(absenceAlerts); - if (!allAlerts.isEmpty()) { - recipientService.enrich(allAlerts); - alertPushService.pushAutomatic(allAlerts); + // 日报只查询和展示数据。预警消息必须通过独立的推送语义指令触发。 + if (!lowHoursAlerts.isEmpty() || !absenceAlerts.isEmpty()) { + List> reportAlerts = new java.util.ArrayList<>( + lowHoursAlerts.size() + absenceAlerts.size()); + reportAlerts.addAll(lowHoursAlerts); + reportAlerts.addAll(absenceAlerts); + recipientService.enrich(reportAlerts); } - if (!scope.self()) lowHoursPushService.pushOnceDailyAsync(request.wecomUserId()); Set pushUserIds = new LinkedHashSet<>(); Map> pushRecipients = new LinkedHashMap<>(); @@ -106,8 +99,8 @@ public class StudentTeachingAttendanceReportService { delivery.put("resolvedRecipientCount", pushUserIds.size()); delivery.put("resolvedWecomUserIds", List.copyOf(pushUserIds)); delivery.put("resolvedRecipients", List.copyOf(pushRecipients.values())); - delivery.put("readyForAutomaticPush", !pushUserIds.isEmpty()); - delivery.put("note", "预警名单已按教练、顾问、馆长解析;配置定时任务后可自动推送"); + delivery.put("readyForManualPush", !pushUserIds.isEmpty()); + delivery.put("note", "日报不会自动发送消息;请使用“推送本月预警数据”指令触发独立推送"); result.put("alertDelivery", delivery); return result; } 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 dc98d9f..0420ea4 100644 --- a/src/main/java/com/company/agentfoundation/teaching/report/TeachingAlertPushService.java +++ b/src/main/java/com/company/agentfoundation/teaching/report/TeachingAlertPushService.java @@ -1,50 +1,42 @@ package com.company.agentfoundation.teaching.report; +import com.company.agentfoundation.config.FoundationProperties; import com.company.agentfoundation.integration.wecom.client.WecomClient; -import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import java.time.LocalDate; -import java.time.ZoneId; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Executor; @Service class TeachingAlertPushService { private static final String APP_KEY = "sports-venue"; - private static final ZoneId BUSINESS_ZONE = ZoneId.of("Asia/Shanghai"); private static final int MAX_STUDENTS_PER_RECIPIENT = 100; private static final int STUDENTS_PER_MESSAGE = 25; private final WecomClient wecom; - private final Executor executor; - private final Set dailyDedup = ConcurrentHashMap.newKeySet(); + private final FoundationProperties properties; - TeachingAlertPushService(WecomClient wecom, @Qualifier("xuebangReportExecutor") Executor executor) { + @Autowired + TeachingAlertPushService(WecomClient wecom, FoundationProperties properties) { this.wecom = wecom; - this.executor = executor; + this.properties = properties; } - void pushAutomatic(List> alerts) { - if (alerts == null || alerts.isEmpty()) return; - executor.execute(() -> push(alerts, Set.of("COACH", "ADVISOR", "VENUE_MANAGER"), false)); + TeachingAlertPushService(WecomClient wecom) { + this(wecom, new FoundationProperties()); } PushResult pushToVenueManagers(List> alerts) { - return push(alerts, Set.of("VENUE_MANAGER"), true); + return push(alerts, Set.of("VENUE_MANAGER")); } - PushResult pushToVenueManagersOnceDaily(List> alerts) { - return push(alerts, Set.of("VENUE_MANAGER"), false); - } - - private PushResult push(List> alerts, Set targetRoles, boolean force) { + private PushResult push(List> alerts, Set targetRoles) { + if (alerts == null || alerts.isEmpty()) return new PushResult(0, 0, 0, 0, 0); Map batches = new LinkedHashMap<>(); long unmatched = 0; for (Map alert : alerts) { @@ -59,9 +51,6 @@ class TeachingAlertPushService { String role = text(recipient, "role"); String userId = text(recipient, "wecomUserId"); if (!targetRoles.contains(role) || userId.isEmpty()) continue; - String dedupKey = LocalDate.now(BUSINESS_ZONE) + "|" + text(alert, "warningType") + "|" - + text(alert, "studentId") + "|" + userId.toLowerCase(Locale.ROOT); - if (!force && !dailyDedup.add(dedupKey)) continue; matched = true; batches.computeIfAbsent(userId.toLowerCase(Locale.ROOT), ignored -> new RecipientBatch(userId, text(recipient, "name"))).alerts().add(alert); @@ -69,6 +58,15 @@ class TeachingAlertPushService { if (!matched) unmatched++; } + int targetRecipientCount = batches.size(); + for (String copyUserId : alertCopyUserIds()) { + String key = copyUserId.toLowerCase(Locale.ROOT); + if (batches.containsKey(key)) continue; + RecipientBatch copyBatch = new RecipientBatch(copyUserId, "管理员测试抄送"); + copyBatch.alerts().addAll(alerts); + if (!copyBatch.alerts().isEmpty()) batches.put(key, copyBatch); + } + int sentMessages = 0; int failedRecipients = 0; for (RecipientBatch batch : batches.values()) { @@ -87,7 +85,13 @@ class TeachingAlertPushService { } if (failed) failedRecipients++; } - return new PushResult(alerts.size(), batches.size(), sentMessages, unmatched, failedRecipients); + return new PushResult(alerts.size(), targetRecipientCount, sentMessages, unmatched, failedRecipients); + } + + private List alertCopyUserIds() { + FoundationProperties.Wecom.App app = properties.getWecom().getApps().get(APP_KEY); + if (app == null || app.getAlertCopyUserIds() == null) return List.of(); + return app.getAlertCopyUserIds().stream().map(String::trim).filter(value -> !value.isEmpty()).distinct().toList(); } private String format(RecipientBatch batch, List> alerts, int offset, int displayed) { diff --git a/src/main/java/com/company/agentfoundation/teaching/report/TeachingWarningPushRequest.java b/src/main/java/com/company/agentfoundation/teaching/report/TeachingWarningPushRequest.java new file mode 100644 index 0000000..eacc1fa --- /dev/null +++ b/src/main/java/com/company/agentfoundation/teaching/report/TeachingWarningPushRequest.java @@ -0,0 +1,9 @@ +package com.company.agentfoundation.teaching.report; + +import java.time.LocalDate; + +public record TeachingWarningPushRequest( + LocalDate startDate, + LocalDate endDate, + String wecomUserId +) {} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 423e46c..878cfc3 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -72,6 +72,9 @@ foundation: - ChenWenJie - XieChangLei - ZhangYunYun + # 测试期间同步接收每次学员运营预警;验证完成后可删除此项。 + alert-copy-user-ids: + - YiNianYiQingJing xuebang: enabled: true base-url: http://api.xuebangsoft.net diff --git a/src/test/java/com/company/agentfoundation/semantic/SemanticRecognitionServiceTest.java b/src/test/java/com/company/agentfoundation/semantic/SemanticRecognitionServiceTest.java index dece81a..1794bd1 100644 --- a/src/test/java/com/company/agentfoundation/semantic/SemanticRecognitionServiceTest.java +++ b/src/test/java/com/company/agentfoundation/semantic/SemanticRecognitionServiceTest.java @@ -57,7 +57,16 @@ class SemanticRecognitionServiceTest { SemanticRecognition recognition = service.recognize( "推送低课时名单", LocalDate.of(2026, 7, 22)); - assertEquals("PUSH_LOW_HOURS_WARNING_LIST", recognition.intent()); + assertEquals("PUSH_TEACHING_WARNING_DATA", recognition.intent()); + assertEquals("DETERMINISTIC_COMMAND", recognition.source()); + } + + @Test + void recognizesMonthlyTeachingWarningPushAsDeterministicCommand() { + SemanticRecognition recognition = service.recognize( + "推送本月预警数据", LocalDate.of(2026, 7, 22)); + + assertEquals("PUSH_TEACHING_WARNING_DATA", recognition.intent()); assertEquals("DETERMINISTIC_COMMAND", recognition.source()); } } 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 db0b727..019678d 100644 --- a/src/test/java/com/company/agentfoundation/teaching/report/TeachingAlertPushServiceTest.java +++ b/src/test/java/com/company/agentfoundation/teaching/report/TeachingAlertPushServiceTest.java @@ -1,5 +1,6 @@ package com.company.agentfoundation.teaching.report; +import com.company.agentfoundation.config.FoundationProperties; import com.company.agentfoundation.integration.wecom.client.WecomClient; import org.junit.jupiter.api.Test; @@ -17,7 +18,7 @@ class TeachingAlertPushServiceTest { @Test void pushesLowHoursListToVenueManager() { WecomClient wecom = mock(WecomClient.class); - TeachingAlertPushService service = new TeachingAlertPushService(wecom, Runnable::run); + TeachingAlertPushService service = new TeachingAlertPushService(wecom); Map alert = new LinkedHashMap<>(); alert.put("studentId", "s1"); alert.put("studentName", "Student A"); alert.put("campusName", "Venue A"); alert.put("remainingHours", "3.5"); @@ -32,4 +33,25 @@ class TeachingAlertPushServiceTest { verify(wecom).sendMarkdown(org.mockito.ArgumentMatchers.eq("sports-venue"), org.mockito.ArgumentMatchers.eq("manager1"), contains("Student A")); } + + @Test + void copiesWarningMessageToConfiguredAdmin() { + WecomClient wecom = mock(WecomClient.class); + FoundationProperties properties = new FoundationProperties(); + FoundationProperties.Wecom.App app = new FoundationProperties.Wecom.App(); + app.setAlertCopyUserIds(List.of("YiNianYiQingJing")); + properties.getWecom().getApps().put("sports-venue", app); + TeachingAlertPushService service = new TeachingAlertPushService(wecom, properties); + Map alert = new LinkedHashMap<>(); + alert.put("studentId", "s1"); alert.put("studentName", "Student A"); + alert.put("campusName", "Venue A"); alert.put("remainingHours", "3.5"); + alert.put("warningType", "LOW_REMAINING_HOURS"); + alert.put("recipients", List.of( + Map.of("role", "VENUE_MANAGER", "name", "Manager", "wecomUserId", "manager1"))); + + service.pushToVenueManagers(List.of(alert)); + + verify(wecom).sendMarkdown(org.mockito.ArgumentMatchers.eq("sports-venue"), + org.mockito.ArgumentMatchers.eq("YiNianYiQingJing"), contains("Student A")); + } }