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;
|
||||
}
|
||||
}
|
||||
正在加载...
在新工单中引用
屏蔽一个用户