feat(im): 添加平台事件通知功能支持应用审核状态实时更新
- 新增 ImPlatformEventController 提供令牌获取接口 - 新增 InternalImPlatformEventController 处理内部通知请求 - 实现 ImPlatformEventService 核心服务逻辑包括令牌签发和消息推送 - 添加 StoreReviewImNotifier 在更新服务中触发审核状态变更通知 - 在前端平台中集成实时审核状态更新功能 - 配置各项目环境版本管理文件 (.java-version, .nvmrc) - 更新 Docker 忽略文件和 Maven 配置以优化构建流程
这个提交包含在:
父节点
dc1ada94ea
当前提交
cf2013a52d
10
.dockerignore
普通文件
10
.dockerignore
普通文件
@ -0,0 +1,10 @@
|
|||||||
|
.git
|
||||||
|
.idea
|
||||||
|
.gradle
|
||||||
|
**/target
|
||||||
|
**/build
|
||||||
|
**/dist
|
||||||
|
**/node_modules
|
||||||
|
**/.DS_Store
|
||||||
|
**/*.log
|
||||||
|
*.iml
|
||||||
48
maven-settings.xml
普通文件
48
maven-settings.xml
普通文件
@ -0,0 +1,48 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<settings xmlns="http://maven.apache.org/SETTINGS/1.2.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.2.0
|
||||||
|
https://maven.apache.org/xsd/settings-1.2.0.xsd">
|
||||||
|
<profiles>
|
||||||
|
<profile>
|
||||||
|
<id>xuqm-nexus</id>
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<id>xuqm-maven-public</id>
|
||||||
|
<url>https://nexus.xuqinmin.com/repository/maven-public/</url>
|
||||||
|
<releases>
|
||||||
|
<enabled>true</enabled>
|
||||||
|
</releases>
|
||||||
|
<snapshots>
|
||||||
|
<enabled>false</enabled>
|
||||||
|
</snapshots>
|
||||||
|
</repository>
|
||||||
|
<repository>
|
||||||
|
<id>xuqm-maven-snapshots</id>
|
||||||
|
<url>https://nexus.xuqinmin.com/repository/maven-snapshots/</url>
|
||||||
|
<releases>
|
||||||
|
<enabled>false</enabled>
|
||||||
|
</releases>
|
||||||
|
<snapshots>
|
||||||
|
<enabled>true</enabled>
|
||||||
|
</snapshots>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
<pluginRepositories>
|
||||||
|
<pluginRepository>
|
||||||
|
<id>xuqm-maven-public</id>
|
||||||
|
<url>https://nexus.xuqinmin.com/repository/maven-public/</url>
|
||||||
|
<releases>
|
||||||
|
<enabled>true</enabled>
|
||||||
|
</releases>
|
||||||
|
<snapshots>
|
||||||
|
<enabled>false</enabled>
|
||||||
|
</snapshots>
|
||||||
|
</pluginRepository>
|
||||||
|
</pluginRepositories>
|
||||||
|
</profile>
|
||||||
|
</profiles>
|
||||||
|
<activeProfiles>
|
||||||
|
<activeProfile>xuqm-nexus</activeProfile>
|
||||||
|
</activeProfiles>
|
||||||
|
</settings>
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
package com.xuqm.tenant.controller;
|
||||||
|
|
||||||
|
import com.xuqm.common.model.ApiResponse;
|
||||||
|
import com.xuqm.tenant.entity.AppEntity;
|
||||||
|
import com.xuqm.tenant.service.ImPlatformEventService;
|
||||||
|
import com.xuqm.tenant.service.SdkAppProvisioningService;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/im/platform-events")
|
||||||
|
public class ImPlatformEventController {
|
||||||
|
|
||||||
|
private final SdkAppProvisioningService provisioningService;
|
||||||
|
private final ImPlatformEventService platformEventService;
|
||||||
|
|
||||||
|
public ImPlatformEventController(SdkAppProvisioningService provisioningService,
|
||||||
|
ImPlatformEventService platformEventService) {
|
||||||
|
this.provisioningService = provisioningService;
|
||||||
|
this.platformEventService = platformEventService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/token")
|
||||||
|
public ResponseEntity<ApiResponse<Map<String, String>>> token(
|
||||||
|
@AuthenticationPrincipal String tenantId,
|
||||||
|
@RequestParam String appKey) {
|
||||||
|
try {
|
||||||
|
AppEntity app = provisioningService.resolveApp(appKey);
|
||||||
|
if (tenantId == null || tenantId.isBlank() || !tenantId.equals(app.getTenantId())) {
|
||||||
|
return ResponseEntity.status(403).body(ApiResponse.error(403, "Forbidden"));
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(platformEventService.issueToken(appKey)));
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.status(500)
|
||||||
|
.body(ApiResponse.error(500, e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
package com.xuqm.tenant.controller;
|
||||||
|
|
||||||
|
import com.xuqm.common.model.ApiResponse;
|
||||||
|
import com.xuqm.tenant.service.ImPlatformEventService;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestHeader;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/internal/im/platform-events")
|
||||||
|
public class InternalImPlatformEventController {
|
||||||
|
|
||||||
|
private final ImPlatformEventService platformEventService;
|
||||||
|
|
||||||
|
@Value("${sdk.internal-token:xuqm-internal-token}")
|
||||||
|
private String internalToken;
|
||||||
|
|
||||||
|
public InternalImPlatformEventController(ImPlatformEventService platformEventService) {
|
||||||
|
this.platformEventService = platformEventService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/notify")
|
||||||
|
public ResponseEntity<ApiResponse<Map<String, String>>> notify(
|
||||||
|
@RequestHeader(value = "X-Internal-Token", required = false) String token,
|
||||||
|
@RequestBody ImPlatformEventService.StoreReviewEventRequest request) {
|
||||||
|
if (token == null || !internalToken.equals(token)) {
|
||||||
|
return ResponseEntity.status(403).body(ApiResponse.error(403, "Forbidden"));
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(platformEventService.notifyStoreReviewChange(request)));
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.status(500)
|
||||||
|
.body(ApiResponse.error(500, e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,165 @@
|
|||||||
|
package com.xuqm.tenant.service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.xuqm.common.security.AppRequestSignatureUtil;
|
||||||
|
import com.xuqm.im.sdk.XuqmImServerSdk;
|
||||||
|
import com.xuqm.tenant.entity.AppEntity;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class ImPlatformEventService {
|
||||||
|
|
||||||
|
private final HttpClient httpClient = HttpClient.newHttpClient();
|
||||||
|
private final SdkAppProvisioningService provisioningService;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@Value("${sdk.im-api-url:https://im.dev.xuqinmin.com}")
|
||||||
|
private String imApiUrl;
|
||||||
|
|
||||||
|
@Value("${sdk.im-platform-events-user-prefix:platform-events:}")
|
||||||
|
private String platformEventsUserPrefix;
|
||||||
|
|
||||||
|
@Value("${sdk.im-platform-events-system-user:platform-events-system}")
|
||||||
|
private String platformEventsSystemUser;
|
||||||
|
|
||||||
|
public ImPlatformEventService(SdkAppProvisioningService provisioningService,
|
||||||
|
ObjectMapper objectMapper) {
|
||||||
|
this.provisioningService = provisioningService;
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> issueToken(String appKey) throws Exception {
|
||||||
|
AppEntity app = provisioningService.resolveApp(appKey);
|
||||||
|
XuqmImServerSdk sdk = sdk(app);
|
||||||
|
String userId = platformEventsUserId(appKey);
|
||||||
|
ensureAccount(sdk, app, userId, "平台通知");
|
||||||
|
String token = requestImToken(app, userId);
|
||||||
|
Map<String, String> result = new LinkedHashMap<>();
|
||||||
|
result.put("appKey", app.getAppKey());
|
||||||
|
result.put("userId", userId);
|
||||||
|
result.put("token", token);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> notifyStoreReviewChange(StoreReviewEventRequest request) throws Exception {
|
||||||
|
AppEntity app = provisioningService.resolveApp(request.appKey());
|
||||||
|
XuqmImServerSdk sdk = sdk(app);
|
||||||
|
String recipientUserId = platformEventsUserId(request.appKey());
|
||||||
|
String senderUserId = platformEventsSystemUserId();
|
||||||
|
|
||||||
|
ensureAccount(sdk, app, recipientUserId, "平台通知");
|
||||||
|
ensureAccount(sdk, app, senderUserId, "系统通知");
|
||||||
|
|
||||||
|
Map<String, Object> contentPayload = new LinkedHashMap<>();
|
||||||
|
contentPayload.put("event", request.event() == null || request.event().isBlank() ? "store_review_update" : request.event());
|
||||||
|
contentPayload.put("appKey", request.appKey());
|
||||||
|
contentPayload.put("versionId", request.versionId() == null ? "" : request.versionId());
|
||||||
|
contentPayload.put("storeType", request.storeType() == null ? "" : request.storeType());
|
||||||
|
contentPayload.put("reviewState", request.reviewState() == null ? "" : request.reviewState());
|
||||||
|
contentPayload.put("reviewReason", request.reviewReason() == null ? "" : request.reviewReason());
|
||||||
|
contentPayload.put("stage", request.stage() == null ? "" : request.stage());
|
||||||
|
contentPayload.put("batchId", request.batchId() == null ? "" : request.batchId());
|
||||||
|
contentPayload.put("publishStatus", request.publishStatus() == null ? "" : request.publishStatus());
|
||||||
|
contentPayload.put("source", request.source() == null ? "update-service" : request.source());
|
||||||
|
contentPayload.put("timestamp", System.currentTimeMillis());
|
||||||
|
String content = objectMapper.writeValueAsString(contentPayload);
|
||||||
|
|
||||||
|
var message = sdk.sendMessage(new XuqmImServerSdk.SendMessageRequest(
|
||||||
|
UUID.randomUUID().toString(),
|
||||||
|
recipientUserId,
|
||||||
|
"SINGLE",
|
||||||
|
"NOTIFY",
|
||||||
|
content,
|
||||||
|
null
|
||||||
|
));
|
||||||
|
|
||||||
|
Map<String, String> result = new LinkedHashMap<>();
|
||||||
|
result.put("appKey", app.getAppKey());
|
||||||
|
result.put("userId", recipientUserId);
|
||||||
|
result.put("messageId", message.id());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureAccount(XuqmImServerSdk sdk, AppEntity app, String userId, String suffix) {
|
||||||
|
sdk.importAccount(
|
||||||
|
userId,
|
||||||
|
app.getName() + " " + suffix,
|
||||||
|
app.getIconUrl(),
|
||||||
|
"UNKNOWN",
|
||||||
|
"ACTIVE"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private XuqmImServerSdk sdk(AppEntity app) {
|
||||||
|
return XuqmImServerSdk.builder()
|
||||||
|
.baseUrl(imApiUrl)
|
||||||
|
.appKey(app.getAppKey())
|
||||||
|
.appSecret(app.getAppSecret())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String requestImToken(AppEntity app, String userId) throws Exception {
|
||||||
|
long timestamp = System.currentTimeMillis();
|
||||||
|
String nonce = UUID.randomUUID().toString();
|
||||||
|
String payload = AppRequestSignatureUtil.payload(app.getAppKey(), userId, timestamp, nonce);
|
||||||
|
String signature = AppRequestSignatureUtil.sign(app.getAppSecret(), payload);
|
||||||
|
URI uri = URI.create(imApiUrl + "/api/im/auth/login?appKey="
|
||||||
|
+ encodeQuery(app.getAppKey())
|
||||||
|
+ "&userId="
|
||||||
|
+ encodeQuery(userId));
|
||||||
|
HttpRequest request = HttpRequest.newBuilder(uri)
|
||||||
|
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
.header("X-App-Timestamp", String.valueOf(timestamp))
|
||||||
|
.header("X-App-Nonce", nonce)
|
||||||
|
.header("X-App-Signature", signature)
|
||||||
|
.POST(HttpRequest.BodyPublishers.noBody())
|
||||||
|
.build();
|
||||||
|
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
if (response.statusCode() < 200 || response.statusCode() >= 300) {
|
||||||
|
throw new IllegalStateException("Failed to issue IM token: HTTP " + response.statusCode());
|
||||||
|
}
|
||||||
|
var root = objectMapper.readTree(response.body());
|
||||||
|
if (root.path("code").asInt() != 200) {
|
||||||
|
throw new IllegalStateException("Failed to issue IM token: " + root.path("message").asText("unknown error"));
|
||||||
|
}
|
||||||
|
String token = root.path("data").path("token").asText(null);
|
||||||
|
if (token == null || token.isBlank()) {
|
||||||
|
throw new IllegalStateException("Failed to issue IM token: empty token");
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String encodeQuery(String value) {
|
||||||
|
return java.net.URLEncoder.encode(value == null ? "" : value, java.nio.charset.StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String platformEventsUserId(String appKey) {
|
||||||
|
return platformEventsUserPrefix + appKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String platformEventsSystemUserId() {
|
||||||
|
return platformEventsUserPrefix + platformEventsSystemUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record StoreReviewEventRequest(
|
||||||
|
String appKey,
|
||||||
|
String versionId,
|
||||||
|
String storeType,
|
||||||
|
String reviewState,
|
||||||
|
String reviewReason,
|
||||||
|
String stage,
|
||||||
|
String batchId,
|
||||||
|
String publishStatus,
|
||||||
|
String event,
|
||||||
|
String source
|
||||||
|
) {}
|
||||||
|
}
|
||||||
@ -0,0 +1,81 @@
|
|||||||
|
package com.xuqm.update.service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class StoreReviewImNotifier {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(StoreReviewImNotifier.class);
|
||||||
|
|
||||||
|
private final HttpClient httpClient = HttpClient.newHttpClient();
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
@Value("${sdk.tenant-service-url:http://xuqm-tenant-service:9001}")
|
||||||
|
private String tenantServiceUrl;
|
||||||
|
|
||||||
|
@Value("${sdk.internal-token:xuqm-internal-token}")
|
||||||
|
private String internalToken;
|
||||||
|
|
||||||
|
public void notifyStoreReviewChange(String appKey,
|
||||||
|
String versionId,
|
||||||
|
String storeType,
|
||||||
|
String reviewState,
|
||||||
|
String reviewReason,
|
||||||
|
String stage,
|
||||||
|
String batchId,
|
||||||
|
String publishStatus,
|
||||||
|
String event) {
|
||||||
|
try {
|
||||||
|
Map<String, Object> payload = new LinkedHashMap<>();
|
||||||
|
payload.put("appKey", appKey);
|
||||||
|
payload.put("versionId", versionId == null ? "" : versionId);
|
||||||
|
payload.put("storeType", storeType == null ? "" : storeType);
|
||||||
|
payload.put("reviewState", reviewState == null ? "" : reviewState);
|
||||||
|
payload.put("reviewReason", reviewReason == null ? "" : reviewReason);
|
||||||
|
payload.put("stage", stage == null ? "" : stage);
|
||||||
|
payload.put("batchId", batchId == null ? "" : batchId);
|
||||||
|
payload.put("publishStatus", publishStatus == null ? "" : publishStatus);
|
||||||
|
payload.put("event", event == null || event.isBlank() ? "store_review_update" : event);
|
||||||
|
payload.put("source", "update-service");
|
||||||
|
|
||||||
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(trimTrailingSlash(tenantServiceUrl) + "/api/internal/im/platform-events/notify"))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("X-Internal-Token", internalToken)
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(payload)))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
|
||||||
|
.thenAccept(response -> {
|
||||||
|
if (response.statusCode() >= 400) {
|
||||||
|
log.warn("IM platform event notify failed appKey={} status={} body={}",
|
||||||
|
appKey, response.statusCode(), response.body());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.exceptionally(e -> {
|
||||||
|
log.warn("IM platform event notify error appKey={} msg={}", appKey, e.getMessage());
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("IM platform event notify build failed appKey={} msg={}", appKey, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String trimTrailingSlash(String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return value.endsWith("/") ? value.substring(0, value.length() - 1) : value;
|
||||||
|
}
|
||||||
|
}
|
||||||
正在加载...
在新工单中引用
屏蔽一个用户