REQ-2010: 添加撤回功能

This commit is contained in:
yanglin 2024-02-26 20:48:19 +08:00
parent 02ba772644
commit cb1b72f4c1
13 changed files with 545 additions and 1 deletions

View File

@ -0,0 +1,25 @@
package cn.axzo.im.channel.netease.client;
import feign.codec.Encoder;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.openfeign.support.SpringEncoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author yanglin
*/
@Configuration
@RequiredArgsConstructor
public class FormConfig {
private final ObjectFactory<HttpMessageConverters> messageConverters;
@Bean
public Encoder feignFormEncoder() {
return new FormEncoder(new SpringEncoder(messageConverters));
}
}

View File

@ -0,0 +1,48 @@
package cn.axzo.im.channel.netease.client;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import feign.RequestTemplate;
import feign.codec.EncodeException;
import feign.codec.Encoder;
import feign.form.spring.SpringFormEncoder;
import org.springframework.core.annotation.AnnotationUtils;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;
/**
* @author yanglin
*/
public class FormEncoder extends SpringFormEncoder {
private final Encoder delegate;
public FormEncoder(Encoder delegate) {
super(delegate);
this.delegate = delegate;
}
@Override
public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {
if (object == null) {
delegate.encode(null, bodyType, template);
return;
}
FormRequest formRequest = AnnotationUtils.getAnnotation(object.getClass(), FormRequest.class);
if (formRequest == null) {
delegate.encode(object, bodyType, template);
return;
}
template.header("Content-Type", "application/x-www-form-urlencoded");
// 为了使用@JSONField
JSONObject jsonReq = JSON.parseObject(JSON.toJSONString(object), JSONObject.class);
Map<String, Object> params = new HashMap<>();
for (String key : jsonReq.keySet()) {
params.put(key, jsonReq.getString(key));
}
super.encode(params, Encoder.MAP_STRING_WILDCARD, template);
}
}

View File

@ -0,0 +1,14 @@
package cn.axzo.im.channel.netease.client;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author yanglin
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface FormRequest {
}

View File

@ -0,0 +1,38 @@
package cn.axzo.im.channel.netease.client;
import cn.axzo.im.channel.netease.dto.QueryEventRequest;
import cn.axzo.im.channel.netease.dto.QueryEventResponse;
import cn.axzo.im.channel.netease.dto.QueryMessageRequest;
import cn.axzo.im.channel.netease.dto.QueryMessageResponse;
import cn.axzo.im.channel.netease.dto.RevokeMessageRequest;
import lombok.Data;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import static cn.axzo.im.channel.netease.client.NimClient.URL;
/**
* @author yanglin
*/
@FeignClient(name = "NimClient", url = URL)
public interface NimClient {
String URL = "https://api.netease.im/nimserver";
@PostMapping(value = "/msg/delMsgOneWay.action")
CodeResponse revoke(RevokeMessageRequest request);
@PostMapping(value = "/history/queryUserEvents.action")
QueryEventResponse queryEvents(QueryEventRequest request);
@PostMapping(value = "/history/querySessionMsg.action")
QueryMessageResponse queryMessages(QueryMessageRequest request);
@Data
class CodeResponse {
private Integer code;
private String desc;
private int size;
}
}

View File

@ -0,0 +1,35 @@
package cn.axzo.im.channel.netease.client;
import cn.axzo.im.channel.netease.AppKeyUtil;
import cn.axzo.im.channel.netease.CheckSumUtil;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.UUID;
/**
* @author yanglin
*/
@Component
@RequiredArgsConstructor
public class NimRequestInterceptor implements RequestInterceptor {
private final AppKeyUtil appKeyUtil;
@Override
public void apply(RequestTemplate template) {
String url = template.feignTarget().url();
if (!url.startsWith(NimClient.URL))
return;
String nonce = UUID.randomUUID().toString();
String curTime = String.valueOf(System.currentTimeMillis() / 1000);
String checkSum = CheckSumUtil.getCheckSum(appKeyUtil.getAppSecret(), nonce, curTime);
template.header("AppKey", appKeyUtil.getAppKey());
template.header("Nonce", nonce);
template.header("CurTime", curTime);
template.header("CheckSum", checkSum);
template.header("Content-Type", "application/x-www-form-urlencoded;charset=utf-8");
}
}

