EC顺序
This commit is contained in:
parent
a6c3692261
commit
60ad824bb9
@ -40,6 +40,7 @@
|
||||
- 课时预警和高频缺勤名单超过聊天展示上限时,统一发送 `.xlsx`,不再发送 `.txt`。
|
||||
- 日期、接收人、预警数量和运动馆数量通过企业微信消息展示。
|
||||
- Excel 只保留详细名单,按运动馆、学员和预警类型逐行展示剩余课时、排课及缺勤次数。
|
||||
- 对发起推送指令的用户,先发送“推送完成”消息,再发送 Excel;其他馆长仍按“预警说明 → Excel”顺序接收。
|
||||
|
||||
## 扩展新意图
|
||||
|
||||
|
||||
@ -34,7 +34,7 @@ public class FrequentAbsenceWarningPushIntentHandler implements SemanticIntentHa
|
||||
try {
|
||||
LowHoursWarningPushService.PushSummary result = service.push(
|
||||
context.wecomUserId(), range.startDate(), range.endDate(), false, true);
|
||||
return "### ✅ 高频缺勤名单推送完成\n"
|
||||
String reply = "### ✅ 高频缺勤名单推送完成\n"
|
||||
+ "> 日期:**" + range.startDate()
|
||||
+ (range.startDate().equals(range.endDate()) ? "" : " 至 " + range.endDate()) + "**\n"
|
||||
+ "> 范围:**" + result.scopeLabel() + "**\n\n"
|
||||
@ -43,6 +43,9 @@ public class FrequentAbsenceWarningPushIntentHandler implements SemanticIntentHa
|
||||
+ " | 成功消息:" + result.sentMessageCount() + "条"
|
||||
+ "\n\n未匹配馆长的学员:" + result.unmatchedStudentCount() + "人"
|
||||
+ " | 发送失败馆长:" + result.failedManagerCount() + "人";
|
||||
result.deferredAttachments().forEach(attachment ->
|
||||
context.deferExcel(attachment.fileName(), attachment.content()));
|
||||
return reply;
|
||||
} catch (ResponseStatusException exception) {
|
||||
if (exception.getStatusCode().value() == 403) {
|
||||
return exception.getReason() == null ? "无权限执行高频缺勤名单推送" : exception.getReason();
|
||||
|
||||
@ -34,7 +34,7 @@ public class LowHoursWarningPushIntentHandler implements SemanticIntentHandler {
|
||||
SemanticDateRangeResolver.DateRange range = dateRangeResolver.resolve(context, recognition);
|
||||
LowHoursWarningPushService.PushSummary result = service.push(
|
||||
context.wecomUserId(), range.startDate(), range.endDate(), true, false);
|
||||
return "### ✅ 课时预警推送完成\n"
|
||||
String reply = "### ✅ 课时预警推送完成\n"
|
||||
+ "> 日期:**" + range.startDate()
|
||||
+ (range.startDate().equals(range.endDate()) ? "" : " 至 " + range.endDate()) + "**\n"
|
||||
+ "> 范围:**" + result.scopeLabel() + "**\n\n"
|
||||
@ -43,6 +43,9 @@ public class LowHoursWarningPushIntentHandler implements SemanticIntentHandler {
|
||||
+ " | 成功消息:" + result.sentMessageCount() + "条"
|
||||
+ "\n\n未匹配馆长的学员:" + result.unmatchedStudentCount() + "人"
|
||||
+ " | 发送失败馆长:" + result.failedManagerCount() + "人";
|
||||
result.deferredAttachments().forEach(attachment ->
|
||||
context.deferExcel(attachment.fileName(), attachment.content()));
|
||||
return reply;
|
||||
} catch (ResponseStatusException exception) {
|
||||
if (exception.getStatusCode().value() == 403) {
|
||||
return exception.getReason() == null ? "无权限执行学员预警推送" : exception.getReason();
|
||||
|
||||
@ -3,6 +3,7 @@ package com.company.agentfoundation.teaching.report;
|
||||
import com.company.agentfoundation.integration.xuebang.XuebangApiClient;
|
||||
import com.company.agentfoundation.security.ReportDataScopeService;
|
||||
import com.company.agentfoundation.security.ReportDataScopeService.Scope;
|
||||
import com.company.agentfoundation.semantic.SemanticReplyAttachment;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@ -96,7 +97,7 @@ public class LowHoursWarningPushService {
|
||||
alert.put("warningEndDate", effectiveEnd.toString());
|
||||
});
|
||||
recipientService.enrich(alerts);
|
||||
TeachingAlertPushService.PushResult pushed = pushService.pushToVenueManagers(alerts);
|
||||
TeachingAlertPushService.PushResult pushed = pushService.pushToVenueManagers(alerts, wecomUserId);
|
||||
log.info("teaching warning push completed userId={} scope={} lowHours={} frequentAbsence={} "
|
||||
+ "attendanceMs={} lowHoursMs={} totalMs={} messages={}",
|
||||
wecomUserId, scope.label(), lowHoursAlerts.size(), snapshot == null ? 0 : snapshot.frequentAbsenceCount(),
|
||||
@ -105,7 +106,7 @@ public class LowHoursWarningPushService {
|
||||
return new PushSummary(scope.label(), lowHoursAlerts.size(),
|
||||
snapshot == null ? 0 : snapshot.frequentAbsenceCount(),
|
||||
pushed.recipientCount(), pushed.sentMessageCount(), pushed.unmatchedAlertCount(),
|
||||
pushed.failedRecipientCount());
|
||||
pushed.failedRecipientCount(), pushed.deferredAttachments());
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> loadLowHoursAlerts(
|
||||
@ -238,7 +239,19 @@ public class LowHoursWarningPushService {
|
||||
|
||||
public record PushSummary(String scopeLabel, long lowHoursStudentCount,
|
||||
long frequentAbsenceStudentCount, int managerCount,
|
||||
int sentMessageCount, long unmatchedStudentCount, int failedManagerCount) {}
|
||||
int sentMessageCount, long unmatchedStudentCount, int failedManagerCount,
|
||||
List<SemanticReplyAttachment> deferredAttachments) {
|
||||
public PushSummary(String scopeLabel, long lowHoursStudentCount,
|
||||
long frequentAbsenceStudentCount, int managerCount,
|
||||
int sentMessageCount, long unmatchedStudentCount, int failedManagerCount) {
|
||||
this(scopeLabel, lowHoursStudentCount, frequentAbsenceStudentCount, managerCount,
|
||||
sentMessageCount, unmatchedStudentCount, failedManagerCount, List.of());
|
||||
}
|
||||
|
||||
public PushSummary {
|
||||
deferredAttachments = deferredAttachments == null ? List.of() : List.copyOf(deferredAttachments);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Balance {
|
||||
private final String studentId;
|
||||
|
||||
@ -3,6 +3,7 @@ package com.company.agentfoundation.teaching.report;
|
||||
import com.company.agentfoundation.config.FoundationProperties;
|
||||
import com.company.agentfoundation.integration.wecom.client.WecomClient;
|
||||
import com.company.agentfoundation.report.ExcelReportService;
|
||||
import com.company.agentfoundation.semantic.SemanticReplyAttachment;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@ -44,11 +45,24 @@ class TeachingAlertPushService {
|
||||
}
|
||||
|
||||
PushResult pushToVenueManagers(List<Map<String, Object>> alerts) {
|
||||
return push(alerts, Set.of("VENUE_MANAGER"));
|
||||
return pushToVenueManagers(alerts, "");
|
||||
}
|
||||
|
||||
private PushResult push(List<Map<String, Object>> alerts, Set<String> targetRoles) {
|
||||
if (alerts == null || alerts.isEmpty()) return new PushResult(0, 0, 0, 0, 0);
|
||||
PushResult pushToVenueManagers(
|
||||
List<Map<String, Object>> alerts,
|
||||
String deferAttachmentUserId
|
||||
) {
|
||||
return push(alerts, Set.of("VENUE_MANAGER"), deferAttachmentUserId);
|
||||
}
|
||||
|
||||
private PushResult push(
|
||||
List<Map<String, Object>> alerts,
|
||||
Set<String> targetRoles,
|
||||
String deferAttachmentUserId
|
||||
) {
|
||||
if (alerts == null || alerts.isEmpty()) {
|
||||
return new PushResult(0, 0, 0, 0, 0, List.of());
|
||||
}
|
||||
Map<String, RecipientBatch> batches = new LinkedHashMap<>();
|
||||
long unmatched = 0;
|
||||
for (Map<String, Object> alert : alerts) {
|
||||
@ -81,6 +95,7 @@ class TeachingAlertPushService {
|
||||
|
||||
int sentMessages = 0;
|
||||
int failedRecipients = 0;
|
||||
List<SemanticReplyAttachment> deferredAttachments = new ArrayList<>();
|
||||
for (RecipientBatch batch : batches.values()) {
|
||||
List<Map<String, Object>> ordered = batch.alerts().stream()
|
||||
.sorted(Comparator.comparing((Map<String, Object> value) -> campusName(value))
|
||||
@ -92,7 +107,12 @@ class TeachingAlertPushService {
|
||||
wecom.sendMarkdown(APP_KEY, batch.userId(), formatFileSummary(batch, ordered));
|
||||
sentMessages++;
|
||||
byte[] workbook = excelReportService.teachingWarnings(ordered);
|
||||
wecom.sendExcelFile(APP_KEY, batch.userId(), fileName(ordered), workbook);
|
||||
String attachmentName = fileName(ordered);
|
||||
if (sameUser(batch.userId(), deferAttachmentUserId)) {
|
||||
deferredAttachments.add(SemanticReplyAttachment.excel(attachmentName, workbook));
|
||||
} else {
|
||||
wecom.sendExcelFile(APP_KEY, batch.userId(), attachmentName, workbook);
|
||||
}
|
||||
sentMessages++;
|
||||
} catch (RuntimeException exception) {
|
||||
failedRecipients++;
|
||||
@ -115,7 +135,12 @@ class TeachingAlertPushService {
|
||||
}
|
||||
if (failed) failedRecipients++;
|
||||
}
|
||||
return new PushResult(alerts.size(), targetRecipientCount, sentMessages, unmatched, failedRecipients);
|
||||
return new PushResult(alerts.size(), targetRecipientCount, sentMessages, unmatched, failedRecipients,
|
||||
deferredAttachments);
|
||||
}
|
||||
|
||||
private boolean sameUser(String first, String second) {
|
||||
return first != null && second != null && !second.isBlank() && first.equalsIgnoreCase(second);
|
||||
}
|
||||
|
||||
private String formatFileSummary(RecipientBatch batch, List<Map<String, Object>> alerts) {
|
||||
@ -221,8 +246,24 @@ class TeachingAlertPushService {
|
||||
Object value = row.get(key); return value == null ? "" : String.valueOf(value).trim();
|
||||
}
|
||||
|
||||
record PushResult(long alertCount, int recipientCount, int sentMessageCount,
|
||||
long unmatchedAlertCount, int failedRecipientCount) {}
|
||||
record PushResult(
|
||||
long alertCount,
|
||||
int recipientCount,
|
||||
int sentMessageCount,
|
||||
long unmatchedAlertCount,
|
||||
int failedRecipientCount,
|
||||
List<SemanticReplyAttachment> deferredAttachments
|
||||
) {
|
||||
PushResult(long alertCount, int recipientCount, int sentMessageCount,
|
||||
long unmatchedAlertCount, int failedRecipientCount) {
|
||||
this(alertCount, recipientCount, sentMessageCount, unmatchedAlertCount,
|
||||
failedRecipientCount, List.of());
|
||||
}
|
||||
|
||||
PushResult {
|
||||
deferredAttachments = deferredAttachments == null ? List.of() : List.copyOf(deferredAttachments);
|
||||
}
|
||||
}
|
||||
|
||||
private record RecipientBatch(String userId, String name, List<Map<String, Object>> alerts) {
|
||||
private RecipientBatch(String userId, String name) { this(userId, name, new ArrayList<>()); }
|
||||
|
||||
@ -9,8 +9,11 @@ import org.junit.jupiter.api.Test;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
@ -21,18 +24,27 @@ class LowHoursWarningPushIntentHandlerTest {
|
||||
LowHoursWarningPushService service = mock(LowHoursWarningPushService.class);
|
||||
SemanticDateRangeResolver resolver = mock(SemanticDateRangeResolver.class);
|
||||
LowHoursWarningPushIntentHandler handler = new LowHoursWarningPushIntentHandler(service, resolver);
|
||||
SemanticRequestContext context = new SemanticRequestContext("YiNianYiQingJing", "推送本月低课时名单",
|
||||
ZonedDateTime.of(2026, 7, 22, 12, 0, 0, 0, ZoneId.of("Asia/Shanghai")));
|
||||
List<com.company.agentfoundation.semantic.SemanticReplyAttachment> attachments = new ArrayList<>();
|
||||
SemanticRequestContext context = new SemanticRequestContext(
|
||||
"YiNianYiQingJing",
|
||||
"推送本月低课时名单",
|
||||
ZonedDateTime.of(2026, 7, 22, 12, 0, 0, 0, ZoneId.of("Asia/Shanghai")),
|
||||
attachments::add);
|
||||
SemanticRecognition recognition = new SemanticRecognition(
|
||||
"PUSH_LOW_HOURS_WARNING", 0.99D, Map.of(), "TEST");
|
||||
SemanticDateRangeResolver.DateRange range = new SemanticDateRangeResolver.DateRange(
|
||||
LocalDate.of(2026, 7, 1), LocalDate.of(2026, 7, 22));
|
||||
when(resolver.resolve(context, recognition)).thenReturn(range);
|
||||
when(service.push("YiNianYiQingJing", range.startDate(), range.endDate(), true, false))
|
||||
.thenReturn(new LowHoursWarningPushService.PushSummary("全部组织", 3, 0, 1, 2, 0, 0));
|
||||
.thenReturn(new LowHoursWarningPushService.PushSummary(
|
||||
"全部组织", 3, 0, 1, 2, 0, 0,
|
||||
List.of(com.company.agentfoundation.semantic.SemanticReplyAttachment.excel(
|
||||
"课时预警.xlsx", new byte[]{1}))));
|
||||
|
||||
handler.handle(context, recognition);
|
||||
|
||||
verify(service).push("YiNianYiQingJing", range.startDate(), range.endDate(), true, false);
|
||||
assertThat(attachments).hasSize(1);
|
||||
assertThat(attachments.get(0).fileName()).isEqualTo("课时预警.xlsx");
|
||||
}
|
||||
}
|
||||
|
||||
@ -58,7 +58,9 @@ class LowHoursWarningPushServiceTest {
|
||||
when(api.getAllPages(eq("/api/contract/findByInstitution"), org.mockito.ArgumentMatchers.anyMap()))
|
||||
.thenReturn(List.of(json("{\"id\":\"c1\",\"studentId\":\"s1\",\"studentName\":\"学员1\","
|
||||
+ "\"contractStatus\":\"NORMAL\",\"contractRefundStatus\":\"NORMAL\"}")));
|
||||
when(pushes.pushToVenueManagers(org.mockito.ArgumentMatchers.anyList()))
|
||||
when(pushes.pushToVenueManagers(
|
||||
org.mockito.ArgumentMatchers.anyList(),
|
||||
eq("admin")))
|
||||
.thenReturn(new TeachingAlertPushService.PushResult(1, 1, 1, 0, 0));
|
||||
LowHoursWarningPushService service = new LowHoursWarningPushService(
|
||||
api, scopes, realtime, recipients, pushes, Runnable::run);
|
||||
|
||||
@ -13,7 +13,9 @@ 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 {
|
||||
@ -96,6 +98,32 @@ class TeachingAlertPushServiceTest {
|
||||
org.mockito.ArgumentMatchers.eq("manager1"), contains("详细名单"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void defersRequesterExcelUntilCallbackSendsCompletionReply() {
|
||||
WecomClient wecom = mock(WecomClient.class);
|
||||
TeachingAlertPushService service = new TeachingAlertPushService(wecom);
|
||||
List<Map<String, Object>> alerts = new ArrayList<>();
|
||||
for (int index = 1; index <= 31; index++) {
|
||||
Map<String, Object> alert = alert("s" + index, "学员" + index,
|
||||
"浦东金桥-跃动运动馆", String.valueOf(index % 10));
|
||||
alert.put("warningStartDate", "2026-07-01");
|
||||
alert.put("warningEndDate", "2026-07-28");
|
||||
alerts.add(alert);
|
||||
}
|
||||
|
||||
TeachingAlertPushService.PushResult result =
|
||||
service.pushToVenueManagers(alerts, "manager1");
|
||||
|
||||
verify(wecom).sendMarkdown(
|
||||
org.mockito.ArgumentMatchers.eq("sports-venue"),
|
||||
org.mockito.ArgumentMatchers.eq("manager1"),
|
||||
contains("详细名单"));
|
||||
verify(wecom, never()).sendExcelFile(
|
||||
anyString(), anyString(), anyString(), any(byte[].class));
|
||||
assertThat(result.deferredAttachments()).hasSize(1);
|
||||
assertThat(result.deferredAttachments().get(0).fileName()).contains("课时预警名单.xlsx");
|
||||
}
|
||||
|
||||
private Map<String, Object> alert(String id, String name, String campus, String hours) {
|
||||
Map<String, Object> alert = new LinkedHashMap<>();
|
||||
alert.put("studentId", id); alert.put("studentName", name);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user