diff --git a/backend/analytics-service/src/main/resources/application.yml b/backend/analytics-service/src/main/resources/application.yml
index 1aa49eb..e70966f 100644
--- a/backend/analytics-service/src/main/resources/application.yml
+++ b/backend/analytics-service/src/main/resources/application.yml
@@ -11,7 +11,7 @@ spring:
name: analytics-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
- url: jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:ai_assistant}?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false
+ url: jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:ai_assistant}?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false
username: ${MYSQL_USERNAME:root}
password: ${MYSQL_PASSWORD:root}
redis:
diff --git a/backend/analytics-service/target/analytics-service-1.0.0-SNAPSHOT.jar b/backend/analytics-service/target/analytics-service-1.0.0-SNAPSHOT.jar
index 3f3d8bc..5dc35c7 100644
Binary files a/backend/analytics-service/target/analytics-service-1.0.0-SNAPSHOT.jar and b/backend/analytics-service/target/analytics-service-1.0.0-SNAPSHOT.jar differ
diff --git a/backend/analytics-service/target/analytics-service-1.0.0-SNAPSHOT.jar.original b/backend/analytics-service/target/analytics-service-1.0.0-SNAPSHOT.jar.original
index 6652a5f..e3647ef 100644
Binary files a/backend/analytics-service/target/analytics-service-1.0.0-SNAPSHOT.jar.original and b/backend/analytics-service/target/analytics-service-1.0.0-SNAPSHOT.jar.original differ
diff --git a/backend/analytics-service/target/classes/application.yml b/backend/analytics-service/target/classes/application.yml
index 1aa49eb..e70966f 100644
--- a/backend/analytics-service/target/classes/application.yml
+++ b/backend/analytics-service/target/classes/application.yml
@@ -11,7 +11,7 @@ spring:
name: analytics-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
- url: jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:ai_assistant}?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false
+ url: jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:ai_assistant}?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false
username: ${MYSQL_USERNAME:root}
password: ${MYSQL_PASSWORD:root}
redis:
diff --git a/backend/analytics-service/target/classes/com/artedu/analytics/AnalyticsServiceApplication.class b/backend/analytics-service/target/classes/com/artedu/analytics/AnalyticsServiceApplication.class
index e96882f..41eb847 100644
Binary files a/backend/analytics-service/target/classes/com/artedu/analytics/AnalyticsServiceApplication.class and b/backend/analytics-service/target/classes/com/artedu/analytics/AnalyticsServiceApplication.class differ
diff --git a/backend/analytics-service/target/classes/com/artedu/analytics/controller/AnalyticsController.class b/backend/analytics-service/target/classes/com/artedu/analytics/controller/AnalyticsController.class
index 7cc1332..b899ad9 100644
Binary files a/backend/analytics-service/target/classes/com/artedu/analytics/controller/AnalyticsController.class and b/backend/analytics-service/target/classes/com/artedu/analytics/controller/AnalyticsController.class differ
diff --git a/backend/analytics-service/target/classes/com/artedu/analytics/service/AnalyticsService.class b/backend/analytics-service/target/classes/com/artedu/analytics/service/AnalyticsService.class
index db5b458..ded7e9c 100644
Binary files a/backend/analytics-service/target/classes/com/artedu/analytics/service/AnalyticsService.class and b/backend/analytics-service/target/classes/com/artedu/analytics/service/AnalyticsService.class differ
diff --git a/backend/archive-service/pom.xml b/backend/archive-service/pom.xml
index 80f4d21..7202414 100644
--- a/backend/archive-service/pom.xml
+++ b/backend/archive-service/pom.xml
@@ -16,9 +16,19 @@
org.springframework.bootspring-boot-starter-web
net.java.dev.jnajna
org.springframework.bootspring-boot-starter-testtest
+ org.bouncycastlebcpkix-jdk18on1.78
+ com.alibabaeasyexcel3.3.4
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ 1.8
+ 1.8
+
+
org.springframework.bootspring-boot-maven-pluginrepackage
diff --git a/backend/archive-service/src/main/java/com/artedu/archive/ArchiveServiceApplication.java b/backend/archive-service/src/main/java/com/artedu/archive/ArchiveServiceApplication.java
index c96e8a4..2d8dd19 100644
--- a/backend/archive-service/src/main/java/com/artedu/archive/ArchiveServiceApplication.java
+++ b/backend/archive-service/src/main/java/com/artedu/archive/ArchiveServiceApplication.java
@@ -1,15 +1,59 @@
package com.artedu.archive;
+import com.artedu.archive.service.ArchivePullService;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.web.client.RestTemplate;
/**
* 会话存档服务启动类
*/
+@EnableAsync
@SpringBootApplication(scanBasePackages = {"com.artedu.archive", "com.artedu.common"})
+@org.springframework.scheduling.annotation.EnableScheduling
public class ArchiveServiceApplication {
+ @Bean
+ public RestTemplate restTemplate() {
+ return new RestTemplate();
+ }
+
public static void main(String[] args) {
- SpringApplication.run(ArchiveServiceApplication.class, args);
+ if (args.length > 0 && "worker".equals(args[0])) {
+ try {
+ com.artedu.archive.worker.SdkWorker.run(
+ java.util.Arrays.copyOfRange(args, 1, args.length));
+ } catch (Exception e) {
+ e.printStackTrace();
+ System.exit(1);
+ }
+ } else {
+ SpringApplication.run(ArchiveServiceApplication.class, args);
+ }
+ }
+
+ @Bean
+ public ApplicationRunner repairRunner(ArchivePullService archivePullService) {
+ return new ApplicationRunner() {
+ @Override
+ public void run(ApplicationArguments args) throws Exception {
+ String repairFlag = System.getenv("ARCHIVE_REPAIR_ON_STARTUP");
+ if ("true".equalsIgnoreCase(repairFlag)) {
+ new Thread(() -> {
+ // 等待应用完全启动
+ try {
+ Thread.sleep(10000);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ archivePullService.repairHistoricalData();
+ }, "archive-repair-thread").start();
+ }
+ }
+ };
}
}
diff --git a/backend/archive-service/src/main/java/com/artedu/archive/client/WeComApiClient.java b/backend/archive-service/src/main/java/com/artedu/archive/client/WeComApiClient.java
new file mode 100644
index 0000000..e373d8e
--- /dev/null
+++ b/backend/archive-service/src/main/java/com/artedu/archive/client/WeComApiClient.java
@@ -0,0 +1,197 @@
+package com.artedu.archive.client;
+
+import com.artedu.common.util.JsonUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 企业微信API客户端
+ * 用于获取员工详情、客户详情等
+ */
+@Slf4j
+@Component
+public class WeComApiClient {
+
+ @Value("${wecom.corp-id:}")
+ private String corpId;
+
+ @Value("${wecom.agent.secret:}")
+ private String agentSecret;
+
+ @Value("${wecom.contact.secret:}")
+ private String contactSecret;
+
+ @Autowired
+ private RestTemplate restTemplate;
+
+ @Autowired
+ private RedisTemplate redisTemplate;
+
+ private static final String ACCESS_TOKEN_KEY = "wecom:access_token";
+ private static final String CONTACT_TOKEN_KEY = "wecom:contact_access_token";
+ private static final long TOKEN_EXPIRE_SECONDS = 7000;
+
+ /**
+ * 获取AccessToken(会话存档/通讯录应用)
+ */
+ public String getAccessToken() {
+ return doGetAccessToken(agentSecret, ACCESS_TOKEN_KEY);
+ }
+
+ /**
+ * 获取客户联系AccessToken(客户联系/通讯录同步应用)
+ */
+ public String getContactAccessToken() {
+ return doGetAccessToken(contactSecret, CONTACT_TOKEN_KEY);
+ }
+
+ private String doGetAccessToken(String secret, String cacheKey) {
+ if (corpId == null || corpId.isEmpty() || secret == null || secret.isEmpty()) {
+ log.warn("企微API配置不完整,无法获取access_token: cacheKey={}", cacheKey);
+ return null;
+ }
+
+ String token = (String) redisTemplate.opsForValue().get(cacheKey);
+ if (token != null) {
+ return token;
+ }
+
+ try {
+ String url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=" + corpId + "&corpsecret=" + secret;
+ ResponseEntity response = restTemplate.getForEntity(url, String.class);
+ Map result = JsonUtils.fromJsonMap(response.getBody());
+
+ if (result == null || !Integer.valueOf(0).equals(result.get("errcode"))) {
+ log.error("获取AccessToken失败: cacheKey={}, resp={}", cacheKey, response.getBody());
+ return null;
+ }
+
+ token = (String) result.get("access_token");
+ if (token != null) {
+ redisTemplate.opsForValue().set(cacheKey, token, TOKEN_EXPIRE_SECONDS, TimeUnit.SECONDS);
+ }
+ return token;
+ } catch (Exception e) {
+ log.error("获取AccessToken异常: cacheKey={}, err={}", cacheKey, e.getMessage());
+ return null;
+ }
+ }
+
+ /**
+ * 获取员工详情
+ */
+ public Map getUserDetail(String userId) {
+ String accessToken = getAccessToken();
+ if (accessToken == null) return Collections.emptyMap();
+
+ try {
+ String url = "https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token=" + accessToken + "&userid=" + userId;
+ ResponseEntity response = restTemplate.getForEntity(url, String.class);
+ Map result = JsonUtils.fromJsonMap(response.getBody());
+
+ if (result == null || !Integer.valueOf(0).equals(result.get("errcode"))) {
+ log.warn("获取员工详情失败: userId={}, resp={}", userId, response.getBody());
+ return Collections.emptyMap();
+ }
+ return result;
+ } catch (Exception e) {
+ log.error("获取员工详情异常: userId={}", userId, e);
+ return Collections.emptyMap();
+ }
+ }
+
+ /**
+ * 获取客户详情
+ */
+ public Map getExternalContact(String externalUserId) {
+ String accessToken = getAccessToken();
+ if (accessToken == null) return Collections.emptyMap();
+
+ try {
+ String url = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get?access_token=" + accessToken + "&external_userid=" + externalUserId;
+ ResponseEntity response = restTemplate.getForEntity(url, String.class);
+ Map result = JsonUtils.fromJsonMap(response.getBody());
+
+ if (result == null || !Integer.valueOf(0).equals(result.get("errcode"))) {
+ log.warn("获取客户详情失败: externalUserId={}, resp={}", externalUserId, response.getBody());
+ return Collections.emptyMap();
+ }
+ return result;
+ } catch (Exception e) {
+ log.error("获取客户详情异常: externalUserId={}", externalUserId, e);
+ return Collections.emptyMap();
+ }
+ }
+
+ /**
+ * 获取客户列表(分页)
+ */
+ public List getExternalContactList(String userId) {
+ String accessToken = getAccessToken();
+ if (accessToken == null) return Collections.emptyList();
+
+ try {
+ String url = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/list?access_token=" + accessToken + "&userid=" + userId;
+ ResponseEntity response = restTemplate.getForEntity(url, String.class);
+ Map result = JsonUtils.fromJsonMap(response.getBody());
+
+ if (result == null || !Integer.valueOf(0).equals(result.get("errcode"))) {
+ log.warn("获取客户列表失败: userId={}, resp={}", userId, response.getBody());
+ return Collections.emptyList();
+ }
+
+ Object list = result.get("external_userid");
+ if (list instanceof List) {
+ return (List) list;
+ }
+ return Collections.emptyList();
+ } catch (Exception e) {
+ log.error("获取客户列表异常: userId={}", userId, e);
+ return Collections.emptyList();
+ }
+ }
+
+ /**
+ * 获取部门成员列表
+ */
+ public List