View File

@ -0,0 +1,52 @@
package cn.axzo.im.channel.netease.dto;
import cn.axzo.framework.domain.ServiceException;
import cn.axzo.im.channel.netease.client.FormRequest;
import com.alibaba.fastjson.annotation.JSONField;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import java.text.ParseException;
import java.text.SimpleDateFormat;
/**
* @author yanglin
*/
@Data
@FormRequest
public class QueryEventRequest {
// 要查询用户的accid
@JSONField(name = "accid")
@NotBlank
private String accountId;
// 开始时间毫秒级
@JSONField(name = "begintime")
@NotBlank
private String beginTime;
// 截止时间毫秒级
@JSONField(name = "endtime")
@NotBlank
private String endTime;
// 本次查询的记录数量上限(最多100条),小于等于0或者大于100会提示参数错误
private int limit;
// 1按时间正序排列2按时间降序排列其它返回参数414错误默认是按降序排列
private int reverse = 2;
public void setBeginTime(String beginTime) {
this.beginTime = parseDate(beginTime);
}
public void setEndTime(String endTime) {
this.endTime = parseDate(endTime);
}
private static String parseDate(String input) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
try {
long time = sdf.parse(input).getTime();
return String.valueOf(time);
} catch (ParseException e) {
throw new ServiceException(e);
}
}
}

View File

@ -0,0 +1,51 @@
package cn.axzo.im.channel.netease.dto;
import cn.axzo.im.channel.netease.client.NimClient;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
/**
* @author yanglin
*/
@Setter
@Getter
public class QueryEventResponse extends NimClient.CodeResponse {
// 总共记录数
private int size;
private List<EventItem> events;
@Setter
@Getter
@SuppressWarnings("LombokSetterMayBeUsed")
public static class EventItem {
// //用户accid
private String accid;
// 发生时间ms
private Integer timestamp;
// //2表示登录3表示登出
private int eventType;
// 用户clientip
private String clientIp;
// sdk 版本
private int sdkVersion;
// //终端
private String clientType;
// 设备ID可选字段
private String deviceId;
// 登录时设置的自定义tag可选字段
private String customTag;
// 登录成功状态200表示成功
private int code;
public void setTimestamp(Integer timestamp) {
this.timestamp = timestamp;
}
public void setEventType(int eventType) {
this.eventType = eventType;
}
}
}

View File

@ -0,0 +1,61 @@
package cn.axzo.im.channel.netease.dto;
import cn.axzo.framework.domain.ServiceException;
import cn.axzo.im.channel.netease.client.FormRequest;
import com.alibaba.fastjson.annotation.JSONField;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import java.text.ParseException;
import java.text.SimpleDateFormat;
/**
* @author yanglin
*/
@Data
@FormRequest
public class QueryMessageRequest {
// 发送者accid
@NotBlank
private String from;
// 接收者accid
@NotBlank
private String to;
// 开始时间毫秒级
@NotBlank
@JSONField(name = "begintime")
private String beginTime;
// 截止时间毫秒级
@NotBlank
@JSONField(name = "endtime")
private String endTime;
// 本次查询的消息条数上限(最多100条),小于等于0或者大于100会提示参数错误
private int limit;
// 1按时间正序排列2按时间降序排列其它返回参数414错误.默认是按降序排列即时间戳最晚的消息排在最前面
private int reverse = 2;
//查询指定的多个消息类型类型之间用","分割不设置该参数则查询全部类型消息格式示例 0,1,2,3
//类型支持0:文本1:图片2:语音3:视频4:地理位置5:通知6:文件10:提示11:Robot100:自定义
private String type = "0,1,6,100";
// 查询结果中是否需要包含无感知消息true包含false不包含默认为 false
private String includeNoSenseMsg = "true";
// 结束查询的最后一条消息的 msgid不包含在查询结果中用于定位锚点
private String excludeMsgid;
public void setBeginTime(String beginTime) {
this.beginTime = parseDate(beginTime);
}
public void setEndTime(String endTime) {
this.endTime = parseDate(endTime);
}
private static String parseDate(String input) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
try {
long time = sdf.parse(input).getTime();
return String.valueOf(time);
} catch (ParseException e) {
throw new ServiceException(e);
}
}
}

