日报推送
This commit is contained in:
parent
c0c2104ed7
commit
2f7ceeca45
@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 "企业微信客户运营日报";
|
||||
|
||||
@ -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()))
|
||||
|
||||
@ -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 "学员运营预警推送失败,请稍后重试。";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
package com.company.agentfoundation.teaching.report;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
public record TeachingWarningPushRequest(
|
||||
LocalDate startDate,
|
||||
LocalDate endDate,
|
||||
String wecomUserId
|
||||
) {}
|
||||
@ -72,6 +72,9 @@ foundation:
|
||||
- ChenWenJie
|
||||
- XieChangLei
|
||||
- ZhangYunYun
|
||||
# 测试期间同步接收每次学员运营预警;验证完成后可删除此项。
|
||||
alert-copy-user-ids:
|
||||
- YiNianYiQingJing
|
||||
xuebang:
|
||||
enabled: true
|
||||
base-url: http://api.xuebangsoft.net
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user