From d8be5307878129e8fec738535728d8495fbf2069 Mon Sep 17 00:00:00 2001 From: tanglong <842690096@qq.com> Date: Fri, 12 Jun 2026 10:40:13 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=A2=E8=B5=84=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...stomerDashboardDailyCapabilityHandler.java | 47 +++++++++-- .../config/FoundationProperties.java | 3 + .../integration/wecom/WecomClient.java | 79 +++++++++++++++++++ .../WecomExternalContactBatchRequest.java | 11 +++ .../WecomExternalContactBatchResponse.java | 31 ++++++++ .../wecom/WecomFollowUserListResponse.java | 15 ++++ .../wecom/WecomStudentContactStats.java | 7 ++ .../WecomStudentContactStatsService.java | 71 +++++++++++++++++ src/main/resources/application.yml | 1 + 9 files changed, 259 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/company/agentfoundation/integration/wecom/WecomExternalContactBatchRequest.java create mode 100644 src/main/java/com/company/agentfoundation/integration/wecom/WecomExternalContactBatchResponse.java create mode 100644 src/main/java/com/company/agentfoundation/integration/wecom/WecomFollowUserListResponse.java create mode 100644 src/main/java/com/company/agentfoundation/integration/wecom/WecomStudentContactStats.java create mode 100644 src/main/java/com/company/agentfoundation/integration/wecom/WecomStudentContactStatsService.java diff --git a/src/main/java/com/company/agentfoundation/agent/capability/CustomerDashboardDailyCapabilityHandler.java b/src/main/java/com/company/agentfoundation/agent/capability/CustomerDashboardDailyCapabilityHandler.java index 6289200..2fb0edf 100644 --- a/src/main/java/com/company/agentfoundation/agent/capability/CustomerDashboardDailyCapabilityHandler.java +++ b/src/main/java/com/company/agentfoundation/agent/capability/CustomerDashboardDailyCapabilityHandler.java @@ -1,17 +1,52 @@ package com.company.agentfoundation.agent.capability; +import com.company.agentfoundation.agent.AgentCapabilityResponse; import com.company.agentfoundation.agent.AgentIntent; +import com.company.agentfoundation.agent.AgentRequestContext; +import com.company.agentfoundation.integration.wecom.WecomStudentContactStats; +import com.company.agentfoundation.integration.wecom.WecomStudentContactStatsService; import org.springframework.stereotype.Component; +import java.time.Instant; import java.util.List; +import java.util.Map; @Component -public class CustomerDashboardDailyCapabilityHandler extends AbstractPlaceholderCapabilityHandler { - public CustomerDashboardDailyCapabilityHandler() { - super( - AgentIntent.CUSTOMER_DASHBOARD_DAILY, - "\u5ba2\u8d44\u5927\u76d8\u6570\u636e\u6b63\u5728\u5b8c\u5584\u4e2d\uff0c\u540e\u7eed\u5c06\u8986\u76d6\u5ba2\u8d44\u65b0\u589e\u3001\u5206\u914d\u3001\u8ddf\u8fdb\u3001\u8f6c\u5316\u3001\u6d41\u5931\u4e0e\u5f02\u5e38\u98ce\u9669\u9884\u8b66\u3002", - List.of("\u5b66\u90a6\u6559\u52a1\u7cfb\u7edf", "\u4f01\u4e1a\u5fae\u4fe1", "CRM \u6570\u636e\u5e93") +public class CustomerDashboardDailyCapabilityHandler implements AgentCapabilityHandler { + private final WecomStudentContactStatsService studentContactStatsService; + + public CustomerDashboardDailyCapabilityHandler(WecomStudentContactStatsService studentContactStatsService) { + this.studentContactStatsService = studentContactStatsService; + } + + @Override + public AgentIntent intent() { + return AgentIntent.CUSTOMER_DASHBOARD_DAILY; + } + + @Override + public AgentCapabilityResponse handle(AgentRequestContext context) { + WecomStudentContactStats stats = studentContactStatsService.fetchTodayStats(); + return new AgentCapabilityResponse( + intent().code(), + intent().title(), + intent().mcpGroup(), + "ready", + """ + 学员联系数据已从企业微信实时获取。 + + 学员总数:%d + 今日新增:%d + 跟进成员数:%d + 今日收款:暂未接入 + """.formatted(stats.totalStudents(), stats.todayNewStudents(), stats.followUserCount()).trim(), + List.of("企业微信学员联系 API"), + Map.of( + "totalStudents", stats.totalStudents(), + "todayNewStudents", stats.todayNewStudents(), + "followUserCount", stats.followUserCount() + ), + Instant.now() ); } } diff --git a/src/main/java/com/company/agentfoundation/config/FoundationProperties.java b/src/main/java/com/company/agentfoundation/config/FoundationProperties.java index 4d0619f..6a657f5 100644 --- a/src/main/java/com/company/agentfoundation/config/FoundationProperties.java +++ b/src/main/java/com/company/agentfoundation/config/FoundationProperties.java @@ -56,6 +56,7 @@ public class FoundationProperties { private String corpId; private Integer agentId; private String secret; + private String studentContactSecret; private String token; private String encodingAesKey; private String crmAgentId = "crm-assistant"; @@ -69,6 +70,8 @@ public class FoundationProperties { public void setAgentId(Integer agentId) { this.agentId = agentId; } public String getSecret() { return secret; } public void setSecret(String secret) { this.secret = secret; } + public String getStudentContactSecret() { return studentContactSecret; } + public void setStudentContactSecret(String studentContactSecret) { this.studentContactSecret = studentContactSecret; } public String getToken() { return token; } public void setToken(String token) { this.token = token; } public String getEncodingAesKey() { return encodingAesKey; } diff --git a/src/main/java/com/company/agentfoundation/integration/wecom/WecomClient.java b/src/main/java/com/company/agentfoundation/integration/wecom/WecomClient.java index f3c7cf5..9fee554 100644 --- a/src/main/java/com/company/agentfoundation/integration/wecom/WecomClient.java +++ b/src/main/java/com/company/agentfoundation/integration/wecom/WecomClient.java @@ -8,6 +8,9 @@ import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.client.WebClient; import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; @Service public class WecomClient { @@ -17,6 +20,7 @@ public class WecomClient { private volatile String cachedToken; private volatile Instant tokenExpireAt = Instant.EPOCH; + private final Map tokenCaches = new ConcurrentHashMap<>(); public WecomClient(FoundationProperties properties, WebClient.Builder builder) { this.properties = properties; @@ -48,6 +52,43 @@ public class WecomClient { log.info("wecom sendText success toUser={}", toUser); } + public WecomFollowUserListResponse getStudentContactFollowUsers() { + String token = getAccessToken(studentContactSecret()); + WecomFollowUserListResponse response = webClient.get() + .uri(uriBuilder -> uriBuilder.path("/cgi-bin/externalcontact/get_follow_user_list") + .queryParam("access_token", token) + .build()) + .retrieve() + .bodyToMono(WecomFollowUserListResponse.class) + .block(); + + if (response == null || !response.success()) { + log.error("wecom student contact get follow users failed err={}", response == null ? "null" : response.errmsg()); + throw new IllegalStateException("WeCom student contact get follow users failed: " + (response == null ? "null" : response.errmsg())); + } + return response; + } + + public WecomExternalContactBatchResponse batchGetStudentContacts(List userIds, String cursor, int limit) { + String token = getAccessToken(studentContactSecret()); + WecomExternalContactBatchRequest request = new WecomExternalContactBatchRequest(userIds, cursor, limit); + WecomExternalContactBatchResponse response = webClient.post() + .uri(uriBuilder -> uriBuilder.path("/cgi-bin/externalcontact/batch/get_by_user") + .queryParam("access_token", token) + .build()) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(request) + .retrieve() + .bodyToMono(WecomExternalContactBatchResponse.class) + .block(); + + if (response == null || !response.success()) { + log.error("wecom student contact batch get failed err={}", response == null ? "null" : response.errmsg()); + throw new IllegalStateException("WeCom student contact batch get failed: " + (response == null ? "null" : response.errmsg())); + } + return response; + } + private String getAccessToken() { if (cachedToken != null && Instant.now().isBefore(tokenExpireAt)) { log.info("wecom gettoken use cached token expireAt={}", tokenExpireAt); @@ -75,6 +116,40 @@ public class WecomClient { return cachedToken; } + private String getAccessToken(String secret) { + String safeSecret = secret == null || secret.isBlank() ? properties.getWecom().getSecret() : secret.trim(); + TokenCache cache = tokenCaches.computeIfAbsent(safeSecret, ignored -> new TokenCache()); + if (cache.token != null && Instant.now().isBefore(cache.expireAt)) { + log.info("wecom gettoken use cached token expireAt={}", cache.expireAt); + return cache.token; + } + + WecomTokenResponse response = webClient.get() + .uri(uriBuilder -> uriBuilder.path("/cgi-bin/gettoken") + .queryParam("corpid", properties.getWecom().getCorpId()) + .queryParam("corpsecret", safeSecret) + .build()) + .retrieve() + .bodyToMono(WecomTokenResponse.class) + .block(); + + if (response == null || !response.success() || response.accessToken() == null) { + log.error("wecom gettoken failed err={}", response == null ? "null" : response.errmsg()); + throw new IllegalStateException("WeCom gettoken failed: " + (response == null ? "null" : response.errmsg())); + } + + cache.token = response.accessToken(); + int expires = response.expiresIn() == null ? 7200 : response.expiresIn(); + cache.expireAt = Instant.now().plusSeconds(Math.max(300, expires - 120)); + log.info("wecom gettoken success expireAt={}", cache.expireAt); + return cache.token; + } + + private String studentContactSecret() { + String secret = properties.getWecom().getStudentContactSecret(); + return secret == null || secret.isBlank() ? properties.getWecom().getSecret() : secret; + } + private String preview(String text) { if (text == null || text.isBlank()) { return ""; @@ -82,4 +157,8 @@ public class WecomClient { return text.length() <= 80 ? text : text.substring(0, 80) + "..."; } + private static class TokenCache { + private volatile String token; + private volatile Instant expireAt = Instant.EPOCH; + } } diff --git a/src/main/java/com/company/agentfoundation/integration/wecom/WecomExternalContactBatchRequest.java b/src/main/java/com/company/agentfoundation/integration/wecom/WecomExternalContactBatchRequest.java new file mode 100644 index 0000000..f45a77a --- /dev/null +++ b/src/main/java/com/company/agentfoundation/integration/wecom/WecomExternalContactBatchRequest.java @@ -0,0 +1,11 @@ +package com.company.agentfoundation.integration.wecom; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public record WecomExternalContactBatchRequest( + @JsonProperty("userid_list") List userIdList, + String cursor, + Integer limit +) {} diff --git a/src/main/java/com/company/agentfoundation/integration/wecom/WecomExternalContactBatchResponse.java b/src/main/java/com/company/agentfoundation/integration/wecom/WecomExternalContactBatchResponse.java new file mode 100644 index 0000000..c40514a --- /dev/null +++ b/src/main/java/com/company/agentfoundation/integration/wecom/WecomExternalContactBatchResponse.java @@ -0,0 +1,31 @@ +package com.company.agentfoundation.integration.wecom; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public record WecomExternalContactBatchResponse( + Integer errcode, + String errmsg, + @JsonProperty("external_contact_list") List externalContactList, + @JsonProperty("next_cursor") String nextCursor +) { + public boolean success() { + return errcode == null || errcode == 0; + } + + public record ExternalContactItem( + @JsonProperty("external_contact") ExternalContact externalContact, + @JsonProperty("follow_info") FollowInfo followInfo + ) {} + + public record ExternalContact( + @JsonProperty("external_userid") String externalUserId, + String name + ) {} + + public record FollowInfo( + String userid, + Long createtime + ) {} +} diff --git a/src/main/java/com/company/agentfoundation/integration/wecom/WecomFollowUserListResponse.java b/src/main/java/com/company/agentfoundation/integration/wecom/WecomFollowUserListResponse.java new file mode 100644 index 0000000..eecf028 --- /dev/null +++ b/src/main/java/com/company/agentfoundation/integration/wecom/WecomFollowUserListResponse.java @@ -0,0 +1,15 @@ +package com.company.agentfoundation.integration.wecom; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public record WecomFollowUserListResponse( + Integer errcode, + String errmsg, + @JsonProperty("follow_user") List followUser +) { + public boolean success() { + return errcode == null || errcode == 0; + } +} diff --git a/src/main/java/com/company/agentfoundation/integration/wecom/WecomStudentContactStats.java b/src/main/java/com/company/agentfoundation/integration/wecom/WecomStudentContactStats.java new file mode 100644 index 0000000..d7155b4 --- /dev/null +++ b/src/main/java/com/company/agentfoundation/integration/wecom/WecomStudentContactStats.java @@ -0,0 +1,7 @@ +package com.company.agentfoundation.integration.wecom; + +public record WecomStudentContactStats( + int totalStudents, + int todayNewStudents, + int followUserCount +) {} diff --git a/src/main/java/com/company/agentfoundation/integration/wecom/WecomStudentContactStatsService.java b/src/main/java/com/company/agentfoundation/integration/wecom/WecomStudentContactStatsService.java new file mode 100644 index 0000000..b9cce64 --- /dev/null +++ b/src/main/java/com/company/agentfoundation/integration/wecom/WecomStudentContactStatsService.java @@ -0,0 +1,71 @@ +package com.company.agentfoundation.integration.wecom; + +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Service +public class WecomStudentContactStatsService { + private static final ZoneId CHINA_ZONE = ZoneId.of("Asia/Shanghai"); + private static final int BATCH_SIZE = 100; + + private final WecomClient wecomClient; + + public WecomStudentContactStatsService(WecomClient wecomClient) { + this.wecomClient = wecomClient; + } + + public WecomStudentContactStats fetchTodayStats() { + List followUsers = wecomClient.getStudentContactFollowUsers().followUser(); + if (followUsers == null || followUsers.isEmpty()) { + return new WecomStudentContactStats(0, 0, 0); + } + + Set allStudents = new HashSet<>(); + Map earliestCreateTimeByStudent = new HashMap<>(); + for (int start = 0; start < followUsers.size(); start += BATCH_SIZE) { + List batchUsers = followUsers.subList(start, Math.min(start + BATCH_SIZE, followUsers.size())); + fetchBatch(batchUsers, allStudents, earliestCreateTimeByStudent); + } + + long todayStart = LocalDate.now(CHINA_ZONE).atStartOfDay(CHINA_ZONE).toEpochSecond(); + long tomorrowStart = LocalDate.now(CHINA_ZONE).plusDays(1).atStartOfDay(CHINA_ZONE).toEpochSecond(); + int todayNew = (int) earliestCreateTimeByStudent.values().stream() + .filter(createTime -> createTime != null && createTime >= todayStart && createTime < tomorrowStart) + .count(); + + return new WecomStudentContactStats(allStudents.size(), todayNew, followUsers.size()); + } + + private void fetchBatch(List userIds, Set allStudents, Map earliestCreateTimeByStudent) { + String cursor = ""; + while (true) { + WecomExternalContactBatchResponse response = wecomClient.batchGetStudentContacts(userIds, cursor, BATCH_SIZE); + List contacts = response.externalContactList(); + if (contacts != null) { + for (WecomExternalContactBatchResponse.ExternalContactItem item : contacts) { + if (item == null || item.externalContact() == null || item.externalContact().externalUserId() == null) { + continue; + } + String externalUserId = item.externalContact().externalUserId(); + allStudents.add(externalUserId); + Long createTime = item.followInfo() == null ? null : item.followInfo().createtime(); + if (createTime != null) { + earliestCreateTimeByStudent.merge(externalUserId, createTime, Math::min); + } + } + } + + cursor = response.nextCursor(); + if (cursor == null || cursor.isBlank()) { + break; + } + } + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 8e41671..679facc 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -50,6 +50,7 @@ foundation: corp-id: ww11a329ca8ee0c40e agent-id: 1000002 secret: 8tnH50ISscWBhQbxSzjRrA6XT2oJBUvEdEpY77hmEK4 + student-contact-secret: 8tnH50ISscWBhQbxSzjRrA6XT2oJBUvEdEpY77hmEK4 token: crmAgentToken2026 encoding-aes-key: k3F8mN2vQ7xLp9sTd4YwH6cRb1JuEz5AaZq7BnVcK2D crm-agent-id: crm-assistant