客资管理
This commit is contained in:
parent
866d14461b
commit
d8be530787
@ -1,17 +1,52 @@
|
|||||||
package com.company.agentfoundation.agent.capability;
|
package com.company.agentfoundation.agent.capability;
|
||||||
|
|
||||||
|
import com.company.agentfoundation.agent.AgentCapabilityResponse;
|
||||||
import com.company.agentfoundation.agent.AgentIntent;
|
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 org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class CustomerDashboardDailyCapabilityHandler extends AbstractPlaceholderCapabilityHandler {
|
public class CustomerDashboardDailyCapabilityHandler implements AgentCapabilityHandler {
|
||||||
public CustomerDashboardDailyCapabilityHandler() {
|
private final WecomStudentContactStatsService studentContactStatsService;
|
||||||
super(
|
|
||||||
AgentIntent.CUSTOMER_DASHBOARD_DAILY,
|
public CustomerDashboardDailyCapabilityHandler(WecomStudentContactStatsService studentContactStatsService) {
|
||||||
"\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",
|
this.studentContactStatsService = studentContactStatsService;
|
||||||
List.of("\u5b66\u90a6\u6559\u52a1\u7cfb\u7edf", "\u4f01\u4e1a\u5fae\u4fe1", "CRM \u6570\u636e\u5e93")
|
}
|
||||||
|
|
||||||
|
@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()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,6 +56,7 @@ public class FoundationProperties {
|
|||||||
private String corpId;
|
private String corpId;
|
||||||
private Integer agentId;
|
private Integer agentId;
|
||||||
private String secret;
|
private String secret;
|
||||||
|
private String studentContactSecret;
|
||||||
private String token;
|
private String token;
|
||||||
private String encodingAesKey;
|
private String encodingAesKey;
|
||||||
private String crmAgentId = "crm-assistant";
|
private String crmAgentId = "crm-assistant";
|
||||||
@ -69,6 +70,8 @@ public class FoundationProperties {
|
|||||||
public void setAgentId(Integer agentId) { this.agentId = agentId; }
|
public void setAgentId(Integer agentId) { this.agentId = agentId; }
|
||||||
public String getSecret() { return secret; }
|
public String getSecret() { return secret; }
|
||||||
public void setSecret(String secret) { this.secret = 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 String getToken() { return token; }
|
||||||
public void setToken(String token) { this.token = token; }
|
public void setToken(String token) { this.token = token; }
|
||||||
public String getEncodingAesKey() { return encodingAesKey; }
|
public String getEncodingAesKey() { return encodingAesKey; }
|
||||||
|
|||||||
@ -8,6 +8,9 @@ import org.springframework.stereotype.Service;
|
|||||||
import org.springframework.web.reactive.function.client.WebClient;
|
import org.springframework.web.reactive.function.client.WebClient;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class WecomClient {
|
public class WecomClient {
|
||||||
@ -17,6 +20,7 @@ public class WecomClient {
|
|||||||
|
|
||||||
private volatile String cachedToken;
|
private volatile String cachedToken;
|
||||||
private volatile Instant tokenExpireAt = Instant.EPOCH;
|
private volatile Instant tokenExpireAt = Instant.EPOCH;
|
||||||
|
private final Map<String, TokenCache> tokenCaches = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
public WecomClient(FoundationProperties properties, WebClient.Builder builder) {
|
public WecomClient(FoundationProperties properties, WebClient.Builder builder) {
|
||||||
this.properties = properties;
|
this.properties = properties;
|
||||||
@ -48,6 +52,43 @@ public class WecomClient {
|
|||||||
log.info("wecom sendText success toUser={}", toUser);
|
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<String> 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() {
|
private String getAccessToken() {
|
||||||
if (cachedToken != null && Instant.now().isBefore(tokenExpireAt)) {
|
if (cachedToken != null && Instant.now().isBefore(tokenExpireAt)) {
|
||||||
log.info("wecom gettoken use cached token expireAt={}", tokenExpireAt);
|
log.info("wecom gettoken use cached token expireAt={}", tokenExpireAt);
|
||||||
@ -75,6 +116,40 @@ public class WecomClient {
|
|||||||
return cachedToken;
|
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) {
|
private String preview(String text) {
|
||||||
if (text == null || text.isBlank()) {
|
if (text == null || text.isBlank()) {
|
||||||
return "";
|
return "";
|
||||||
@ -82,4 +157,8 @@ public class WecomClient {
|
|||||||
return text.length() <= 80 ? text : text.substring(0, 80) + "...";
|
return text.length() <= 80 ? text : text.substring(0, 80) + "...";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class TokenCache {
|
||||||
|
private volatile String token;
|
||||||
|
private volatile Instant expireAt = Instant.EPOCH;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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<String> userIdList,
|
||||||
|
String cursor,
|
||||||
|
Integer limit
|
||||||
|
) {}
|
||||||
@ -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<ExternalContactItem> 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
|
||||||
|
) {}
|
||||||
|
}
|
||||||
@ -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<String> followUser
|
||||||
|
) {
|
||||||
|
public boolean success() {
|
||||||
|
return errcode == null || errcode == 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
package com.company.agentfoundation.integration.wecom;
|
||||||
|
|
||||||
|
public record WecomStudentContactStats(
|
||||||
|
int totalStudents,
|
||||||
|
int todayNewStudents,
|
||||||
|
int followUserCount
|
||||||
|
) {}
|
||||||
@ -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<String> followUsers = wecomClient.getStudentContactFollowUsers().followUser();
|
||||||
|
if (followUsers == null || followUsers.isEmpty()) {
|
||||||
|
return new WecomStudentContactStats(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> allStudents = new HashSet<>();
|
||||||
|
Map<String, Long> earliestCreateTimeByStudent = new HashMap<>();
|
||||||
|
for (int start = 0; start < followUsers.size(); start += BATCH_SIZE) {
|
||||||
|
List<String> 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<String> userIds, Set<String> allStudents, Map<String, Long> earliestCreateTimeByStudent) {
|
||||||
|
String cursor = "";
|
||||||
|
while (true) {
|
||||||
|
WecomExternalContactBatchResponse response = wecomClient.batchGetStudentContacts(userIds, cursor, BATCH_SIZE);
|
||||||
|
List<WecomExternalContactBatchResponse.ExternalContactItem> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -50,6 +50,7 @@ foundation:
|
|||||||
corp-id: ww11a329ca8ee0c40e
|
corp-id: ww11a329ca8ee0c40e
|
||||||
agent-id: 1000002
|
agent-id: 1000002
|
||||||
secret: 8tnH50ISscWBhQbxSzjRrA6XT2oJBUvEdEpY77hmEK4
|
secret: 8tnH50ISscWBhQbxSzjRrA6XT2oJBUvEdEpY77hmEK4
|
||||||
|
student-contact-secret: 8tnH50ISscWBhQbxSzjRrA6XT2oJBUvEdEpY77hmEK4
|
||||||
token: crmAgentToken2026
|
token: crmAgentToken2026
|
||||||
encoding-aes-key: k3F8mN2vQ7xLp9sTd4YwH6cRb1JuEz5AaZq7BnVcK2D
|
encoding-aes-key: k3F8mN2vQ7xLp9sTd4YwH6cRb1JuEz5AaZq7BnVcK2D
|
||||||
crm-agent-id: crm-assistant
|
crm-agent-id: crm-assistant
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user