diff --git a/admin-web/src/api/channel.ts b/admin-web/src/api/channel.ts
index 066edf2..36f357e 100644
--- a/admin-web/src/api/channel.ts
+++ b/admin-web/src/api/channel.ts
@@ -2,6 +2,10 @@ import client from './client'
export * from './channelSubject'
export * from './channelAccount'
+export interface ChannelIdentityQuery {
+ keyword?: string
+}
+
export function createConversation(data: any) {
return client.post('/api/v1/conversation', data)
}
@@ -25,3 +29,15 @@ export function bindConversationAccount(conversationId: number, data: any) {
export function bindConversationTags(conversationId: number, data: any) {
return client.post(`/api/v1/conversation/${conversationId}/bind-tags`, data)
}
+
+export function listChannelIdentities(params?: ChannelIdentityQuery) {
+ return client.get('/api/v1/channel-identity/list', { params })
+}
+
+export function listConversationParticipants(conversationId: number) {
+ return client.get(`/api/v1/conversation/${conversationId}/participants`)
+}
+
+export function listConversationTags(conversationId: number) {
+ return client.get(`/api/v1/conversation/${conversationId}/tags`)
+}
diff --git a/admin-web/src/components/Layout.vue b/admin-web/src/components/Layout.vue
index 9ae9152..8d2aa0a 100644
--- a/admin-web/src/components/Layout.vue
+++ b/admin-web/src/components/Layout.vue
@@ -27,6 +27,7 @@
渠道主体
渠道应用
渠道账号
+ 渠道身份
diff --git a/admin-web/src/router/index.ts b/admin-web/src/router/index.ts
index 528791c..2d72397 100644
--- a/admin-web/src/router/index.ts
+++ b/admin-web/src/router/index.ts
@@ -4,6 +4,7 @@ import Dashboard from '../views/Dashboard.vue'
import ChannelSubject from '../views/ChannelSubject.vue'
import ChannelApp from '../views/ChannelApp.vue'
import ChannelAccount from '../views/ChannelAccount.vue'
+import ChannelIdentity from '../views/ChannelIdentity.vue'
import Person from '../views/Person.vue'
import Conversation from '../views/Conversation.vue'
import Tag from '../views/Tag.vue'
@@ -30,6 +31,7 @@ const routes = [
{ path: 'channel/subject', component: ChannelSubject, meta: { title: '渠道主体' } },
{ path: 'channel/app', component: ChannelApp, meta: { title: '渠道应用' } },
{ path: 'channel/account', component: ChannelAccount, meta: { title: '渠道账号' } },
+ { path: 'channel/identity', component: ChannelIdentity, meta: { title: '渠道身份' } },
{ path: 'customer/person', component: Person, meta: { title: '联系人' } },
{ path: 'customer/group', component: Conversation, meta: { title: '群聊' } },
{ path: 'customer/tag', component: Tag, meta: { title: '标签体系' } },
diff --git a/admin-web/src/views/ChannelIdentity.vue b/admin-web/src/views/ChannelIdentity.vue
new file mode 100644
index 0000000..7b42345
--- /dev/null
+++ b/admin-web/src/views/ChannelIdentity.vue
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ channelLabel(row.channelType) }}
+
+
+
+
+
+
+
+
+ {{ formatPersonType(row.personType) }}
+
+
+
+
+
+ {{ row.status === 1 ? '启用' : '禁用' }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin-web/src/views/Conversation.vue b/admin-web/src/views/Conversation.vue
index b577b3a..bd81609 100644
--- a/admin-web/src/views/Conversation.vue
+++ b/admin-web/src/views/Conversation.vue
@@ -151,6 +151,38 @@
+
+
+
+
群成员
+
+
+
+ {{ formatParticipantType(row.participantType) }}
+
+
+
+
+
+
+ {{ formatParticipantPersonType(row.personType) }}
+
+
+
+
+
+
+
+
+
已绑定标签
+
+
+ {{ t.tagName }}
+
+
+
+
+
取消
删除
@@ -219,6 +251,8 @@ import {
createConversation,
deleteConversation,
listChannelAccounts,
+ listConversationParticipants,
+ listConversationTags,
listConversations,
updateConversation,
} from '@/api/channel'
@@ -248,6 +282,8 @@ const currentPage = ref(1)
const pageSize = ref(10)
const drawerVisible = ref(false)
const currentConversation = ref({})
+const participants = ref([])
+const boundTags = ref([])
const filteredConversations = computed(() => {
const q = searchQuery.value.trim().toLowerCase()
@@ -305,6 +341,8 @@ function openDrawer(row: any) {
editingId.value = row.id
fillDialogForm(row)
drawerVisible.value = true
+ loadParticipants(row.id)
+ loadBoundTags(row.id)
}
const accountMap = computed(() => {
@@ -334,6 +372,45 @@ function formatConversationType(type: number | undefined) {
return type ?? '-'
}
+function formatParticipantType(type: number | undefined) {
+ if (type === 1) return '群主'
+ return '成员'
+}
+
+function formatParticipantPersonType(type: number | undefined) {
+ if (type === 3) return '客户'
+ if (type == null) return '-'
+ return '内部'
+}
+
+async function loadParticipants(conversationId: number | undefined) {
+ if (!conversationId) {
+ participants.value = []
+ return
+ }
+ try {
+ const res: any = await listConversationParticipants(conversationId)
+ participants.value = res.data || []
+ } catch (e: any) {
+ ElMessage.error(e.message)
+ participants.value = []
+ }
+}
+
+async function loadBoundTags(conversationId: number | undefined) {
+ if (!conversationId) {
+ boundTags.value = []
+ return
+ }
+ try {
+ const res: any = await listConversationTags(conversationId)
+ boundTags.value = res.data || []
+ } catch (e: any) {
+ ElMessage.error(e.message)
+ boundTags.value = []
+ }
+}
+
async function loadConversations() {
try {
const res: any = await listConversations()
@@ -504,4 +581,18 @@ onMounted(() => {
display: flex;
gap: 16px;
}
+
+.detail-section {
+ margin-bottom: 8px;
+}
+
+.tag-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.tag-item {
+ margin-right: 0;
+}
diff --git a/admin-web/tsconfig.app.tsbuildinfo b/admin-web/tsconfig.app.tsbuildinfo
index 44af9c2..e7a4e7f 100644
--- a/admin-web/tsconfig.app.tsbuildinfo
+++ b/admin-web/tsconfig.app.tsbuildinfo
@@ -1 +1 @@
-{"root":["./src/main.ts","./src/vite-env.d.ts","./src/api/account.ts","./src/api/channel.ts","./src/api/channelaccount.ts","./src/api/channelsubject.ts","./src/api/client.ts","./src/api/crm.ts","./src/api/dashboard.ts","./src/api/flow.ts","./src/api/inbound.ts","./src/api/inboundflow.ts","./src/api/inboundwebhook.ts","./src/api/ivrflow.ts","./src/api/send.ts","./src/api/sendpolicy.ts","./src/api/sendrecord.ts","./src/api/synctask.ts","./src/router/index.ts","./src/stores/auth.ts","./src/app.vue","./src/components/layout.vue","./src/components/pagecard.vue","./src/views/channelaccount.vue","./src/views/channelapp.vue","./src/views/channelsubject.vue","./src/views/conversation.vue","./src/views/dashboard.vue","./src/views/flowtrace.vue","./src/views/inboundflow.vue","./src/views/inboundwebhook.vue","./src/views/ivrflow.vue","./src/views/login.vue","./src/views/messagetest.vue","./src/views/messages.vue","./src/views/person.vue","./src/views/sendpolicy.vue","./src/views/sendrecord.vue","./src/views/tag.vue","./src/views/tenantmanagement.vue","./src/views/usermanagement.vue"],"version":"6.0.3"}
\ No newline at end of file
+{"root":["./src/main.ts","./src/vite-env.d.ts","./src/api/account.ts","./src/api/channel.ts","./src/api/channelaccount.ts","./src/api/channelsubject.ts","./src/api/client.ts","./src/api/crm.ts","./src/api/dashboard.ts","./src/api/flow.ts","./src/api/inbound.ts","./src/api/inboundflow.ts","./src/api/inboundwebhook.ts","./src/api/ivrflow.ts","./src/api/send.ts","./src/api/sendpolicy.ts","./src/api/sendrecord.ts","./src/api/synctask.ts","./src/router/index.ts","./src/stores/auth.ts","./src/app.vue","./src/components/layout.vue","./src/components/pagecard.vue","./src/views/channelaccount.vue","./src/views/channelapp.vue","./src/views/channelidentity.vue","./src/views/channelsubject.vue","./src/views/conversation.vue","./src/views/dashboard.vue","./src/views/flowtrace.vue","./src/views/inboundflow.vue","./src/views/inboundwebhook.vue","./src/views/ivrflow.vue","./src/views/login.vue","./src/views/messagetest.vue","./src/views/messages.vue","./src/views/person.vue","./src/views/sendpolicy.vue","./src/views/sendrecord.vue","./src/views/tag.vue","./src/views/tenantmanagement.vue","./src/views/usermanagement.vue"],"version":"6.0.3"}
\ No newline at end of file
diff --git a/backend/mci-channel-wecom/src/main/java/com/sino/mci/channel/wecom/sync/RealWeComSyncClient.java b/backend/mci-channel-wecom/src/main/java/com/sino/mci/channel/wecom/sync/RealWeComSyncClient.java
index 52da478..a681fda 100644
--- a/backend/mci-channel-wecom/src/main/java/com/sino/mci/channel/wecom/sync/RealWeComSyncClient.java
+++ b/backend/mci-channel-wecom/src/main/java/com/sino/mci/channel/wecom/sync/RealWeComSyncClient.java
@@ -206,6 +206,7 @@ public class RealWeComSyncClient implements WeComSyncClient {
members.add(WeComConversationMember.builder()
.userId(member.getUserId())
.name(resolveMemberName(service, member))
+ .type(member.getType())
.build());
}
}
diff --git a/backend/mci-channel-wecom/src/main/java/com/sino/mci/channel/wecom/sync/WeComConversationMember.java b/backend/mci-channel-wecom/src/main/java/com/sino/mci/channel/wecom/sync/WeComConversationMember.java
index b9f2db5..261fb39 100644
--- a/backend/mci-channel-wecom/src/main/java/com/sino/mci/channel/wecom/sync/WeComConversationMember.java
+++ b/backend/mci-channel-wecom/src/main/java/com/sino/mci/channel/wecom/sync/WeComConversationMember.java
@@ -13,4 +13,5 @@ public class WeComConversationMember {
private String userId;
private String name;
+ private Integer type;
}
diff --git a/backend/mci-server/src/main/java/com/sino/mci/channel/controller/ChannelIdentityController.java b/backend/mci-server/src/main/java/com/sino/mci/channel/controller/ChannelIdentityController.java
new file mode 100644
index 0000000..0f5d241
--- /dev/null
+++ b/backend/mci-server/src/main/java/com/sino/mci/channel/controller/ChannelIdentityController.java
@@ -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> listChannelIdentities() {
+ String tenantCode = AuthContext.currentTenantCode();
+ List identities = channelIdentityMapper.selectList(
+ new LambdaQueryWrapper()
+ .eq(ChannelIdentity::getTenantCode, tenantCode)
+ .orderByDesc(ChannelIdentity::getId));
+
+ if (identities.isEmpty()) {
+ return ApiResponse.ok(Collections.emptyList());
+ }
+
+ Set personIds = identities.stream()
+ .map(ChannelIdentity::getPersonId)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toSet());
+ Set subjectIds = identities.stream()
+ .map(ChannelIdentity::getSubjectId)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toSet());
+
+ Map personMap = personIds.isEmpty() ? Collections.emptyMap()
+ : personMapper.selectBatchIds(personIds).stream()
+ .collect(Collectors.toMap(Person::getId, p -> p));
+ Map subjectMap = subjectIds.isEmpty() ? Collections.emptyMap()
+ : channelSubjectMapper.selectBatchIds(subjectIds).stream()
+ .collect(Collectors.toMap(ChannelSubject::getId, s -> s));
+
+ List 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);
+ }
+}
diff --git a/backend/mci-server/src/main/java/com/sino/mci/channel/controller/ConversationController.java b/backend/mci-server/src/main/java/com/sino/mci/channel/controller/ConversationController.java
index 716e8a2..2a009f4 100644
--- a/backend/mci-server/src/main/java/com/sino/mci/channel/controller/ConversationController.java
+++ b/backend/mci-server/src/main/java/com/sino/mci/channel/controller/ConversationController.java
@@ -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> 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> listTags(
+ @RequestHeader("X-Tenant-Code") String tenantCode,
+ @PathVariable("conversation_id") Long conversationId) {
+ return ApiResponse.ok(conversationService.listTags(tenantCode, conversationId));
+ }
}
diff --git a/backend/mci-server/src/main/java/com/sino/mci/channel/dto/ChannelIdentityResponse.java b/backend/mci-server/src/main/java/com/sino/mci/channel/dto/ChannelIdentityResponse.java
new file mode 100644
index 0000000..ff2ef7b
--- /dev/null
+++ b/backend/mci-server/src/main/java/com/sino/mci/channel/dto/ChannelIdentityResponse.java
@@ -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;
+}
diff --git a/backend/mci-server/src/main/java/com/sino/mci/channel/dto/ConversationParticipantResponse.java b/backend/mci-server/src/main/java/com/sino/mci/channel/dto/ConversationParticipantResponse.java
new file mode 100644
index 0000000..f71b7b1
--- /dev/null
+++ b/backend/mci-server/src/main/java/com/sino/mci/channel/dto/ConversationParticipantResponse.java
@@ -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;
+}
diff --git a/backend/mci-server/src/main/java/com/sino/mci/channel/dto/TagResponse.java b/backend/mci-server/src/main/java/com/sino/mci/channel/dto/TagResponse.java
new file mode 100644
index 0000000..7122b30
--- /dev/null
+++ b/backend/mci-server/src/main/java/com/sino/mci/channel/dto/TagResponse.java
@@ -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;
+}
diff --git a/backend/mci-server/src/main/java/com/sino/mci/channel/repository/ConversationParticipantMapper.java b/backend/mci-server/src/main/java/com/sino/mci/channel/repository/ConversationParticipantMapper.java
index aaea9d5..51e1d0e 100644
--- a/backend/mci-server/src/main/java/com/sino/mci/channel/repository/ConversationParticipantMapper.java
+++ b/backend/mci-server/src/main/java/com/sino/mci/channel/repository/ConversationParticipantMapper.java
@@ -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 {
+
+ @Delete("DELETE FROM mci_conversation_participant WHERE conversation_id = #{conversationId}")
+ void physicalDeleteByConversationId(Long conversationId);
}
diff --git a/backend/mci-server/src/main/java/com/sino/mci/channel/service/ConversationService.java b/backend/mci-server/src/main/java/com/sino/mci/channel/service/ConversationService.java
index 47d6c2d..885373a 100644
--- a/backend/mci-server/src/main/java/com/sino/mci/channel/service/ConversationService.java
+++ b/backend/mci-server/src/main/java/com/sino/mci/channel/service/ConversationService.java
@@ -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 listConversations(String tenantCode) {
return conversationMapper.selectList(
@@ -160,4 +176,95 @@ public class ConversationService {
}
}
}
+
+ public List listParticipants(String tenantCode, Long conversationId) {
+ getConversation(tenantCode, conversationId);
+
+ List participants = participantMapper.selectList(
+ new LambdaQueryWrapper()
+ .eq(ConversationParticipant::getConversationId, conversationId)
+ .orderByAsc(ConversationParticipant::getId));
+
+ if (participants.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ Set channelIdentityIds = participants.stream()
+ .map(ConversationParticipant::getChannelIdentityId)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toSet());
+ Set personIds = participants.stream()
+ .map(ConversationParticipant::getPersonId)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toSet());
+
+ Map identityMap = channelIdentityIds.isEmpty() ? Collections.emptyMap()
+ : channelIdentityMapper.selectBatchIds(channelIdentityIds).stream()
+ .collect(Collectors.toMap(ChannelIdentity::getId, Function.identity()));
+ Map 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 listTags(String tenantCode, Long conversationId) {
+ getConversation(tenantCode, conversationId);
+
+ List bindings = conversationTagBindingMapper.selectList(
+ new LambdaQueryWrapper()
+ .eq(ConversationTagBinding::getConversationId, conversationId)
+ .orderByAsc(ConversationTagBinding::getId));
+
+ if (bindings.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ Set tagIds = bindings.stream()
+ .map(ConversationTagBinding::getTagId)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toSet());
+
+ Map 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());
+ }
}
diff --git a/backend/mci-server/src/main/java/com/sino/mci/channel/service/SyncDataPersister.java b/backend/mci-server/src/main/java/com/sino/mci/channel/service/SyncDataPersister.java
index e194f41..fbc95f1 100644
--- a/backend/mci-server/src/main/java/com/sino/mci/channel/service/SyncDataPersister.java
+++ b/backend/mci-server/src/main/java/com/sino/mci/channel/service/SyncDataPersister.java
@@ -135,11 +135,10 @@ public class SyncDataPersister {
@Transactional
public void refreshParticipants(String tenantCode, Long subjectId, Conversation conversation, WeComConversation conv) {
- conversationParticipantMapper.delete(
- new LambdaQueryWrapper()
- .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()