!88 czh-20260723-可选字段传了才设值不传不发ksim

* czh-20260723-可选字段传了才设值不传不发ksim
* czh-20260723-测试接口字段名toUserValues统一改为toUserValueList
* czh-20260723-IM模块重构为sendBatch批量发送-移除定时刷新改用RedisTTL过期
* czh-20260723-IM模块linkOpenMode增加白名单校验Exe-Web-None
This commit is contained in:
陈志浩
2026-07-23 07:08:12 +00:00
parent 847c0cd06d
commit 8b747dccda
7 changed files with 97 additions and 75 deletions
@@ -66,8 +66,8 @@ public class KsimApiClient {
JSONObject respBody = resp.getBody(); JSONObject respBody = resp.getBody();
int code = respBody != null ? respBody.getIntValue("code") : -1; int code = respBody != null ? respBody.getIntValue("code") : -1;
log.info("【即时通推送-消息发送】请求完成, toUser: {}, title: {}, code: {}", log.info("【即时通推送-消息发送】请求完成, toUserCount: {}, title: {}, code: {}",
dto.getToUserValue(), dto.getTitle(), code); dto.getToUserValueList() != null ? dto.getToUserValueList().size() : 0, dto.getTitle(), code);
return respBody != null ? respBody.toJavaObject(KsimApiResult.class) : null; return respBody != null ? respBody.toJavaObject(KsimApiResult.class) : null;
} }
@@ -1,9 +0,0 @@
package org.jeecg.modules.ksim.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
@Configuration
@EnableScheduling
public class KsimScheduleConfig {
}
@@ -15,8 +15,12 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@Slf4j @Slf4j
@RestController @RestController
@@ -24,8 +28,10 @@ import java.util.Map;
@RequestMapping("/ksim/test") @RequestMapping("/ksim/test")
public class KsimTestController { public class KsimTestController {
@PostMapping("/send") private static final Set<String> ALLOWED_LINK_OPEN_MODES = Set.of("Exe", "Web", "None");
public Result<Map<String, Object>> send(@RequestBody Map<String, Object> params) {
@PostMapping("/sendBatch")
public Result<Map<String, Object>> sendBatch(@RequestBody Map<String, Object> params) {
Map<String, Object> result = new LinkedHashMap<>(); Map<String, Object> result = new LinkedHashMap<>();
Map<String, Object> requestInfo = new LinkedHashMap<>(); Map<String, Object> requestInfo = new LinkedHashMap<>();
result.put("request", requestInfo); result.put("request", requestInfo);
@@ -35,14 +41,15 @@ public class KsimTestController {
String appId = (String) params.getOrDefault("appId", ""); String appId = (String) params.getOrDefault("appId", "");
String appSecret = (String) params.getOrDefault("appSecret", ""); String appSecret = (String) params.getOrDefault("appSecret", "");
String toUserKeyType = (String) params.getOrDefault("toUserKeyType", ""); String toUserKeyType = (String) params.getOrDefault("toUserKeyType", "");
String toUserValue = (String) params.getOrDefault("toUserValue", ""); String toUserValueListStr = (String) params.getOrDefault("toUserValueList", "");
List<String> toUserValueList = parseList(toUserValueListStr);
String title = (String) params.getOrDefault("title", ""); String title = (String) params.getOrDefault("title", "");
String content = (String) params.getOrDefault("content", ""); String content = (String) params.getOrDefault("content", "");
String linkUrl = (String) params.getOrDefault("linkUrl", ""); String linkUrl = (String) params.getOrDefault("linkUrl", "");
String linkTitle = (String) params.getOrDefault("linkTitle", ""); String linkTitle = (String) params.getOrDefault("linkTitle", "");
String linkOpenMode = (String) params.getOrDefault("linkOpenMode", "Exe"); String linkOpenMode = (String) params.getOrDefault("linkOpenMode", "Exe");
String dataFormat = (String) params.getOrDefault("dataFormat", "JsonListView"); String dataFormat = (String) params.getOrDefault("dataFormat", "JsonListView");
Object data = params.get("data"); String data = (String) params.getOrDefault("data", "");
// --- Step 1: 创建 Token --- // --- Step 1: 创建 Token ---
JSONObject tokenBody = new JSONObject(); JSONObject tokenBody = new JSONObject();
@@ -72,12 +79,23 @@ public class KsimTestController {
// --- Step 2: 发送消息 --- // --- Step 2: 发送消息 ---
KsimMsgSendDTO dto = new KsimMsgSendDTO(); KsimMsgSendDTO dto = new KsimMsgSendDTO();
dto.setToUserKeyType(toUserKeyType); dto.setToUserKeyType(toUserKeyType);
dto.setToUserValue(toUserValue); dto.setToUserValueList(toUserValueList);
dto.setTitle(title); dto.setTitle(title);
dto.setContent(content); dto.setContent(content);
dto.setData(data != null ? data : new JSONObject()); // 可选字段:传了才设置,不传则不发给 ksim
dto.setLinkOpenMode(linkOpenMode != null && !linkOpenMode.isEmpty() ? linkOpenMode : "Exe"); if (data != null && !data.isEmpty()) {
dto.setDataFormat(dataFormat != null && !dataFormat.isEmpty() ? dataFormat : "JsonListView"); dto.setData(data);
}
if (linkOpenMode != null && !linkOpenMode.isEmpty()) {
if (!ALLOWED_LINK_OPEN_MODES.contains(linkOpenMode)) {
log.warn("【即时通测试-sendBatch】非法的 linkOpenMode: {}, 已跳过", linkOpenMode);
} else {
dto.setLinkOpenMode(linkOpenMode);
}
}
if (dataFormat != null && !dataFormat.isEmpty()) {
dto.setDataFormat(dataFormat);
}
if (linkTitle != null && !linkTitle.isEmpty()) { if (linkTitle != null && !linkTitle.isEmpty()) {
dto.setLinkTitle(linkTitle); dto.setLinkTitle(linkTitle);
} }
@@ -98,7 +116,7 @@ public class KsimTestController {
JSONObject msgRespBody = msgResp.getBody(); JSONObject msgRespBody = msgResp.getBody();
result.put("msgResponse", msgRespBody); result.put("msgResponse", msgRespBody);
log.info("【即时通测试-消息发送】完成, code: {}", log.info("【即时通测试-sendBatch】完成, code: {}",
msgRespBody != null ? msgRespBody.getIntValue("code") : -1); msgRespBody != null ? msgRespBody.getIntValue("code") : -1);
if (msgRespBody != null) { if (msgRespBody != null) {
@@ -114,7 +132,7 @@ public class KsimTestController {
} }
} catch (Exception e) { } catch (Exception e) {
log.error("【即时通测试-发送】异常, error: {}", e.getMessage(), e); log.error("【即时通测试-sendBatch】异常, error: {}", e.getMessage(), e);
result.put("success", false); result.put("success", false);
result.put("errorMessage", e.getMessage()); result.put("errorMessage", e.getMessage());
} }
@@ -122,6 +140,19 @@ public class KsimTestController {
return Result.OK(result); return Result.OK(result);
} }
/**
* 逗号分隔字符串 → List
*/
private List<String> parseList(String str) {
if (str == null || str.trim().isEmpty()) {
return Arrays.asList();
}
return Arrays.stream(str.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.collect(Collectors.toList());
}
private HttpHeaders jsonHeaders() { private HttpHeaders jsonHeaders() {
return RestUtil.getHeaderApplicationJson(); return RestUtil.getHeaderApplicationJson();
} }
@@ -1,5 +1,7 @@
package org.jeecg.modules.ksim.dto; package org.jeecg.modules.ksim.dto;
import java.util.List;
/** /**
* 对应即时通文档 3.4.3 ReqMsgSendDTO * 对应即时通文档 3.4.3 ReqMsgSendDTO
*/ */
@@ -8,8 +10,8 @@ public class KsimMsgSendDTO {
private String title; private String title;
private String content; private String content;
private String toUserKeyType; private String toUserKeyType;
private String toUserValue; private List<String> toUserValueList;
private Object data; private String data;
private String dataFormat; private String dataFormat;
private String linkOpenMode; private String linkOpenMode;
private String linkTitle; private String linkTitle;
@@ -21,10 +23,10 @@ public class KsimMsgSendDTO {
public void setContent(String content) { this.content = content; } public void setContent(String content) { this.content = content; }
public String getToUserKeyType() { return toUserKeyType; } public String getToUserKeyType() { return toUserKeyType; }
public void setToUserKeyType(String toUserKeyType) { this.toUserKeyType = toUserKeyType; } public void setToUserKeyType(String toUserKeyType) { this.toUserKeyType = toUserKeyType; }
public String getToUserValue() { return toUserValue; } public List<String> getToUserValueList() { return toUserValueList; }
public void setToUserValue(String toUserValue) { this.toUserValue = toUserValue; } public void setToUserValueList(List<String> toUserValueList) { this.toUserValueList = toUserValueList; }
public Object getData() { return data; } public String getData() { return data; }
public void setData(Object data) { this.data = data; } public void setData(String data) { this.data = data; }
public String getDataFormat() { return dataFormat; } public String getDataFormat() { return dataFormat; }
public void setDataFormat(String dataFormat) { this.dataFormat = dataFormat; } public void setDataFormat(String dataFormat) { this.dataFormat = dataFormat; }
public String getLinkOpenMode() { return linkOpenMode; } public String getLinkOpenMode() { return linkOpenMode; }
@@ -2,13 +2,15 @@ package org.jeecg.modules.ksim.service;
import org.jeecg.common.api.vo.Result; import org.jeecg.common.api.vo.Result;
import java.util.List;
/** /**
* 即时通消息推送服务(对外唯一入口) * 即时通消息推送服务(对外唯一入口)
*/ */
public interface IKsimMessageService { public interface IKsimMessageService {
Result<String> sendMessage(String toUserKeyType, String toUserValue, Result<String> sendBatch(String toUserKeyType, List<String> toUserValueList,
String title, String content, String title, String content,
String linkUrl, String linkTitle, String linkUrl, String linkTitle,
Object data, String linkOpenMode, String dataFormat); String data, String linkOpenMode, String dataFormat);
} }
@@ -11,10 +11,15 @@ import org.jeecg.modules.ksim.token.KsimTokenManager;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.List;
import java.util.Set;
@Slf4j @Slf4j
@Service @Service
public class KsimMessageServiceImpl implements IKsimMessageService { public class KsimMessageServiceImpl implements IKsimMessageService {
private static final Set<String> ALLOWED_LINK_OPEN_MODES = Set.of("Exe", "Web", "None");
@Resource @Resource
private KsimTokenManager tokenManager; private KsimTokenManager tokenManager;
@@ -22,23 +27,31 @@ public class KsimMessageServiceImpl implements IKsimMessageService {
private KsimApiClient apiClient; private KsimApiClient apiClient;
@Override @Override
public Result<String> sendMessage(String toUserKeyType, String toUserValue, public Result<String> sendBatch(String toUserKeyType, List<String> toUserValueList,
String title, String content, String title, String content,
String linkUrl, String linkTitle, String linkUrl, String linkTitle,
Object data, String linkOpenMode, String dataFormat) { String data, String linkOpenMode, String dataFormat) {
String token = tokenManager.getToken(); String token = tokenManager.getToken();
KsimMsgSendDTO dto = new KsimMsgSendDTO(); KsimMsgSendDTO dto = new KsimMsgSendDTO();
dto.setToUserKeyType(toUserKeyType); dto.setToUserKeyType(toUserKeyType);
dto.setToUserValue(toUserValue); dto.setToUserValueList(toUserValueList);
dto.setTitle(title); dto.setTitle(title);
dto.setContent(content); dto.setContent(content);
// data 默认 {} // 以下可选字段:传了才设置,不传则不发给 ksim
dto.setData(data != null ? data : new JSONObject()); if (data != null && !data.isEmpty()) {
// linkOpenMode 默认 "Exe" dto.setData(data);
dto.setLinkOpenMode(linkOpenMode != null && !linkOpenMode.isEmpty() ? linkOpenMode : "Exe"); }
// dataFormat 默认 "JsonListView" if (linkOpenMode != null && !linkOpenMode.isEmpty()) {
dto.setDataFormat(dataFormat != null && !dataFormat.isEmpty() ? dataFormat : "JsonListView"); if (!ALLOWED_LINK_OPEN_MODES.contains(linkOpenMode)) {
log.warn("【即时通推送-sendBatch】非法的 linkOpenMode: {}, 已跳过", linkOpenMode);
} else {
dto.setLinkOpenMode(linkOpenMode);
}
}
if (dataFormat != null && !dataFormat.isEmpty()) {
dto.setDataFormat(dataFormat);
}
if (linkTitle != null && !linkTitle.isEmpty()) { if (linkTitle != null && !linkTitle.isEmpty()) {
dto.setLinkTitle(linkTitle); dto.setLinkTitle(linkTitle);
} }
@@ -48,18 +61,21 @@ public class KsimMessageServiceImpl implements IKsimMessageService {
KsimApiResult<JSONObject> apiResult = apiClient.sendMsg(token, dto); KsimApiResult<JSONObject> apiResult = apiClient.sendMsg(token, dto);
if (apiResult == null) { if (apiResult == null) {
log.error("【即时通推送-消息发送】响应为空, toUser: {}, title: {}", toUserValue, title); log.error("【即时通推送-sendBatch】响应为空, toUserCount: {}, title: {}",
toUserValueList != null ? toUserValueList.size() : 0, title);
return Result.error("即时通响应为空"); return Result.error("即时通响应为空");
} }
if (!apiResult.isSuccess()) { if (!apiResult.isSuccess()) {
log.error("【即时通推送-消息发送】发送失败, code: {}, message: {}, toUser: {}, title: {}", log.error("【即时通推送-sendBatch】发送失败, code: {}, message: {}, toUserCount: {}, title: {}",
apiResult.getCode(), apiResult.getMessage(), toUserValue, title); apiResult.getCode(), apiResult.getMessage(),
toUserValueList != null ? toUserValueList.size() : 0, title);
return Result.error(apiResult.getCode(), "即时通发送失败: " + apiResult.getMessage()); return Result.error(apiResult.getCode(), "即时通发送失败: " + apiResult.getMessage());
} }
String messageId = apiResult.getDataBody() != null String messageId = apiResult.getDataBody() != null
? apiResult.getDataBody().getString("messageId") : ""; ? apiResult.getDataBody().getString("messageId") : "";
log.info("【即时通推送-消息发送】成功, messageId: {}, toUser: {}, title: {}", messageId, toUserValue, title); log.info("【即时通推送-sendBatch】成功, messageId: {}, toUserCount: {}, title: {}", messageId,
toUserValueList != null ? toUserValueList.size() : 0, title);
return Result.OK(messageId); return Result.OK(messageId);
} }
} }
@@ -6,12 +6,15 @@ import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.modules.ksim.client.KsimApiClient; import org.jeecg.modules.ksim.client.KsimApiClient;
import org.jeecg.modules.ksim.config.KsimProperties; import org.jeecg.modules.ksim.config.KsimProperties;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/**
* Token 管理:首次获取或 Redis 过期时自动创建,无定时任务。
* Redis TTL = tokenTimeoutSeconds - 600,提前 10 分钟过期确保 Token 始终有效。
*/
@Slf4j @Slf4j
@Component @Component
public class KsimTokenManager { public class KsimTokenManager {
@@ -35,32 +38,8 @@ public class KsimTokenManager {
return createAndCache(); 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() { private String createAndCache() {
log.info("【即时通推送-Token懒加载】创建新Token"); log.info("【即时通推送-Token】Redis无缓存或已过期,创建新Token");
JSONObject result = ksimApiClient.createToken( JSONObject result = ksimApiClient.createToken(
ksimProperties.getAppId(), ksimProperties.getAppSecret(), ksimProperties.getTokenTimeoutSeconds()); ksimProperties.getAppId(), ksimProperties.getAppSecret(), ksimProperties.getTokenTimeoutSeconds());
return cacheToken(result, ksimProperties.getTokenTimeoutSeconds()); return cacheToken(result, ksimProperties.getTokenTimeoutSeconds());
@@ -68,13 +47,14 @@ public class KsimTokenManager {
private String cacheToken(JSONObject result, int timeoutSeconds) { private String cacheToken(JSONObject result, int timeoutSeconds) {
if (result == null || result.getJSONObject("dataBody") == null) { if (result == null || result.getJSONObject("dataBody") == null) {
throw new JeecgBootException("【即时通推送】Token创建/刷新返回为空"); throw new JeecgBootException("【即时通推送】Token创建返回为空");
} }
JSONObject dataBody = result.getJSONObject("dataBody"); JSONObject dataBody = result.getJSONObject("dataBody");
String token = dataBody.getString("token"); String token = dataBody.getString("token");
int ttl = Math.max(timeoutSeconds - 60, 60); // TTL 提前 600s,确保 Token 不会在有效期内被 Redis 误返回
int ttl = Math.max(timeoutSeconds - 600, 60);
redisTemplate.opsForValue().set(TOKEN_KEY, token, ttl, TimeUnit.SECONDS); redisTemplate.opsForValue().set(TOKEN_KEY, token, ttl, TimeUnit.SECONDS);
log.info("【即时通推送-Token缓存】已写入Redis, ttl: {}s", ttl); log.info("【即时通推送-Token】已写入Redis, ttl: {}s", ttl);
return token; return token;
} }
} }