View File

@ -0,0 +1,19 @@
package cn.axzo.im.channel.netease.dto;
import cn.axzo.im.channel.netease.client.NimClient;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
import java.util.Map;
/**
* @author yanglin
*/
@Setter
@Getter
public class QueryMessageResponse extends NimClient.CodeResponse {
// 消息集合
private List<Map<String, ?>> msgs;
}

View File

@ -0,0 +1,21 @@
package cn.axzo.im.channel.netease.dto;
import cn.axzo.im.channel.netease.client.FormRequest;
import com.alibaba.fastjson.annotation.JSONField;
import lombok.Data;
/**
* @author yanglin
*/
@Data
@FormRequest
public class RevokeMessageRequest {
@JSONField(name = "deleteMsgid")
private String messageId;
// 消息发送者的云信 IM 账号accid
private String from;
// 消息接收方如果待撤回消息为单聊消息则需传入消息接收者的云信 IM 账号accid如果是群消息则需传入对应群的ID tid
private String to;
// 待撤回消息的类型只能传入 13 1413 表示点对点消息撤回14 表示群消息撤回传入其他值均判定为参数错误
private int type = 13;
}

View File

@ -0,0 +1,38 @@
package cn.axzo.im.controller;
import cn.axzo.im.channel.netease.client.NimClient;
import cn.axzo.im.channel.netease.dto.QueryEventRequest;
import cn.axzo.im.channel.netease.dto.QueryMessageRequest;
import cn.axzo.im.channel.netease.dto.RevokeMessageRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
/**
* @author yanglin
*/
@RestController
@RequiredArgsConstructor
public class PrivateController {
private final NimClient nimClient;
@PostMapping("/private/revoke")
public Object revoke(@Valid @RequestBody RevokeMessageRequest request) {
return nimClient.revoke(request);
}
@PostMapping("/private/queryEvents")
public Object queryEvents(@Valid @RequestBody QueryEventRequest request) {
return nimClient.queryEvents(request);
}
@PostMapping("/private/queryMessages")
public Object queryMessages(@Valid @RequestBody QueryMessageRequest request) {
return nimClient.queryMessages(request);
}
}

View File

