feat(mci-server): Phase 1 数据模型调整

- 新增 V11 媒体文件表 mci_message_media 及实体/Mapper
- 新增 V12 建群任务表 mci_conversation_create_task 及实体/Mapper
- 新增 V13 策略组表 mci_policy_group 及实体/Mapper/Service/Controller
- V14 将 mci_flow_definition.flow_type 由 VARCHAR 迁移为 TINYINT,实体改为 Integer
- FlowDefinitionService 兼容字符串/数值 flowType,FlowEngine 按数值 1 过滤入站流程
- MybatisPlusConfig 增加 message、policy 仓库扫描包
This commit is contained in:
2026-07-07 18:06:59 +08:00
parent 0795979ba5
commit d43fbbcc3c
19 changed files with 382 additions and 5 deletions

View File

@@ -0,0 +1,25 @@
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;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("mci_conversation_create_task")
public class ConversationCreateTask extends BaseEntity {
private String tenantCode;
private Integer taskType;
private String channelType;
private Long accountId;
private Long conversationId;
private String participantIds;
private Integer taskStatus;
private String channelResult;
private Integer retryCount;
private LocalDateTime finishTime;
}

View File

@@ -0,0 +1,9 @@
package com.sino.mci.channel.repository;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.sino.mci.channel.entity.ConversationCreateTask;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ConversationCreateTaskMapper extends BaseMapper<ConversationCreateTask> {
}

View File

