feat(crm): add person, identity, conversation, tag tables and APIs
This commit is contained in:
@@ -0,0 +1,50 @@
|
|||||||
|
package com.sino.mci.channel.controller;
|
||||||
|
|
||||||
|
import com.sino.mci.channel.dto.BindConversationAccountRequest;
|
||||||
|
import com.sino.mci.channel.dto.BindConversationTagsRequest;
|
||||||
|
import com.sino.mci.channel.dto.CreateConversationRequest;
|
||||||
|
import com.sino.mci.channel.service.ConversationService;
|
||||||
|
import com.sino.mci.shared.web.ApiResponse;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/conversation")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ConversationController {
|
||||||
|
|
||||||
|
private final ConversationService conversationService;
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ApiResponse<?> createConversation(
|
||||||
|
@RequestHeader("X-Tenant-Code") String tenantCode,
|
||||||
|
@Valid @RequestBody CreateConversationRequest request) {
|
||||||
|
return ApiResponse.ok(conversationService.createConversation(tenantCode, request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{conversation_id}")
|
||||||
|
public ApiResponse<?> getConversation(
|
||||||
|
@RequestHeader("X-Tenant-Code") String tenantCode,
|
||||||
|
@PathVariable("conversation_id") Long conversationId) {
|
||||||
|
return ApiResponse.ok(conversationService.getConversation(tenantCode, conversationId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{conversation_id}/bind-account")
|
||||||
|
public ApiResponse<?> bindAccount(
|
||||||
|
@RequestHeader("X-Tenant-Code") String tenantCode,
|
||||||
|
@PathVariable("conversation_id") Long conversationId,
|
||||||
|
@Valid @RequestBody BindConversationAccountRequest request) {
|
||||||
|
conversationService.bindAccount(tenantCode, conversationId, request);
|
||||||
|
return ApiResponse.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{conversation_id}/bind-tags")
|
||||||
|
public ApiResponse<?> bindTags(
|
||||||
|
@RequestHeader("X-Tenant-Code") String tenantCode,
|
||||||
|
@PathVariable("conversation_id") Long conversationId,
|
||||||
|
@Valid @RequestBody BindConversationTagsRequest request) {
|
||||||
|
conversationService.bindTags(tenantCode, conversationId, request);
|
||||||
|
return ApiResponse.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.sino.mci.channel.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class BindConversationAccountRequest {
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Long accountId;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Integer bindingType;
|
||||||
|
|
||||||
|
private Integer sortOrder;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.sino.mci.channel.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class BindConversationTagsRequest {
|
||||||
|
|
||||||
|
@NotEmpty
|
||||||
|
private List<Long> tagIds;
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.sino.mci.channel.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class CreateConversationRequest {
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Integer conversationType;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
private String channelType;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
private String channelConversationId;
|
||||||
|
|
||||||
|
private Long ownerAccountId;
|
||||||
|
private Long subjectId;
|
||||||
|
private String conversationName;
|
||||||
|
private Integer createSource;
|
||||||
|
private Map<String, Object> receiveConfig;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.sino.mci.channel.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.sino.mci.shared.entity.BaseEntity;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@TableName("mci_channel_account_conversation")
|
||||||
|
public class ChannelAccountConversation extends BaseEntity {
|
||||||
|
|
||||||
|
private Long accountId;
|
||||||
|
private Long conversationId;
|
||||||
|
private Integer bindingType;
|
||||||
|
private Integer sortOrder;
|
||||||
|
private Integer status;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.sino.mci.channel.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.sino.mci.shared.entity.BaseEntity;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@TableName("mci_conversation")
|
||||||
|
public class Conversation extends BaseEntity {
|
||||||
|
|
||||||
|
private String tenantCode;
|
||||||
|
private Integer conversationType;
|
||||||
|
private String channelType;
|
||||||
|
private String channelConversationId;
|
||||||
|
private Long ownerAccountId;
|
||||||
|
private Long subjectId;
|
||||||
|
private String conversationName;
|
||||||
|
private Integer createSource;
|
||||||
|
private Long createTaskId;
|
||||||
|
private Long institutionId;
|
||||||
|
private Long supplierId;
|
||||||
|
private Long contractId;
|
||||||
|
private String channelCode;
|
||||||
|
private String receiveConfig;
|
||||||
|
private Integer status;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.sino.mci.channel.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@TableName("mci_conversation_participant")
|
||||||
|
public class ConversationParticipant {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private Long conversationId;
|
||||||
|
private Long personId;
|
||||||
|
private Long channelIdentityId;
|
||||||
|
private Integer participantType;
|
||||||
|
private LocalDateTime joinTime;
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.sino.mci.channel.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@TableName("mci_conversation_tag_binding")
|
||||||
|
public class ConversationTagBinding {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String tenantCode;
|
||||||
|
private Long tagId;
|
||||||
|
private Long conversationId;
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.sino.mci.channel.repository;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.sino.mci.channel.entity.ChannelAccountConversation;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface ChannelAccountConversationMapper extends BaseMapper<ChannelAccountConversation> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.sino.mci.channel.repository;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.sino.mci.channel.entity.Conversation;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface ConversationMapper extends BaseMapper<Conversation> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
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.Mapper;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface ConversationParticipantMapper extends BaseMapper<ConversationParticipant> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.sino.mci.channel.repository;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.sino.mci.channel.entity.ConversationTagBinding;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface ConversationTagBindingMapper extends BaseMapper<ConversationTagBinding> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
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.CreateConversationRequest;
|
||||||
|
import com.sino.mci.channel.entity.ChannelAccountConversation;
|
||||||
|
import com.sino.mci.channel.entity.Conversation;
|
||||||
|
import com.sino.mci.channel.entity.ConversationTagBinding;
|
||||||
|
import com.sino.mci.channel.repository.ChannelAccountConversationMapper;
|
||||||
|
import com.sino.mci.channel.repository.ConversationMapper;
|
||||||
|
import com.sino.mci.channel.repository.ConversationTagBindingMapper;
|
||||||
|
import com.sino.mci.crm.entity.Tag;
|
||||||
|
import com.sino.mci.crm.repository.TagMapper;
|
||||||
|
import com.sino.mci.exception.BusinessException;
|
||||||
|
import com.sino.mci.shared.util.JsonUtils;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ConversationService {
|
||||||
|
|
||||||
|
private final ConversationMapper conversationMapper;
|
||||||
|
private final ChannelAccountConversationMapper bindingMapper;
|
||||||
|
private final ConversationTagBindingMapper conversationTagBindingMapper;
|
||||||
|
private final TagMapper tagMapper;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Conversation createConversation(String tenantCode, CreateConversationRequest request) {
|
||||||
|
Conversation existing = conversationMapper.selectOne(
|
||||||
|
new LambdaQueryWrapper<Conversation>()
|
||||||
|
.eq(Conversation::getTenantCode, tenantCode)
|
||||||
|
.eq(Conversation::getChannelType, request.getChannelType())
|
||||||
|
.eq(Conversation::getChannelConversationId, request.getChannelConversationId()));
|
||||||
|
if (existing != null) {
|
||||||
|
throw new BusinessException("会话已存在: " + request.getChannelConversationId());
|
||||||
|
}
|
||||||
|
|
||||||
|
Conversation conversation = new Conversation();
|
||||||
|
conversation.setTenantCode(tenantCode);
|
||||||
|
conversation.setConversationType(request.getConversationType());
|
||||||
|
conversation.setChannelType(request.getChannelType());
|
||||||
|
conversation.setChannelConversationId(request.getChannelConversationId());
|
||||||
|
conversation.setOwnerAccountId(request.getOwnerAccountId());
|
||||||
|
conversation.setSubjectId(request.getSubjectId());
|
||||||
|
conversation.setConversationName(request.getConversationName());
|
||||||
|
conversation.setCreateSource(request.getCreateSource() != null ? request.getCreateSource() : 2);
|
||||||
|
conversation.setReceiveConfig(JsonUtils.toJson(request.getReceiveConfig()));
|
||||||
|
conversation.setStatus(1);
|
||||||
|
conversationMapper.insert(conversation);
|
||||||
|
return conversation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Conversation getConversation(String tenantCode, Long conversationId) {
|
||||||
|
Conversation conversation = conversationMapper.selectById(conversationId);
|
||||||
|
if (conversation == null || !tenantCode.equals(conversation.getTenantCode())) {
|
||||||
|
throw new BusinessException("会话不存在");
|
||||||
|
}
|
||||||
|
return conversation;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void bindAccount(String tenantCode, Long conversationId, BindConversationAccountRequest request) {
|
||||||
|
getConversation(tenantCode, conversationId);
|
||||||
|
|
||||||
|
ChannelAccountConversation existing = bindingMapper.selectOne(
|
||||||
|
new LambdaQueryWrapper<ChannelAccountConversation>()
|
||||||
|
.eq(ChannelAccountConversation::getAccountId, request.getAccountId())
|
||||||
|
.eq(ChannelAccountConversation::getConversationId, conversationId)
|
||||||
|
.eq(ChannelAccountConversation::getBindingType, request.getBindingType()));
|
||||||
|
if (existing != null) {
|
||||||
|
throw new BusinessException("该账号已绑定此会话的相同类型");
|
||||||
|
}
|
||||||
|
|
||||||
|
ChannelAccountConversation binding = new ChannelAccountConversation();
|
||||||
|
binding.setAccountId(request.getAccountId());
|
||||||
|
binding.setConversationId(conversationId);
|
||||||
|
binding.setBindingType(request.getBindingType());
|
||||||
|
binding.setSortOrder(request.getSortOrder() != null ? request.getSortOrder() : 0);
|
||||||
|
binding.setStatus(1);
|
||||||
|
bindingMapper.insert(binding);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void bindTags(String tenantCode, Long conversationId, BindConversationTagsRequest request) {
|
||||||
|
getConversation(tenantCode, conversationId);
|
||||||
|
for (Long tagId : request.getTagIds()) {
|
||||||
|
Tag tag = tagMapper.selectById(tagId);
|
||||||
|
if (tag == null || !tenantCode.equals(tag.getTenantCode())) {
|
||||||
|
throw new BusinessException("标签不存在: " + tagId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
conversationTagBindingMapper.delete(
|
||||||
|
new LambdaQueryWrapper<ConversationTagBinding>()
|
||||||
|
.eq(ConversationTagBinding::getConversationId, conversationId));
|
||||||
|
|
||||||
|
if (!CollectionUtils.isEmpty(request.getTagIds())) {
|
||||||
|
List<ConversationTagBinding> bindings = request.getTagIds().stream()
|
||||||
|
.distinct()
|
||||||
|
.map(tagId -> {
|
||||||
|
ConversationTagBinding binding = new ConversationTagBinding();
|
||||||
|
binding.setTenantCode(tenantCode);
|
||||||
|
binding.setTagId(tagId);
|
||||||
|
binding.setConversationId(conversationId);
|
||||||
|
return binding;
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
for (ConversationTagBinding binding : bindings) {
|
||||||
|
conversationTagBindingMapper.insert(binding);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
import javax.sql.DataSource;
|
import javax.sql.DataSource;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@MapperScan({"com.sino.mci.admin.repository", "com.sino.mci.channel.repository"})
|
@MapperScan({"com.sino.mci.admin.repository", "com.sino.mci.channel.repository", "com.sino.mci.crm.repository"})
|
||||||
public class MybatisPlusConfig {
|
public class MybatisPlusConfig {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package com.sino.mci.crm.controller;
|
||||||
|
|
||||||
|
import com.sino.mci.crm.dto.BindTagsRequest;
|
||||||
|
import com.sino.mci.crm.dto.CreateIdentityRequest;
|
||||||
|
import com.sino.mci.crm.dto.CreatePersonRequest;
|
||||||
|
import com.sino.mci.crm.service.PersonService;
|
||||||
|
import com.sino.mci.shared.web.ApiResponse;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/person")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class PersonController {
|
||||||
|
|
||||||
|
private final PersonService personService;
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ApiResponse<?> createPerson(
|
||||||
|
@RequestHeader("X-Tenant-Code") String tenantCode,
|
||||||
|
@Valid @RequestBody CreatePersonRequest request) {
|
||||||
|
return ApiResponse.ok(personService.createPerson(tenantCode, request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{person_id}")
|
||||||
|
public ApiResponse<?> getPerson(
|
||||||
|
@RequestHeader("X-Tenant-Code") String tenantCode,
|
||||||
|
@PathVariable("person_id") Long personId) {
|
||||||
|
return ApiResponse.ok(personService.getPerson(tenantCode, personId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{person_id}/identities")
|
||||||
|
public ApiResponse<?> createIdentity(
|
||||||
|
@RequestHeader("X-Tenant-Code") String tenantCode,
|
||||||
|
@PathVariable("person_id") Long personId,
|
||||||
|
@Valid @RequestBody CreateIdentityRequest request) {
|
||||||
|
request.setPersonId(personId);
|
||||||
|
return ApiResponse.ok(personService.createIdentity(tenantCode, request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{person_id}/identities")
|
||||||
|
public ApiResponse<?> listIdentities(
|
||||||
|
@RequestHeader("X-Tenant-Code") String tenantCode,
|
||||||
|
@PathVariable("person_id") Long personId) {
|
||||||
|
return ApiResponse.ok(personService.listIdentities(tenantCode, personId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{person_id}/bind-tags")
|
||||||
|
public ApiResponse<?> bindTags(
|
||||||
|
@RequestHeader("X-Tenant-Code") String tenantCode,
|
||||||
|
@PathVariable("person_id") Long personId,
|
||||||
|
@Valid @RequestBody BindTagsRequest request) {
|
||||||
|
personService.bindTags(tenantCode, personId, request);
|
||||||
|
return ApiResponse.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.sino.mci.crm.controller;
|
||||||
|
|
||||||
|
import com.sino.mci.crm.dto.CreateTagRequest;
|
||||||
|
import com.sino.mci.crm.service.TagService;
|
||||||
|
import com.sino.mci.shared.web.ApiResponse;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/tag")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class TagController {
|
||||||
|
|
||||||
|
private final TagService tagService;
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ApiResponse<?> createTag(
|
||||||
|
@RequestHeader("X-Tenant-Code") String tenantCode,
|
||||||
|
@Valid @RequestBody CreateTagRequest request) {
|
||||||
|
return ApiResponse.ok(tagService.createTag(tenantCode, request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/list")
|
||||||
|
public ApiResponse<?> listTags(@RequestHeader("X-Tenant-Code") String tenantCode) {
|
||||||
|
return ApiResponse.ok(tagService.listTags(tenantCode));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.sino.mci.crm.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class BindTagsRequest {
|
||||||
|
|
||||||
|
@NotEmpty
|
||||||
|
private List<Long> tagIds;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.sino.mci.crm.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class CreateIdentityRequest {
|
||||||
|
|
||||||
|
private Long personId;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
private String channelType;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
private String channelUserId;
|
||||||
|
|
||||||
|
private String channelUserName;
|
||||||
|
private Long subjectId;
|
||||||
|
private Map<String, Object> extInfo;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.sino.mci.crm.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class CreatePersonRequest {
|
||||||
|
|
||||||
|
private String personCode;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
private String personName;
|
||||||
|
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Integer personType;
|
||||||
|
|
||||||
|
private Long institutionId;
|
||||||
|
private Long supplierId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.sino.mci.crm.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class CreateTagRequest {
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Long parentId;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
private String tagName;
|
||||||
|
|
||||||
|
private String tagSource;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.sino.mci.crm.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.sino.mci.shared.entity.BaseEntity;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@TableName("mci_channel_identity")
|
||||||
|
public class ChannelIdentity extends BaseEntity {
|
||||||
|
|
||||||
|
private String tenantCode;
|
||||||
|
private Long personId;
|
||||||
|
private String channelType;
|
||||||
|
private String channelUserId;
|
||||||
|
private String channelUserName;
|
||||||
|
private Long subjectId;
|
||||||
|
private Integer status;
|
||||||
|
private String extInfo;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.sino.mci.crm.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.sino.mci.shared.entity.BaseEntity;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@TableName("mci_person")
|
||||||
|
public class Person extends BaseEntity {
|
||||||
|
|
||||||
|
private String tenantCode;
|
||||||
|
private String personCode;
|
||||||
|
private String personName;
|
||||||
|
private String phone;
|
||||||
|
private Integer personType;
|
||||||
|
private Long institutionId;
|
||||||
|
private Long supplierId;
|
||||||
|
private Integer status;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.sino.mci.crm.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@TableName("mci_person_tag_binding")
|
||||||
|
public class PersonTagBinding {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String tenantCode;
|
||||||
|
private Long tagId;
|
||||||
|
private Long personId;
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.sino.mci.crm.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.sino.mci.shared.entity.BaseEntity;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@TableName("mci_tag")
|
||||||
|
public class Tag extends BaseEntity {
|
||||||
|
|
||||||
|
private String tenantCode;
|
||||||
|
private Long parentId;
|
||||||
|
private String tagPath;
|
||||||
|
private Integer level;
|
||||||
|
private String tagName;
|
||||||
|
private String tagSource;
|
||||||
|
private Integer status;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.sino.mci.crm.repository;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.sino.mci.crm.entity.ChannelIdentity;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface ChannelIdentityMapper extends BaseMapper<ChannelIdentity> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.sino.mci.crm.repository;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.sino.mci.crm.entity.Person;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface PersonMapper extends BaseMapper<Person> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.sino.mci.crm.repository;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.sino.mci.crm.entity.PersonTagBinding;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface PersonTagBindingMapper extends BaseMapper<PersonTagBinding> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.sino.mci.crm.repository;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.sino.mci.crm.entity.Tag;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface TagMapper extends BaseMapper<Tag> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
package com.sino.mci.crm.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.sino.mci.crm.dto.BindTagsRequest;
|
||||||
|
import com.sino.mci.crm.dto.CreateIdentityRequest;
|
||||||
|
import com.sino.mci.crm.dto.CreatePersonRequest;
|
||||||
|
import com.sino.mci.crm.entity.ChannelIdentity;
|
||||||
|
import com.sino.mci.crm.entity.Person;
|
||||||
|
import com.sino.mci.crm.entity.PersonTagBinding;
|
||||||
|
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.PersonTagBindingMapper;
|
||||||
|
import com.sino.mci.crm.repository.TagMapper;
|
||||||
|
import com.sino.mci.exception.BusinessException;
|
||||||
|
import com.sino.mci.shared.util.JsonUtils;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class PersonService {
|
||||||
|
|
||||||
|
private final PersonMapper personMapper;
|
||||||
|
private final ChannelIdentityMapper identityMapper;
|
||||||
|
private final PersonTagBindingMapper personTagBindingMapper;
|
||||||
|
private final TagMapper tagMapper;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Person createPerson(String tenantCode, CreatePersonRequest request) {
|
||||||
|
Person person = new Person();
|
||||||
|
person.setTenantCode(tenantCode);
|
||||||
|
person.setPersonCode(request.getPersonCode());
|
||||||
|
person.setPersonName(request.getPersonName());
|
||||||
|
person.setPhone(request.getPhone());
|
||||||
|
person.setPersonType(request.getPersonType());
|
||||||
|
person.setInstitutionId(request.getInstitutionId());
|
||||||
|
person.setSupplierId(request.getSupplierId());
|
||||||
|
person.setStatus(1);
|
||||||
|
personMapper.insert(person);
|
||||||
|
return person;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Person getPerson(String tenantCode, Long personId) {
|
||||||
|
Person person = personMapper.selectById(personId);
|
||||||
|
if (person == null || !tenantCode.equals(person.getTenantCode())) {
|
||||||
|
throw new BusinessException("联系人不存在");
|
||||||
|
}
|
||||||
|
return person;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ChannelIdentity> listIdentities(String tenantCode, Long personId) {
|
||||||
|
getPerson(tenantCode, personId);
|
||||||
|
return identityMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<ChannelIdentity>()
|
||||||
|
.eq(ChannelIdentity::getTenantCode, tenantCode)
|
||||||
|
.eq(ChannelIdentity::getPersonId, personId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public ChannelIdentity createIdentity(String tenantCode, CreateIdentityRequest request) {
|
||||||
|
getPerson(tenantCode, request.getPersonId());
|
||||||
|
|
||||||
|
ChannelIdentity existing = identityMapper.selectOne(
|
||||||
|
new LambdaQueryWrapper<ChannelIdentity>()
|
||||||
|
.eq(ChannelIdentity::getTenantCode, tenantCode)
|
||||||
|
.eq(ChannelIdentity::getChannelType, request.getChannelType())
|
||||||
|
.eq(ChannelIdentity::getChannelUserId, request.getChannelUserId()));
|
||||||
|
if (existing != null) {
|
||||||
|
throw new BusinessException("渠道身份已存在: " + request.getChannelUserId());
|
||||||
|
}
|
||||||
|
|
||||||
|
ChannelIdentity identity = new ChannelIdentity();
|
||||||
|
identity.setTenantCode(tenantCode);
|
||||||
|
identity.setPersonId(request.getPersonId());
|
||||||
|
identity.setChannelType(request.getChannelType());
|
||||||
|
identity.setChannelUserId(request.getChannelUserId());
|
||||||
|
identity.setChannelUserName(request.getChannelUserName());
|
||||||
|
identity.setSubjectId(request.getSubjectId());
|
||||||
|
identity.setStatus(1);
|
||||||
|
identity.setExtInfo(JsonUtils.toJson(request.getExtInfo()));
|
||||||
|
identityMapper.insert(identity);
|
||||||
|
return identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void bindTags(String tenantCode, Long personId, BindTagsRequest request) {
|
||||||
|
getPerson(tenantCode, personId);
|
||||||
|
for (Long tagId : request.getTagIds()) {
|
||||||
|
Tag tag = tagMapper.selectById(tagId);
|
||||||
|
if (tag == null || !tenantCode.equals(tag.getTenantCode())) {
|
||||||
|
throw new BusinessException("标签不存在: " + tagId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
personTagBindingMapper.delete(
|
||||||
|
new LambdaQueryWrapper<PersonTagBinding>()
|
||||||
|
.eq(PersonTagBinding::getPersonId, personId));
|
||||||
|
|
||||||
|
if (!CollectionUtils.isEmpty(request.getTagIds())) {
|
||||||
|
List<PersonTagBinding> bindings = request.getTagIds().stream()
|
||||||
|
.distinct()
|
||||||
|
.map(tagId -> {
|
||||||
|
PersonTagBinding binding = new PersonTagBinding();
|
||||||
|
binding.setTenantCode(tenantCode);
|
||||||
|
binding.setTagId(tagId);
|
||||||
|
binding.setPersonId(personId);
|
||||||
|
return binding;
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
for (PersonTagBinding binding : bindings) {
|
||||||
|
personTagBindingMapper.insert(binding);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package com.sino.mci.crm.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.sino.mci.crm.dto.CreateTagRequest;
|
||||||
|
import com.sino.mci.crm.entity.Tag;
|
||||||
|
import com.sino.mci.crm.repository.TagMapper;
|
||||||
|
import com.sino.mci.exception.BusinessException;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class TagService {
|
||||||
|
|
||||||
|
private final TagMapper tagMapper;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Tag createTag(String tenantCode, CreateTagRequest request) {
|
||||||
|
Long parentId = request.getParentId() != null ? request.getParentId() : 0L;
|
||||||
|
int level = 1;
|
||||||
|
String parentPath = "";
|
||||||
|
|
||||||
|
if (parentId > 0) {
|
||||||
|
Tag parent = tagMapper.selectById(parentId);
|
||||||
|
if (parent == null || !tenantCode.equals(parent.getTenantCode())) {
|
||||||
|
throw new BusinessException("父标签不存在");
|
||||||
|
}
|
||||||
|
level = parent.getLevel() + 1;
|
||||||
|
parentPath = parent.getTagPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
String tagPath = parentPath.isEmpty() ? request.getTagName() : parentPath + "/" + request.getTagName();
|
||||||
|
|
||||||
|
Tag existing = tagMapper.selectOne(
|
||||||
|
new LambdaQueryWrapper<Tag>()
|
||||||
|
.eq(Tag::getTenantCode, tenantCode)
|
||||||
|
.eq(Tag::getTagPath, tagPath));
|
||||||
|
if (existing != null) {
|
||||||
|
throw new BusinessException("标签路径已存在: " + tagPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
Tag tag = new Tag();
|
||||||
|
tag.setTenantCode(tenantCode);
|
||||||
|
tag.setParentId(parentId);
|
||||||
|
tag.setTagPath(tagPath);
|
||||||
|
tag.setLevel(level);
|
||||||
|
tag.setTagName(request.getTagName());
|
||||||
|
tag.setTagSource(request.getTagSource() != null ? request.getTagSource() : "manual");
|
||||||
|
tag.setStatus(1);
|
||||||
|
tagMapper.insert(tag);
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Tag> listTags(String tenantCode) {
|
||||||
|
return tagMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<Tag>()
|
||||||
|
.eq(Tag::getTenantCode, tenantCode)
|
||||||
|
.eq(Tag::getStatus, 1)
|
||||||
|
.orderByAsc(Tag::getTagPath));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS mci_person (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
tenant_code VARCHAR(64) NOT NULL COMMENT '所属租户',
|
||||||
|
person_code VARCHAR(64) COMMENT '业务人员编码',
|
||||||
|
person_name VARCHAR(128) NOT NULL COMMENT '姓名',
|
||||||
|
phone VARCHAR(32) COMMENT '手机号',
|
||||||
|
person_type TINYINT NOT NULL DEFAULT 1 COMMENT '类型:1内部人员 2供应商人员 3客户 4司机',
|
||||||
|
institution_id BIGINT COMMENT '所属机构/公司',
|
||||||
|
supplier_id BIGINT COMMENT '供应商 ID',
|
||||||
|
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0禁用 1启用',
|
||||||
|
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
deleted TINYINT NOT NULL DEFAULT 0,
|
||||||
|
UNIQUE KEY uk_tenant_person_code (tenant_code, person_code, deleted),
|
||||||
|
KEY idx_person_name (person_name),
|
||||||
|
KEY idx_phone (phone)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='联系人表';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS mci_channel_identity (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
tenant_code VARCHAR(64) NOT NULL COMMENT '所属租户',
|
||||||
|
person_id BIGINT NOT NULL COMMENT '联系人 ID',
|
||||||
|
channel_type VARCHAR(32) NOT NULL COMMENT '渠道:wecom / wechat_personal / dingtalk / feishu',
|
||||||
|
channel_user_id VARCHAR(128) NOT NULL COMMENT '渠道用户 ID',
|
||||||
|
channel_user_name VARCHAR(128) COMMENT '渠道昵称',
|
||||||
|
subject_id BIGINT COMMENT '所属渠道主体 mci_channel_subject',
|
||||||
|
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0禁用 1启用',
|
||||||
|
ext_info JSON COMMENT '渠道扩展信息',
|
||||||
|
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
deleted TINYINT NOT NULL DEFAULT 0,
|
||||||
|
UNIQUE KEY uk_tenant_channel_user (tenant_code, channel_type, channel_user_id, deleted),
|
||||||
|
KEY idx_person_id (person_id),
|
||||||
|
KEY idx_subject_id (subject_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='渠道身份表';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS mci_conversation (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
tenant_code VARCHAR(64) NOT NULL COMMENT '所属租户',
|
||||||
|
conversation_type TINYINT NOT NULL DEFAULT 1 COMMENT '1单聊 2群聊',
|
||||||
|
channel_type VARCHAR(32) NOT NULL COMMENT 'wecom / wechat_personal / dingtalk / feishu',
|
||||||
|
channel_conversation_id VARCHAR(128) NOT NULL COMMENT '渠道会话 ID',
|
||||||
|
owner_account_id BIGINT COMMENT '群主/所有者账号',
|
||||||
|
subject_id BIGINT COMMENT '所属渠道主体',
|
||||||
|
conversation_name VARCHAR(256) COMMENT '会话名称',
|
||||||
|
create_source TINYINT NOT NULL DEFAULT 1 COMMENT '来源:1渠道同步 2API 创建 3手动创建',
|
||||||
|
create_task_id BIGINT COMMENT '建群任务 ID',
|
||||||
|
institution_id BIGINT COMMENT '关联机构',
|
||||||
|
supplier_id BIGINT COMMENT '关联供应商',
|
||||||
|
contract_id BIGINT COMMENT '关联合同',
|
||||||
|
channel_code VARCHAR(64) COMMENT '业务渠道编码',
|
||||||
|
receive_config JSON COMMENT '收消息配置',
|
||||||
|
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0禁用 1启用',
|
||||||
|
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
deleted TINYINT NOT NULL DEFAULT 0,
|
||||||
|
UNIQUE KEY uk_tenant_channel_conv (tenant_code, channel_type, channel_conversation_id, deleted),
|
||||||
|
KEY idx_owner_account (owner_account_id),
|
||||||
|
KEY idx_subject_id (subject_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会话表';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS mci_conversation_participant (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
conversation_id BIGINT NOT NULL COMMENT '会话 ID',
|
||||||
|
person_id BIGINT COMMENT '人员 ID',
|
||||||
|
channel_identity_id BIGINT COMMENT '渠道身份 ID',
|
||||||
|
participant_type TINYINT NOT NULL DEFAULT 1 COMMENT '1成员 2群主 3管理员',
|
||||||
|
join_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY uk_conv_participant (conversation_id, person_id, channel_identity_id),
|
||||||
|
KEY idx_conversation_id (conversation_id),
|
||||||
|
KEY idx_person_id (person_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会话参与者表';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS mci_channel_account_conversation (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
account_id BIGINT NOT NULL COMMENT '渠道账号 ID',
|
||||||
|
conversation_id BIGINT NOT NULL COMMENT '会话 ID',
|
||||||
|
binding_type TINYINT NOT NULL COMMENT '1发送主号 2发送备用号 3接收主号 4接收备用号',
|
||||||
|
sort_order INT NOT NULL DEFAULT 0 COMMENT '优先级',
|
||||||
|
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0禁用 1启用',
|
||||||
|
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY uk_account_conv_binding (account_id, conversation_id, binding_type),
|
||||||
|
KEY idx_conversation_id (conversation_id),
|
||||||
|
KEY idx_account_id (account_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='账号-会话绑定表';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS mci_tag (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
tenant_code VARCHAR(64) NOT NULL COMMENT '所属租户',
|
||||||
|
parent_id BIGINT NOT NULL DEFAULT 0 COMMENT '父标签 ID,0 表示根标签',
|
||||||
|
tag_path VARCHAR(512) NOT NULL COMMENT '标签路径,如 销售/销售一部/华北组',
|
||||||
|
level TINYINT NOT NULL DEFAULT 1 COMMENT '层级',
|
||||||
|
tag_name VARCHAR(128) NOT NULL COMMENT '标签名',
|
||||||
|
tag_source VARCHAR(32) NOT NULL DEFAULT 'manual' COMMENT '来源:system / business_sync / manual',
|
||||||
|
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0禁用 1启用',
|
||||||
|
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
deleted TINYINT NOT NULL DEFAULT 0,
|
||||||
|
UNIQUE KEY uk_tenant_tag_path (tenant_code, tag_path, deleted),
|
||||||
|
KEY idx_parent_id (parent_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='标签表';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS mci_person_tag_binding (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
tenant_code VARCHAR(64) NOT NULL COMMENT '所属租户',
|
||||||
|
tag_id BIGINT NOT NULL COMMENT '标签 ID',
|
||||||
|
person_id BIGINT NOT NULL COMMENT '联系人 ID',
|
||||||
|
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY uk_person_tag (tag_id, person_id),
|
||||||
|
KEY idx_person_id (person_id),
|
||||||
|
KEY idx_tag_id (tag_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='人员标签绑定表';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS mci_conversation_tag_binding (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
tenant_code VARCHAR(64) NOT NULL COMMENT '所属租户',
|
||||||
|
tag_id BIGINT NOT NULL COMMENT '标签 ID',
|
||||||
|
conversation_id BIGINT NOT NULL COMMENT '会话 ID',
|
||||||
|
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY uk_conversation_tag (tag_id, conversation_id),
|
||||||
|
KEY idx_conversation_id (conversation_id),
|
||||||
|
KEY idx_tag_id (tag_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会话标签绑定表';
|
||||||
|
|
||||||
|
ALTER TABLE mci_channel_identity
|
||||||
|
ADD CONSTRAINT fk_identity_person_id FOREIGN KEY (person_id) REFERENCES mci_person (id),
|
||||||
|
ADD CONSTRAINT fk_identity_subject_id FOREIGN KEY (subject_id) REFERENCES mci_channel_subject (id);
|
||||||
|
|
||||||
|
ALTER TABLE mci_conversation
|
||||||
|
ADD CONSTRAINT fk_conversation_owner_account FOREIGN KEY (owner_account_id) REFERENCES mci_channel_account (id),
|
||||||
|
ADD CONSTRAINT fk_conversation_subject_id FOREIGN KEY (subject_id) REFERENCES mci_channel_subject (id);
|
||||||
|
|
||||||
|
ALTER TABLE mci_conversation_participant
|
||||||
|
ADD CONSTRAINT fk_participant_conversation_id FOREIGN KEY (conversation_id) REFERENCES mci_conversation (id),
|
||||||
|
ADD CONSTRAINT fk_participant_person_id FOREIGN KEY (person_id) REFERENCES mci_person (id),
|
||||||
|
ADD CONSTRAINT fk_participant_identity_id FOREIGN KEY (channel_identity_id) REFERENCES mci_channel_identity (id);
|
||||||
|
|
||||||
|
ALTER TABLE mci_channel_account_conversation
|
||||||
|
ADD CONSTRAINT fk_binding_account_id FOREIGN KEY (account_id) REFERENCES mci_channel_account (id),
|
||||||
|
ADD CONSTRAINT fk_binding_conversation_id FOREIGN KEY (conversation_id) REFERENCES mci_conversation (id);
|
||||||
|
|
||||||
|
ALTER TABLE mci_person_tag_binding
|
||||||
|
ADD CONSTRAINT fk_person_tag_tag_id FOREIGN KEY (tag_id) REFERENCES mci_tag (id),
|
||||||
|
ADD CONSTRAINT fk_person_tag_person_id FOREIGN KEY (person_id) REFERENCES mci_person (id);
|
||||||
|
|
||||||
|
ALTER TABLE mci_conversation_tag_binding
|
||||||
|
ADD CONSTRAINT fk_conv_tag_tag_id FOREIGN KEY (tag_id) REFERENCES mci_tag (id),
|
||||||
|
ADD CONSTRAINT fk_conv_tag_conversation_id FOREIGN KEY (conversation_id) REFERENCES mci_conversation (id);
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package com.sino.mci.crm.controller;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
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
|
||||||
|
@Transactional
|
||||||
|
public class CrmControllerTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MockMvc mockMvc;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCreatePersonTagAndConversation() throws Exception {
|
||||||
|
String tenantCode = "crm-" + UUID.randomUUID().toString().substring(0, 8);
|
||||||
|
String tenantPayload = String.format("{\"tenantCode\":\"%s\",\"tenantName\":\"CRM测试\"}", tenantCode);
|
||||||
|
mockMvc.perform(post("/msg-platform/api/v1/account/tenant")
|
||||||
|
.contextPath("/msg-platform")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(tenantPayload))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(0));
|
||||||
|
|
||||||
|
String personPayload = "{\"personCode\":\"P001\",\"personName\":\"张三\",\"phone\":\"13800138000\",\"personType\":3}";
|
||||||
|
mockMvc.perform(post("/msg-platform/api/v1/person")
|
||||||
|
.contextPath("/msg-platform")
|
||||||
|
.header("X-Tenant-Code", tenantCode)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(personPayload))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(0))
|
||||||
|
.andExpect(jsonPath("$.data.personName").value("张三"));
|
||||||
|
|
||||||
|
String tagPayload = "{\"parentId\":0,\"tagName\":\"客户\"}";
|
||||||
|
mockMvc.perform(post("/msg-platform/api/v1/tag")
|
||||||
|
.contextPath("/msg-platform")
|
||||||
|
.header("X-Tenant-Code", tenantCode)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(tagPayload))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(0))
|
||||||
|
.andExpect(jsonPath("$.data.tagPath").value("客户"));
|
||||||
|
|
||||||
|
String conversationPayload = "{\"conversationType\":2,\"channelType\":\"wecom\",\"channelConversationId\":\"roomid_123\",\"conversationName\":\"测试群\"}";
|
||||||
|
mockMvc.perform(post("/msg-platform/api/v1/conversation")
|
||||||
|
.contextPath("/msg-platform")
|
||||||
|
.header("X-Tenant-Code", tenantCode)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(conversationPayload))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(0))
|
||||||
|
.andExpect(jsonPath("$.data.conversationName").value("测试群"));
|
||||||
|
|
||||||
|
mockMvc.perform(get("/msg-platform/api/v1/tag/list")
|
||||||
|
.contextPath("/msg-platform")
|
||||||
|
.header("X-Tenant-Code", tenantCode))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(0))
|
||||||
|
.andExpect(jsonPath("$.data.size()").value(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,5 +21,13 @@
|
|||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
<optional>true</optional>
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>slf4j-api</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.sino.mci.shared.util;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public final class JsonUtils {
|
||||||
|
|
||||||
|
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||||
|
|
||||||
|
private JsonUtils() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String toJson(Object value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return OBJECT_MAPPER.writeValueAsString(value);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
log.warn("JSON serialize failed", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> T fromJson(String json, Class<T> clazz) {
|
||||||
|
if (json == null || json.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return OBJECT_MAPPER.readValue(json, clazz);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
log.warn("JSON deserialize failed", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user