feat(inbound): add flow definition, message event, intent recognition and webhook push
This commit is contained in:
@@ -2,7 +2,9 @@ package com.sino.mci;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
|
||||
@EnableAsync
|
||||
@SpringBootApplication
|
||||
public class MciServerApplication {
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import org.springframework.context.annotation.Configuration;
|
||||
import javax.sql.DataSource;
|
||||
|
||||
@Configuration
|
||||
@MapperScan({"com.sino.mci.admin.repository", "com.sino.mci.channel.repository", "com.sino.mci.crm.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"})
|
||||
public class MybatisPlusConfig {
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.sino.mci.flow.controller;
|
||||
|
||||
import com.sino.mci.flow.dto.CreateFlowRequest;
|
||||
import com.sino.mci.flow.service.FlowDefinitionService;
|
||||
import com.sino.mci.shared.web.ApiResponse;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/inbound/flow")
|
||||
@RequiredArgsConstructor
|
||||
public class FlowDefinitionController {
|
||||
|
||||
private final FlowDefinitionService flowDefinitionService;
|
||||
|
||||
@PostMapping
|
||||
public ApiResponse<?> createFlow(
|
||||
@RequestHeader("X-Tenant-Code") String tenantCode,
|
||||
@Valid @RequestBody CreateFlowRequest request) {
|
||||
return ApiResponse.ok(flowDefinitionService.createFlow(tenantCode, request));
|
||||
}
|
||||
|
||||
@GetMapping("/{flow_code}")
|
||||
public ApiResponse<?> getFlow(
|
||||
@RequestHeader("X-Tenant-Code") String tenantCode,
|
||||
@PathVariable("flow_code") String flowCode) {
|
||||
return ApiResponse.ok(flowDefinitionService.getFlow(tenantCode, flowCode));
|
||||
}
|
||||
|
||||
@GetMapping("/list")
|
||||
public ApiResponse<?> listFlows(@RequestHeader("X-Tenant-Code") String tenantCode) {
|
||||
return ApiResponse.ok(flowDefinitionService.listFlows(tenantCode));
|
||||
}
|
||||
|
||||
@PostMapping("/{flow_code}")
|
||||
public ApiResponse<?> updateFlow(
|
||||
@RequestHeader("X-Tenant-Code") String tenantCode,
|
||||
@PathVariable("flow_code") String flowCode,
|
||||
@Valid @RequestBody CreateFlowRequest request) {
|
||||
return ApiResponse.ok(flowDefinitionService.updateFlow(tenantCode, flowCode, request));
|
||||
}
|
||||
|
||||
@PostMapping("/{flow_code}/publish")
|
||||
public ApiResponse<?> publishFlow(
|
||||
@RequestHeader("X-Tenant-Code") String tenantCode,
|
||||
@PathVariable("flow_code") String flowCode) {
|
||||
flowDefinitionService.publishFlow(tenantCode, flowCode);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/{flow_code}/offline")
|
||||
public ApiResponse<?> offlineFlow(
|
||||
@RequestHeader("X-Tenant-Code") String tenantCode,
|
||||
@PathVariable("flow_code") String flowCode) {
|
||||
flowDefinitionService.offlineFlow(tenantCode, flowCode);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.sino.mci.flow.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
public class CreateFlowRequest {
|
||||
|
||||
@NotBlank
|
||||
private String flowCode;
|
||||
|
||||
@NotBlank
|
||||
private String flowName;
|
||||
|
||||
@NotBlank
|
||||
private String flowType;
|
||||
|
||||
@NotNull
|
||||
private Map<String, Object> definitionJson;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.sino.mci.flow.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_flow_definition")
|
||||
public class FlowDefinition extends BaseEntity {
|
||||
|
||||
private String tenantCode;
|
||||
private String flowCode;
|
||||
private String flowName;
|
||||
private String flowType;
|
||||
private String definitionJson;
|
||||
private Integer status;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.sino.mci.flow.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.sino.mci.flow.entity.FlowDefinition;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface FlowDefinitionMapper extends BaseMapper<FlowDefinition> {
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.sino.mci.flow.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.sino.mci.exception.BusinessException;
|
||||
import com.sino.mci.flow.dto.CreateFlowRequest;
|
||||
import com.sino.mci.flow.entity.FlowDefinition;
|
||||
import com.sino.mci.flow.repository.FlowDefinitionMapper;
|
||||
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 FlowDefinitionService {
|
||||
|
||||
private final FlowDefinitionMapper flowDefinitionMapper;
|
||||
|
||||
@Transactional
|
||||
public FlowDefinition createFlow(String tenantCode, CreateFlowRequest request) {
|
||||
FlowDefinition existing = flowDefinitionMapper.selectOne(
|
||||
new LambdaQueryWrapper<FlowDefinition>()
|
||||
.eq(FlowDefinition::getTenantCode, tenantCode)
|
||||
.eq(FlowDefinition::getFlowCode, request.getFlowCode()));
|
||||
if (existing != null) {
|
||||
throw new BusinessException("流程编码已存在: " + request.getFlowCode());
|
||||
}
|
||||
|
||||
FlowDefinition flow = new FlowDefinition();
|
||||
flow.setTenantCode(tenantCode);
|
||||
flow.setFlowCode(request.getFlowCode());
|
||||
flow.setFlowName(request.getFlowName());
|
||||
flow.setFlowType(request.getFlowType());
|
||||
flow.setDefinitionJson(JsonUtils.toJson(request.getDefinitionJson()));
|
||||
flow.setStatus(0);
|
||||
flowDefinitionMapper.insert(flow);
|
||||
return flow;
|
||||
}
|
||||
|
||||
public FlowDefinition getFlow(String tenantCode, String flowCode) {
|
||||
FlowDefinition flow = flowDefinitionMapper.selectOne(
|
||||
new LambdaQueryWrapper<FlowDefinition>()
|
||||
.eq(FlowDefinition::getTenantCode, tenantCode)
|
||||
.eq(FlowDefinition::getFlowCode, flowCode));
|
||||
if (flow == null) {
|
||||
throw new BusinessException("流程不存在: " + flowCode);
|
||||
}
|
||||
return flow;
|
||||
}
|
||||
|
||||
public List<FlowDefinition> listFlows(String tenantCode) {
|
||||
return flowDefinitionMapper.selectList(
|
||||
new LambdaQueryWrapper<FlowDefinition>()
|
||||
.eq(FlowDefinition::getTenantCode, tenantCode)
|
||||
.eq(FlowDefinition::getDeleted, 0)
|
||||
.orderByDesc(FlowDefinition::getCreateTime));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public FlowDefinition updateFlow(String tenantCode, String flowCode, CreateFlowRequest request) {
|
||||
FlowDefinition flow = getFlow(tenantCode, flowCode);
|
||||
flow.setFlowName(request.getFlowName());
|
||||
flow.setFlowType(request.getFlowType());
|
||||
flow.setDefinitionJson(JsonUtils.toJson(request.getDefinitionJson()));
|
||||
flowDefinitionMapper.updateById(flow);
|
||||
return flow;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void publishFlow(String tenantCode, String flowCode) {
|
||||
FlowDefinition flow = getFlow(tenantCode, flowCode);
|
||||
flow.setStatus(1);
|
||||
flowDefinitionMapper.updateById(flow);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void offlineFlow(String tenantCode, String flowCode) {
|
||||
FlowDefinition flow = getFlow(tenantCode, flowCode);
|
||||
flow.setStatus(0);
|
||||
flowDefinitionMapper.updateById(flow);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.sino.mci.inbound.controller;
|
||||
|
||||
import com.sino.mci.inbound.dto.ReceiveMessageRequest;
|
||||
import com.sino.mci.inbound.service.InboundMessageService;
|
||||
import com.sino.mci.shared.web.ApiResponse;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/inbound")
|
||||
@RequiredArgsConstructor
|
||||
public class InboundMessageController {
|
||||
|
||||
private final InboundMessageService inboundMessageService;
|
||||
|
||||
@PostMapping("/receive")
|
||||
public ApiResponse<?> receiveMessage(
|
||||
@RequestHeader("X-Tenant-Code") String tenantCode,
|
||||
@Valid @RequestBody ReceiveMessageRequest request) {
|
||||
return ApiResponse.ok(inboundMessageService.receiveMessage(tenantCode, request));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.sino.mci.inbound.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
public class ReceiveMessageRequest {
|
||||
|
||||
@NotBlank
|
||||
private String eventId;
|
||||
|
||||
@NotBlank
|
||||
private String channelType;
|
||||
|
||||
private Long channelSubjectId;
|
||||
|
||||
@NotBlank
|
||||
private String eventType;
|
||||
|
||||
@NotBlank
|
||||
private String messageSource;
|
||||
|
||||
private Long conversationId;
|
||||
private Long personId;
|
||||
private Long accountId;
|
||||
|
||||
private Map<String, Object> content;
|
||||
private Map<String, Object> rawPayload;
|
||||
private Map<String, Object> businessContext;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.sino.mci.inbound.engine;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.sino.mci.flow.entity.FlowDefinition;
|
||||
import com.sino.mci.flow.repository.FlowDefinitionMapper;
|
||||
import com.sino.mci.inbound.entity.MessageEvent;
|
||||
import com.sino.mci.inbound.repository.MessageEventMapper;
|
||||
import com.sino.mci.shared.util.JsonUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class FlowEngine {
|
||||
|
||||
private final FlowDefinitionMapper flowDefinitionMapper;
|
||||
private final MessageEventMapper messageEventMapper;
|
||||
private final IntentRecognitionService intentRecognitionService;
|
||||
private final WebhookPushService webhookPushService;
|
||||
|
||||
public void executeInboundFlow(Long eventId) {
|
||||
MessageEvent event = messageEventMapper.selectById(eventId);
|
||||
if (event == null) {
|
||||
log.warn("Message event not found: {}", eventId);
|
||||
return;
|
||||
}
|
||||
|
||||
FlowDefinition flow = findPublishedFlow(event.getTenantCode());
|
||||
if (flow == null) {
|
||||
log.info("No published inbound flow for tenant {}, skipping flow execution", event.getTenantCode());
|
||||
markProcessed(event);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Map<String, Object> definition = JsonUtils.fromJson(flow.getDefinitionJson(), Map.class);
|
||||
if (definition == null) {
|
||||
log.warn("Invalid flow definition JSON for flow {}", flow.getFlowCode());
|
||||
markProcessed(event);
|
||||
return;
|
||||
}
|
||||
|
||||
IntentRecognitionService.IntentResult intent = intentRecognitionService.recognize(event);
|
||||
log.info("Intent recognized: eventId={}, intent={}, confidence={}",
|
||||
event.getEventId(), intent.intent(), intent.confidence());
|
||||
|
||||
webhookPushService.pushToBusiness(event.getTenantCode(), event, intent);
|
||||
markProcessed(event);
|
||||
} catch (Exception e) {
|
||||
log.error("Flow execution failed for event {}", eventId, e);
|
||||
}
|
||||
}
|
||||
|
||||
private FlowDefinition findPublishedFlow(String tenantCode) {
|
||||
List<FlowDefinition> flows = flowDefinitionMapper.selectList(
|
||||
new LambdaQueryWrapper<FlowDefinition>()
|
||||
.eq(FlowDefinition::getTenantCode, tenantCode)
|
||||
.eq(FlowDefinition::getFlowType, "inbound")
|
||||
.eq(FlowDefinition::getStatus, 1)
|
||||
.orderByDesc(FlowDefinition::getUpdateTime));
|
||||
return flows.isEmpty() ? null : flows.get(0);
|
||||
}
|
||||
|
||||
private void markProcessed(MessageEvent event) {
|
||||
event.setIsProcessed(1);
|
||||
messageEventMapper.updateById(event);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.sino.mci.inbound.engine;
|
||||
|
||||
import com.sino.mci.inbound.entity.MessageEvent;
|
||||
|
||||
public interface IntentRecognitionService {
|
||||
|
||||
IntentResult recognize(MessageEvent event);
|
||||
|
||||
record IntentResult(String intent, double confidence, String reason) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.sino.mci.inbound.engine;
|
||||
|
||||
import com.sino.mci.inbound.entity.MessageEvent;
|
||||
import com.sino.mci.shared.util.JsonUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class MockIntentRecognitionService implements IntentRecognitionService {
|
||||
|
||||
@Override
|
||||
public IntentResult recognize(MessageEvent event) {
|
||||
String text = extractText(event);
|
||||
if (text == null || text.isBlank()) {
|
||||
return new IntentResult("unknown", 0.0, "empty content");
|
||||
}
|
||||
String lower = text.toLowerCase();
|
||||
if (lower.contains("救援") || lower.contains("拖车") || lower.contains("故障")) {
|
||||
return new IntentResult("rescue_request", 0.95, "contains rescue keywords");
|
||||
}
|
||||
if (lower.contains("咨询") || lower.contains("问一下")) {
|
||||
return new IntentResult("consult", 0.85, "contains consult keywords");
|
||||
}
|
||||
if (lower.contains("投诉") || lower.contains("不满")) {
|
||||
return new IntentResult("complaint", 0.88, "contains complaint keywords");
|
||||
}
|
||||
return new IntentResult("other", 0.6, "no clear intent matched");
|
||||
}
|
||||
|
||||
private String extractText(MessageEvent event) {
|
||||
String contentJson = event.getContent();
|
||||
if (contentJson == null) {
|
||||
return null;
|
||||
}
|
||||
Map<String, Object> content = JsonUtils.fromJson(contentJson, Map.class);
|
||||
if (content == null) {
|
||||
return null;
|
||||
}
|
||||
Object body = content.get("body");
|
||||
return body != null ? body.toString() : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.sino.mci.inbound.engine;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.sino.mci.admin.entity.Tenant;
|
||||
import com.sino.mci.admin.repository.TenantMapper;
|
||||
import com.sino.mci.inbound.entity.MessageEvent;
|
||||
import com.sino.mci.shared.util.JsonUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestClientException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class WebhookPushService {
|
||||
|
||||
private final TenantMapper tenantMapper;
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
public void pushToBusiness(String tenantCode, MessageEvent event, IntentRecognitionService.IntentResult intent) {
|
||||
Tenant tenant = tenantMapper.selectOne(
|
||||
new LambdaQueryWrapper<Tenant>().eq(Tenant::getTenantCode, tenantCode));
|
||||
if (tenant == null) {
|
||||
log.warn("Tenant not found for webhook push: {}", tenantCode);
|
||||
return;
|
||||
}
|
||||
String webhookUrl = tenant.getInboundWebhookUrl();
|
||||
if (webhookUrl == null || webhookUrl.isBlank()) {
|
||||
log.warn("Tenant {} has no inbound webhook URL configured", tenantCode);
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, Object> payload = new HashMap<>();
|
||||
payload.put("eventId", event.getEventId());
|
||||
payload.put("tenantCode", tenantCode);
|
||||
payload.put("channelType", event.getChannelType());
|
||||
payload.put("conversationId", event.getConversationId());
|
||||
payload.put("personId", event.getPersonId());
|
||||
payload.put("accountId", event.getAccountId());
|
||||
payload.put("content", event.getContent());
|
||||
payload.put("intent", intent.intent());
|
||||
payload.put("confidence", intent.confidence());
|
||||
payload.put("reason", intent.reason());
|
||||
|
||||
try {
|
||||
ResponseEntity<String> response = restTemplate.postForEntity(webhookUrl, payload, String.class);
|
||||
log.info("Webhook pushed to {}, status={}", webhookUrl, response.getStatusCode());
|
||||
} catch (RestClientException e) {
|
||||
log.warn("Webhook push failed: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.sino.mci.inbound.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_event")
|
||||
public class MessageEvent extends BaseEntity {
|
||||
|
||||
private String eventId;
|
||||
private String tenantCode;
|
||||
private String channelType;
|
||||
private Long channelSubjectId;
|
||||
private Integer direction;
|
||||
private String eventType;
|
||||
private String messageSource;
|
||||
private Long conversationId;
|
||||
private Long personId;
|
||||
private Long accountId;
|
||||
private String content;
|
||||
private String rawPayload;
|
||||
private String businessContext;
|
||||
private Integer isProcessed;
|
||||
private Integer status;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.sino.mci.inbound.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.sino.mci.inbound.entity.MessageEvent;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface MessageEventMapper extends BaseMapper<MessageEvent> {
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.sino.mci.inbound.service;
|
||||
|
||||
import com.sino.mci.inbound.dto.ReceiveMessageRequest;
|
||||
import com.sino.mci.inbound.entity.MessageEvent;
|
||||
import com.sino.mci.inbound.engine.FlowEngine;
|
||||
import com.sino.mci.inbound.repository.MessageEventMapper;
|
||||
import com.sino.mci.shared.util.JsonUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class InboundMessageService {
|
||||
|
||||
private final MessageEventMapper messageEventMapper;
|
||||
private final FlowEngine flowEngine;
|
||||
|
||||
@Transactional
|
||||
public MessageEvent receiveMessage(String tenantCode, ReceiveMessageRequest request) {
|
||||
MessageEvent event = new MessageEvent();
|
||||
event.setEventId(request.getEventId());
|
||||
event.setTenantCode(tenantCode);
|
||||
event.setChannelType(request.getChannelType());
|
||||
event.setChannelSubjectId(request.getChannelSubjectId());
|
||||
event.setDirection(1);
|
||||
event.setEventType(request.getEventType());
|
||||
event.setMessageSource(request.getMessageSource());
|
||||
event.setConversationId(request.getConversationId());
|
||||
event.setPersonId(request.getPersonId());
|
||||
event.setAccountId(request.getAccountId());
|
||||
event.setContent(JsonUtils.toJson(request.getContent()));
|
||||
event.setRawPayload(JsonUtils.toJson(request.getRawPayload()));
|
||||
event.setBusinessContext(JsonUtils.toJson(request.getBusinessContext()));
|
||||
event.setIsProcessed(0);
|
||||
event.setStatus(1);
|
||||
messageEventMapper.insert(event);
|
||||
log.info("Inbound message received: tenant={}, eventId={}, conversationId={}",
|
||||
tenantCode, request.getEventId(), request.getConversationId());
|
||||
|
||||
flowEngine.executeInboundFlow(event.getId());
|
||||
return messageEventMapper.selectById(event.getId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
CREATE TABLE IF NOT EXISTS mci_flow_definition (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
tenant_code VARCHAR(64) NOT NULL COMMENT '所属租户',
|
||||
flow_code VARCHAR(64) NOT NULL COMMENT '流程编码',
|
||||
flow_name VARCHAR(128) NOT NULL COMMENT '流程名称',
|
||||
flow_type VARCHAR(32) NOT NULL COMMENT '流程类型:inbound / ivr',
|
||||
definition_json JSON NOT NULL COMMENT '流程定义 JSON',
|
||||
status TINYINT NOT NULL DEFAULT 0 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_flow_code (tenant_code, flow_code, deleted)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='流程定义表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS mci_message_event (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
event_id VARCHAR(128) NOT NULL COMMENT '事件唯一 ID',
|
||||
tenant_code VARCHAR(64) NOT NULL COMMENT '所属租户/业务系统',
|
||||
channel_type VARCHAR(32) NOT NULL COMMENT '渠道',
|
||||
channel_subject_id BIGINT COMMENT '渠道主体',
|
||||
direction TINYINT NOT NULL DEFAULT 1 COMMENT '方向:1入站 2出站',
|
||||
event_type VARCHAR(32) NOT NULL COMMENT '事件类型:message / callback / status / keypress',
|
||||
message_source VARCHAR(32) NOT NULL COMMENT '来源:realtime / session_archive',
|
||||
conversation_id BIGINT COMMENT '会话 ID',
|
||||
person_id BIGINT COMMENT '联系人 ID',
|
||||
account_id BIGINT COMMENT '发送/接收账号',
|
||||
content JSON COMMENT '内容:{type, body}',
|
||||
raw_payload JSON COMMENT '渠道原始报文',
|
||||
business_context JSON COMMENT '业务上下文',
|
||||
is_processed TINYINT NOT NULL DEFAULT 0 COMMENT '是否已处理:0否 1是',
|
||||
status TINYINT NOT NULL DEFAULT 1 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_event_id (event_id),
|
||||
KEY idx_tenant_source_time (tenant_code, message_source, create_time),
|
||||
KEY idx_conversation_time (conversation_id, create_time),
|
||||
KEY idx_person_time (person_id, create_time),
|
||||
KEY idx_account_time (account_id, create_time)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息事件表';
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.sino.mci.inbound.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 InboundFlowControllerTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Test
|
||||
void shouldCreateFlowAndReceiveMessage() throws Exception {
|
||||
String tenantCode = "flow-" + UUID.randomUUID().toString().substring(0, 8);
|
||||
String tenantPayload = String.format("{\"tenantCode\":\"%s\",\"tenantName\":\"流程测试\",\"inboundWebhookUrl\":\"http://localhost:9999/webhook\"}", 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 flowPayload = "{\"flowCode\":\"inbound_default\",\"flowName\":\"默认入站流程\",\"flowType\":\"inbound\",\"definitionJson\":{\"nodes\":[{\"id\":\"receive\",\"type\":\"receive\"},{\"id\":\"intent\",\"type\":\"intent\"},{\"id\":\"webhook\",\"type\":\"webhook\"}]}}";
|
||||
mockMvc.perform(post("/msg-platform/api/v1/inbound/flow")
|
||||
.contextPath("/msg-platform")
|
||||
.header("X-Tenant-Code", tenantCode)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(flowPayload))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0));
|
||||
|
||||
mockMvc.perform(post("/msg-platform/api/v1/inbound/flow/inbound_default/publish")
|
||||
.contextPath("/msg-platform")
|
||||
.header("X-Tenant-Code", tenantCode))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0));
|
||||
|
||||
mockMvc.perform(get("/msg-platform/api/v1/inbound/flow/inbound_default")
|
||||
.contextPath("/msg-platform")
|
||||
.header("X-Tenant-Code", tenantCode))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.status").value(1));
|
||||
|
||||
String eventId = "evt-" + UUID.randomUUID().toString().substring(0, 8);
|
||||
String messagePayload = String.format(
|
||||
"{\"eventId\":\"%s\",\"channelType\":\"wecom\",\"eventType\":\"message\",\"messageSource\":\"realtime\",\"conversationId\":1,\"personId\":1,\"content\":{\"type\":\"text\",\"body\":\"我需要救援\"}}",
|
||||
eventId);
|
||||
mockMvc.perform(post("/msg-platform/api/v1/inbound/receive")
|
||||
.contextPath("/msg-platform")
|
||||
.header("X-Tenant-Code", tenantCode)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(messagePayload))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.eventId").value(eventId));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user