@ -0,0 +1,142 @@
package cn.axzo.im.utils;
import cn.axzo.basics.common.exception.ServiceException;
import cn.axzo.basics.common.util.AssertUtil;
import cn.axzo.framework.domain.web.result.ApiListResult;
import cn.axzo.framework.domain.web.result.ApiResult;
import cn.azxo.framework.common.model.CommonResponse;
import cn.hutool.http.HttpStatus;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.helpers.MessageFormatter;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
/**
* @author yanglin
*/
@Slf4j
public class BizAssertions {
/**
* 断言为NULL
*/
public static void assertNull(Object actual, String message, Object... args) {
AssertUtil.isNull(actual, MessageFormatter.arrayFormat(message, args).getMessage());
}
/**
* 断言不为NULL
*/
public static void assertNotNull(Object actual, String message, Object... args) {
AssertUtil.notNull(actual, MessageFormatter.arrayFormat(message, args).getMessage());
}
/**
* 断言集合不为空
*/
public static void assertNotEmpty(Collection<?> actual, String message, Object... args) {
AssertUtil.notEmpty(actual, MessageFormatter.arrayFormat(message, args).getMessage());
}
/**
* 断言数组不为空
*/
public static <T> void assertNotEmpty(T[] actual, String message, Object... args) {
AssertUtil.notEmpty(actual, MessageFormatter.arrayFormat(message, args).getMessage());
}
/**
* 断言集合为空
*/
public static void assertEmpty(Collection<?> actual, String message, Object... args) {
AssertUtil.isEmpty(actual, MessageFormatter.arrayFormat(message, args).getMessage());
}
/**
* 断言值为真
*/
public static void assertTrue(boolean actual, String message, Object... args) {
AssertUtil.isTrue(actual, MessageFormatter.arrayFormat(message, args).getMessage());
}
/**
* 断言值不为真
*/
public static void assertFalse(boolean actual, String message, Object... args) {
AssertUtil.isFalse(actual, MessageFormatter.arrayFormat(message, args).getMessage());
}
/**
* 断言值2个值是否equals
*/
public static void assertEquals(Object expected, Object actual, String message, Object... args) {
if (!Objects.equals(expected, actual)) {
throw new ServiceException(MessageFormatter.arrayFormat(message, args).getMessage());
}
}
/**
* 断言值2个值是否不equals
*/
public static void assertNotEquals(Object expected, Object actual, String message, Object... args) {
if (Objects.equals(expected, actual)) {
throw new ServiceException(MessageFormatter.arrayFormat(message, args).getMessage());
}
}
public static <T> T assertResponse(CommonResponse<T> response) {
return assertResponse(response, "error resp={}", JSON.toJSONString(response));
}
public static <T> T assertResponse(CommonResponse<T> response, String message, Object... args) {
if (response.getCode() != HttpStatus.HTTP_OK) {
String finalMsg = MessageFormatter.arrayFormat(message, args).getMessage();
if (StringUtils.isNotBlank(response.getMsg())) {
finalMsg += ": " + response.getMsg();
}
ServiceException e = new ServiceException(finalMsg);
log.warn("remote call response with error. response={}", JSON.toJSONString(response), e);
throw e;
}
return response.getData();
}
public static <T> T assertResponse(ApiResult<T> response) {
return assertResponse(response, "error resp={}", JSON.toJSONString(response));
}
public static <T> T assertResponse(ApiResult<T> response, String message, Object... args) {
if (!response.isSuccess()) {
String finalMsg = MessageFormatter.arrayFormat(message, args).getMessage();
if (StringUtils.isNotBlank(response.getMsg())) {
finalMsg += ": " + response.getMsg();
}
ServiceException e = new ServiceException(finalMsg);
log.warn("remote call response with error. response={}", JSON.toJSONString(response), e);
throw e;
}
return response.getData();
}
public static <T> List<T> assertResponse(ApiListResult<T> response) {
return assertResponse(response, "error resp={}", JSON.toJSONString(response));
}
public static <T> List<T> assertResponse(ApiListResult<T> response, String message, Object... args) {
if (!response.isSuccess()) {
String finalMsg = MessageFormatter.arrayFormat(message, args).getMessage();
if (StringUtils.isNotBlank(response.getMsg())) {
finalMsg += ": " + response.getMsg();
}
ServiceException e = new ServiceException(finalMsg);
log.warn("remote call response with error. response={}", JSON.toJSONString(response), e);
throw e;
}
return response.getData();
}
}

View File

@ -50,7 +50,7 @@ spring:
cloud:
nacos:
config:
server-addr: ${NACOS_HOST:test-nacos.axzo.cn}:${NACOS_PORT:80}
server-addr: ${NACOS_HOST:https://test-nacos.axzo.cn}:${NACOS_PORT:443}
file-extension: yaml
namespace: ${NACOS_NAMESPACE_ID:f3c0f0d2-bac4-4498-bee7-9c3636b3afdf}
---