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.admin.security.AuthContext;
|
||||||
import com.sino.mci.channel.dto.ChannelAccountInitRequest;
|
import com.sino.mci.channel.dto.ChannelAccountInitRequest;
|
||||||
import com.sino.mci.channel.dto.CreateChannelAccountRequest;
|
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.ChannelAccount;
|
||||||
import com.sino.mci.channel.service.ChannelAccountService;
|
import com.sino.mci.channel.service.ChannelAccountService;
|
||||||
import com.sino.mci.shared.web.ApiResponse;
|
import com.sino.mci.shared.web.ApiResponse;
|
||||||
@@ -37,4 +38,18 @@ public class ChannelAccountController {
|
|||||||
public ApiResponse<List<ChannelAccount>> listAccounts() {
|
public ApiResponse<List<ChannelAccount>> listAccounts() {
|
||||||
return ApiResponse.ok(accountService.listAccounts(AuthContext.currentTenantCode()));
|
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;
|
package com.sino.mci.channel.service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
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.ChannelAccountInitRequest;
|
||||||
import com.sino.mci.channel.dto.CreateChannelAccountRequest;
|
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.ChannelAccount;
|
||||||
|
import com.sino.mci.channel.entity.ChannelAccountConversation;
|
||||||
import com.sino.mci.channel.entity.ChannelSubject;
|
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.ChannelAccountMapper;
|
||||||
import com.sino.mci.channel.repository.ChannelSubjectMapper;
|
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.exception.BusinessException;
|
||||||
|
import com.sino.mci.shared.util.JsonUtils;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ChannelAccountService {
|
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 ChannelAccountMapper accountMapper;
|
||||||
private final ChannelSubjectMapper subjectMapper;
|
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) {
|
public List<ChannelAccount> listAccounts(String tenantCode) {
|
||||||
return accountMapper.selectList(
|
return accountMapper.selectList(
|
||||||
@@ -69,4 +107,215 @@ public class ChannelAccountService {
|
|||||||
// Phase 2 placeholder: actual adapter dispatch will be implemented later
|
// Phase 2 placeholder: actual adapter dispatch will be implemented later
|
||||||
throw new BusinessException("渠道账号初始化适配器尚未实现: " + request.getChannelType());
|
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