feat(channel-wecom): 实现企微联系人/群聊同步
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
package com.sino.mci.channel.wecom.sync;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 企业微信同步客户端的 mock 实现。
|
||||
*
|
||||
* <p>不发起真实企微 API 调用,仅返回固定样例数据,供开发与测试使用。</p>
|
||||
*/
|
||||
@Component
|
||||
public class MockWeComSyncClient implements WeComSyncClient {
|
||||
|
||||
@Override
|
||||
public List<WeComContact> syncContacts(WeComSyncConfig config) {
|
||||
return List.of(
|
||||
WeComContact.builder()
|
||||
.userId("user001")
|
||||
.name("张三")
|
||||
.mobile("13800138001")
|
||||
.type(WeComContactType.INTERNAL)
|
||||
.departmentIds(List.of(1L, 2L))
|
||||
.build(),
|
||||
WeComContact.builder()
|
||||
.userId("wmexternal001")
|
||||
.name("客户李四")
|
||||
.mobile("13900139001")
|
||||
.type(WeComContactType.EXTERNAL)
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<WeComConversation> syncConversations(WeComSyncConfig config) {
|
||||
return List.of(
|
||||
WeComConversation.builder()
|
||||
.chatId("chat001")
|
||||
.chatName("测试客户群")
|
||||
.ownerUserId("user001")
|
||||
.memberList(List.of(
|
||||
WeComConversationMember.builder()
|
||||
.userId("user001")
|
||||
.name("张三")
|
||||
.build(),
|
||||
WeComConversationMember.builder()
|
||||
.userId("wmexternal001")
|
||||
.name("客户李四")
|
||||
.build()
|
||||
))
|
||||
.build()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.sino.mci.channel.wecom.sync;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class WeComContact {
|
||||
|
||||
private String userId;
|
||||
private String name;
|
||||
private String mobile;
|
||||
private WeComContactType type;
|
||||
private List<Long> departmentIds;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.sino.mci.channel.wecom.sync;
|
||||
|
||||
public enum WeComContactType {
|
||||
INTERNAL,
|
||||
EXTERNAL
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.sino.mci.channel.wecom.sync;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class WeComConversation {
|
||||
|
||||
private String chatId;
|
||||
private String chatName;
|
||||
private String ownerUserId;
|
||||
private List<WeComConversationMember> memberList;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.sino.mci.channel.wecom.sync;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class WeComConversationMember {
|
||||
|
||||
private String userId;
|
||||
private String name;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.sino.mci.channel.wecom.sync;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 企业微信通讯录与群聊同步客户端。
|
||||
*
|
||||
* <p>封装企微「获取部门列表 / 部门成员详情 / 外部联系人列表 / 外部联系人详情 /
|
||||
* 客户群列表 / 客户群详情」等接口调用。实际生产环境可替换为基于 {@code WxCpService}
|
||||
* 的真实实现;默认提供 mock 实现用于开发/测试。</p>
|
||||
*/
|
||||
public interface WeComSyncClient {
|
||||
|
||||
/**
|
||||
* 同步联系人(企业内部成员 + 外部联系人)。
|
||||
*
|
||||
* @param config 企微配置
|
||||
* @return 联系人列表
|
||||
*/
|
||||
List<WeComContact> syncContacts(WeComSyncConfig config);
|
||||
|
||||
/**
|
||||
* 同步群聊(客户群)。
|
||||
*
|
||||
* @param config 企微配置
|
||||
* @return 群聊列表
|
||||
*/
|
||||
List<WeComConversation> syncConversations(WeComSyncConfig config);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.sino.mci.channel.wecom.sync;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class WeComSyncConfig {
|
||||
|
||||
private String corpId;
|
||||
private String agentId;
|
||||
private String secret;
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.sino.mci.channel.controller;
|
||||
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.SyncResult;
|
||||
import com.sino.mci.channel.entity.ChannelAccount;
|
||||
import com.sino.mci.channel.service.ChannelAccountService;
|
||||
import com.sino.mci.shared.web.ApiResponse;
|
||||
@@ -37,4 +38,18 @@ public class ChannelAccountController {
|
||||
public ApiResponse<List<ChannelAccount>> listAccounts() {
|
||||
return ApiResponse.ok(accountService.listAccounts(AuthContext.currentTenantCode()));
|
||||
}
|
||||
|
||||
@PostMapping("/{account_id}/sync-contacts")
|
||||
public ApiResponse<SyncResult> syncContacts(
|
||||
@RequestHeader("X-Tenant-Code") String tenantCode,
|
||||
@PathVariable("account_id") Long accountId) {
|
||||
return ApiResponse.ok(accountService.syncContacts(accountId));
|
||||
}
|
||||
|
||||
@PostMapping("/{account_id}/sync-conversations")
|
||||
public ApiResponse<SyncResult> syncConversations(
|
||||
@RequestHeader("X-Tenant-Code") String tenantCode,
|
||||
@PathVariable("account_id") Long accountId) {
|
||||
return ApiResponse.ok(accountService.syncConversations(accountId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.sino.mci.channel.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class SyncResult {
|
||||
|
||||
private int syncedCount;
|
||||
}
|
||||
@@ -1,25 +1,63 @@
|
||||
package com.sino.mci.channel.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.sino.mci.channel.common.model.ChannelType;
|
||||
import com.sino.mci.channel.dto.ChannelAccountInitRequest;
|
||||
import com.sino.mci.channel.dto.CreateChannelAccountRequest;
|
||||
import com.sino.mci.channel.dto.SyncResult;
|
||||
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.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 org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
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 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;
|
||||
|
||||
public List<ChannelAccount> listAccounts(String tenantCode) {
|
||||
return accountMapper.selectList(
|
||||
@@ -69,4 +107,215 @@ public class ChannelAccountService {
|
||||
// Phase 2 placeholder: actual adapter dispatch will be implemented later
|
||||
throw new BusinessException("渠道账号初始化适配器尚未实现: " + request.getChannelType());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public SyncResult syncContacts(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();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public SyncResult syncConversations(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();
|
||||
}
|
||||
|
||||
private ChannelAccount getWeComAccount(Long accountId) {
|
||||
ChannelAccount account = accountMapper.selectById(accountId);
|
||||
if (account == null) {
|
||||
throw new BusinessException("渠道账号不存在: " + accountId);
|
||||
}
|
||||
if (!CHANNEL_WECOM.equals(account.getChannelType())) {
|
||||
throw new BusinessException("仅支持企微渠道账号同步");
|
||||
}
|
||||
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());
|
||||
person.setPersonName(contact.getName());
|
||||
person.setPhone(contact.getMobile());
|
||||
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) {
|
||||
conversation.setConversationName(conv.getChatName());
|
||||
conversation.setOwnerAccountId(account.getId());
|
||||
conversation.setSubjectId(subjectId);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
package com.sino.mci.channel.controller;
|
||||
|
||||
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.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.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.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 org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
||||
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 java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static com.sino.mci.test.AuthTestHelper.createTenantAndLogin;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@TestPropertySource(properties = {
|
||||
"spring.rabbitmq.listener.simple.auto-startup=false",
|
||||
"spring.main.allow-bean-definition-overriding=true"
|
||||
})
|
||||
@Transactional
|
||||
class WeComSyncTest {
|
||||
|
||||
private static final String TENANT_CODE = "wecom-sync-" + UUID.randomUUID().toString().substring(0, 8);
|
||||
private static final String CHANNEL_TYPE = ChannelType.WECOM.name().toLowerCase();
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private ChannelSubjectMapper subjectMapper;
|
||||
|
||||
@Autowired
|
||||
private ChannelAccountMapper accountMapper;
|
||||
|
||||
@Autowired
|
||||
private PersonMapper personMapper;
|
||||
|
||||
@Autowired
|
||||
private ChannelIdentityMapper channelIdentityMapper;
|
||||
|
||||
@Autowired
|
||||
private ConversationMapper conversationMapper;
|
||||
|
||||
@Autowired
|
||||
private ConversationParticipantMapper conversationParticipantMapper;
|
||||
|
||||
@Autowired
|
||||
private ChannelAccountConversationMapper accountConversationMapper;
|
||||
|
||||
@Test
|
||||
void shouldSyncContactsAndPersistPersonAndIdentity() throws Exception {
|
||||
String authToken = createTenantAndLogin(mockMvc, TENANT_CODE, "企微同步租户");
|
||||
ChannelAccount account = createWeComAccount();
|
||||
|
||||
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)
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.syncedCount").value(2));
|
||||
|
||||
Person internalPerson = personMapper.selectOne(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Person>()
|
||||
.eq(Person::getTenantCode, TENANT_CODE)
|
||||
.eq(Person::getPersonCode, "user001"));
|
||||
assertThat(internalPerson).isNotNull();
|
||||
assertThat(internalPerson.getPersonName()).isEqualTo("张三");
|
||||
assertThat(internalPerson.getPhone()).isEqualTo("13800138001");
|
||||
assertThat(internalPerson.getPersonType()).isEqualTo(1);
|
||||
|
||||
ChannelIdentity internalIdentity = channelIdentityMapper.selectOne(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ChannelIdentity>()
|
||||
.eq(ChannelIdentity::getTenantCode, TENANT_CODE)
|
||||
.eq(ChannelIdentity::getChannelType, CHANNEL_TYPE)
|
||||
.eq(ChannelIdentity::getChannelUserId, "user001"));
|
||||
assertThat(internalIdentity).isNotNull();
|
||||
assertThat(internalIdentity.getPersonId()).isEqualTo(internalPerson.getId());
|
||||
assertThat(internalIdentity.getChannelUserName()).isEqualTo("张三");
|
||||
|
||||
Person externalPerson = personMapper.selectOne(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Person>()
|
||||
.eq(Person::getTenantCode, TENANT_CODE)
|
||||
.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::getChannelType, CHANNEL_TYPE)
|
||||
.eq(ChannelIdentity::getChannelUserId, "wmexternal001"));
|
||||
assertThat(externalIdentity).isNotNull();
|
||||
assertThat(externalIdentity.getPersonId()).isEqualTo(externalPerson.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSyncConversationsAndPersistParticipantsAndBindings() throws Exception {
|
||||
String authToken = createTenantAndLogin(mockMvc, TENANT_CODE, "企微同步租户");
|
||||
ChannelAccount account = createWeComAccount();
|
||||
|
||||
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)
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.syncedCount").value(1));
|
||||
|
||||
Conversation conversation = conversationMapper.selectOne(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Conversation>()
|
||||
.eq(Conversation::getTenantCode, TENANT_CODE)
|
||||
.eq(Conversation::getChannelType, CHANNEL_TYPE)
|
||||
.eq(Conversation::getChannelConversationId, "chat001"));
|
||||
assertThat(conversation).isNotNull();
|
||||
assertThat(conversation.getConversationType()).isEqualTo(2);
|
||||
assertThat(conversation.getConversationName()).isEqualTo("测试客户群");
|
||||
assertThat(conversation.getOwnerAccountId()).isEqualTo(account.getId());
|
||||
|
||||
List<ConversationParticipant> participants = conversationParticipantMapper.selectList(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ConversationParticipant>()
|
||||
.eq(ConversationParticipant::getConversationId, conversation.getId()));
|
||||
assertThat(participants).hasSize(2);
|
||||
|
||||
ConversationParticipant ownerParticipant = participants.stream()
|
||||
.filter(p -> p.getParticipantType() == 2)
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
Person ownerPerson = personMapper.selectById(ownerParticipant.getPersonId());
|
||||
assertThat(ownerPerson.getPersonCode()).isEqualTo("user001");
|
||||
|
||||
ChannelAccountConversation sendMaster = accountConversationMapper.selectOne(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ChannelAccountConversation>()
|
||||
.eq(ChannelAccountConversation::getAccountId, account.getId())
|
||||
.eq(ChannelAccountConversation::getConversationId, conversation.getId())
|
||||
.eq(ChannelAccountConversation::getBindingType, 1));
|
||||
assertThat(sendMaster).isNotNull();
|
||||
assertThat(sendMaster.getStatus()).isEqualTo(1);
|
||||
|
||||
ChannelAccountConversation receiveMaster = accountConversationMapper.selectOne(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ChannelAccountConversation>()
|
||||
.eq(ChannelAccountConversation::getAccountId, account.getId())
|
||||
.eq(ChannelAccountConversation::getConversationId, conversation.getId())
|
||||
.eq(ChannelAccountConversation::getBindingType, 3));
|
||||
assertThat(receiveMaster).isNotNull();
|
||||
}
|
||||
|
||||
private ChannelAccount createWeComAccount() {
|
||||
ChannelSubject subject = new ChannelSubject();
|
||||
subject.setTenantCode(TENANT_CODE);
|
||||
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.setStatus(1);
|
||||
subjectMapper.insert(subject);
|
||||
|
||||
ChannelAccount account = new ChannelAccount();
|
||||
account.setTenantCode(TENANT_CODE);
|
||||
account.setAccountType(1);
|
||||
account.setChannelType(CHANNEL_TYPE);
|
||||
account.setSubjectId(subject.getId());
|
||||
account.setAccountId("sync-account");
|
||||
account.setAccountName("同步账号");
|
||||
account.setStatus(1);
|
||||
accountMapper.insert(account);
|
||||
return account;
|
||||
}
|
||||
|
||||
@TestConfiguration
|
||||
static class MockConfig {
|
||||
|
||||
@Bean
|
||||
public WeComSyncClient weComSyncClient() {
|
||||
WeComSyncClient client = mock(WeComSyncClient.class);
|
||||
when(client.syncContacts(any())).thenReturn(List.of(
|
||||
WeComContact.builder()
|
||||
.userId("user001")
|
||||
.name("张三")
|
||||
.mobile("13800138001")
|
||||
.type(WeComContactType.INTERNAL)
|
||||
.build(),
|
||||
WeComContact.builder()
|
||||
.userId("wmexternal001")
|
||||
.name("客户李四")
|
||||
.mobile("13900139001")
|
||||
.type(WeComContactType.EXTERNAL)
|
||||
.build()
|
||||
));
|
||||
when(client.syncConversations(any())).thenReturn(List.of(
|
||||
WeComConversation.builder()
|
||||
.chatId("chat001")
|
||||
.chatName("测试客户群")
|
||||
.ownerUserId("user001")
|
||||
.memberList(List.of(
|
||||
WeComConversationMember.builder().userId("user001").name("张三").build(),
|
||||
WeComConversationMember.builder().userId("wmexternal001").name("客户李四").build()
|
||||
))
|
||||
.build()
|
||||
));
|
||||
return client;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user