@@ -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", "com.sino.mci.crm.repository", "com.sino.mci.flow.repository", "com.sino.mci.inbound.repository", "com.sino.mci.send.repository"}) @MapperScan({"com.sino.mci.admin.repository", "com.sino.mci.channel.repository", "com.sino.mci.crm.repository", "com.sino.mci.flow.repository", "com.sino.mci.inbound.repository", "com.sino.mci.message.repository", "com.sino.mci.policy.repository", "com.sino.mci.send.repository"})
public class MybatisPlusConfig { public class MybatisPlusConfig {
@Bean @Bean

View File

@@ -13,7 +13,7 @@ public class FlowDefinition extends BaseEntity {
private String tenantCode; private String tenantCode;
private String flowCode; private String flowCode;
private String flowName; private String flowName;
private String flowType; private Integer flowType;
private String definitionJson; private String definitionJson;
private Integer status; private Integer status;
} }

View File

@@ -32,7 +32,7 @@ public class FlowDefinitionService {
flow.setTenantCode(tenantCode); flow.setTenantCode(tenantCode);
flow.setFlowCode(request.getFlowCode()); flow.setFlowCode(request.getFlowCode());
flow.setFlowName(request.getFlowName()); flow.setFlowName(request.getFlowName());
flow.setFlowType(request.getFlowType()); flow.setFlowType(parseFlowType(request.getFlowType()));
flow.setDefinitionJson(JsonUtils.toJson(request.getDefinitionJson())); flow.setDefinitionJson(JsonUtils.toJson(request.getDefinitionJson()));
flow.setStatus(0); flow.setStatus(0);
flowDefinitionMapper.insert(flow); flowDefinitionMapper.insert(flow);
@@ -62,7 +62,7 @@ public class FlowDefinitionService {
public FlowDefinition updateFlow(String tenantCode, String flowCode, CreateFlowRequest request) { public FlowDefinition updateFlow(String tenantCode, String flowCode, CreateFlowRequest request) {
FlowDefinition flow = getFlow(tenantCode, flowCode); FlowDefinition flow = getFlow(tenantCode, flowCode);
flow.setFlowName(request.getFlowName()); flow.setFlowName(request.getFlowName());
flow.setFlowType(request.getFlowType()); flow.setFlowType(parseFlowType(request.getFlowType()));
flow.setDefinitionJson(JsonUtils.toJson(request.getDefinitionJson())); flow.setDefinitionJson(JsonUtils.toJson(request.getDefinitionJson()));
flowDefinitionMapper.updateById(flow); flowDefinitionMapper.updateById(flow);
return flow; return flow;
@@ -81,4 +81,16 @@ public class FlowDefinitionService {
flow.setStatus(0); flow.setStatus(0);
flowDefinitionMapper.updateById(flow); flowDefinitionMapper.updateById(flow);
} }
private Integer parseFlowType(String flowType) {
if (flowType == null || flowType.isBlank()) {
return 1;
}
String normalized = flowType.trim().toLowerCase();
return switch (normalized) {
case "inbound", "1" -> 1;
case "ivr", "2" -> 2;
default -> 1;
};
}
} }

View File

@@ -77,7 +77,7 @@ public class FlowEngine {
List<FlowDefinition> flows = flowDefinitionMapper.selectList( List<FlowDefinition> flows = flowDefinitionMapper.selectList(
new LambdaQueryWrapper<FlowDefinition>() new LambdaQueryWrapper<FlowDefinition>()
.eq(FlowDefinition::getTenantCode, tenantCode) .eq(FlowDefinition::getTenantCode, tenantCode)
.eq(FlowDefinition::getFlowType, "inbound") .eq(FlowDefinition::getFlowType, 1)
.eq(FlowDefinition::getStatus, 1) .eq(FlowDefinition::getStatus, 1)
.orderByDesc(FlowDefinition::getUpdateTime)); .orderByDesc(FlowDefinition::getUpdateTime));
return flows.isEmpty() ? null : flows.get(0); return flows.isEmpty() ? null : flows.get(0);

View File

@@ -0,0 +1,25 @@
package com.sino.mci.message.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_message_media")
public class MessageMedia extends BaseEntity {
private String tenantCode;
private String msgid;
private String sdkfileid;
private String mediaType;
private String fileName;
private Long fileSize;
private String md5sum;
private Integer storageType;
private String bucket;
private String objectKey;
private String accessUrl;
private Integer status;
}

View File

@@ -0,0 +1,9 @@
package com.sino.mci.message.repository;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.sino.mci.message.entity.MessageMedia;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface MessageMediaMapper extends BaseMapper<MessageMedia> {
}

View File

@@ -0,0 +1,65 @@
package com.sino.mci.policy.controller;
import com.sino.mci.policy.dto.CreatePolicyGroupRequest;
import com.sino.mci.policy.dto.UpdatePolicyGroupRequest;
import com.sino.mci.policy.dto.UpdatePolicyGroupStatusRequest;
import com.sino.mci.policy.entity.PolicyGroup;
import com.sino.mci.policy.service.PolicyGroupService;
import com.sino.mci.shared.web.ApiResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/v1/policy-group")
@RequiredArgsConstructor
public class PolicyGroupController {
private final PolicyGroupService policyGroupService;
@PostMapping
public ApiResponse<PolicyGroup> createPolicyGroup(
@RequestHeader("X-Tenant-Code") String tenantCode,
@Valid @RequestBody CreatePolicyGroupRequest request) {
return ApiResponse.ok(policyGroupService.createPolicyGroup(tenantCode, request));
}
@GetMapping("/list")
public ApiResponse<List<PolicyGroup>> listPolicyGroups(
@RequestHeader("X-Tenant-Code") String tenantCode) {
return ApiResponse.ok(policyGroupService.listPolicyGroups(tenantCode));
}
@GetMapping("/{groupCode}")
public ApiResponse<PolicyGroup> getPolicyGroup(
@RequestHeader("X-Tenant-Code") String tenantCode,
@PathVariable("groupCode") String groupCode) {
return ApiResponse.ok(policyGroupService.getPolicyGroup(tenantCode, groupCode));
}
@PostMapping("/{groupCode}")
public ApiResponse<PolicyGroup> updatePolicyGroup(
@RequestHeader("X-Tenant-Code") String tenantCode,
@PathVariable("groupCode") String groupCode,
@Valid @RequestBody UpdatePolicyGroupRequest request) {
return ApiResponse.ok(policyGroupService.updatePolicyGroup(tenantCode, groupCode, request));
}
@PostMapping("/{groupCode}/status")
public ApiResponse<PolicyGroup> updatePolicyGroupStatus(
@RequestHeader("X-Tenant-Code") String tenantCode,
@PathVariable("groupCode") String groupCode,
@Valid @RequestBody UpdatePolicyGroupStatusRequest request) {
return ApiResponse.ok(policyGroupService.updatePolicyGroupStatus(tenantCode, groupCode, request.getStatus()));
}
@PostMapping("/{groupCode}/delete")
public ApiResponse<?> deletePolicyGroup(
@RequestHeader("X-Tenant-Code") String tenantCode,
@PathVariable("groupCode") String groupCode) {
policyGroupService.deletePolicyGroup(tenantCode, groupCode);
return ApiResponse.ok();
}
}

View File

@@ -0,0 +1,22 @@
package com.sino.mci.policy.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.Map;
@Data
public class CreatePolicyGroupRequest {
@NotBlank
private String groupCode;
@NotBlank
private String groupName;
private String channelType;
@NotNull
private Map<String, Object> policies;
}

View File

@@ -0,0 +1,19 @@
package com.sino.mci.policy.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.Map;
@Data
public class UpdatePolicyGroupRequest {
@NotBlank
private String groupName;
private String channelType;
@NotNull
private Map<String, Object> policies;
}

View File

@@ -0,0 +1,11 @@
package com.sino.mci.policy.dto;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
public class UpdatePolicyGroupStatusRequest {
@NotNull
private Integer status;
}

View File

@@ -0,0 +1,19 @@
package com.sino.mci.policy.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_policy_group")
public class PolicyGroup extends BaseEntity {
private String tenantCode;
private String groupCode;
private String groupName;
private String channelType;
private String policies;
private Integer status;
}

View File

@@ -0,0 +1,9 @@
package com.sino.mci.policy.repository;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.sino.mci.policy.entity.PolicyGroup;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface PolicyGroupMapper extends BaseMapper<PolicyGroup> {
}

View File

@@ -0,0 +1,84 @@
package com.sino.mci.policy.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.sino.mci.exception.BusinessException;
import com.sino.mci.policy.dto.CreatePolicyGroupRequest;
import com.sino.mci.policy.dto.UpdatePolicyGroupRequest;
import com.sino.mci.policy.entity.PolicyGroup;
import com.sino.mci.policy.repository.PolicyGroupMapper;
import com.sino.mci.shared.util.JsonUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@RequiredArgsConstructor
public class PolicyGroupService {
private final PolicyGroupMapper policyGroupMapper;
@Transactional
public PolicyGroup createPolicyGroup(String tenantCode, CreatePolicyGroupRequest request) {
PolicyGroup existing = policyGroupMapper.selectOne(
new LambdaQueryWrapper<PolicyGroup>()
.eq(PolicyGroup::getTenantCode, tenantCode)
.eq(PolicyGroup::getGroupCode, request.getGroupCode()));
if (existing != null) {
throw new BusinessException("策略组编码已存在: " + request.getGroupCode());
}
PolicyGroup group = new PolicyGroup();
group.setTenantCode(tenantCode);
group.setGroupCode(request.getGroupCode());
group.setGroupName(request.getGroupName());
group.setChannelType(request.getChannelType());
group.setPolicies(JsonUtils.toJson(request.getPolicies()));
group.setStatus(1);
policyGroupMapper.insert(group);
return group;
}
public PolicyGroup getPolicyGroup(String tenantCode, String groupCode) {
PolicyGroup group = policyGroupMapper.selectOne(
new LambdaQueryWrapper<PolicyGroup>()
.eq(PolicyGroup::getTenantCode, tenantCode)
.eq(PolicyGroup::getGroupCode, groupCode));
if (group == null) {
throw new BusinessException("策略组不存在: " + groupCode);
}
return group;
}
public List<PolicyGroup> listPolicyGroups(String tenantCode) {
return policyGroupMapper.selectList(
new LambdaQueryWrapper<PolicyGroup>()
.eq(PolicyGroup::getTenantCode, tenantCode)
.orderByDesc(PolicyGroup::getCreateTime));
}
@Transactional
public PolicyGroup updatePolicyGroup(String tenantCode, String groupCode, UpdatePolicyGroupRequest request) {
PolicyGroup group = getPolicyGroup(tenantCode, groupCode);
group.setGroupName(request.getGroupName());
group.setChannelType(request.getChannelType());
group.setPolicies(JsonUtils.toJson(request.getPolicies()));
policyGroupMapper.updateById(group);
return group;
}
@Transactional
public PolicyGroup updatePolicyGroupStatus(String tenantCode, String groupCode, Integer status) {
PolicyGroup group = getPolicyGroup(tenantCode, groupCode);
group.setStatus(status);
policyGroupMapper.updateById(group);
return group;
}
@Transactional
public void deletePolicyGroup(String tenantCode, String groupCode) {
PolicyGroup group = getPolicyGroup(tenantCode, groupCode);
policyGroupMapper.deleteById(group.getId());
}
}

View File

@@ -0,0 +1,20 @@
CREATE TABLE IF NOT EXISTS mci_message_media (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
tenant_code VARCHAR(64) NOT NULL COMMENT '所属租户',
msgid VARCHAR(128) COMMENT '关联的消息 ID',
sdkfileid VARCHAR(256) COMMENT '企微媒体文件 ID',
media_type VARCHAR(32) COMMENT '媒体类型image / voice / video / file / emotion',
file_name VARCHAR(256) COMMENT '文件名',
file_size BIGINT COMMENT '文件大小',
md5sum VARCHAR(64) COMMENT 'MD5',
storage_type TINYINT NOT NULL DEFAULT 2 COMMENT '存储类型2SeaweedFS',
bucket VARCHAR(128) COMMENT 'SeaweedFS bucket',
object_key VARCHAR(512) COMMENT 'SeaweedFS object key',
access_url VARCHAR(512) COMMENT '访问 URL',
status TINYINT NOT NULL DEFAULT 0 COMMENT '状态0未下载 1下载中 2已下载 3下载失败',
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,
KEY idx_tenant_msgid (tenant_code, msgid),
KEY idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='媒体文件表';

View File

@@ -0,0 +1,18 @@
CREATE TABLE IF NOT EXISTS mci_conversation_create_task (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
tenant_code VARCHAR(64) NOT NULL COMMENT '所属租户',
task_type TINYINT NOT NULL DEFAULT 1 COMMENT '任务类型1创建群聊 2邀请成员',
channel_type VARCHAR(32) NOT NULL COMMENT '渠道wecom / wechat_personal / dingtalk / feishu',
account_id BIGINT COMMENT '使用哪个账号创建',
conversation_id BIGINT COMMENT '创建成功后回填的会话 ID',
participant_ids JSON COMMENT '参与者人员 ID 列表',
task_status TINYINT NOT NULL DEFAULT 0 COMMENT '任务状态0待执行 1执行中 2成功 3失败',
channel_result JSON COMMENT '渠道返回结果',
retry_count INT NOT NULL DEFAULT 0 COMMENT '重试次数',
finish_time DATETIME 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,
KEY idx_tenant_status (tenant_code, task_status),
KEY idx_conversation_id (conversation_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='建群任务表';

View File

@@ -0,0 +1,14 @@
CREATE TABLE IF NOT EXISTS mci_policy_group (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
tenant_code VARCHAR(64) NOT NULL COMMENT '所属租户',
group_code VARCHAR(64) NOT NULL COMMENT '策略组编码',
group_name VARCHAR(128) NOT NULL COMMENT '策略组名称',
channel_type VARCHAR(32) COMMENT '渠道类型wecom / wechat_personal / dingtalk / feishu',
policies JSON NOT NULL COMMENT '策略规则 JSON',
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_group_code (tenant_code, group_code, deleted),
KEY idx_tenant_status (tenant_code, status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='策略组表';

View File

@@ -0,0 +1,16 @@
-- 将流程类型的字符串值安全迁移为数值,之后改为 TINYINT 以对齐架构定义
UPDATE mci_flow_definition
SET flow_type = '1'
WHERE LOWER(flow_type) = 'inbound';
UPDATE mci_flow_definition
SET flow_type = '2'
WHERE LOWER(flow_type) = 'ivr';
-- 兜底:任何未识别的旧值统一按入站处理
UPDATE mci_flow_definition
SET flow_type = '1'
WHERE flow_type NOT IN ('1', '2');
ALTER TABLE mci_flow_definition
MODIFY COLUMN flow_type TINYINT NOT NULL DEFAULT 1 COMMENT '流程类型1入站 2IVR';