日报推送

This commit is contained in:
tanglong 2026-07-22 12:15:50 +08:00
parent c0c2104ed7
commit 2f7ceeca45
13 changed files with 182 additions and 93 deletions

View File

@ -132,6 +132,7 @@ public class FoundationProperties {
private String secret;
private String callbackPath;
private List<String> adminUserIds = new ArrayList<>();
private List<String> 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<String> getAdminUserIds() { return adminUserIds; }
public void setAdminUserIds(List<String> adminUserIds) { this.adminUserIds = adminUserIds; }
public List<String> getAlertCopyUserIds() { return alertCopyUserIds; }
public void setAlertCopyUserIds(List<String> alertCopyUserIds) { this.alertCopyUserIds = alertCopyUserIds; }
}
}

View File

@ -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"
+ "<font color=\"comment\">查询完成后将自动发送结果,请勿重复提交日报指令。</font>";
}
private String busyMessage(String reportLabel) {
return "### ⏳ 查询任务进行中\n"
+ "> **" + reportLabel + "** 尚未完成\n\n"
+ "<font color=\"warning\">请等待当前任务完成后,再查询其他日报。</font>";
}
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 "企业微信客户运营日报";

View File

@ -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()))

View File

@ -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课时:<font color=\"warning\">" + result.warningStudentCount() + "人</font>"
+ "低于10课时:<font color=\"warning\">" + result.lowHoursStudentCount() + "人</font>"
+ " | 高频缺勤:<font color=\"warning\">" + result.frequentAbsenceStudentCount() + "人</font>"
+ " | 匹配馆长:" + 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 "学员运营预警推送失败,请稍后重试。";
}
}
}

View File

@ -97,13 +97,13 @@ public class StudentTeachingAttendanceReportIntentHandler implements SemanticInt
}
reply.append("\n\n**高频缺勤** 排课≥3次且缺勤≥2次:<font color=\"warning\">")
.append(absence.path("studentCount").asInt()).append("人</font>")
.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<font color=\"warning\">部分预警未匹配企业微信 UserID,请补全人员映射。</font>");
@ -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);

View File

@ -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<String> 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<Map<String, Object>> lowHoursAlerts = loadLowHoursAlerts(scope);
List<Map<String, Object>> 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<Map<String, Object>> loadLowHoursAlerts(Scope scope) {
Map<String, Object> studentParameters = new LinkedHashMap<>();
studentParameters.put("stuStatus", 1);
studentParameters.put("studentContractType", "OFFICIAL_STUDENT");
@ -86,28 +105,10 @@ public class LowHoursWarningPushService {
balance.add(product);
}
List<Map<String, Object>> 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<JsonNode> fetchScoped(String path, Map<String, Object> 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 {

View File

@ -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<String, Object> 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());
}
}

View File

@ -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<String, Object> generate(StudentTeachingAttendanceReportRequest request) {
@ -58,14 +51,14 @@ public class StudentTeachingAttendanceReportService {
XuebangTeachingReportDataService.Snapshot snapshot = realtimeData.load(startDate, endDate, scope);
List<Map<String, Object>> lowHoursAlerts = snapshot.lowHoursStudents();
List<Map<String, Object>> absenceAlerts = snapshot.frequentAbsenceStudents();
List<Map<String, Object>> 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<Map<String, Object>> 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<String> pushUserIds = new LinkedHashSet<>();
Map<String, Map<String, Object>> 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;
}

View File

@ -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<String> 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<Map<String, Object>> 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<Map<String, Object>> alerts) {
return push(alerts, Set.of("VENUE_MANAGER"), true);
return push(alerts, Set.of("VENUE_MANAGER"));
}
PushResult pushToVenueManagersOnceDaily(List<Map<String, Object>> alerts) {
return push(alerts, Set.of("VENUE_MANAGER"), false);
}
private PushResult push(List<Map<String, Object>> alerts, Set<String> targetRoles, boolean force) {
private PushResult push(List<Map<String, Object>> alerts, Set<String> targetRoles) {
if (alerts == null || alerts.isEmpty()) return new PushResult(0, 0, 0, 0, 0);
Map<String, RecipientBatch> batches = new LinkedHashMap<>();
long unmatched = 0;
for (Map<String, Object> 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<String> 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<Map<String, Object>> alerts, int offset, int displayed) {

View File

@ -0,0 +1,9 @@
package com.company.agentfoundation.teaching.report;
import java.time.LocalDate;
public record TeachingWarningPushRequest(
LocalDate startDate,
LocalDate endDate,
String wecomUserId
) {}

View File

@ -72,6 +72,9 @@ foundation:
- ChenWenJie
- XieChangLei
- ZhangYunYun
# 测试期间同步接收每次学员运营预警;验证完成后可删除此项。
alert-copy-user-ids:
- YiNianYiQingJing
xuebang:
enabled: true
base-url: http://api.xuebangsoft.net

View File

@ -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());
}
}

View File

@ -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<String, Object> 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<String, Object> 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"));
}
}