Compare commits
2 Commits
feat/chann
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 31d4a34ebb | |||
| 749856c455 |
@@ -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`)
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
<el-menu-item index="/channel/subject">渠道主体</el-menu-item>
|
||||
<el-menu-item index="/channel/app">渠道应用</el-menu-item>
|
||||
<el-menu-item index="/channel/account">渠道账号</el-menu-item>
|
||||
<el-menu-item index="/channel/identity">渠道身份</el-menu-item>
|
||||
</el-sub-menu>
|
||||
|
||||
<el-sub-menu index="/customer">
|
||||
|
||||
@@ -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: '标签体系' } },
|
||||
|
||||
112
admin-web/src/views/ChannelIdentity.vue
Normal file
112
admin-web/src/views/ChannelIdentity.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<PageCard>
|
||||
<template #toolbar-left>
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索渠道用户ID / 渠道用户名 / 联系人姓名"
|
||||
clearable
|
||||
style="width: 320px"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</template>
|
||||
|
||||
<el-table :data="pagedIdentities" class="sino-table">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="channelType" label="渠道类型" width="110">
|
||||
<template #default="{ row }">
|
||||
{{ channelLabel(row.channelType) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="channelUserId" label="渠道用户ID" show-overflow-tooltip />
|
||||
<el-table-column prop="channelUserName" label="渠道用户名" show-overflow-tooltip />
|
||||
<el-table-column prop="subjectName" label="所属主体" show-overflow-tooltip />
|
||||
<el-table-column prop="personName" label="关联联系人" show-overflow-tooltip />
|
||||
<el-table-column prop="personType" label="人员类型" width="110">
|
||||
<template #default="{ row }">
|
||||
{{ formatPersonType(row.personType) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
|
||||
{{ row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<template #pagination>
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:total="filteredIdentities.length"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
background
|
||||
/>
|
||||
</template>
|
||||
</PageCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { listChannelIdentities } from '@/api/channel'
|
||||
import PageCard from '@/components/PageCard.vue'
|
||||
|
||||
const identities = ref<any[]>([])
|
||||
const searchKeyword = ref('')
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
|
||||
const filteredIdentities = computed(() => {
|
||||
const kw = searchKeyword.value.trim().toLowerCase()
|
||||
if (!kw) return identities.value
|
||||
return identities.value.filter((item) =>
|
||||
[item.channelUserId, item.channelUserName, item.personName].some((v) =>
|
||||
String(v || '').toLowerCase().includes(kw)
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
const pagedIdentities = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
return filteredIdentities.value.slice(start, start + pageSize.value)
|
||||
})
|
||||
|
||||
watch([searchKeyword], () => {
|
||||
currentPage.value = 1
|
||||
})
|
||||
|
||||
function channelLabel(type: string | undefined) {
|
||||
const map: Record<string, string> = {
|
||||
wecom: '企微',
|
||||
wechat_personal: '个微',
|
||||
dingtalk: '钉钉',
|
||||
feishu: '飞书',
|
||||
ivr: 'IVR',
|
||||
}
|
||||
return map[type || ''] || type || '-'
|
||||
}
|
||||
|
||||
function formatPersonType(type: number | undefined) {
|
||||
if (type === 3) return '客户'
|
||||
if (type == null) return '-'
|
||||
return '内部'
|
||||
}
|
||||
|
||||
async function loadIdentities() {
|
||||
try {
|
||||
const res: any = await listChannelIdentities({ keyword: searchKeyword.value.trim() })
|
||||
identities.value = res.data || []
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadIdentities)
|
||||
</script>
|
||||
@@ -151,6 +151,38 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<div class="detail-section">
|
||||
<div class="section-title">群成员</div>
|
||||
<el-table :data="participants" class="sino-table" size="small" max-height="320">
|
||||
<el-table-column prop="participantType" label="类型" width="90">
|
||||
<template #default="{ row }">
|
||||
{{ formatParticipantType(row.participantType) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="channelUserId" label="渠道用户ID" show-overflow-tooltip />
|
||||
<el-table-column prop="personName" label="姓名" show-overflow-tooltip />
|
||||
<el-table-column prop="personType" label="人员类型" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ formatParticipantPersonType(row.personType) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<div class="detail-section">
|
||||
<div class="section-title">已绑定标签</div>
|
||||
<div v-if="boundTags.length" class="tag-list">
|
||||
<el-tag v-for="t in boundTags" :key="t.id" class="tag-item" size="small">
|
||||
{{ t.tagName }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<el-empty v-else description="暂无标签" :image-size="60" />
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="drawerVisible = false">取消</el-button>
|
||||
<el-button type="danger" @click="handleDelete(currentConversation)">删除</el-button>
|
||||
@@ -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<any>({})
|
||||
const participants = ref<any[]>([])
|
||||
const boundTags = ref<any[]>([])
|
||||
|
||||
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;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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"}
|
||||
{"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"}
|
||||
@@ -206,6 +206,7 @@ public class RealWeComSyncClient implements WeComSyncClient {
|
||||
members.add(WeComConversationMember.builder()
|
||||
.userId(member.getUserId())
|
||||
.name(resolveMemberName(service, member))
|
||||
.type(member.getType())
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,4 +13,5 @@ public class WeComConversationMember {
|
||||
|
||||
private String userId;
|
||||
private String name;
|
||||
private Integer type;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>()
|
||||
|
||||
@@ -12,6 +12,7 @@ import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.jdbc.Sql;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@@ -22,6 +23,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@Transactional
|
||||
@Sql(scripts = "/sql/test-channel-account.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
|
||||
class ChannelAccountEventControllerTest {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user