!84 czh-20260717-修复-测试接口补充linkTitle参数

* czh-20260717-修复-测试接口补充linkTitle参数
* czh-20260717-新增-sendMessage增加linkTitle参数
* czh-20260717-新增-sendMessage增加data参数支持
* czh-20260717-优化-去掉bizId和createDatetime字段
* czh-20260717-修复system-start缺少jeecg-module-im依赖
* czh-20260717-修复测试接口SSRF和日志泄漏问题
* czh-20260717-新增即时通消息推送模块及测试接口
This commit is contained in:
陈志浩
2026-07-22 01:52:49 +00:00
parent 785352914a
commit 111c0e2c8d
13 changed files with 492 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>jeecg-boot-module</artifactId>
<groupId>org.jeecgframework.boot</groupId>
<version>${jeecgProjectVersion}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jeecg-module-im</artifactId>
<dependencies>
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-base-core</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,74 @@
package org.jeecg.modules.ksim.client;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.util.RestUtil;
import org.jeecg.modules.ksim.config.KsimProperties;
import org.jeecg.modules.ksim.dto.KsimApiResult;
import org.jeecg.modules.ksim.dto.KsimMsgSendDTO;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
@Slf4j
@Component
public class KsimApiClient {
@Resource
private KsimProperties ksimProperties;
private HttpHeaders jsonHeaders() {
return RestUtil.getHeaderApplicationJson();
}
public JSONObject createToken(String appId, String appSecret, int timeoutSeconds) {
JSONObject body = new JSONObject();
body.put("appId", appId);
body.put("appSecret", appSecret);
body.put("tokenTimeoutSeconds", timeoutSeconds);
String url = ksimProperties.getHost() + "/api-open/v1/token/create";
ResponseEntity<JSONObject> resp = RestUtil.request(url, HttpMethod.POST, jsonHeaders(), null, body, JSONObject.class);
log.info("【即时通推送-Token创建】请求完成, appId: {}, 获取Token: {}",
appId, resp.getBody() != null && resp.getBody().getJSONObject("dataBody") != null ? "成功" : "失败");
return resp.getBody();
}
public JSONObject refreshToken(String oldToken, int timeoutSeconds) {
JSONObject body = new JSONObject();
body.put("tokenTimeoutSeconds", timeoutSeconds);
HttpHeaders headers = jsonHeaders();
headers.add("imOpenApiToken", oldToken);
String url = ksimProperties.getHost() + "/api-open/v1/token/refresh";
ResponseEntity<JSONObject> resp = RestUtil.request(url, HttpMethod.POST, headers, null, body, JSONObject.class);
log.info("【即时通推送-Token刷新】请求完成, 获取Token: {}",
resp.getBody() != null && resp.getBody().getJSONObject("dataBody") != null ? "成功" : "失败");
return resp.getBody();
}
public KsimApiResult<JSONObject> sendMsg(String token, KsimMsgSendDTO dto) {
HttpHeaders headers = jsonHeaders();
headers.add("imOpenApiToken", token);
String bodyJson = JSON.toJSONString(dto);
String url = ksimProperties.getHost() + "/api-open/v1/msg/send";
ResponseEntity<JSONObject> resp = RestUtil.request(url, HttpMethod.POST, headers, null,
JSON.parseObject(bodyJson), JSONObject.class);
JSONObject respBody = resp.getBody();
int code = respBody != null ? respBody.getIntValue("code") : -1;
log.info("【即时通推送-消息发送】请求完成, toUser: {}, title: {}, code: {}",
dto.getToUserValue(), dto.getTitle(), code);
return respBody != null ? respBody.toJavaObject(KsimApiResult.class) : null;
}
}
@@ -0,0 +1,30 @@
package org.jeecg.modules.ksim.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "ksim")
public class KsimProperties {
/** 即时通服务地址,如 http://10.2.0.54:19010 */
private String host = "http://10.2.0.54:19010";
/** 应用ID,在 application-{env}.yml 中配置 */
private String appId;
/** 应用密钥,在 application-{env}.yml 中配置 */
private String appSecret;
/** Token 有效期(秒),默认 7200(2小时) */
private int tokenTimeoutSeconds = 7200;
public String getHost() { return host; }
public void setHost(String host) { this.host = host; }
public String getAppId() { return appId; }
public void setAppId(String appId) { this.appId = appId; }
public String getAppSecret() { return appSecret; }
public void setAppSecret(String appSecret) { this.appSecret = appSecret; }
public int getTokenTimeoutSeconds() { return tokenTimeoutSeconds; }
public void setTokenTimeoutSeconds(int tokenTimeoutSeconds) { this.tokenTimeoutSeconds = tokenTimeoutSeconds; }
}
@@ -0,0 +1,9 @@
package org.jeecg.modules.ksim.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
@Configuration
@EnableScheduling
public class KsimScheduleConfig {
}
@@ -0,0 +1,127 @@
package org.jeecg.modules.ksim.controller;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.util.RestUtil;
import org.jeecg.modules.ksim.dto.KsimMsgSendDTO;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.context.annotation.Profile;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.LinkedHashMap;
import java.util.Map;
@Slf4j
@RestController
@Profile("dev")
@RequestMapping("/ksim/test")
public class KsimTestController {
@PostMapping("/send")
public Result<Map<String, Object>> send(@RequestBody Map<String, Object> params) {
Map<String, Object> result = new LinkedHashMap<>();
Map<String, Object> requestInfo = new LinkedHashMap<>();
result.put("request", requestInfo);
try {
String host = (String) params.getOrDefault("host", "");
String appId = (String) params.getOrDefault("appId", "");
String appSecret = (String) params.getOrDefault("appSecret", "");
String toUserKeyType = (String) params.getOrDefault("toUserKeyType", "");
String toUserValue = (String) params.getOrDefault("toUserValue", "");
String title = (String) params.getOrDefault("title", "");
String content = (String) params.getOrDefault("content", "");
String linkUrl = (String) params.getOrDefault("linkUrl", "");
String linkTitle = (String) params.getOrDefault("linkTitle", "");
Object data = params.get("data");
// --- Step 1: 创建 Token ---
JSONObject tokenBody = new JSONObject();
tokenBody.put("appId", appId);
tokenBody.put("appSecret", appSecret);
tokenBody.put("tokenTimeoutSeconds", 7200);
String tokenUrl = host + "/api-open/v1/token/create";
requestInfo.put("tokenRequestUrl", tokenUrl);
requestInfo.put("tokenRequestBody", tokenBody);
ResponseEntity<JSONObject> tokenResp = RestUtil.request(
tokenUrl, HttpMethod.POST, jsonHeaders(), null, tokenBody, JSONObject.class);
JSONObject tokenRespBody = tokenResp.getBody();
result.put("tokenResponse", tokenRespBody);
log.info("【即时通测试-Token创建】完成, 成功: {}",
tokenRespBody != null && tokenRespBody.getJSONObject("dataBody") != null);
if (tokenRespBody == null || tokenRespBody.getJSONObject("dataBody") == null) {
result.put("success", false);
result.put("errorMessage", "Token 创建失败:响应为空");
return Result.OK(result);
}
String token = tokenRespBody.getJSONObject("dataBody").getString("token");
// --- Step 2: 发送消息 ---
KsimMsgSendDTO dto = new KsimMsgSendDTO();
dto.setToUserKeyType(toUserKeyType);
dto.setToUserValue(toUserValue);
dto.setTitle(title);
dto.setContent(content);
if (data != null) {
dto.setData(data);
}
if (linkTitle != null && !linkTitle.isEmpty()) {
dto.setLinkTitle(linkTitle);
}
if (linkUrl != null && !linkUrl.isEmpty()) {
dto.setLinkUrl(linkUrl);
dto.setLinkOpenMode("Web");
}
String msgUrl = host + "/api-open/v1/msg/send";
requestInfo.put("msgRequestUrl", msgUrl);
requestInfo.put("msgRequestBody", dto);
HttpHeaders msgHeaders = jsonHeaders();
msgHeaders.add("imOpenApiToken", token);
ResponseEntity<JSONObject> msgResp = RestUtil.request(
msgUrl, HttpMethod.POST, msgHeaders, null,
JSON.parseObject(JSON.toJSONString(dto)), JSONObject.class);
JSONObject msgRespBody = msgResp.getBody();
result.put("msgResponse", msgRespBody);
log.info("【即时通测试-消息发送】完成, code: {}",
msgRespBody != null ? msgRespBody.getIntValue("code") : -1);
if (msgRespBody != null) {
int code = msgRespBody.getIntValue("code");
result.put("success", code == 0 || code == 200);
if (msgRespBody.getJSONObject("dataBody") != null) {
result.put("messageId", msgRespBody.getJSONObject("dataBody").getString("messageId"));
}
result.put("responseCode", code);
} else {
result.put("success", false);
result.put("errorMessage", "消息发送失败:响应为空");
}
} catch (Exception e) {
log.error("【即时通测试-发送】异常, error: {}", e.getMessage(), e);
result.put("success", false);
result.put("errorMessage", e.getMessage());
}
return Result.OK(result);
}
private HttpHeaders jsonHeaders() {
return RestUtil.getHeaderApplicationJson();
}
}
@@ -0,0 +1,32 @@
package org.jeecg.modules.ksim.dto;
import com.alibaba.fastjson.annotation.JSONField;
/**
* 即时通 API 通用响应体
*/
public class KsimApiResult<T> {
private int code;
private String message;
private String timestamp;
private String requestId;
@JSONField(name = "dataBody")
private T dataBody;
public boolean isSuccess() {
return code == 0 || code == 200;
}
public int getCode() { return code; }
public void setCode(int code) { this.code = code; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public String getTimestamp() { return timestamp; }
public void setTimestamp(String timestamp) { this.timestamp = timestamp; }
public String getRequestId() { return requestId; }
public void setRequestId(String requestId) { this.requestId = requestId; }
public T getDataBody() { return dataBody; }
public void setDataBody(T dataBody) { this.dataBody = dataBody; }
}
@@ -0,0 +1,36 @@
package org.jeecg.modules.ksim.dto;
/**
* 对应即时通文档 3.4.3 ReqMsgSendDTO
*/
public class KsimMsgSendDTO {
private String title;
private String content;
private String toUserKeyType;
private String toUserValue;
private Object data;
private String dataFormat;
private String linkOpenMode;
private String linkTitle;
private String linkUrl;
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public String getToUserKeyType() { return toUserKeyType; }
public void setToUserKeyType(String toUserKeyType) { this.toUserKeyType = toUserKeyType; }
public String getToUserValue() { return toUserValue; }
public void setToUserValue(String toUserValue) { this.toUserValue = toUserValue; }
public Object getData() { return data; }
public void setData(Object data) { this.data = data; }
public String getDataFormat() { return dataFormat; }
public void setDataFormat(String dataFormat) { this.dataFormat = dataFormat; }
public String getLinkOpenMode() { return linkOpenMode; }
public void setLinkOpenMode(String linkOpenMode) { this.linkOpenMode = linkOpenMode; }
public String getLinkTitle() { return linkTitle; }
public void setLinkTitle(String linkTitle) { this.linkTitle = linkTitle; }
public String getLinkUrl() { return linkUrl; }
public void setLinkUrl(String linkUrl) { this.linkUrl = linkUrl; }
}
@@ -0,0 +1 @@
package org.jeecg.modules.ksim;
@@ -0,0 +1,14 @@
package org.jeecg.modules.ksim.service;
import org.jeecg.common.api.vo.Result;
/**
* 即时通消息推送服务(对外唯一入口)
*/
public interface IKsimMessageService {
Result<String> sendMessage(String toUserKeyType, String toUserValue,
String title, String content,
String linkUrl, String linkTitle,
Object data);
}
@@ -0,0 +1,61 @@
package org.jeecg.modules.ksim.service.impl;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.jeecg.modules.ksim.client.KsimApiClient;
import org.jeecg.modules.ksim.dto.KsimApiResult;
import org.jeecg.modules.ksim.dto.KsimMsgSendDTO;
import org.jeecg.modules.ksim.service.IKsimMessageService;
import org.jeecg.modules.ksim.token.KsimTokenManager;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Slf4j
@Service
public class KsimMessageServiceImpl implements IKsimMessageService {
@Resource
private KsimTokenManager tokenManager;
@Resource
private KsimApiClient apiClient;
@Override
public Result<String> sendMessage(String toUserKeyType, String toUserValue,
String title, String content,
String linkUrl, String linkTitle,
Object data) {
String token = tokenManager.getToken();
KsimMsgSendDTO dto = new KsimMsgSendDTO();
dto.setToUserKeyType(toUserKeyType);
dto.setToUserValue(toUserValue);
dto.setTitle(title);
dto.setContent(content);
dto.setData(data);
if (linkTitle != null && !linkTitle.isEmpty()) {
dto.setLinkTitle(linkTitle);
}
if (linkUrl != null && !linkUrl.isEmpty()) {
dto.setLinkUrl(linkUrl);
dto.setLinkOpenMode("Web");
}
KsimApiResult<JSONObject> apiResult = apiClient.sendMsg(token, dto);
if (apiResult == null) {
log.error("【即时通推送-消息发送】响应为空, toUser: {}, title: {}", toUserValue, title);
return Result.error("即时通响应为空");
}
if (!apiResult.isSuccess()) {
log.error("【即时通推送-消息发送】发送失败, code: {}, message: {}, toUser: {}, title: {}",
apiResult.getCode(), apiResult.getMessage(), toUserValue, title);
return Result.error(apiResult.getCode(), "即时通发送失败: " + apiResult.getMessage());
}
String messageId = apiResult.getDataBody() != null
? apiResult.getDataBody().getString("messageId") : "";
log.info("【即时通推送-消息发送】成功, messageId: {}, toUser: {}, title: {}", messageId, toUserValue, title);
return Result.OK(messageId);
}
}
@@ -0,0 +1,80 @@
package org.jeecg.modules.ksim.token;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.modules.ksim.client.KsimApiClient;
import org.jeecg.modules.ksim.config.KsimProperties;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
@Slf4j
@Component
public class KsimTokenManager {
private static final String TOKEN_KEY = "ksim:token";
@Resource
private KsimProperties ksimProperties;
@Resource
private KsimApiClient ksimApiClient;
@Resource
private RedisTemplate<String, Object> redisTemplate;
public String getToken() {
Object cached = redisTemplate.opsForValue().get(TOKEN_KEY);
if (cached != null) {
return cached.toString();
}
return createAndCache();
}
@Scheduled(cron = "0 0 * * * ?")
public void refreshTask() {
log.info("【即时通推送-Token定时刷新】开始");
try {
Object old = redisTemplate.opsForValue().get(TOKEN_KEY);
JSONObject result;
int timeout = ksimProperties.getTokenTimeoutSeconds();
if (old != null) {
try {
result = ksimApiClient.refreshToken(old.toString(), timeout);
} catch (Exception e) {
log.warn("【即时通推送-Token定时刷新】刷新失败,回退到创建, error: {}", e.getMessage());
result = ksimApiClient.createToken(ksimProperties.getAppId(), ksimProperties.getAppSecret(), timeout);
}
} else {
result = ksimApiClient.createToken(ksimProperties.getAppId(), ksimProperties.getAppSecret(), timeout);
}
cacheToken(result, timeout);
log.info("【即时通推送-Token定时刷新】完成");
} catch (Exception e) {
log.error("【即时通推送-Token定时刷新】失败, error: {}", e.getMessage(), e);
}
}
private String createAndCache() {
log.info("【即时通推送-Token懒加载】创建新Token");
JSONObject result = ksimApiClient.createToken(
ksimProperties.getAppId(), ksimProperties.getAppSecret(), ksimProperties.getTokenTimeoutSeconds());
return cacheToken(result, ksimProperties.getTokenTimeoutSeconds());
}
private String cacheToken(JSONObject result, int timeoutSeconds) {
if (result == null || result.getJSONObject("dataBody") == null) {
throw new JeecgBootException("【即时通推送】Token创建/刷新返回为空");
}
JSONObject dataBody = result.getJSONObject("dataBody");
String token = dataBody.getString("token");
int ttl = Math.max(timeoutSeconds - 60, 60);
redisTemplate.opsForValue().set(TOKEN_KEY, token, ttl, TimeUnit.SECONDS);
log.info("【即时通推送-Token缓存】已写入Redis, ttl: {}s", ttl);
return token;
}
}