fix: 渠道账号群聊同步缓存 key 及成员名称兜底;统一 Docker/Maven 项目约定

This commit is contained in:
2026-07-10 15:55:30 +08:00
parent 560bd4599b
commit 758ed73f9d
56 changed files with 7689 additions and 1127 deletions

View File

@@ -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()

View File

@@ -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;
}
}

View File

@@ -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);
}