feat(channel): add channel-common SPI, channel tables, subject/account APIs and WeCom callback placeholder
This commit is contained in:
22
backend/mci-channel-common/pom.xml
Normal file
22
backend/mci-channel-common/pom.xml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>com.sino</groupId>
|
||||||
|
<artifactId>sino-mci-parent</artifactId>
|
||||||
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
<artifactId>mci-channel-common</artifactId>
|
||||||
|
<name>mci-channel-common</name>
|
||||||
|
<description>Channel adapter SPI and shared models</description>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.sino.mci.channel.common.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class CallbackPayload {
|
||||||
|
|
||||||
|
private String channelType;
|
||||||
|
private String eventType;
|
||||||
|
private String rawBody;
|
||||||
|
private Map<String, Object> normalized;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.sino.mci.channel.common.dto;
|
||||||
|
|
||||||
|
import com.sino.mci.channel.common.model.AccountType;
|
||||||
|
import com.sino.mci.channel.common.model.ChannelType;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class ChannelInitRequest {
|
||||||
|
|
||||||
|
private ChannelType channelType;
|
||||||
|
private AccountType accountType;
|
||||||
|
private Long subjectId;
|
||||||
|
private Map<String, Object> config;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.sino.mci.channel.common.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class ChannelInitResult {
|
||||||
|
|
||||||
|
private boolean success;
|
||||||
|
private String accountId;
|
||||||
|
private String accountName;
|
||||||
|
private String qrCodeUrl;
|
||||||
|
private String message;
|
||||||
|
private Map<String, Object> extra;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.sino.mci.channel.common.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class SendMessageRequest {
|
||||||
|
|
||||||
|
private String conversationId;
|
||||||
|
private String receiverId;
|
||||||
|
private String contentType;
|
||||||
|
private Map<String, Object> content;
|
||||||
|
private Map<String, Object> extra;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.sino.mci.channel.common.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class SendMessageResult {
|
||||||
|
|
||||||
|
private boolean success;
|
||||||
|
private String channelMessageId;
|
||||||
|
private String message;
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.sino.mci.channel.common.model;
|
||||||
|
|
||||||
|
public enum AccountType {
|
||||||
|
WECOM_INTERNAL(1, "企微内部账号"),
|
||||||
|
WECOM_AGENT(2, "企微应用"),
|
||||||
|
WECHAT_PERSONAL(3, "pywechat 个人号"),
|
||||||
|
DINGTALK_BOT(4, "钉钉机器人"),
|
||||||
|
FEISHU_BOT(5, "飞书机器人");
|
||||||
|
|
||||||
|
private final int code;
|
||||||
|
private final String label;
|
||||||
|
|
||||||
|
AccountType(int code, String label) {
|
||||||
|
this.code = code;
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLabel() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AccountType of(int code) {
|
||||||
|
for (AccountType type : values()) {
|
||||||
|
if (type.code == code) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.sino.mci.channel.common.model;
|
||||||
|
|
||||||
|
public enum ChannelType {
|
||||||
|
WECOM,
|
||||||
|
WECHAT_PERSONAL,
|
||||||
|
DINGTALK,
|
||||||
|
FEISHU,
|
||||||
|
IVR
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.sino.mci.channel.common.spi;
|
||||||
|
|
||||||
|
import com.sino.mci.channel.common.dto.CallbackPayload;
|
||||||
|
import com.sino.mci.channel.common.dto.ChannelInitRequest;
|
||||||
|
import com.sino.mci.channel.common.dto.ChannelInitResult;
|
||||||
|
import com.sino.mci.channel.common.dto.SendMessageRequest;
|
||||||
|
import com.sino.mci.channel.common.dto.SendMessageResult;
|
||||||
|
import com.sino.mci.channel.common.model.ChannelType;
|
||||||
|
|
||||||
|
public interface ChannelAdapter {
|
||||||
|
|
||||||
|
ChannelType getChannelType();
|
||||||
|
|
||||||
|
ChannelInitResult initAccount(ChannelInitRequest request);
|
||||||
|
|
||||||
|
SendMessageResult sendMessage(SendMessageRequest request);
|
||||||
|
|
||||||
|
CallbackPayload parseCallback(String rawBody);
|
||||||
|
}
|
||||||
@@ -16,6 +16,10 @@
|
|||||||
<groupId>com.sino</groupId>
|
<groupId>com.sino</groupId>
|
||||||
<artifactId>mci-shared</artifactId>
|
<artifactId>mci-shared</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.sino</groupId>
|
||||||
|
<artifactId>mci-channel-common</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.sino.mci.channel.controller;
|
||||||
|
|
||||||
|
import com.sino.mci.channel.dto.ChannelAccountInitRequest;
|
||||||
|
import com.sino.mci.channel.dto.CreateChannelAccountRequest;
|
||||||
|
import com.sino.mci.channel.service.ChannelAccountService;
|
||||||
|
import com.sino.mci.shared.web.ApiResponse;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/channel-account")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ChannelAccountController {
|
||||||
|
|
||||||
|
private final ChannelAccountService accountService;
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ApiResponse<?> createAccount(
|
||||||
|
@RequestHeader("X-Tenant-Code") String tenantCode,
|
||||||
|
@Valid @RequestBody CreateChannelAccountRequest request) {
|
||||||
|
return ApiResponse.ok(accountService.createAccount(tenantCode, request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/init")
|
||||||
|
public ApiResponse<?> initAccount(
|
||||||
|
@RequestHeader("X-Tenant-Code") String tenantCode,
|
||||||
|
@Valid @RequestBody ChannelAccountInitRequest request) {
|
||||||
|
return ApiResponse.ok(accountService.initAccount(tenantCode, request));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.sino.mci.channel.controller;
|
||||||
|
|
||||||
|
import com.sino.mci.channel.dto.CreateChannelSubjectRequest;
|
||||||
|
import com.sino.mci.channel.service.ChannelSubjectService;
|
||||||
|
import com.sino.mci.shared.web.ApiResponse;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/channel-subject")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ChannelSubjectController {
|
||||||
|
|
||||||
|
private final ChannelSubjectService subjectService;
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ApiResponse<?> createSubject(
|
||||||
|
@RequestHeader("X-Tenant-Code") String tenantCode,
|
||||||
|
@Valid @RequestBody CreateChannelSubjectRequest request) {
|
||||||
|
return ApiResponse.ok(subjectService.createSubject(tenantCode, request));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.sino.mci.channel.controller;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.Enumeration;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/callback/wecom")
|
||||||
|
public class WeComCallbackController {
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public String verifyUrl(HttpServletRequest request) {
|
||||||
|
log.info("WeCom callback URL verification: {}", request.getQueryString());
|
||||||
|
return request.getParameter("echostr") != null ? request.getParameter("echostr") : "success";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public String receiveCallback(@RequestBody String body, HttpServletRequest request) {
|
||||||
|
log.info("WeCom callback received, body length={}, query={}", body.length(), request.getQueryString());
|
||||||
|
log.debug("WeCom callback body: {}", body);
|
||||||
|
Enumeration<String> headerNames = request.getHeaderNames();
|
||||||
|
while (headerNames.hasMoreElements()) {
|
||||||
|
String name = headerNames.nextElement();
|
||||||
|
log.debug("Header {}: {}", name, request.getHeader(name));
|
||||||
|
}
|
||||||
|
// Phase 2: only accept and acknowledge; event parsing and session archive pull will be implemented later
|
||||||
|
return "success";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
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 ChannelAccountInitRequest {
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
private String channelType;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Integer accountType;
|
||||||
|
|
||||||
|
private Long subjectId;
|
||||||
|
|
||||||
|
private Map<String, Object> config;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.sino.mci.channel.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class CreateChannelAccountRequest {
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Integer accountType;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
private String channelType;
|
||||||
|
|
||||||
|
private Long subjectId;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
private String accountId;
|
||||||
|
|
||||||
|
private String accountName;
|
||||||
|
private Integer sessionArchiveEnabled;
|
||||||
|
private String serverHost;
|
||||||
|
private String extInfo;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.sino.mci.channel.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class CreateChannelSubjectRequest {
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
private String channelType;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
private String subjectCode;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
private String subjectName;
|
||||||
|
|
||||||
|
private String corpId;
|
||||||
|
private String corpSecret;
|
||||||
|
private String agentId;
|
||||||
|
private String agentSecret;
|
||||||
|
private Integer sessionArchiveEnabled;
|
||||||
|
private String sessionArchivePrivateKey;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
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_channel_account")
|
||||||
|
public class ChannelAccount extends BaseEntity {
|
||||||
|
|
||||||
|
private String tenantCode;
|
||||||
|
private Integer accountType;
|
||||||
|
private String channelType;
|
||||||
|
private Long subjectId;
|
||||||
|
private String accountId;
|
||||||
|
private String accountName;
|
||||||
|
private Integer sessionArchiveEnabled;
|
||||||
|
private String serverHost;
|
||||||
|
private Integer loginStatus;
|
||||||
|
private Integer onlineStatus;
|
||||||
|
private Integer riskLevel;
|
||||||
|
private String extInfo;
|
||||||
|
private Integer status;
|
||||||
|
private LocalDateTime lastHeartbeat;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
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_subject")
|
||||||
|
public class ChannelSubject extends BaseEntity {
|
||||||
|
|
||||||
|
private String tenantCode;
|
||||||
|
private String channelType;
|
||||||
|
private String subjectCode;
|
||||||
|
private String subjectName;
|
||||||
|
private String corpId;
|
||||||
|
private String corpSecret;
|
||||||
|
private String agentId;
|
||||||
|
private String agentSecret;
|
||||||
|
private Integer sessionArchiveEnabled;
|
||||||
|
private String sessionArchivePrivateKey;
|
||||||
|
private Integer status;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.sino.mci.channel.repository;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.sino.mci.channel.entity.ChannelAccount;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface ChannelAccountMapper extends BaseMapper<ChannelAccount> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.sino.mci.channel.repository;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.sino.mci.channel.entity.ChannelSubject;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface ChannelSubjectMapper extends BaseMapper<ChannelSubject> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package com.sino.mci.channel.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.sino.mci.channel.dto.ChannelAccountInitRequest;
|
||||||
|
import com.sino.mci.channel.dto.CreateChannelAccountRequest;
|
||||||
|
import com.sino.mci.channel.entity.ChannelAccount;
|
||||||
|
import com.sino.mci.channel.entity.ChannelSubject;
|
||||||
|
import com.sino.mci.channel.repository.ChannelAccountMapper;
|
||||||
|
import com.sino.mci.channel.repository.ChannelSubjectMapper;
|
||||||
|
import com.sino.mci.exception.BusinessException;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ChannelAccountService {
|
||||||
|
|
||||||
|
private final ChannelAccountMapper accountMapper;
|
||||||
|
private final ChannelSubjectMapper subjectMapper;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public ChannelAccount createAccount(String tenantCode, CreateChannelAccountRequest request) {
|
||||||
|
if (request.getSubjectId() != null) {
|
||||||
|
ChannelSubject subject = subjectMapper.selectById(request.getSubjectId());
|
||||||
|
if (subject == null) {
|
||||||
|
throw new BusinessException("渠道主体不存在");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ChannelAccount existing = accountMapper.selectOne(
|
||||||
|
new LambdaQueryWrapper<ChannelAccount>()
|
||||||
|
.eq(ChannelAccount::getTenantCode, tenantCode)
|
||||||
|
.eq(ChannelAccount::getChannelType, request.getChannelType())
|
||||||
|
.eq(ChannelAccount::getAccountId, request.getAccountId()));
|
||||||
|
if (existing != null) {
|
||||||
|
throw new BusinessException("渠道账号已存在: " + request.getAccountId());
|
||||||
|
}
|
||||||
|
|
||||||
|
ChannelAccount account = new ChannelAccount();
|
||||||
|
account.setTenantCode(tenantCode);
|
||||||
|
account.setAccountType(request.getAccountType());
|
||||||
|
account.setChannelType(request.getChannelType());
|
||||||
|
account.setSubjectId(request.getSubjectId());
|
||||||
|
account.setAccountId(request.getAccountId());
|
||||||
|
account.setAccountName(request.getAccountName());
|
||||||
|
account.setSessionArchiveEnabled(request.getSessionArchiveEnabled() != null ? request.getSessionArchiveEnabled() : 0);
|
||||||
|
account.setServerHost(request.getServerHost());
|
||||||
|
account.setExtInfo(request.getExtInfo());
|
||||||
|
account.setLoginStatus(0);
|
||||||
|
account.setOnlineStatus(0);
|
||||||
|
account.setRiskLevel(0);
|
||||||
|
account.setStatus(1);
|
||||||
|
accountMapper.insert(account);
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChannelAccount initAccount(String tenantCode, ChannelAccountInitRequest request) {
|
||||||
|
// Phase 2 placeholder: actual adapter dispatch will be implemented later
|
||||||
|
throw new BusinessException("渠道账号初始化适配器尚未实现: " + request.getChannelType());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.sino.mci.channel.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.sino.mci.channel.dto.CreateChannelSubjectRequest;
|
||||||
|
import com.sino.mci.channel.entity.ChannelSubject;
|
||||||
|
import com.sino.mci.channel.repository.ChannelSubjectMapper;
|
||||||
|
import com.sino.mci.exception.BusinessException;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ChannelSubjectService {
|
||||||
|
|
||||||
|
private final ChannelSubjectMapper subjectMapper;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public ChannelSubject createSubject(String tenantCode, CreateChannelSubjectRequest request) {
|
||||||
|
ChannelSubject existing = subjectMapper.selectOne(
|
||||||
|
new LambdaQueryWrapper<ChannelSubject>()
|
||||||
|
.eq(ChannelSubject::getTenantCode, tenantCode)
|
||||||
|
.eq(ChannelSubject::getChannelType, request.getChannelType())
|
||||||
|
.eq(ChannelSubject::getSubjectCode, request.getSubjectCode()));
|
||||||
|
if (existing != null) {
|
||||||
|
throw new BusinessException("渠道主体已存在: " + request.getSubjectCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
ChannelSubject subject = new ChannelSubject();
|
||||||
|
subject.setTenantCode(tenantCode);
|
||||||
|
subject.setChannelType(request.getChannelType());
|
||||||
|
subject.setSubjectCode(request.getSubjectCode());
|
||||||
|
subject.setSubjectName(request.getSubjectName());
|
||||||
|
subject.setCorpId(request.getCorpId());
|
||||||
|
subject.setCorpSecret(request.getCorpSecret());
|
||||||
|
subject.setAgentId(request.getAgentId());
|
||||||
|
subject.setAgentSecret(request.getAgentSecret());
|
||||||
|
subject.setSessionArchiveEnabled(request.getSessionArchiveEnabled() != null ? request.getSessionArchiveEnabled() : 0);
|
||||||
|
subject.setSessionArchivePrivateKey(request.getSessionArchivePrivateKey());
|
||||||
|
subject.setStatus(1);
|
||||||
|
subjectMapper.insert(subject);
|
||||||
|
return subject;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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")
|
@MapperScan({"com.sino.mci.admin.repository", "com.sino.mci.channel.repository"})
|
||||||
public class MybatisPlusConfig {
|
public class MybatisPlusConfig {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS mci_channel_subject (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
tenant_code VARCHAR(64) COMMENT '所属租户,为空表示平台级共享主体',
|
||||||
|
channel_type VARCHAR(32) NOT NULL COMMENT '渠道类型:wecom / dingtalk / feishu',
|
||||||
|
subject_code VARCHAR(64) NOT NULL COMMENT '主体编码',
|
||||||
|
subject_name VARCHAR(128) NOT NULL COMMENT '主体名称',
|
||||||
|
corp_id VARCHAR(128) COMMENT '企微 corp_id(企微用)',
|
||||||
|
corp_secret VARCHAR(512) COMMENT '通讯录 secret(企微用)',
|
||||||
|
agent_id VARCHAR(64) COMMENT '应用 agent_id(企微用)',
|
||||||
|
agent_secret VARCHAR(512) COMMENT '应用 secret(企微用)',
|
||||||
|
session_archive_enabled TINYINT NOT NULL DEFAULT 0 COMMENT '是否开通会话存档能力:0否 1是',
|
||||||
|
session_archive_private_key TEXT COMMENT '会话存档 RSA 私钥',
|
||||||
|
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_subject (tenant_code, channel_type, subject_code, deleted)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='渠道主体表';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS mci_channel_account (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
tenant_code VARCHAR(64) NOT NULL COMMENT '所属租户',
|
||||||
|
account_type TINYINT NOT NULL COMMENT '1企微内部账号 2企微应用 3pywechat个人号 4钉钉机器人 5飞书机器人',
|
||||||
|
channel_type VARCHAR(32) NOT NULL COMMENT 'wecom / wechat_personal / dingtalk / feishu',
|
||||||
|
subject_id BIGINT COMMENT '所属渠道主体 mci_channel_subject',
|
||||||
|
account_id VARCHAR(128) COMMENT '账号唯一标识:企微员工=userid,企微应用=agent_id,个微=wxid/bot_code,机器人=robot_id',
|
||||||
|
account_name VARCHAR(128) COMMENT '账号名称',
|
||||||
|
session_archive_enabled TINYINT NOT NULL DEFAULT 0 COMMENT '是否开启会话存档(企微内部账号用)',
|
||||||
|
server_host VARCHAR(128) COMMENT '所在服务器(pywechat 用)',
|
||||||
|
login_status TINYINT NOT NULL DEFAULT 0 COMMENT '登录状态:0未登录 1登录中 2已登录',
|
||||||
|
online_status TINYINT NOT NULL DEFAULT 0 COMMENT '在线状态:0离线 1在线',
|
||||||
|
risk_level TINYINT NOT NULL DEFAULT 0 COMMENT '风险等级',
|
||||||
|
ext_info JSON COMMENT '渠道扩展字段',
|
||||||
|
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0禁用 1启用',
|
||||||
|
last_heartbeat 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,
|
||||||
|
UNIQUE KEY uk_tenant_channel_account (tenant_code, channel_type, account_id, deleted),
|
||||||
|
KEY idx_subject_id (subject_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='渠道账号表';
|
||||||
|
|
||||||
|
ALTER TABLE mci_channel_account
|
||||||
|
ADD CONSTRAINT fk_channel_account_subject_id FOREIGN KEY (subject_id) REFERENCES mci_channel_subject (id);
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package com.sino.mci.channel.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 ChannelControllerTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MockMvc mockMvc;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCreateChannelSubjectAndAccount() throws Exception {
|
||||||
|
String tenantCode = "channel-" + UUID.randomUUID().toString().substring(0, 8);
|
||||||
|
String tenantPayload = String.format("{\"tenantCode\":\"%s\",\"tenantName\":\"渠道测试\"}", 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 subjectPayload = "{\"channelType\":\"wecom\",\"subjectCode\":\"sino\",\"subjectName\":\"Sino\"}";
|
||||||
|
mockMvc.perform(post("/msg-platform/api/v1/channel-subject")
|
||||||
|
.contextPath("/msg-platform")
|
||||||
|
.header("X-Tenant-Code", tenantCode)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(subjectPayload))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(0))
|
||||||
|
.andExpect(jsonPath("$.data.channelType").value("wecom"));
|
||||||
|
|
||||||
|
String accountPayload = "{\"accountType\":1,\"channelType\":\"wecom\",\"accountId\":\"ZhangSan\",\"accountName\":\"张三\"}";
|
||||||
|
mockMvc.perform(post("/msg-platform/api/v1/channel-account")
|
||||||
|
.contextPath("/msg-platform")
|
||||||
|
.header("X-Tenant-Code", tenantCode)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(accountPayload))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(0))
|
||||||
|
.andExpect(jsonPath("$.data.accountId").value("ZhangSan"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnEchoStrForWeComCallbackVerification() throws Exception {
|
||||||
|
mockMvc.perform(get("/msg-platform/callback/wecom")
|
||||||
|
.contextPath("/msg-platform")
|
||||||
|
.param("echostr", "hello"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(result -> result.getResponse().getContentAsString().equals("hello"));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user