fix: 渠道账号群聊同步缓存 key 及成员名称兜底;统一 Docker/Maven 项目约定
This commit is contained in:
2
backend/.mvn/maven.config
Normal file
2
backend/.mvn/maven.config
Normal file
@@ -0,0 +1,2 @@
|
||||
--toolchains
|
||||
/Users/marsal/Projects/sino-project/sino-mci/backend/.mvn/toolchains.xml
|
||||
13
backend/.mvn/toolchains.xml
Normal file
13
backend/.mvn/toolchains.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<toolchains>
|
||||
<toolchain>
|
||||
<type>jdk</type>
|
||||
<provides>
|
||||
<version>21</version>
|
||||
<vendor>zulu</vendor>
|
||||
</provides>
|
||||
<configuration>
|
||||
<jdkHome>/Library/Java/JavaVirtualMachines/zulu-21.jdk/Contents/Home</jdkHome>
|
||||
</configuration>
|
||||
</toolchain>
|
||||
</toolchains>
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sino.mci.channel.wecom.sync;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
@@ -10,10 +11,11 @@ import java.util.List;
|
||||
* <p>不发起真实企微 API 调用,仅返回固定样例数据,供开发与测试使用。</p>
|
||||
*/
|
||||
@Component
|
||||
@ConditionalOnProperty(name = "mci.wecom.sync.mock", havingValue = "true")
|
||||
public class MockWeComSyncClient implements WeComSyncClient {
|
||||
|
||||
@Override
|
||||
public List<WeComContact> syncContacts(WeComSyncConfig config) {
|
||||
public List<WeComContact> syncEnterpriseContacts(WeComSyncConfig config) {
|
||||
return List.of(
|
||||
WeComContact.builder()
|
||||
.userId("user001")
|
||||
@@ -21,7 +23,13 @@ public class MockWeComSyncClient implements WeComSyncClient {
|
||||
.mobile("13800138001")
|
||||
.type(WeComContactType.INTERNAL)
|
||||
.departmentIds(List.of(1L, 2L))
|
||||
.build(),
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<WeComContact> syncAccountContacts(WeComSyncConfig config, String userId) {
|
||||
return List.of(
|
||||
WeComContact.builder()
|
||||
.userId("wmexternal001")
|
||||
.name("客户李四")
|
||||
@@ -32,15 +40,15 @@ public class MockWeComSyncClient implements WeComSyncClient {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<WeComConversation> syncConversations(WeComSyncConfig config) {
|
||||
public List<WeComConversation> syncAccountConversations(WeComSyncConfig config, String userId) {
|
||||
return List.of(
|
||||
WeComConversation.builder()
|
||||
.chatId("chat001")
|
||||
.chatName("测试客户群")
|
||||
.ownerUserId("user001")
|
||||
.ownerUserId(userId)
|
||||
.memberList(List.of(
|
||||
WeComConversationMember.builder()
|
||||
.userId("user001")
|
||||
.userId(userId)
|
||||
.name("张三")
|
||||
.build(),
|
||||
WeComConversationMember.builder()
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
package com.sino.mci.channel.wecom.sync;
|
||||
|
||||
import com.sino.mci.channel.wecom.support.WeComServiceFactory;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.chanjar.weixin.common.error.WxErrorException;
|
||||
import me.chanjar.weixin.cp.api.WxCpService;
|
||||
import me.chanjar.weixin.cp.bean.WxCpDepart;
|
||||
import me.chanjar.weixin.cp.bean.WxCpUser;
|
||||
import me.chanjar.weixin.cp.bean.external.WxCpUserExternalGroupChatInfo;
|
||||
import me.chanjar.weixin.cp.bean.external.WxCpUserExternalGroupChatList;
|
||||
import me.chanjar.weixin.cp.bean.external.contact.WxCpExternalContactInfo;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 企业微信通讯录与群聊真实同步客户端。
|
||||
*
|
||||
* <p>基于 {@link WxCpService} 调用企微开放接口,拉取企业内部成员、外部联系人及客户群。</p>
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnProperty(name = "mci.wecom.sync.mock", havingValue = "false", matchIfMissing = true)
|
||||
public class RealWeComSyncClient implements WeComSyncClient {
|
||||
|
||||
private final WeComServiceFactory serviceFactory;
|
||||
|
||||
@Override
|
||||
public List<WeComContact> syncEnterpriseContacts(WeComSyncConfig config) {
|
||||
WxCpService service = resolveService(config);
|
||||
if (service == null) {
|
||||
log.warn("[WeComSync] 无法创建 WxCpService,跳过企业通讯录同步");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<WeComContact> contacts = new ArrayList<>();
|
||||
contacts.addAll(syncInternalUsers(service, config));
|
||||
contacts.addAll(syncExternalContacts(service, config));
|
||||
log.info("[WeComSync] 同步企业通讯录完成,共 {} 条", contacts.size());
|
||||
return contacts;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<WeComContact> syncAccountContacts(WeComSyncConfig config, String userId) {
|
||||
WxCpService service = resolveService(config);
|
||||
if (service == null) {
|
||||
log.warn("[WeComSync] 无法创建 WxCpService,跳过账号客户同步");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<WeComContact> contacts = syncExternalContactsForUser(service, config, userId);
|
||||
log.info("[WeComSync] 同步账号 {} 的客户完成,共 {} 条", userId, contacts.size());
|
||||
return contacts;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<WeComConversation> syncAccountConversations(WeComSyncConfig config, String userId) {
|
||||
WxCpService service = resolveService(config);
|
||||
if (service == null) {
|
||||
log.warn("[WeComSync] 无法创建 WxCpService,跳过账号群聊同步");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<WeComConversation> conversations = new ArrayList<>();
|
||||
try {
|
||||
String cursor = "";
|
||||
do {
|
||||
WxCpUserExternalGroupChatList result = service.getExternalContactService()
|
||||
.listGroupChat(1000, cursor, 0, new String[]{userId});
|
||||
if (result == null || result.getGroupChatList() == null) {
|
||||
break;
|
||||
}
|
||||
for (WxCpUserExternalGroupChatList.ChatStatus chat : result.getGroupChatList()) {
|
||||
WeComConversation conversation = fetchGroupChatDetail(service, chat.getChatId());
|
||||
if (conversation != null) {
|
||||
conversations.add(conversation);
|
||||
}
|
||||
}
|
||||
cursor = result.getNextCursor();
|
||||
} while (cursor != null && !cursor.isEmpty());
|
||||
} catch (WxErrorException e) {
|
||||
log.warn("[WeComSync] 拉取账号 {} 的客户群列表失败,可能应用缺少客户联系权限: {}", userId, e.getMessage());
|
||||
} catch (Exception e) {
|
||||
log.warn("[WeComSync] 拉取账号 {} 的客户群列表异常", userId, e);
|
||||
}
|
||||
log.info("[WeComSync] 同步账号 {} 的群聊完成,共 {} 条", userId, conversations.size());
|
||||
return conversations;
|
||||
}
|
||||
|
||||
private WxCpService resolveService(WeComSyncConfig config) {
|
||||
if (config == null || config.getCorpId() == null || config.getAgentId() == null || config.getSecret() == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return serviceFactory.getOrCreateService(
|
||||
config.getCorpId(), Integer.valueOf(config.getAgentId()), config.getSecret());
|
||||
} catch (Exception e) {
|
||||
log.warn("[WeComSync] 创建 WxCpService 失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private List<WeComContact> syncInternalUsers(WxCpService service, WeComSyncConfig config) {
|
||||
List<WeComContact> contacts = new ArrayList<>();
|
||||
try {
|
||||
List<WxCpDepart> departments = service.getDepartmentService().list(null);
|
||||
if (departments == null || departments.isEmpty()) {
|
||||
return contacts;
|
||||
}
|
||||
for (WxCpDepart dept : departments) {
|
||||
try {
|
||||
List<WxCpUser> users = service.getUserService().listByDepartment(dept.getId(), true, 0);
|
||||
if (users == null) {
|
||||
continue;
|
||||
}
|
||||
for (WxCpUser user : users) {
|
||||
contacts.add(WeComContact.builder()
|
||||
.userId(user.getUserId())
|
||||
.name(user.getName())
|
||||
.mobile(user.getMobile())
|
||||
.type(WeComContactType.INTERNAL)
|
||||
.departmentIds(user.getDepartIds() == null
|
||||
? Collections.emptyList()
|
||||
: Arrays.stream(user.getDepartIds()).collect(Collectors.toList()))
|
||||
.build());
|
||||
}
|
||||
} catch (WxErrorException e) {
|
||||
log.warn("[WeComSync] 获取部门 {} 成员失败: {}", dept.getId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
} catch (WxErrorException e) {
|
||||
log.warn("[WeComSync] 拉取部门列表失败,可能应用缺少通讯录权限: {}", e.getMessage());
|
||||
} catch (Exception e) {
|
||||
log.warn("[WeComSync] 拉取内部成员异常", e);
|
||||
}
|
||||
return contacts;
|
||||
}
|
||||
|
||||
private List<WeComContact> syncExternalContacts(WxCpService service, WeComSyncConfig config) {
|
||||
List<WeComContact> contacts = new ArrayList<>();
|
||||
try {
|
||||
List<String> followUsers = service.getExternalContactService().listFollowers();
|
||||
if (followUsers == null || followUsers.isEmpty()) {
|
||||
return contacts;
|
||||
}
|
||||
for (String userId : followUsers) {
|
||||
contacts.addAll(syncExternalContactsForUser(service, config, userId));
|
||||
}
|
||||
} catch (WxErrorException e) {
|
||||
log.warn("[WeComSync] 拉取客户联系成员失败,可能应用缺少客户联系权限: {}", e.getMessage());
|
||||
} catch (Exception e) {
|
||||
log.warn("[WeComSync] 拉取外部联系人异常", e);
|
||||
}
|
||||
return contacts;
|
||||
}
|
||||
|
||||
private List<WeComContact> syncExternalContactsForUser(WxCpService service, WeComSyncConfig config, String userId) {
|
||||
List<WeComContact> contacts = new ArrayList<>();
|
||||
try {
|
||||
List<String> externalUserIds = service.getExternalContactService().listExternalContacts(userId);
|
||||
if (externalUserIds == null) {
|
||||
return contacts;
|
||||
}
|
||||
for (String externalUserId : externalUserIds) {
|
||||
try {
|
||||
WxCpExternalContactInfo info = service.getExternalContactService()
|
||||
.getContactDetail(externalUserId, null);
|
||||
if (info == null || info.getExternalContact() == null) {
|
||||
continue;
|
||||
}
|
||||
contacts.add(WeComContact.builder()
|
||||
.userId(externalUserId)
|
||||
.name(info.getExternalContact().getName())
|
||||
.mobile(null)
|
||||
.type(WeComContactType.EXTERNAL)
|
||||
.departmentIds(Collections.emptyList())
|
||||
.build());
|
||||
} catch (WxErrorException e) {
|
||||
log.warn("[WeComSync] 获取外部联系人 {} 详情失败: {}", externalUserId, e.getMessage());
|
||||
}
|
||||
}
|
||||
} catch (WxErrorException e) {
|
||||
log.warn("[WeComSync] 获取成员 {} 的客户列表失败: {}", userId, e.getMessage());
|
||||
}
|
||||
return contacts;
|
||||
}
|
||||
|
||||
private WeComConversation fetchGroupChatDetail(WxCpService service, String chatId) {
|
||||
try {
|
||||
WxCpUserExternalGroupChatInfo detail = service.getExternalContactService().getGroupChat(chatId, 0);
|
||||
if (detail == null || detail.getGroupChat() == null) {
|
||||
return null;
|
||||
}
|
||||
WxCpUserExternalGroupChatInfo.GroupChat groupChat = detail.getGroupChat();
|
||||
List<WeComConversationMember> members = new ArrayList<>();
|
||||
if (groupChat.getMemberList() != null) {
|
||||
for (WxCpUserExternalGroupChatInfo.GroupMember member : groupChat.getMemberList()) {
|
||||
members.add(WeComConversationMember.builder()
|
||||
.userId(member.getUserId())
|
||||
.name(resolveMemberName(service, member))
|
||||
.build());
|
||||
}
|
||||
}
|
||||
return WeComConversation.builder()
|
||||
.chatId(groupChat.getChatId())
|
||||
.chatName(groupChat.getName())
|
||||
.ownerUserId(groupChat.getOwner())
|
||||
.memberList(members)
|
||||
.build();
|
||||
} catch (WxErrorException e) {
|
||||
log.warn("[WeComSync] 获取群聊 {} 详情失败: {}", chatId, e.getMessage());
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
log.warn("[WeComSync] 获取群聊 {} 详情异常", chatId, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveMemberName(WxCpService service, WxCpUserExternalGroupChatInfo.GroupMember member) {
|
||||
if (StringUtils.isNotBlank(member.getName())) {
|
||||
return member.getName();
|
||||
}
|
||||
String userId = member.getUserId();
|
||||
if (StringUtils.isBlank(userId)) {
|
||||
return null;
|
||||
}
|
||||
// 外部联系人:通过客户详情接口补全名称
|
||||
if (userId.startsWith("wm")) {
|
||||
try {
|
||||
WxCpExternalContactInfo info = service.getExternalContactService().getContactDetail(userId, null);
|
||||
if (info != null && info.getExternalContact() != null
|
||||
&& StringUtils.isNotBlank(info.getExternalContact().getName())) {
|
||||
return info.getExternalContact().getName();
|
||||
}
|
||||
} catch (WxErrorException e) {
|
||||
log.debug("[WeComSync] 获取外部联系人 {} 名称失败: {}", userId, e.getMessage());
|
||||
}
|
||||
}
|
||||
// 内部成员:通过通讯录接口补全名称
|
||||
try {
|
||||
WxCpUser user = service.getUserService().getById(userId);
|
||||
if (user != null && StringUtils.isNotBlank(user.getName())) {
|
||||
return user.getName();
|
||||
}
|
||||
} catch (WxErrorException e) {
|
||||
log.debug("[WeComSync] 获取内部成员 {} 名称失败: {}", userId, e.getMessage());
|
||||
}
|
||||
return userId;
|
||||
}
|
||||
}
|
||||
@@ -12,18 +12,28 @@ import java.util.List;
|
||||
public interface WeComSyncClient {
|
||||
|
||||
/**
|
||||
* 同步联系人(企业内部成员 + 外部联系人)。
|
||||
* 同步企业通讯录(部门 + 内部成员)。
|
||||
*
|
||||
* @param config 企微配置
|
||||
* @return 联系人列表
|
||||
*/
|
||||
List<WeComContact> syncContacts(WeComSyncConfig config);
|
||||
List<WeComContact> syncEnterpriseContacts(WeComSyncConfig config);
|
||||
|
||||
/**
|
||||
* 同步群聊(客户群)。
|
||||
* 同步某个成员的客户(外部联系人)。
|
||||
*
|
||||
* @param config 企微配置
|
||||
* @param userId 成员 userId
|
||||
* @return 联系人列表
|
||||
*/
|
||||
List<WeComContact> syncAccountContacts(WeComSyncConfig config, String userId);
|
||||
|
||||
/**
|
||||
* 同步某个成员的客户群。
|
||||
*
|
||||
* @param config 企微配置
|
||||
* @param userId 成员 userId
|
||||
* @return 群聊列表
|
||||
*/
|
||||
List<WeComConversation> syncConversations(WeComSyncConfig config);
|
||||
List<WeComConversation> syncAccountConversations(WeComSyncConfig config, String userId);
|
||||
}
|
||||
|
||||
@@ -70,6 +70,11 @@
|
||||
<artifactId>sa-token-spring-boot3-starter</artifactId>
|
||||
<version>1.39.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-redis-jackson</artifactId>
|
||||
<version>1.39.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
|
||||
|
||||
@@ -4,9 +4,9 @@ import com.sino.mci.admin.security.AuthContext;
|
||||
import com.sino.mci.channel.dto.ChannelAccountInitRequest;
|
||||
import com.sino.mci.channel.dto.CreateChannelAccountRequest;
|
||||
import com.sino.mci.channel.dto.HeartbeatRequest;
|
||||
import com.sino.mci.channel.dto.SyncResult;
|
||||
import com.sino.mci.channel.dto.UpdateStatusRequest;
|
||||
import com.sino.mci.channel.entity.ChannelAccount;
|
||||
import com.sino.mci.channel.entity.SyncTask;
|
||||
import com.sino.mci.channel.service.ChannelAccountService;
|
||||
import com.sino.mci.shared.web.ApiResponse;
|
||||
import jakarta.validation.Valid;
|
||||
@@ -78,16 +78,16 @@ public class ChannelAccountController {
|
||||
}
|
||||
|
||||
@PostMapping("/{account_id}/sync-contacts")
|
||||
public ApiResponse<SyncResult> syncContacts(
|
||||
public ApiResponse<SyncTask> syncContacts(
|
||||
@RequestHeader("X-Tenant-Code") String tenantCode,
|
||||
@PathVariable("account_id") Long accountId) {
|
||||
return ApiResponse.ok(accountService.syncContacts(accountId));
|
||||
return ApiResponse.ok(accountService.submitContactsSyncTask(accountId));
|
||||
}
|
||||
|
||||
@PostMapping("/{account_id}/sync-conversations")
|
||||
public ApiResponse<SyncResult> syncConversations(
|
||||
public ApiResponse<SyncTask> syncConversations(
|
||||
@RequestHeader("X-Tenant-Code") String tenantCode,
|
||||
@PathVariable("account_id") Long accountId) {
|
||||
return ApiResponse.ok(accountService.syncConversations(accountId));
|
||||
return ApiResponse.ok(accountService.submitConversationsSyncTask(accountId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,4 +54,11 @@ public class ChannelSubjectController {
|
||||
public ApiResponse<List<ChannelSubject>> listSubjects() {
|
||||
return ApiResponse.ok(subjectService.listSubjects(AuthContext.currentTenantCode()));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/sync-contacts")
|
||||
public ApiResponse<com.sino.mci.channel.entity.SyncTask> syncEnterpriseContacts(
|
||||
@RequestHeader("X-Tenant-Code") String tenantCode,
|
||||
@PathVariable("id") Long id) {
|
||||
return ApiResponse.ok(subjectService.submitEnterpriseContactsSyncTask(id, tenantCode));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.sino.mci.channel.controller;
|
||||
|
||||
import com.sino.mci.channel.entity.SyncTask;
|
||||
import com.sino.mci.channel.service.SyncTaskService;
|
||||
import com.sino.mci.shared.web.ApiResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/sync-task")
|
||||
@RequiredArgsConstructor
|
||||
public class SyncTaskController {
|
||||
|
||||
private final SyncTaskService syncTaskService;
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ApiResponse<SyncTask> getTask(
|
||||
@RequestHeader("X-Tenant-Code") String tenantCode,
|
||||
@PathVariable("id") Long id) {
|
||||
return ApiResponse.ok(syncTaskService.getTask(id, tenantCode));
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,7 @@ public class WeComCallbackController {
|
||||
command.setChannelSubjectId(subject.getId());
|
||||
command.setTenantCode(subject.getTenantCode());
|
||||
command.setCorpId(subject.getCorpId());
|
||||
command.setSecret(subject.getCorpSecret());
|
||||
command.setSecret(subject.getAgentSecret());
|
||||
command.setPrivateKey(subject.getSessionArchivePrivateKey());
|
||||
|
||||
rabbitTemplate.convertAndSend(RabbitConfig.SESSION_ARCHIVE_EXCHANGE,
|
||||
|
||||
@@ -16,7 +16,6 @@ public class CreateChannelSubjectRequest {
|
||||
private String subjectName;
|
||||
|
||||
private String corpId;
|
||||
private String corpSecret;
|
||||
private String agentId;
|
||||
private String agentSecret;
|
||||
private Integer sessionArchiveEnabled;
|
||||
|
||||
@@ -15,7 +15,6 @@ public class ChannelSubject extends BaseEntity {
|
||||
private String subjectCode;
|
||||
private String subjectName;
|
||||
private String corpId;
|
||||
private String corpSecret;
|
||||
private String agentId;
|
||||
private String agentSecret;
|
||||
private Integer sessionArchiveEnabled;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.sino.mci.channel.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.sino.mci.shared.entity.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("mci_sync_task")
|
||||
public class SyncTask extends BaseEntity {
|
||||
|
||||
public static final int STATUS_PENDING = 0;
|
||||
public static final int STATUS_RUNNING = 1;
|
||||
public static final int STATUS_SUCCESS = 2;
|
||||
public static final int STATUS_FAILED = 3;
|
||||
|
||||
private String tenantCode;
|
||||
private String taskType;
|
||||
private String channelType;
|
||||
private Long targetId;
|
||||
private String targetCode;
|
||||
private Integer taskStatus;
|
||||
private Integer syncedCount;
|
||||
private Integer failedCount;
|
||||
private String errorMessage;
|
||||
private Integer retryCount;
|
||||
private java.time.LocalDateTime finishTime;
|
||||
private Integer deleted;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.sino.mci.channel.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.sino.mci.channel.entity.SyncTask;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface SyncTaskMapper extends BaseMapper<SyncTask> {
|
||||
}
|
||||
@@ -13,27 +13,12 @@ import com.sino.mci.channel.dto.HeartbeatRequest;
|
||||
import com.sino.mci.channel.dto.SyncResult;
|
||||
import com.sino.mci.channel.dto.UpdateStatusRequest;
|
||||
import com.sino.mci.channel.entity.ChannelAccount;
|
||||
import com.sino.mci.channel.entity.ChannelAccountConversation;
|
||||
import com.sino.mci.channel.entity.ChannelSubject;
|
||||
import com.sino.mci.channel.entity.Conversation;
|
||||
import com.sino.mci.channel.entity.ConversationParticipant;
|
||||
import com.sino.mci.channel.repository.ChannelAccountConversationMapper;
|
||||
import com.sino.mci.channel.entity.SyncTask;
|
||||
import com.sino.mci.channel.repository.ChannelAccountMapper;
|
||||
import com.sino.mci.channel.repository.ChannelSubjectMapper;
|
||||
import com.sino.mci.channel.repository.ConversationMapper;
|
||||
import com.sino.mci.channel.repository.ConversationParticipantMapper;
|
||||
import com.sino.mci.channel.wecom.sync.WeComContact;
|
||||
import com.sino.mci.channel.wecom.sync.WeComContactType;
|
||||
import com.sino.mci.channel.wecom.sync.WeComConversation;
|
||||
import com.sino.mci.channel.wecom.sync.WeComConversationMember;
|
||||
import com.sino.mci.channel.wecom.sync.WeComSyncClient;
|
||||
import com.sino.mci.channel.wecom.sync.WeComSyncConfig;
|
||||
import com.sino.mci.crm.entity.ChannelIdentity;
|
||||
import com.sino.mci.crm.entity.Person;
|
||||
import com.sino.mci.crm.repository.ChannelIdentityMapper;
|
||||
import com.sino.mci.crm.repository.PersonMapper;
|
||||
import com.sino.mci.exception.BusinessException;
|
||||
import com.sino.mci.shared.util.JsonUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -51,16 +36,6 @@ public class ChannelAccountService {
|
||||
|
||||
private static final String CHANNEL_WECOM = ChannelType.WECOM.name().toLowerCase();
|
||||
|
||||
private static final int BINDING_SEND_MASTER = 1;
|
||||
private static final int BINDING_RECEIVE_MASTER = 3;
|
||||
|
||||
private static final int PERSON_TYPE_INTERNAL = 1;
|
||||
private static final int PERSON_TYPE_CUSTOMER = 3;
|
||||
|
||||
private static final int CONVERSATION_TYPE_GROUP = 2;
|
||||
private static final int PARTICIPANT_TYPE_MEMBER = 1;
|
||||
private static final int PARTICIPANT_TYPE_OWNER = 2;
|
||||
|
||||
private static final int ONLINE_STATUS_OFFLINE = 0;
|
||||
private static final int ONLINE_STATUS_ONLINE = 1;
|
||||
private static final int ACCOUNT_STATUS_ENABLED = 1;
|
||||
@@ -68,13 +43,10 @@ public class ChannelAccountService {
|
||||
|
||||
private final ChannelAccountMapper accountMapper;
|
||||
private final ChannelSubjectMapper subjectMapper;
|
||||
private final PersonMapper personMapper;
|
||||
private final ChannelIdentityMapper channelIdentityMapper;
|
||||
private final ConversationMapper conversationMapper;
|
||||
private final ConversationParticipantMapper conversationParticipantMapper;
|
||||
private final ChannelAccountConversationMapper accountConversationMapper;
|
||||
private final WeComSyncClient weComSyncClient;
|
||||
private final ChannelAdapterRegistry adapterRegistry;
|
||||
private final SyncTaskService syncTaskService;
|
||||
private final SyncTaskExecutor syncTaskExecutor;
|
||||
|
||||
public List<ChannelAccount> listAccounts(String tenantCode) {
|
||||
return accountMapper.selectList(
|
||||
@@ -119,7 +91,7 @@ public class ChannelAccountService {
|
||||
account.setAccountName(request.getAccountName());
|
||||
account.setSessionArchiveEnabled(request.getSessionArchiveEnabled() != null ? request.getSessionArchiveEnabled() : 0);
|
||||
account.setServerHost(request.getServerHost());
|
||||
account.setExtInfo(request.getExtInfo());
|
||||
account.setExtInfo(normalizeExtInfo(request.getExtInfo()));
|
||||
account.setLoginStatus(0);
|
||||
account.setOnlineStatus(0);
|
||||
account.setRiskLevel(0);
|
||||
@@ -157,11 +129,15 @@ public class ChannelAccountService {
|
||||
account.setAccountName(request.getAccountName());
|
||||
account.setSessionArchiveEnabled(request.getSessionArchiveEnabled() != null ? request.getSessionArchiveEnabled() : 0);
|
||||
account.setServerHost(request.getServerHost());
|
||||
account.setExtInfo(request.getExtInfo());
|
||||
account.setExtInfo(normalizeExtInfo(request.getExtInfo()));
|
||||
accountMapper.updateById(account);
|
||||
return account;
|
||||
}
|
||||
|
||||
private String normalizeExtInfo(String extInfo) {
|
||||
return (extInfo == null || extInfo.isBlank()) ? null : extInfo;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteAccount(Long id, String tenantCode) {
|
||||
ChannelAccount account = getAccount(id, tenantCode);
|
||||
@@ -268,8 +244,8 @@ public class ChannelAccountService {
|
||||
if (subject.getCorpId() != null) {
|
||||
config.putIfAbsent("corp_id", subject.getCorpId());
|
||||
}
|
||||
if (subject.getCorpSecret() != null) {
|
||||
config.putIfAbsent("secret", subject.getCorpSecret());
|
||||
if (subject.getAgentSecret() != null) {
|
||||
config.putIfAbsent("secret", subject.getAgentSecret());
|
||||
}
|
||||
if (subject.getAgentId() != null) {
|
||||
config.putIfAbsent("agent_id", subject.getAgentId());
|
||||
@@ -322,32 +298,20 @@ public class ChannelAccountService {
|
||||
return result;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public SyncResult syncContacts(Long accountId) {
|
||||
public SyncTask submitContactsSyncTask(Long accountId) {
|
||||
ChannelAccount account = getWeComAccount(accountId);
|
||||
ChannelSubject subject = getSubject(account);
|
||||
WeComSyncConfig config = buildSyncConfig(subject);
|
||||
|
||||
List<WeComContact> contacts = weComSyncClient.syncContacts(config);
|
||||
for (WeComContact contact : contacts) {
|
||||
upsertContact(account.getTenantCode(), account.getSubjectId(), contact);
|
||||
}
|
||||
return SyncResult.builder().syncedCount(contacts.size()).build();
|
||||
SyncTask task = syncTaskService.createTask(
|
||||
account.getTenantCode(), "account_contacts", account.getChannelType(), accountId, account.getAccountId());
|
||||
syncTaskExecutor.executeAccountContactsSync(task.getId());
|
||||
return task;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public SyncResult syncConversations(Long accountId) {
|
||||
public SyncTask submitConversationsSyncTask(Long accountId) {
|
||||
ChannelAccount account = getWeComAccount(accountId);
|
||||
ChannelSubject subject = getSubject(account);
|
||||
WeComSyncConfig config = buildSyncConfig(subject);
|
||||
|
||||
List<WeComConversation> conversations = weComSyncClient.syncConversations(config);
|
||||
for (WeComConversation conversation : conversations) {
|
||||
Conversation saved = upsertConversation(account, subject.getId(), conversation);
|
||||
refreshParticipants(account.getTenantCode(), account.getSubjectId(), saved, conversation);
|
||||
bindAccountAsDefault(accountId, saved.getId());
|
||||
}
|
||||
return SyncResult.builder().syncedCount(conversations.size()).build();
|
||||
SyncTask task = syncTaskService.createTask(
|
||||
account.getTenantCode(), "account_conversations", account.getChannelType(), accountId, account.getAccountId());
|
||||
syncTaskExecutor.executeAccountConversationsSync(task.getId());
|
||||
return task;
|
||||
}
|
||||
|
||||
private ChannelAccount getWeComAccount(Long accountId) {
|
||||
@@ -361,193 +325,4 @@ public class ChannelAccountService {
|
||||
return account;
|
||||
}
|
||||
|
||||
private ChannelSubject getSubject(ChannelAccount account) {
|
||||
if (account.getSubjectId() == null) {
|
||||
throw new BusinessException("企微账号未绑定渠道主体");
|
||||
}
|
||||
ChannelSubject subject = subjectMapper.selectById(account.getSubjectId());
|
||||
if (subject == null) {
|
||||
throw new BusinessException("渠道主体不存在: " + account.getSubjectId());
|
||||
}
|
||||
if (subject.getCorpId() == null || subject.getCorpSecret() == null) {
|
||||
throw new BusinessException("渠道主体缺少 corp_id / corp_secret");
|
||||
}
|
||||
return subject;
|
||||
}
|
||||
|
||||
private WeComSyncConfig buildSyncConfig(ChannelSubject subject) {
|
||||
return WeComSyncConfig.builder()
|
||||
.corpId(subject.getCorpId())
|
||||
.agentId(subject.getAgentId())
|
||||
.secret(subject.getCorpSecret())
|
||||
.build();
|
||||
}
|
||||
|
||||
private void upsertContact(String tenantCode, Long subjectId, WeComContact contact) {
|
||||
ChannelIdentity identity = channelIdentityMapper.selectOne(
|
||||
new LambdaQueryWrapper<ChannelIdentity>()
|
||||
.eq(ChannelIdentity::getTenantCode, tenantCode)
|
||||
.eq(ChannelIdentity::getChannelType, CHANNEL_WECOM)
|
||||
.eq(ChannelIdentity::getChannelUserId, contact.getUserId()));
|
||||
|
||||
Person person;
|
||||
if (identity != null) {
|
||||
person = personMapper.selectById(identity.getPersonId());
|
||||
if (person == null) {
|
||||
log.warn("ChannelIdentity {} references missing person {}", identity.getId(), identity.getPersonId());
|
||||
return;
|
||||
}
|
||||
Long originalInstitutionId = person.getInstitutionId();
|
||||
Long originalSupplierId = person.getSupplierId();
|
||||
Integer originalPersonType = person.getPersonType();
|
||||
person.setPersonName(contact.getName());
|
||||
person.setPhone(contact.getMobile());
|
||||
person.setInstitutionId(originalInstitutionId);
|
||||
person.setSupplierId(originalSupplierId);
|
||||
person.setPersonType(originalPersonType);
|
||||
personMapper.updateById(person);
|
||||
|
||||
identity.setChannelUserName(contact.getName());
|
||||
identity.setExtInfo(JsonUtils.toJson(contact));
|
||||
channelIdentityMapper.updateById(identity);
|
||||
} else {
|
||||
person = new Person();
|
||||
person.setTenantCode(tenantCode);
|
||||
person.setPersonCode(contact.getUserId());
|
||||
person.setPersonName(contact.getName());
|
||||
person.setPhone(contact.getMobile());
|
||||
person.setPersonType(resolvePersonType(contact));
|
||||
person.setStatus(1);
|
||||
personMapper.insert(person);
|
||||
|
||||
ChannelIdentity newIdentity = new ChannelIdentity();
|
||||
newIdentity.setTenantCode(tenantCode);
|
||||
newIdentity.setPersonId(person.getId());
|
||||
newIdentity.setChannelType(CHANNEL_WECOM);
|
||||
newIdentity.setChannelUserId(contact.getUserId());
|
||||
newIdentity.setChannelUserName(contact.getName());
|
||||
newIdentity.setSubjectId(subjectId);
|
||||
newIdentity.setStatus(1);
|
||||
newIdentity.setExtInfo(JsonUtils.toJson(contact));
|
||||
channelIdentityMapper.insert(newIdentity);
|
||||
}
|
||||
}
|
||||
|
||||
private Conversation upsertConversation(ChannelAccount account, Long subjectId, WeComConversation conv) {
|
||||
Conversation conversation = conversationMapper.selectOne(
|
||||
new LambdaQueryWrapper<Conversation>()
|
||||
.eq(Conversation::getTenantCode, account.getTenantCode())
|
||||
.eq(Conversation::getChannelType, CHANNEL_WECOM)
|
||||
.eq(Conversation::getChannelConversationId, conv.getChatId()));
|
||||
|
||||
if (conversation != null) {
|
||||
Long originalInstitutionId = conversation.getInstitutionId();
|
||||
Long originalSupplierId = conversation.getSupplierId();
|
||||
Long originalContractId = conversation.getContractId();
|
||||
String originalChannelCode = conversation.getChannelCode();
|
||||
conversation.setConversationName(conv.getChatName());
|
||||
conversation.setOwnerAccountId(account.getId());
|
||||
conversation.setSubjectId(subjectId);
|
||||
conversation.setInstitutionId(originalInstitutionId);
|
||||
conversation.setSupplierId(originalSupplierId);
|
||||
conversation.setContractId(originalContractId);
|
||||
conversation.setChannelCode(originalChannelCode);
|
||||
conversationMapper.updateById(conversation);
|
||||
return conversation;
|
||||
}
|
||||
|
||||
Conversation newConversation = new Conversation();
|
||||
newConversation.setTenantCode(account.getTenantCode());
|
||||
newConversation.setConversationType(CONVERSATION_TYPE_GROUP);
|
||||
newConversation.setChannelType(CHANNEL_WECOM);
|
||||
newConversation.setChannelConversationId(conv.getChatId());
|
||||
newConversation.setOwnerAccountId(account.getId());
|
||||
newConversation.setSubjectId(subjectId);
|
||||
newConversation.setConversationName(conv.getChatName());
|
||||
newConversation.setCreateSource(1);
|
||||
newConversation.setStatus(1);
|
||||
conversationMapper.insert(newConversation);
|
||||
return newConversation;
|
||||
}
|
||||
|
||||
private void refreshParticipants(String tenantCode, Long subjectId, Conversation conversation, WeComConversation conv) {
|
||||
conversationParticipantMapper.delete(
|
||||
new LambdaQueryWrapper<ConversationParticipant>()
|
||||
.eq(ConversationParticipant::getConversationId, conversation.getId()));
|
||||
|
||||
for (WeComConversationMember member : conv.getMemberList()) {
|
||||
ChannelIdentity identity = findOrCreateIdentity(tenantCode, subjectId, member);
|
||||
|
||||
ConversationParticipant participant = new ConversationParticipant();
|
||||
participant.setConversationId(conversation.getId());
|
||||
participant.setPersonId(identity.getPersonId());
|
||||
participant.setChannelIdentityId(identity.getId());
|
||||
participant.setParticipantType(resolveParticipantType(member, conv.getOwnerUserId()));
|
||||
participant.setJoinTime(LocalDateTime.now());
|
||||
conversationParticipantMapper.insert(participant);
|
||||
}
|
||||
}
|
||||
|
||||
private ChannelIdentity findOrCreateIdentity(String tenantCode, Long subjectId, WeComConversationMember member) {
|
||||
ChannelIdentity identity = channelIdentityMapper.selectOne(
|
||||
new LambdaQueryWrapper<ChannelIdentity>()
|
||||
.eq(ChannelIdentity::getTenantCode, tenantCode)
|
||||
.eq(ChannelIdentity::getChannelType, CHANNEL_WECOM)
|
||||
.eq(ChannelIdentity::getChannelUserId, member.getUserId()));
|
||||
|
||||
if (identity != null) {
|
||||
return identity;
|
||||
}
|
||||
|
||||
Person person = new Person();
|
||||
person.setTenantCode(tenantCode);
|
||||
person.setPersonCode(member.getUserId());
|
||||
person.setPersonName(member.getName());
|
||||
person.setPersonType(PERSON_TYPE_CUSTOMER);
|
||||
person.setStatus(1);
|
||||
personMapper.insert(person);
|
||||
|
||||
ChannelIdentity newIdentity = new ChannelIdentity();
|
||||
newIdentity.setTenantCode(tenantCode);
|
||||
newIdentity.setPersonId(person.getId());
|
||||
newIdentity.setChannelType(CHANNEL_WECOM);
|
||||
newIdentity.setChannelUserId(member.getUserId());
|
||||
newIdentity.setChannelUserName(member.getName());
|
||||
newIdentity.setSubjectId(subjectId);
|
||||
newIdentity.setStatus(1);
|
||||
channelIdentityMapper.insert(newIdentity);
|
||||
return newIdentity;
|
||||
}
|
||||
|
||||
private void bindAccountAsDefault(Long accountId, Long conversationId) {
|
||||
ensureBinding(accountId, conversationId, BINDING_SEND_MASTER);
|
||||
ensureBinding(accountId, conversationId, BINDING_RECEIVE_MASTER);
|
||||
}
|
||||
|
||||
private void ensureBinding(Long accountId, Long conversationId, int bindingType) {
|
||||
ChannelAccountConversation existing = accountConversationMapper.selectOne(
|
||||
new LambdaQueryWrapper<ChannelAccountConversation>()
|
||||
.eq(ChannelAccountConversation::getAccountId, accountId)
|
||||
.eq(ChannelAccountConversation::getConversationId, conversationId)
|
||||
.eq(ChannelAccountConversation::getBindingType, bindingType));
|
||||
if (existing != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ChannelAccountConversation binding = new ChannelAccountConversation();
|
||||
binding.setAccountId(accountId);
|
||||
binding.setConversationId(conversationId);
|
||||
binding.setBindingType(bindingType);
|
||||
binding.setSortOrder(0);
|
||||
binding.setStatus(1);
|
||||
accountConversationMapper.insert(binding);
|
||||
}
|
||||
|
||||
private int resolvePersonType(WeComContact contact) {
|
||||
return contact.getType() == WeComContactType.INTERNAL ? PERSON_TYPE_INTERNAL : PERSON_TYPE_CUSTOMER;
|
||||
}
|
||||
|
||||
private int resolveParticipantType(WeComConversationMember member, String ownerUserId) {
|
||||
return ownerUserId != null && ownerUserId.equals(member.getUserId()) ? PARTICIPANT_TYPE_OWNER : PARTICIPANT_TYPE_MEMBER;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.sino.mci.channel.dto.CreateChannelSubjectRequest;
|
||||
import com.sino.mci.channel.dto.UpdateStatusRequest;
|
||||
import com.sino.mci.channel.entity.ChannelSubject;
|
||||
import com.sino.mci.channel.entity.SyncTask;
|
||||
import com.sino.mci.channel.repository.ChannelSubjectMapper;
|
||||
import com.sino.mci.exception.BusinessException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -17,6 +18,8 @@ import java.util.List;
|
||||
public class ChannelSubjectService {
|
||||
|
||||
private final ChannelSubjectMapper subjectMapper;
|
||||
private final SyncTaskService syncTaskService;
|
||||
private final SyncTaskExecutor syncTaskExecutor;
|
||||
|
||||
public List<ChannelSubject> listSubjects(String tenantCode) {
|
||||
return subjectMapper.selectList(
|
||||
@@ -51,7 +54,6 @@ public class ChannelSubjectService {
|
||||
subject.setSubjectCode(request.getSubjectCode());
|
||||
subject.setSubjectName(request.getSubjectName());
|
||||
subject.setCorpId(request.getCorpId());
|
||||
subject.setCorpSecret(request.getCorpSecret());
|
||||
subject.setAgentId(request.getAgentId());
|
||||
subject.setAgentSecret(request.getAgentSecret());
|
||||
subject.setSessionArchiveEnabled(request.getSessionArchiveEnabled() != null ? request.getSessionArchiveEnabled() : 0);
|
||||
@@ -80,7 +82,6 @@ public class ChannelSubjectService {
|
||||
subject.setSubjectCode(request.getSubjectCode());
|
||||
subject.setSubjectName(request.getSubjectName());
|
||||
subject.setCorpId(request.getCorpId());
|
||||
subject.setCorpSecret(request.getCorpSecret());
|
||||
subject.setAgentId(request.getAgentId());
|
||||
subject.setAgentSecret(request.getAgentSecret());
|
||||
subject.setSessionArchiveEnabled(request.getSessionArchiveEnabled() != null ? request.getSessionArchiveEnabled() : 0);
|
||||
@@ -103,4 +104,12 @@ public class ChannelSubjectService {
|
||||
subjectMapper.updateById(subject);
|
||||
return subject;
|
||||
}
|
||||
|
||||
public SyncTask submitEnterpriseContactsSyncTask(Long id, String tenantCode) {
|
||||
ChannelSubject subject = getSubject(id, tenantCode);
|
||||
SyncTask task = syncTaskService.createTask(
|
||||
tenantCode, "enterprise_contacts", subject.getChannelType(), id, subject.getSubjectCode());
|
||||
syncTaskExecutor.executeEnterpriseContactsSync(task.getId());
|
||||
return task;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
package com.sino.mci.channel.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.sino.mci.channel.entity.ChannelAccount;
|
||||
import com.sino.mci.channel.entity.ChannelAccountConversation;
|
||||
import com.sino.mci.channel.entity.Conversation;
|
||||
import com.sino.mci.channel.entity.ConversationParticipant;
|
||||
import com.sino.mci.channel.repository.ChannelAccountConversationMapper;
|
||||
import com.sino.mci.channel.repository.ConversationMapper;
|
||||
import com.sino.mci.channel.repository.ConversationParticipantMapper;
|
||||
import com.sino.mci.channel.wecom.sync.WeComContact;
|
||||
import com.sino.mci.channel.wecom.sync.WeComContactType;
|
||||
import com.sino.mci.channel.wecom.sync.WeComConversation;
|
||||
import com.sino.mci.channel.wecom.sync.WeComConversationMember;
|
||||
import com.sino.mci.crm.entity.ChannelIdentity;
|
||||
import com.sino.mci.crm.entity.Person;
|
||||
import com.sino.mci.crm.repository.ChannelIdentityMapper;
|
||||
import com.sino.mci.crm.repository.PersonMapper;
|
||||
import com.sino.mci.shared.util.JsonUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class SyncDataPersister {
|
||||
|
||||
private static final String CHANNEL_WECOM = "wecom";
|
||||
private static final int BINDING_SEND_MASTER = 1;
|
||||
private static final int BINDING_RECEIVE_MASTER = 3;
|
||||
private static final int PERSON_TYPE_INTERNAL = 1;
|
||||
private static final int PERSON_TYPE_CUSTOMER = 3;
|
||||
private static final int CONVERSATION_TYPE_GROUP = 2;
|
||||
private static final int PARTICIPANT_TYPE_MEMBER = 1;
|
||||
private static final int PARTICIPANT_TYPE_OWNER = 2;
|
||||
|
||||
private final PersonMapper personMapper;
|
||||
private final ChannelIdentityMapper channelIdentityMapper;
|
||||
private final ConversationMapper conversationMapper;
|
||||
private final ConversationParticipantMapper conversationParticipantMapper;
|
||||
private final ChannelAccountConversationMapper accountConversationMapper;
|
||||
|
||||
@Transactional
|
||||
public void upsertContact(String tenantCode, Long subjectId, WeComContact contact) {
|
||||
ChannelIdentity identity = channelIdentityMapper.selectOne(
|
||||
new LambdaQueryWrapper<ChannelIdentity>()
|
||||
.eq(ChannelIdentity::getTenantCode, tenantCode)
|
||||
.eq(ChannelIdentity::getChannelType, CHANNEL_WECOM)
|
||||
.eq(ChannelIdentity::getChannelUserId, contact.getUserId()));
|
||||
|
||||
Person person;
|
||||
if (identity != null) {
|
||||
person = personMapper.selectById(identity.getPersonId());
|
||||
if (person == null) {
|
||||
log.warn("ChannelIdentity {} references missing person {}", identity.getId(), identity.getPersonId());
|
||||
return;
|
||||
}
|
||||
Long originalInstitutionId = person.getInstitutionId();
|
||||
Long originalSupplierId = person.getSupplierId();
|
||||
Integer originalPersonType = person.getPersonType();
|
||||
person.setPersonName(contact.getName());
|
||||
person.setPhone(contact.getMobile());
|
||||
person.setInstitutionId(originalInstitutionId);
|
||||
person.setSupplierId(originalSupplierId);
|
||||
person.setPersonType(originalPersonType);
|
||||
personMapper.updateById(person);
|
||||
|
||||
identity.setChannelUserName(contact.getName());
|
||||
identity.setExtInfo(JsonUtils.toJson(contact));
|
||||
channelIdentityMapper.updateById(identity);
|
||||
} else {
|
||||
person = new Person();
|
||||
person.setTenantCode(tenantCode);
|
||||
person.setPersonCode(contact.getUserId());
|
||||
person.setPersonName(contact.getName());
|
||||
person.setPhone(contact.getMobile());
|
||||
person.setPersonType(resolvePersonType(contact));
|
||||
person.setStatus(1);
|
||||
personMapper.insert(person);
|
||||
|
||||
ChannelIdentity newIdentity = new ChannelIdentity();
|
||||
newIdentity.setTenantCode(tenantCode);
|
||||
newIdentity.setPersonId(person.getId());
|
||||
newIdentity.setChannelType(CHANNEL_WECOM);
|
||||
newIdentity.setChannelUserId(contact.getUserId());
|
||||
newIdentity.setChannelUserName(contact.getName());
|
||||
newIdentity.setSubjectId(subjectId);
|
||||
newIdentity.setStatus(1);
|
||||
newIdentity.setExtInfo(JsonUtils.toJson(contact));
|
||||
channelIdentityMapper.insert(newIdentity);
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Conversation upsertConversation(ChannelAccount account, Long subjectId, WeComConversation conv) {
|
||||
Conversation conversation = conversationMapper.selectOne(
|
||||
new LambdaQueryWrapper<Conversation>()
|
||||
.eq(Conversation::getTenantCode, account.getTenantCode())
|
||||
.eq(Conversation::getChannelType, CHANNEL_WECOM)
|
||||
.eq(Conversation::getChannelConversationId, conv.getChatId()));
|
||||
|
||||
if (conversation != null) {
|
||||
Long originalInstitutionId = conversation.getInstitutionId();
|
||||
Long originalSupplierId = conversation.getSupplierId();
|
||||
Long originalContractId = conversation.getContractId();
|
||||
String originalChannelCode = conversation.getChannelCode();
|
||||
conversation.setConversationName(conv.getChatName());
|
||||
conversation.setOwnerAccountId(account.getId());
|
||||
conversation.setSubjectId(subjectId);
|
||||
conversation.setInstitutionId(originalInstitutionId);
|
||||
conversation.setSupplierId(originalSupplierId);
|
||||
conversation.setContractId(originalContractId);
|
||||
conversation.setChannelCode(originalChannelCode);
|
||||
conversationMapper.updateById(conversation);
|
||||
return conversation;
|
||||
}
|
||||
|
||||
Conversation newConversation = new Conversation();
|
||||
newConversation.setTenantCode(account.getTenantCode());
|
||||
newConversation.setConversationType(CONVERSATION_TYPE_GROUP);
|
||||
newConversation.setChannelType(CHANNEL_WECOM);
|
||||
newConversation.setChannelConversationId(conv.getChatId());
|
||||
newConversation.setOwnerAccountId(account.getId());
|
||||
newConversation.setSubjectId(subjectId);
|
||||
newConversation.setConversationName(conv.getChatName());
|
||||
newConversation.setCreateSource(1);
|
||||
newConversation.setStatus(1);
|
||||
conversationMapper.insert(newConversation);
|
||||
return newConversation;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void refreshParticipants(String tenantCode, Long subjectId, Conversation conversation, WeComConversation conv) {
|
||||
conversationParticipantMapper.delete(
|
||||
new LambdaQueryWrapper<ConversationParticipant>()
|
||||
.eq(ConversationParticipant::getConversationId, conversation.getId()));
|
||||
|
||||
for (WeComConversationMember member : conv.getMemberList()) {
|
||||
ChannelIdentity identity = findOrCreateIdentity(tenantCode, subjectId, member);
|
||||
|
||||
ConversationParticipant participant = new ConversationParticipant();
|
||||
participant.setConversationId(conversation.getId());
|
||||
participant.setPersonId(identity.getPersonId());
|
||||
participant.setChannelIdentityId(identity.getId());
|
||||
participant.setParticipantType(resolveParticipantType(member, conv.getOwnerUserId()));
|
||||
participant.setJoinTime(LocalDateTime.now());
|
||||
conversationParticipantMapper.insert(participant);
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void bindAccountAsDefault(Long accountId, Long conversationId) {
|
||||
ensureBinding(accountId, conversationId, BINDING_SEND_MASTER);
|
||||
ensureBinding(accountId, conversationId, BINDING_RECEIVE_MASTER);
|
||||
}
|
||||
|
||||
private ChannelIdentity findOrCreateIdentity(String tenantCode, Long subjectId, WeComConversationMember member) {
|
||||
ChannelIdentity identity = channelIdentityMapper.selectOne(
|
||||
new LambdaQueryWrapper<ChannelIdentity>()
|
||||
.eq(ChannelIdentity::getTenantCode, tenantCode)
|
||||
.eq(ChannelIdentity::getChannelType, CHANNEL_WECOM)
|
||||
.eq(ChannelIdentity::getChannelUserId, member.getUserId()));
|
||||
|
||||
if (identity != null) {
|
||||
return identity;
|
||||
}
|
||||
|
||||
String displayName = resolveDisplayName(member.getName(), member.getUserId());
|
||||
Person person = new Person();
|
||||
person.setTenantCode(tenantCode);
|
||||
person.setPersonCode(member.getUserId());
|
||||
person.setPersonName(displayName);
|
||||
person.setPersonType(PERSON_TYPE_CUSTOMER);
|
||||
person.setStatus(1);
|
||||
personMapper.insert(person);
|
||||
|
||||
ChannelIdentity newIdentity = new ChannelIdentity();
|
||||
newIdentity.setTenantCode(tenantCode);
|
||||
newIdentity.setPersonId(person.getId());
|
||||
newIdentity.setChannelType(CHANNEL_WECOM);
|
||||
newIdentity.setChannelUserId(member.getUserId());
|
||||
newIdentity.setChannelUserName(displayName);
|
||||
newIdentity.setSubjectId(subjectId);
|
||||
newIdentity.setStatus(1);
|
||||
channelIdentityMapper.insert(newIdentity);
|
||||
return newIdentity;
|
||||
}
|
||||
|
||||
private String resolveDisplayName(String name, String userId) {
|
||||
if (name != null && !name.isBlank()) {
|
||||
return name;
|
||||
}
|
||||
return userId != null && !userId.isBlank() ? userId : "未知";
|
||||
}
|
||||
|
||||
private void ensureBinding(Long accountId, Long conversationId, int bindingType) {
|
||||
ChannelAccountConversation existing = accountConversationMapper.selectOne(
|
||||
new LambdaQueryWrapper<ChannelAccountConversation>()
|
||||
.eq(ChannelAccountConversation::getAccountId, accountId)
|
||||
.eq(ChannelAccountConversation::getConversationId, conversationId)
|
||||
.eq(ChannelAccountConversation::getBindingType, bindingType));
|
||||
if (existing != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ChannelAccountConversation binding = new ChannelAccountConversation();
|
||||
binding.setAccountId(accountId);
|
||||
binding.setConversationId(conversationId);
|
||||
binding.setBindingType(bindingType);
|
||||
binding.setSortOrder(0);
|
||||
binding.setStatus(1);
|
||||
accountConversationMapper.insert(binding);
|
||||
}
|
||||
|
||||
private int resolvePersonType(WeComContact contact) {
|
||||
return contact.getType() == WeComContactType.INTERNAL ? PERSON_TYPE_INTERNAL : PERSON_TYPE_CUSTOMER;
|
||||
}
|
||||
|
||||
private int resolveParticipantType(WeComConversationMember member, String ownerUserId) {
|
||||
return ownerUserId != null && ownerUserId.equals(member.getUserId()) ? PARTICIPANT_TYPE_OWNER : PARTICIPANT_TYPE_MEMBER;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package com.sino.mci.channel.service;
|
||||
|
||||
import com.sino.mci.channel.entity.ChannelAccount;
|
||||
import com.sino.mci.channel.entity.ChannelSubject;
|
||||
import com.sino.mci.channel.entity.Conversation;
|
||||
import com.sino.mci.channel.entity.SyncTask;
|
||||
import com.sino.mci.channel.repository.ChannelAccountMapper;
|
||||
import com.sino.mci.channel.repository.ChannelSubjectMapper;
|
||||
import com.sino.mci.channel.wecom.sync.WeComContact;
|
||||
import com.sino.mci.channel.wecom.sync.WeComConversation;
|
||||
import com.sino.mci.channel.wecom.sync.WeComSyncClient;
|
||||
import com.sino.mci.channel.wecom.sync.WeComSyncConfig;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class SyncTaskExecutor {
|
||||
|
||||
private final SyncTaskService syncTaskService;
|
||||
private final ChannelAccountMapper accountMapper;
|
||||
private final ChannelSubjectMapper subjectMapper;
|
||||
private final SyncDataPersister syncDataPersister;
|
||||
private final WeComSyncClient weComSyncClient;
|
||||
|
||||
@Async("syncTaskPool")
|
||||
public void executeEnterpriseContactsSync(Long taskId) {
|
||||
SyncTask task = syncTaskService.getTask(taskId, null);
|
||||
if (task == null) {
|
||||
return;
|
||||
}
|
||||
syncTaskService.markRunning(taskId);
|
||||
try {
|
||||
ChannelSubject subject = subjectMapper.selectById(task.getTargetId());
|
||||
if (subject == null) {
|
||||
throw new RuntimeException("渠道主体不存在");
|
||||
}
|
||||
WeComSyncConfig config = WeComSyncConfig.builder()
|
||||
.corpId(subject.getCorpId())
|
||||
.agentId(subject.getAgentId())
|
||||
.secret(subject.getAgentSecret())
|
||||
.build();
|
||||
List<WeComContact> contacts = weComSyncClient.syncEnterpriseContacts(config);
|
||||
for (WeComContact contact : contacts) {
|
||||
syncDataPersister.upsertContact(subject.getTenantCode(), subject.getId(), contact);
|
||||
}
|
||||
syncTaskService.markSuccess(taskId, contacts.size());
|
||||
} catch (Exception e) {
|
||||
log.error("[SyncTask] 企业通讯录同步失败 taskId={}", taskId, e);
|
||||
syncTaskService.markFailed(taskId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Async("syncTaskPool")
|
||||
public void executeAccountContactsSync(Long taskId) {
|
||||
SyncTask task = syncTaskService.getTask(taskId, null);
|
||||
if (task == null) {
|
||||
return;
|
||||
}
|
||||
syncTaskService.markRunning(taskId);
|
||||
try {
|
||||
ChannelAccount account = accountMapper.selectById(task.getTargetId());
|
||||
if (account == null) {
|
||||
throw new RuntimeException("渠道账号不存在");
|
||||
}
|
||||
ChannelSubject subject = subjectMapper.selectById(account.getSubjectId());
|
||||
if (subject == null) {
|
||||
throw new RuntimeException("渠道主体不存在");
|
||||
}
|
||||
WeComSyncConfig config = WeComSyncConfig.builder()
|
||||
.corpId(subject.getCorpId())
|
||||
.agentId(subject.getAgentId())
|
||||
.secret(subject.getAgentSecret())
|
||||
.build();
|
||||
List<WeComContact> contacts = weComSyncClient.syncAccountContacts(config, account.getAccountId());
|
||||
for (WeComContact contact : contacts) {
|
||||
syncDataPersister.upsertContact(account.getTenantCode(), account.getSubjectId(), contact);
|
||||
}
|
||||
syncTaskService.markSuccess(taskId, contacts.size());
|
||||
} catch (Exception e) {
|
||||
log.error("[SyncTask] 账号客户同步失败 taskId={}", taskId, e);
|
||||
syncTaskService.markFailed(taskId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Async("syncTaskPool")
|
||||
public void executeAccountConversationsSync(Long taskId) {
|
||||
SyncTask task = syncTaskService.getTask(taskId, null);
|
||||
if (task == null) {
|
||||
return;
|
||||
}
|
||||
syncTaskService.markRunning(taskId);
|
||||
try {
|
||||
ChannelAccount account = accountMapper.selectById(task.getTargetId());
|
||||
if (account == null) {
|
||||
throw new RuntimeException("渠道账号不存在");
|
||||
}
|
||||
ChannelSubject subject = subjectMapper.selectById(account.getSubjectId());
|
||||
if (subject == null) {
|
||||
throw new RuntimeException("渠道主体不存在");
|
||||
}
|
||||
WeComSyncConfig config = WeComSyncConfig.builder()
|
||||
.corpId(subject.getCorpId())
|
||||
.agentId(subject.getAgentId())
|
||||
.secret(subject.getAgentSecret())
|
||||
.build();
|
||||
List<WeComConversation> conversations = weComSyncClient.syncAccountConversations(config, account.getAccountId());
|
||||
for (WeComConversation conversation : conversations) {
|
||||
Conversation saved = syncDataPersister.upsertConversation(account, subject.getId(), conversation);
|
||||
syncDataPersister.refreshParticipants(account.getTenantCode(), account.getSubjectId(), saved, conversation);
|
||||
syncDataPersister.bindAccountAsDefault(account.getId(), saved.getId());
|
||||
}
|
||||
syncTaskService.markSuccess(taskId, conversations.size());
|
||||
} catch (Exception e) {
|
||||
log.error("[SyncTask] 账号群聊同步失败 taskId={}", taskId, e);
|
||||
syncTaskService.markFailed(taskId, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.sino.mci.channel.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.sino.mci.channel.entity.SyncTask;
|
||||
import com.sino.mci.channel.repository.SyncTaskMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SyncTaskService {
|
||||
|
||||
private final SyncTaskMapper syncTaskMapper;
|
||||
|
||||
@Transactional
|
||||
public SyncTask createTask(String tenantCode, String taskType, String channelType, Long targetId, String targetCode) {
|
||||
SyncTask task = new SyncTask();
|
||||
task.setTenantCode(tenantCode);
|
||||
task.setTaskType(taskType);
|
||||
task.setChannelType(channelType);
|
||||
task.setTargetId(targetId);
|
||||
task.setTargetCode(targetCode);
|
||||
task.setTaskStatus(SyncTask.STATUS_PENDING);
|
||||
task.setSyncedCount(0);
|
||||
task.setFailedCount(0);
|
||||
task.setRetryCount(0);
|
||||
task.setDeleted(0);
|
||||
syncTaskMapper.insert(task);
|
||||
return task;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void markRunning(Long taskId) {
|
||||
SyncTask task = syncTaskMapper.selectById(taskId);
|
||||
if (task == null) {
|
||||
return;
|
||||
}
|
||||
task.setTaskStatus(SyncTask.STATUS_RUNNING);
|
||||
task.setUpdateTime(LocalDateTime.now());
|
||||
syncTaskMapper.updateById(task);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void markSuccess(Long taskId, int syncedCount) {
|
||||
SyncTask task = syncTaskMapper.selectById(taskId);
|
||||
if (task == null) {
|
||||
return;
|
||||
}
|
||||
task.setTaskStatus(SyncTask.STATUS_SUCCESS);
|
||||
task.setSyncedCount(syncedCount);
|
||||
task.setFinishTime(LocalDateTime.now());
|
||||
task.setUpdateTime(LocalDateTime.now());
|
||||
syncTaskMapper.updateById(task);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void markFailed(Long taskId, String errorMessage) {
|
||||
SyncTask task = syncTaskMapper.selectById(taskId);
|
||||
if (task == null) {
|
||||
return;
|
||||
}
|
||||
task.setTaskStatus(SyncTask.STATUS_FAILED);
|
||||
task.setErrorMessage(errorMessage);
|
||||
task.setFinishTime(LocalDateTime.now());
|
||||
task.setUpdateTime(LocalDateTime.now());
|
||||
syncTaskMapper.updateById(task);
|
||||
}
|
||||
|
||||
public SyncTask getTask(Long taskId, String tenantCode) {
|
||||
SyncTask task = syncTaskMapper.selectById(taskId);
|
||||
if (task == null) {
|
||||
return null;
|
||||
}
|
||||
if (tenantCode != null && !tenantCode.equals(task.getTenantCode())) {
|
||||
return null;
|
||||
}
|
||||
return task;
|
||||
}
|
||||
|
||||
public SyncTask getTaskById(Long taskId) {
|
||||
return syncTaskMapper.selectById(taskId);
|
||||
}
|
||||
|
||||
public List<SyncTask> listRecentTasks(String tenantCode, String taskType, Long targetId, int limit) {
|
||||
return syncTaskMapper.selectList(
|
||||
new LambdaQueryWrapper<SyncTask>()
|
||||
.eq(SyncTask::getTenantCode, tenantCode)
|
||||
.eq(SyncTask::getTaskType, taskType)
|
||||
.eq(targetId != null, SyncTask::getTargetId, targetId)
|
||||
.orderByDesc(SyncTask::getId)
|
||||
.last("LIMIT " + limit));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.sino.mci.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
|
||||
@Configuration
|
||||
@EnableAsync
|
||||
public class AsyncConfig {
|
||||
|
||||
@Bean("syncTaskPool")
|
||||
public Executor syncTaskPool() {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
executor.setCorePoolSize(4);
|
||||
executor.setMaxPoolSize(16);
|
||||
executor.setQueueCapacity(200);
|
||||
executor.setThreadNamePrefix("sync-task-");
|
||||
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
|
||||
executor.initialize();
|
||||
return executor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE mci_channel_subject DROP COLUMN corp_secret;
|
||||
@@ -0,0 +1,20 @@
|
||||
CREATE TABLE IF NOT EXISTS mci_sync_task (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
tenant_code VARCHAR(64) NOT NULL COMMENT '所属租户',
|
||||
task_type VARCHAR(32) NOT NULL COMMENT '任务类型:enterprise_contacts / account_contacts / account_conversations',
|
||||
channel_type VARCHAR(32) NOT NULL COMMENT '渠道:wecom / wechat_personal / dingtalk / feishu',
|
||||
target_id BIGINT COMMENT '关联目标 ID:主体 ID 或账号 ID',
|
||||
target_code VARCHAR(128) COMMENT '关联目标编码:如 userId',
|
||||
task_status TINYINT NOT NULL DEFAULT 0 COMMENT '任务状态:0待执行 1执行中 2成功 3失败',
|
||||
synced_count INT NOT NULL DEFAULT 0 COMMENT '已同步数量',
|
||||
failed_count INT NOT NULL DEFAULT 0 COMMENT '失败数量',
|
||||
error_message VARCHAR(1024) COMMENT '错误信息',
|
||||
retry_count INT NOT NULL DEFAULT 0 COMMENT '重试次数',
|
||||
finish_time DATETIME COMMENT '完成时间',
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted TINYINT NOT NULL DEFAULT 0,
|
||||
KEY idx_tenant_status (tenant_code, task_status),
|
||||
KEY idx_target (task_type, target_id),
|
||||
KEY idx_create_time (create_time)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='同步任务表';
|
||||
@@ -48,7 +48,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
||||
@TestPropertySource(properties = {
|
||||
"spring.rabbitmq.listener.simple.auto-startup=false",
|
||||
"spring.main.allow-bean-definition-overriding=true",
|
||||
"sino.channel.wecom.session-archive.listener-enabled=false"
|
||||
"sino.channel.wecom.session-archive.listener-enabled=false",
|
||||
"mci.wecom.sync.mock=true"
|
||||
})
|
||||
@Transactional
|
||||
class WeComSessionArchiveTest {
|
||||
@@ -133,7 +134,7 @@ class WeComSessionArchiveTest {
|
||||
command.setChannelSubjectId(subject.getId());
|
||||
command.setTenantCode(subject.getTenantCode());
|
||||
command.setCorpId(subject.getCorpId());
|
||||
command.setSecret(subject.getCorpSecret());
|
||||
command.setSecret(subject.getAgentSecret());
|
||||
command.setPrivateKey(subject.getSessionArchivePrivateKey());
|
||||
|
||||
sessionArchivePullService.onPullCommand(command);
|
||||
@@ -177,7 +178,7 @@ class WeComSessionArchiveTest {
|
||||
command.setChannelSubjectId(subject.getId());
|
||||
command.setTenantCode(subject.getTenantCode());
|
||||
command.setCorpId(subject.getCorpId());
|
||||
command.setSecret(subject.getCorpSecret());
|
||||
command.setSecret(subject.getAgentSecret());
|
||||
command.setPrivateKey(subject.getSessionArchivePrivateKey());
|
||||
|
||||
sessionArchivePullService.onPullCommand(command);
|
||||
@@ -202,7 +203,7 @@ class WeComSessionArchiveTest {
|
||||
subject.setSubjectCode("archive");
|
||||
subject.setSubjectName("会话存档主体");
|
||||
subject.setCorpId(CORP_ID);
|
||||
subject.setCorpSecret("secret-" + UUID.randomUUID());
|
||||
subject.setAgentSecret("secret-" + UUID.randomUUID());
|
||||
subject.setSessionArchiveEnabled(1);
|
||||
subject.setSessionArchivePrivateKey("private-key");
|
||||
subject.setStatus(1);
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
package com.sino.mci.channel.controller;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.sino.mci.channel.common.model.ChannelType;
|
||||
import com.sino.mci.channel.entity.ChannelAccount;
|
||||
import com.sino.mci.channel.entity.ChannelAccountConversation;
|
||||
import com.sino.mci.channel.entity.ChannelSubject;
|
||||
import com.sino.mci.channel.entity.Conversation;
|
||||
import com.sino.mci.channel.entity.ConversationParticipant;
|
||||
import com.sino.mci.channel.entity.SyncTask;
|
||||
import com.sino.mci.channel.repository.ChannelAccountConversationMapper;
|
||||
import com.sino.mci.channel.repository.ChannelAccountMapper;
|
||||
import com.sino.mci.channel.repository.ChannelSubjectMapper;
|
||||
import com.sino.mci.channel.repository.ConversationMapper;
|
||||
import com.sino.mci.channel.repository.ConversationParticipantMapper;
|
||||
import com.sino.mci.channel.service.SyncTaskService;
|
||||
import com.sino.mci.channel.wecom.sync.WeComContact;
|
||||
import com.sino.mci.channel.wecom.sync.WeComContactType;
|
||||
import com.sino.mci.channel.wecom.sync.WeComConversation;
|
||||
@@ -29,7 +32,7 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
@@ -47,14 +50,16 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
||||
@AutoConfigureMockMvc
|
||||
@TestPropertySource(properties = {
|
||||
"spring.rabbitmq.listener.simple.auto-startup=false",
|
||||
"spring.main.allow-bean-definition-overriding=true"
|
||||
"spring.main.allow-bean-definition-overriding=true",
|
||||
"mci.wecom.sync.mock=true"
|
||||
})
|
||||
@Transactional
|
||||
class WeComSyncTest {
|
||||
|
||||
private static final String TENANT_CODE = "wecom-sync-" + UUID.randomUUID().toString().substring(0, 8);
|
||||
private final String tenantCode = "wecom-sync-" + UUID.randomUUID().toString().substring(0, 8);
|
||||
private static final String CHANNEL_TYPE = ChannelType.WECOM.name().toLowerCase();
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@@ -79,23 +84,31 @@ class WeComSyncTest {
|
||||
@Autowired
|
||||
private ChannelAccountConversationMapper accountConversationMapper;
|
||||
|
||||
@Test
|
||||
void shouldSyncContactsAndPersistPersonAndIdentity() throws Exception {
|
||||
String authToken = createTenantAndLogin(mockMvc, TENANT_CODE, "企微同步租户");
|
||||
ChannelAccount account = createWeComAccount();
|
||||
@Autowired
|
||||
private SyncTaskService syncTaskService;
|
||||
|
||||
mockMvc.perform(post("/msg-platform/api/v1/channel-account/{account_id}/sync-contacts", account.getId())
|
||||
@Test
|
||||
void shouldSyncEnterpriseContactsViaSubjectAndPersistInternalPerson() throws Exception {
|
||||
String authToken = createTenantAndLogin(mockMvc, tenantCode, "企微同步租户");
|
||||
ChannelSubject subject = createWeComSubject();
|
||||
|
||||
MvcResult result = mockMvc.perform(post("/msg-platform/api/v1/channel-subject/{id}/sync-contacts", subject.getId())
|
||||
.contextPath("/msg-platform")
|
||||
.header("Authorization", authToken)
|
||||
.header("X-Tenant-Code", TENANT_CODE)
|
||||
.header("X-Tenant-Code", tenantCode)
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.syncedCount").value(2));
|
||||
.andReturn();
|
||||
|
||||
Long taskId = extractTaskId(result);
|
||||
SyncTask task = waitForTask(taskId);
|
||||
assertThat(task.getTaskStatus()).isEqualTo(SyncTask.STATUS_SUCCESS);
|
||||
assertThat(task.getSyncedCount()).isEqualTo(2);
|
||||
|
||||
Person internalPerson = personMapper.selectOne(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Person>()
|
||||
.eq(Person::getTenantCode, TENANT_CODE)
|
||||
.eq(Person::getTenantCode, tenantCode)
|
||||
.eq(Person::getPersonCode, "user001"));
|
||||
assertThat(internalPerson).isNotNull();
|
||||
assertThat(internalPerson.getPersonName()).isEqualTo("张三");
|
||||
@@ -104,23 +117,42 @@ class WeComSyncTest {
|
||||
|
||||
ChannelIdentity internalIdentity = channelIdentityMapper.selectOne(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ChannelIdentity>()
|
||||
.eq(ChannelIdentity::getTenantCode, TENANT_CODE)
|
||||
.eq(ChannelIdentity::getTenantCode, tenantCode)
|
||||
.eq(ChannelIdentity::getChannelType, CHANNEL_TYPE)
|
||||
.eq(ChannelIdentity::getChannelUserId, "user001"));
|
||||
assertThat(internalIdentity).isNotNull();
|
||||
assertThat(internalIdentity.getPersonId()).isEqualTo(internalPerson.getId());
|
||||
assertThat(internalIdentity.getChannelUserName()).isEqualTo("张三");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSyncAccountContactsAndPersistExternalPerson() throws Exception {
|
||||
String authToken = createTenantAndLogin(mockMvc, tenantCode, "企微同步租户");
|
||||
ChannelAccount account = createWeComAccount();
|
||||
|
||||
MvcResult result = mockMvc.perform(post("/msg-platform/api/v1/channel-account/{account_id}/sync-contacts", account.getId())
|
||||
.contextPath("/msg-platform")
|
||||
.header("Authorization", authToken)
|
||||
.header("X-Tenant-Code", tenantCode)
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andReturn();
|
||||
|
||||
Long taskId = extractTaskId(result);
|
||||
SyncTask task = waitForTask(taskId);
|
||||
assertThat(task.getTaskStatus()).isEqualTo(SyncTask.STATUS_SUCCESS);
|
||||
|
||||
Person externalPerson = personMapper.selectOne(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Person>()
|
||||
.eq(Person::getTenantCode, TENANT_CODE)
|
||||
.eq(Person::getTenantCode, tenantCode)
|
||||
.eq(Person::getPersonCode, "wmexternal001"));
|
||||
assertThat(externalPerson).isNotNull();
|
||||
assertThat(externalPerson.getPersonType()).isEqualTo(3);
|
||||
|
||||
ChannelIdentity externalIdentity = channelIdentityMapper.selectOne(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ChannelIdentity>()
|
||||
.eq(ChannelIdentity::getTenantCode, TENANT_CODE)
|
||||
.eq(ChannelIdentity::getTenantCode, tenantCode)
|
||||
.eq(ChannelIdentity::getChannelType, CHANNEL_TYPE)
|
||||
.eq(ChannelIdentity::getChannelUserId, "wmexternal001"));
|
||||
assertThat(externalIdentity).isNotNull();
|
||||
@@ -129,21 +161,25 @@ class WeComSyncTest {
|
||||
|
||||
@Test
|
||||
void shouldSyncConversationsAndPersistParticipantsAndBindings() throws Exception {
|
||||
String authToken = createTenantAndLogin(mockMvc, TENANT_CODE, "企微同步租户");
|
||||
String authToken = createTenantAndLogin(mockMvc, tenantCode, "企微同步租户");
|
||||
ChannelAccount account = createWeComAccount();
|
||||
|
||||
mockMvc.perform(post("/msg-platform/api/v1/channel-account/{account_id}/sync-conversations", account.getId())
|
||||
MvcResult result = mockMvc.perform(post("/msg-platform/api/v1/channel-account/{account_id}/sync-conversations", account.getId())
|
||||
.contextPath("/msg-platform")
|
||||
.header("Authorization", authToken)
|
||||
.header("X-Tenant-Code", TENANT_CODE)
|
||||
.header("X-Tenant-Code", tenantCode)
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.syncedCount").value(1));
|
||||
.andReturn();
|
||||
|
||||
Long taskId = extractTaskId(result);
|
||||
SyncTask task = waitForTask(taskId);
|
||||
assertThat(task.getTaskStatus()).isEqualTo(SyncTask.STATUS_SUCCESS);
|
||||
|
||||
Conversation conversation = conversationMapper.selectOne(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Conversation>()
|
||||
.eq(Conversation::getTenantCode, TENANT_CODE)
|
||||
.eq(Conversation::getTenantCode, tenantCode)
|
||||
.eq(Conversation::getChannelType, CHANNEL_TYPE)
|
||||
.eq(Conversation::getChannelConversationId, "chat001"));
|
||||
assertThat(conversation).isNotNull();
|
||||
@@ -181,53 +217,57 @@ class WeComSyncTest {
|
||||
|
||||
@Test
|
||||
void shouldPreservePersonBusinessFieldsDuringContactSync() throws Exception {
|
||||
String authToken = createTenantAndLogin(mockMvc, TENANT_CODE, "企微同步租户");
|
||||
String authToken = createTenantAndLogin(mockMvc, tenantCode, "企微同步租户");
|
||||
ChannelAccount account = createWeComAccount();
|
||||
|
||||
Person person = new Person();
|
||||
person.setTenantCode(TENANT_CODE);
|
||||
person.setPersonCode("user001");
|
||||
person.setTenantCode(tenantCode);
|
||||
person.setPersonCode("wmexternal001");
|
||||
person.setPersonName("原始姓名");
|
||||
person.setPhone("13800138001");
|
||||
// Seed with a personType that differs from what resolvePersonType(user001/INTERNAL) would assign (1).
|
||||
person.setPersonType(3);
|
||||
person.setPhone("13900139001");
|
||||
person.setPersonType(1);
|
||||
person.setInstitutionId(100L);
|
||||
person.setSupplierId(200L);
|
||||
person.setStatus(1);
|
||||
personMapper.insert(person);
|
||||
|
||||
ChannelIdentity identity = new ChannelIdentity();
|
||||
identity.setTenantCode(TENANT_CODE);
|
||||
identity.setTenantCode(tenantCode);
|
||||
identity.setPersonId(person.getId());
|
||||
identity.setChannelType(CHANNEL_TYPE);
|
||||
identity.setChannelUserId("user001");
|
||||
identity.setChannelUserId("wmexternal001");
|
||||
identity.setChannelUserName("原始姓名");
|
||||
identity.setSubjectId(account.getSubjectId());
|
||||
identity.setStatus(1);
|
||||
channelIdentityMapper.insert(identity);
|
||||
|
||||
mockMvc.perform(post("/msg-platform/api/v1/channel-account/{account_id}/sync-contacts", account.getId())
|
||||
MvcResult result = mockMvc.perform(post("/msg-platform/api/v1/channel-account/{account_id}/sync-contacts", account.getId())
|
||||
.contextPath("/msg-platform")
|
||||
.header("Authorization", authToken)
|
||||
.header("X-Tenant-Code", TENANT_CODE)
|
||||
.header("X-Tenant-Code", tenantCode)
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0));
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andReturn();
|
||||
|
||||
Long taskId = extractTaskId(result);
|
||||
SyncTask task = waitForTask(taskId);
|
||||
assertThat(task.getTaskStatus()).isEqualTo(SyncTask.STATUS_SUCCESS);
|
||||
|
||||
Person updated = personMapper.selectById(person.getId());
|
||||
assertThat(updated.getPersonName()).isEqualTo("张三");
|
||||
assertThat(updated.getPersonName()).isEqualTo("客户李四");
|
||||
assertThat(updated.getInstitutionId()).isEqualTo(100L);
|
||||
assertThat(updated.getSupplierId()).isEqualTo(200L);
|
||||
assertThat(updated.getPersonType()).isEqualTo(3);
|
||||
assertThat(updated.getPersonType()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPreserveConversationBusinessFieldsDuringConversationSync() throws Exception {
|
||||
String authToken = createTenantAndLogin(mockMvc, TENANT_CODE, "企微同步租户");
|
||||
String authToken = createTenantAndLogin(mockMvc, tenantCode, "企微同步租户");
|
||||
ChannelAccount account = createWeComAccount();
|
||||
|
||||
Conversation conversation = new Conversation();
|
||||
conversation.setTenantCode(TENANT_CODE);
|
||||
conversation.setTenantCode(tenantCode);
|
||||
conversation.setConversationType(2);
|
||||
conversation.setChannelType(CHANNEL_TYPE);
|
||||
conversation.setChannelConversationId("chat001");
|
||||
@@ -242,13 +282,18 @@ class WeComSyncTest {
|
||||
conversation.setStatus(1);
|
||||
conversationMapper.insert(conversation);
|
||||
|
||||
mockMvc.perform(post("/msg-platform/api/v1/channel-account/{account_id}/sync-conversations", account.getId())
|
||||
MvcResult result = mockMvc.perform(post("/msg-platform/api/v1/channel-account/{account_id}/sync-conversations", account.getId())
|
||||
.contextPath("/msg-platform")
|
||||
.header("Authorization", authToken)
|
||||
.header("X-Tenant-Code", TENANT_CODE)
|
||||
.header("X-Tenant-Code", tenantCode)
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0));
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andReturn();
|
||||
|
||||
Long taskId = extractTaskId(result);
|
||||
SyncTask task = waitForTask(taskId);
|
||||
assertThat(task.getTaskStatus()).isEqualTo(SyncTask.STATUS_SUCCESS);
|
||||
|
||||
Conversation updated = conversationMapper.selectById(conversation.getId());
|
||||
assertThat(updated.getConversationName()).isEqualTo("测试客户群");
|
||||
@@ -258,20 +303,40 @@ class WeComSyncTest {
|
||||
assertThat(updated.getChannelCode()).isEqualTo("ORIGINAL");
|
||||
}
|
||||
|
||||
private ChannelAccount createWeComAccount() {
|
||||
private Long extractTaskId(MvcResult result) throws Exception {
|
||||
return objectMapper.readTree(result.getResponse().getContentAsString()).path("data").path("id").asLong();
|
||||
}
|
||||
|
||||
private SyncTask waitForTask(Long taskId) throws InterruptedException {
|
||||
for (int i = 0; i < 50; i++) {
|
||||
SyncTask task = syncTaskService.getTaskById(taskId);
|
||||
if (task != null && (task.getTaskStatus() == SyncTask.STATUS_SUCCESS || task.getTaskStatus() == SyncTask.STATUS_FAILED)) {
|
||||
return task;
|
||||
}
|
||||
Thread.sleep(200);
|
||||
}
|
||||
throw new RuntimeException("同步任务未在预期时间内完成: taskId=" + taskId);
|
||||
}
|
||||
|
||||
private ChannelSubject createWeComSubject() {
|
||||
ChannelSubject subject = new ChannelSubject();
|
||||
subject.setTenantCode(TENANT_CODE);
|
||||
subject.setTenantCode(tenantCode);
|
||||
subject.setChannelType(CHANNEL_TYPE);
|
||||
subject.setSubjectCode("sino");
|
||||
subject.setSubjectName("Sino");
|
||||
subject.setCorpId("ww-sync-" + UUID.randomUUID().toString().substring(0, 8));
|
||||
subject.setCorpSecret("secret-" + UUID.randomUUID());
|
||||
subject.setAgentId("1000002");
|
||||
subject.setAgentSecret("secret-" + UUID.randomUUID());
|
||||
subject.setStatus(1);
|
||||
subjectMapper.insert(subject);
|
||||
return subject;
|
||||
}
|
||||
|
||||
private ChannelAccount createWeComAccount() {
|
||||
ChannelSubject subject = createWeComSubject();
|
||||
|
||||
ChannelAccount account = new ChannelAccount();
|
||||
account.setTenantCode(TENANT_CODE);
|
||||
account.setTenantCode(tenantCode);
|
||||
account.setAccountType(1);
|
||||
account.setChannelType(CHANNEL_TYPE);
|
||||
account.setSubjectId(subject.getId());
|
||||
@@ -288,7 +353,7 @@ class WeComSyncTest {
|
||||
@Bean
|
||||
public WeComSyncClient weComSyncClient() {
|
||||
WeComSyncClient client = mock(WeComSyncClient.class);
|
||||
when(client.syncContacts(any())).thenReturn(List.of(
|
||||
when(client.syncEnterpriseContacts(any())).thenReturn(List.of(
|
||||
WeComContact.builder()
|
||||
.userId("user001")
|
||||
.name("张三")
|
||||
@@ -302,7 +367,15 @@ class WeComSyncTest {
|
||||
.type(WeComContactType.EXTERNAL)
|
||||
.build()
|
||||
));
|
||||
when(client.syncConversations(any())).thenReturn(List.of(
|
||||
when(client.syncAccountContacts(any(), any())).thenReturn(List.of(
|
||||
WeComContact.builder()
|
||||
.userId("wmexternal001")
|
||||
.name("客户李四")
|
||||
.mobile("13900139001")
|
||||
.type(WeComContactType.EXTERNAL)
|
||||
.build()
|
||||
));
|
||||
when(client.syncAccountConversations(any(), any())).thenReturn(List.of(
|
||||
WeComConversation.builder()
|
||||
.chatId("chat001")
|
||||
.chatName("测试客户群")
|
||||
|
||||
@@ -84,6 +84,28 @@
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-toolchains-plugin</artifactId>
|
||||
<version>3.2.0</version>
|
||||
<configuration>
|
||||
<toolchains>
|
||||
<jdk>
|
||||
<version>21</version>
|
||||
<vendor>zulu</vendor>
|
||||
</jdk>
|
||||
</toolchains>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>toolchain</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<repositories>
|
||||
|
||||
Reference in New Issue
Block a user