feat: 新增渠道身份菜单、会话成员/标签展示、修复群聊成员 person_type 及重复同步问题

This commit is contained in:
2026-07-13 09:38:03 +08:00
parent 749856c455
commit 31d4a34ebb
16 changed files with 502 additions and 5 deletions

View File

@@ -0,0 +1,82 @@
package com.sino.mci.channel.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.sino.mci.admin.security.AuthContext;
import com.sino.mci.channel.dto.ChannelIdentityResponse;
import com.sino.mci.channel.entity.ChannelSubject;
import com.sino.mci.channel.repository.ChannelSubjectMapper;
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.web.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/v1/channel-identity")
@RequiredArgsConstructor
public class ChannelIdentityController {
private final ChannelIdentityMapper channelIdentityMapper;
private final PersonMapper personMapper;
private final ChannelSubjectMapper channelSubjectMapper;
@GetMapping("/list")
public ApiResponse<List<ChannelIdentityResponse>> listChannelIdentities() {
String tenantCode = AuthContext.currentTenantCode();
List<ChannelIdentity> identities = channelIdentityMapper.selectList(
new LambdaQueryWrapper<ChannelIdentity>()
.eq(ChannelIdentity::getTenantCode, tenantCode)
.orderByDesc(ChannelIdentity::getId));
if (identities.isEmpty()) {
return ApiResponse.ok(Collections.emptyList());
}
Set<Long> personIds = identities.stream()
.map(ChannelIdentity::getPersonId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
Set<Long> subjectIds = identities.stream()
.map(ChannelIdentity::getSubjectId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
Map<Long, Person> personMap = personIds.isEmpty() ? Collections.emptyMap()
: personMapper.selectBatchIds(personIds).stream()
.collect(Collectors.toMap(Person::getId, p -> p));
Map<Long, ChannelSubject> subjectMap = subjectIds.isEmpty() ? Collections.emptyMap()
: channelSubjectMapper.selectBatchIds(subjectIds).stream()
.collect(Collectors.toMap(ChannelSubject::getId, s -> s));
List<ChannelIdentityResponse> responses = identities.stream().map(identity -> {
ChannelIdentityResponse response = new ChannelIdentityResponse();
response.setId(identity.getId());
response.setTenantCode(identity.getTenantCode());
response.setChannelType(identity.getChannelType());
response.setChannelUserId(identity.getChannelUserId());
response.setChannelUserName(identity.getChannelUserName());
response.setSubjectId(identity.getSubjectId());
ChannelSubject subject = identity.getSubjectId() == null ? null : subjectMap.get(identity.getSubjectId());
response.setSubjectName(subject == null ? null : subject.getSubjectName());
response.setPersonId(identity.getPersonId());
Person person = identity.getPersonId() == null ? null : personMap.get(identity.getPersonId());
response.setPersonName(person == null ? null : person.getPersonName());
response.setPersonType(person == null ? null : person.getPersonType());
response.setStatus(identity.getStatus());
return response;
}).collect(Collectors.toList());
return ApiResponse.ok(responses);
}
}

View File

@@ -3,7 +3,9 @@ package com.sino.mci.channel.controller;
import com.sino.mci.admin.security.AuthContext;
import com.sino.mci.channel.dto.BindConversationAccountRequest;
import com.sino.mci.channel.dto.BindConversationTagsRequest;
import com.sino.mci.channel.dto.ConversationParticipantResponse;
import com.sino.mci.channel.dto.CreateConversationRequest;
import com.sino.mci.channel.dto.TagResponse;
import com.sino.mci.channel.dto.UpdateConversationRequest;
import com.sino.mci.channel.entity.Conversation;
import com.sino.mci.channel.service.ConversationService;
@@ -73,4 +75,18 @@ public class ConversationController {
conversationService.bindTags(tenantCode, conversationId, request);
return ApiResponse.ok();
}
@GetMapping("/{conversation_id}/participants")
public ApiResponse<List<ConversationParticipantResponse>> listParticipants(
@RequestHeader("X-Tenant-Code") String tenantCode,
@PathVariable("conversation_id") Long conversationId) {
return ApiResponse.ok(conversationService.listParticipants(tenantCode, conversationId));
}
@GetMapping("/{conversation_id}/tags")
public ApiResponse<List<TagResponse>> listTags(
@RequestHeader("X-Tenant-Code") String tenantCode,
@PathVariable("conversation_id") Long conversationId) {
return ApiResponse.ok(conversationService.listTags(tenantCode, conversationId));
}
}

View File

@@ -0,0 +1,19 @@
package com.sino.mci.channel.dto;
import lombok.Data;
@Data
public class ChannelIdentityResponse {
private Long id;
private String tenantCode;
private String channelType;
private String channelUserId;
private String channelUserName;
private Long subjectId;
private String subjectName;
private Long personId;
private String personName;
private Integer personType;
private Integer status;
}

View File

@@ -0,0 +1,19 @@
package com.sino.mci.channel.dto;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class ConversationParticipantResponse {
private Long id;
private Integer participantType;
private LocalDateTime joinTime;
private Long channelIdentityId;
private String channelUserId;
private String channelUserName;
private Long personId;
private String personName;
private Integer personType;
}

View File

@@ -0,0 +1,16 @@
package com.sino.mci.channel.dto;
import lombok.Data;
@Data
public class TagResponse {
private Long id;
private String tenantCode;
private Long parentId;
private String tagPath;
private Integer level;
private String tagName;
private String tagSource;
private Integer status;
}

View File

@@ -2,8 +2,12 @@ package com.sino.mci.channel.repository;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.sino.mci.channel.entity.ConversationParticipant;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ConversationParticipantMapper extends BaseMapper<ConversationParticipant> {
@Delete("DELETE FROM mci_conversation_participant WHERE conversation_id = #{conversationId}")
void physicalDeleteByConversationId(Long conversationId);
}

View File

@@ -3,17 +3,25 @@ package com.sino.mci.channel.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.sino.mci.channel.dto.BindConversationAccountRequest;
import com.sino.mci.channel.dto.BindConversationTagsRequest;
import com.sino.mci.channel.dto.ConversationParticipantResponse;
import com.sino.mci.channel.dto.CreateConversationRequest;
import com.sino.mci.channel.dto.TagResponse;
import com.sino.mci.channel.dto.UpdateConversationRequest;
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.entity.ConversationTagBinding;
import com.sino.mci.channel.repository.ChannelAccountConversationMapper;
import com.sino.mci.channel.repository.ChannelAccountMapper;
import com.sino.mci.channel.repository.ConversationMapper;
import com.sino.mci.channel.repository.ConversationParticipantMapper;
import com.sino.mci.channel.repository.ConversationTagBindingMapper;
import com.sino.mci.crm.entity.ChannelIdentity;
import com.sino.mci.crm.entity.Person;
import com.sino.mci.crm.entity.Tag;
import com.sino.mci.crm.repository.ChannelIdentityMapper;
import com.sino.mci.crm.repository.PersonMapper;
import com.sino.mci.crm.repository.TagMapper;
import com.sino.mci.exception.BusinessException;
import com.sino.mci.shared.util.JsonUtils;
@@ -22,7 +30,12 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
@Service
@@ -34,6 +47,9 @@ public class ConversationService {
private final ConversationTagBindingMapper conversationTagBindingMapper;
private final TagMapper tagMapper;
private final ChannelAccountMapper accountMapper;
private final ConversationParticipantMapper participantMapper;
private final ChannelIdentityMapper channelIdentityMapper;
private final PersonMapper personMapper;
public List<Conversation> listConversations(String tenantCode) {
return conversationMapper.selectList(
@@ -160,4 +176,95 @@ public class ConversationService {
}
}
}
public List<ConversationParticipantResponse> listParticipants(String tenantCode, Long conversationId) {
getConversation(tenantCode, conversationId);
List<ConversationParticipant> participants = participantMapper.selectList(
new LambdaQueryWrapper<ConversationParticipant>()
.eq(ConversationParticipant::getConversationId, conversationId)
.orderByAsc(ConversationParticipant::getId));
if (participants.isEmpty()) {
return Collections.emptyList();
}
Set<Long> channelIdentityIds = participants.stream()
.map(ConversationParticipant::getChannelIdentityId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
Set<Long> personIds = participants.stream()
.map(ConversationParticipant::getPersonId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
Map<Long, ChannelIdentity> identityMap = channelIdentityIds.isEmpty() ? Collections.emptyMap()
: channelIdentityMapper.selectBatchIds(channelIdentityIds).stream()
.collect(Collectors.toMap(ChannelIdentity::getId, Function.identity()));
Map<Long, Person> personMap = personIds.isEmpty() ? Collections.emptyMap()
: personMapper.selectBatchIds(personIds).stream()
.collect(Collectors.toMap(Person::getId, Function.identity()));
return participants.stream().map(participant -> {
ConversationParticipantResponse response = new ConversationParticipantResponse();
response.setId(participant.getId());
response.setParticipantType(participant.getParticipantType());
response.setJoinTime(participant.getJoinTime());
response.setChannelIdentityId(participant.getChannelIdentityId());
ChannelIdentity identity = participant.getChannelIdentityId() == null
? null : identityMap.get(participant.getChannelIdentityId());
response.setChannelUserId(identity == null ? null : identity.getChannelUserId());
response.setChannelUserName(identity == null ? null : identity.getChannelUserName());
Person person = participant.getPersonId() == null
? null : personMap.get(participant.getPersonId());
response.setPersonId(participant.getPersonId());
response.setPersonName(person == null ? null : person.getPersonName());
response.setPersonType(person == null ? null : person.getPersonType());
return response;
}).collect(Collectors.toList());
}
public List<TagResponse> listTags(String tenantCode, Long conversationId) {
getConversation(tenantCode, conversationId);
List<ConversationTagBinding> bindings = conversationTagBindingMapper.selectList(
new LambdaQueryWrapper<ConversationTagBinding>()
.eq(ConversationTagBinding::getConversationId, conversationId)
.orderByAsc(ConversationTagBinding::getId));
if (bindings.isEmpty()) {
return Collections.emptyList();
}
Set<Long> tagIds = bindings.stream()
.map(ConversationTagBinding::getTagId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
Map<Long, Tag> tagMap = tagMapper.selectBatchIds(tagIds).stream()
.collect(Collectors.toMap(Tag::getId, Function.identity()));
return bindings.stream()
.map(binding -> {
Tag tag = tagMap.get(binding.getTagId());
if (tag == null) {
return null;
}
TagResponse response = new TagResponse();
response.setId(tag.getId());
response.setTenantCode(tag.getTenantCode());
response.setParentId(tag.getParentId());
response.setTagPath(tag.getTagPath());
response.setLevel(tag.getLevel());
response.setTagName(tag.getTagName());
response.setTagSource(tag.getTagSource());
response.setStatus(tag.getStatus());
return response;
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
}

View File

@@ -135,11 +135,10 @@ public class SyncDataPersister {
@Transactional
public void refreshParticipants(String tenantCode, Long subjectId, Conversation conversation, WeComConversation conv) {
conversationParticipantMapper.delete(
new LambdaQueryWrapper<ConversationParticipant>()
.eq(ConversationParticipant::getConversationId, conversation.getId()));
conversationParticipantMapper.physicalDeleteByConversationId(conversation.getId());
for (WeComConversationMember member : conv.getMemberList()) {
log.debug("[SyncDataPersister] 群聊成员 userId={}, type={}", member.getUserId(), member.getType());
ChannelIdentity identity = findOrCreateIdentity(tenantCode, subjectId, member);
ConversationParticipant participant = new ConversationParticipant();
@@ -165,7 +164,13 @@ public class SyncDataPersister {
.eq(ChannelIdentity::getChannelType, CHANNEL_WECOM)
.eq(ChannelIdentity::getChannelUserId, member.getUserId()));
int personType = resolveMemberPersonType(member.getType());
if (identity != null) {
Person existPerson = personMapper.selectById(identity.getPersonId());
if (existPerson != null && !existPerson.getPersonType().equals(personType)) {
existPerson.setPersonType(personType);
personMapper.updateById(existPerson);
}
return identity;
}
@@ -174,7 +179,7 @@ public class SyncDataPersister {
person.setTenantCode(tenantCode);
person.setPersonCode(member.getUserId());
person.setPersonName(displayName);
person.setPersonType(PERSON_TYPE_CUSTOMER);
person.setPersonType(personType);
person.setStatus(1);
personMapper.insert(person);
@@ -197,6 +202,11 @@ public class SyncDataPersister {
return userId != null && !userId.isBlank() ? userId : "未知";
}
private int resolveMemberPersonType(Integer memberType) {
// 企微群聊成员类型1=企业成员2=外部联系人
return memberType != null && memberType == 1 ? PERSON_TYPE_INTERNAL : PERSON_TYPE_CUSTOMER;
}
private void ensureBinding(Long accountId, Long conversationId, int bindingType) {
ChannelAccountConversation existing = accountConversationMapper.selectOne(
new LambdaQueryWrapper<ChannelAccountConversation>()