feat(deploy): 添加生产环境部署配置和联调环境切换功能
- 新增 .env.production.example 配置文件,包含所有微服务的数据库和Redis配置 - 添加 compose.production.yaml Docker Compose部署文件,定义web和各服务容器 - 实现Android SDK环境切换功能,支持外部服务和本地联调模式切换 - 添加推送注册状态管理和接收开关设置界面 - 集成演示服务的应用密钥客户端和认证服务实现 - 完善文档说明各SDK模块的集成和使用方法
这个提交包含在:
父节点
32b0e49e61
当前提交
f6c06db04b
1
Jenkinsfile
vendored
1
Jenkinsfile
vendored
@ -2,6 +2,7 @@ pipeline {
|
|||||||
agent any
|
agent any
|
||||||
|
|
||||||
parameters {
|
parameters {
|
||||||
|
string(name: 'BRANCH', defaultValue: 'main', description: 'Git 分支名')
|
||||||
choice(name: 'SERVICE', choices: ['tenant-service', 'im-service', 'push-service', 'update-service', 'demo-service', 'file-service'], description: '要构建的服务模块')
|
choice(name: 'SERVICE', choices: ['tenant-service', 'im-service', 'push-service', 'update-service', 'demo-service', 'file-service'], description: '要构建的服务模块')
|
||||||
string(name: 'IMAGE_TAG', defaultValue: 'latest', description: '镜像 Tag(如 v1.2.3 或 latest)')
|
string(name: 'IMAGE_TAG', defaultValue: 'latest', description: '镜像 Tag(如 v1.2.3 或 latest)')
|
||||||
booleanParam(name: 'DEPLOY', defaultValue: true, description: '构建后是否自动部署到生产服务器')
|
booleanParam(name: 'DEPLOY', defaultValue: true, description: '构建后是否自动部署到生产服务器')
|
||||||
|
|||||||
@ -21,7 +21,7 @@ public class DemoAppSecretClient {
|
|||||||
private final RestTemplate restTemplate;
|
private final RestTemplate restTemplate;
|
||||||
private final Map<String, String> cache = new ConcurrentHashMap<>();
|
private final Map<String, String> cache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
@Value("${demo.tenant-service-url:http://192.168.116.9:8081}")
|
@Value("${demo.tenant-service-url:http://127.0.0.1:8081}")
|
||||||
private String tenantServiceUrl;
|
private String tenantServiceUrl;
|
||||||
|
|
||||||
@Value("${demo.internal-token:xuqm-internal-token}")
|
@Value("${demo.internal-token:xuqm-internal-token}")
|
||||||
|
|||||||
@ -35,7 +35,7 @@ public class DemoAuthService {
|
|||||||
private final RestTemplate restTemplate;
|
private final RestTemplate restTemplate;
|
||||||
private final DemoAppSecretClient appSecretClient;
|
private final DemoAppSecretClient appSecretClient;
|
||||||
|
|
||||||
@Value("${demo.im-service-url:http://192.168.116.9:8082}")
|
@Value("${demo.im-service-url:http://127.0.0.1:8082}")
|
||||||
private String imServiceUrl;
|
private String imServiceUrl;
|
||||||
|
|
||||||
public DemoAuthService(DemoUserRepository userRepository,
|
public DemoAuthService(DemoUserRepository userRepository,
|
||||||
|
|||||||
@ -35,9 +35,9 @@ jwt:
|
|||||||
expiration: 86400000
|
expiration: 86400000
|
||||||
|
|
||||||
demo:
|
demo:
|
||||||
tenant-service-url: ${TENANT_SERVICE_URL:http://192.168.116.9:8081}
|
tenant-service-url: ${TENANT_SERVICE_URL:http://127.0.0.1:8081}
|
||||||
internal-token: ${SDK_INTERNAL_TOKEN:xuqm-internal-token}
|
internal-token: ${SDK_INTERNAL_TOKEN:xuqm-internal-token}
|
||||||
im-service-url: ${IM_SERVICE_URL:http://192.168.116.9:8082}
|
im-service-url: ${IM_SERVICE_URL:http://127.0.0.1:8082}
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
|
|||||||
@ -88,7 +88,6 @@
|
|||||||
| PUT | `/api/apps/{id}` | 是 | 更新应用 |
|
| PUT | `/api/apps/{id}` | 是 | 更新应用 |
|
||||||
| DELETE | `/api/apps/{id}` | 是 | 删除应用 |
|
| DELETE | `/api/apps/{id}` | 是 | 删除应用 |
|
||||||
| GET | `/api/apps/{appId}/services` | 是 | 服务列表 |
|
| GET | `/api/apps/{appId}/services` | 是 | 服务列表 |
|
||||||
| GET | `/api/apps/{appId}/services/item` | 是 | 按平台和服务类型查询单条服务配置 |
|
|
||||||
| PUT | `/api/apps/{appId}/services/config` | 是 | 更新服务配置,IM 和 UPDATE 走各自的配置模型 |
|
| PUT | `/api/apps/{appId}/services/config` | 是 | 更新服务配置,IM 和 UPDATE 走各自的配置模型 |
|
||||||
| POST | `/api/apps/{appId}/services/toggle` | 是 | 开关服务 |
|
| POST | `/api/apps/{appId}/services/toggle` | 是 | 开关服务 |
|
||||||
| POST | `/api/apps/{appId}/services/{id}/regenerate-key` | 是 | 重新生成服务密钥 |
|
| POST | `/api/apps/{appId}/services/{id}/regenerate-key` | 是 | 重新生成服务密钥 |
|
||||||
@ -173,6 +172,8 @@
|
|||||||
- 租户平台里的“发布配置”标签页保存灰度默认模式、成员目录同步回调和成员选择回调;当默认模式切到成员灰度时,至少要配置一个回调才允许保存,保存前也会做连通性校验。
|
- 租户平台里的“发布配置”标签页保存灰度默认模式、成员目录同步回调和成员选择回调;当默认模式切到成员灰度时,至少要配置一个回调才允许保存,保存前也会做连通性校验。
|
||||||
- 上下架、上传、发布、灰度、市场提交、商店配置变更都会写入 `update_operation_log`,可通过 `GET /api/v1/updates/ops/logs?appId=...` 查询。
|
- 上下架、上传、发布、灰度、市场提交、商店配置变更都会写入 `update_operation_log`,可通过 `GET /api/v1/updates/ops/logs?appId=...` 查询。
|
||||||
- 提交应用市场会真实调用已实现的厂商接口。小米、OPPO、vivo 和华为/荣耀当前支持服务端提交;App Store、Google Play、鸿蒙仍以跳转页和人工流程为主。
|
- 提交应用市场会真实调用已实现的厂商接口。小米、OPPO、vivo 和华为/荣耀当前支持服务端提交;App Store、Google Play、鸿蒙仍以跳转页和人工流程为主。
|
||||||
|
- 租户平台控制台新增 `GET /api/dashboard/stats`,返回当前租户的应用数、已开通服务数和子账号数,同时会写一条 `CONSOLE / DASHBOARD / VIEW_DASHBOARD` 操作日志。
|
||||||
|
- 租户平台“操作日志”菜单现在集中查看租户平台与版本管理两类日志;版本管理日志继续按 `appId` 查询,控制台访问日志则落在 `t_operation_log`。
|
||||||
|
|
||||||
## update-sdk 自动发版
|
## update-sdk 自动发版
|
||||||
|
|
||||||
|
|||||||
@ -54,7 +54,6 @@ public class SecurityConfig {
|
|||||||
config.setAllowedOriginPatterns(List.of(
|
config.setAllowedOriginPatterns(List.of(
|
||||||
"http://localhost:*",
|
"http://localhost:*",
|
||||||
"http://127.0.0.1:*",
|
"http://127.0.0.1:*",
|
||||||
"http://192.168.116.9:*",
|
|
||||||
"http://*.xuqinmin.com",
|
"http://*.xuqinmin.com",
|
||||||
"https://*.xuqinmin.com"
|
"https://*.xuqinmin.com"
|
||||||
));
|
));
|
||||||
|
|||||||
@ -41,7 +41,7 @@ jwt:
|
|||||||
|
|
||||||
file:
|
file:
|
||||||
upload-dir: ${FILE_UPLOAD_DIR:/tmp/xuqm-file-upload}
|
upload-dir: ${FILE_UPLOAD_DIR:/tmp/xuqm-file-upload}
|
||||||
base-url: ${FILE_BASE_URL:http://192.168.116.9:8086}
|
base-url: ${FILE_BASE_URL:https://file.dev.xuqinmin.com}
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
|
|||||||
@ -56,7 +56,8 @@ public class SecurityConfig {
|
|||||||
config.setAllowedOriginPatterns(List.of(
|
config.setAllowedOriginPatterns(List.of(
|
||||||
"http://localhost:*",
|
"http://localhost:*",
|
||||||
"http://127.0.0.1:*",
|
"http://127.0.0.1:*",
|
||||||
"http://192.168.116.9:*"
|
"http://*.xuqinmin.com",
|
||||||
|
"https://*.xuqinmin.com"
|
||||||
));
|
));
|
||||||
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
|
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
|
||||||
config.setAllowedHeaders(List.of("*"));
|
config.setAllowedHeaders(List.of("*"));
|
||||||
|
|||||||
@ -2,6 +2,8 @@ package com.xuqm.im.controller;
|
|||||||
|
|
||||||
import com.xuqm.common.exception.BusinessException;
|
import com.xuqm.common.exception.BusinessException;
|
||||||
import com.xuqm.common.model.ApiResponse;
|
import com.xuqm.common.model.ApiResponse;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||||
@ -12,6 +14,8 @@ import org.springframework.web.bind.annotation.RestControllerAdvice;
|
|||||||
@RestControllerAdvice
|
@RestControllerAdvice
|
||||||
public class GlobalExceptionHandler {
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
||||||
|
|
||||||
@ExceptionHandler(BusinessException.class)
|
@ExceptionHandler(BusinessException.class)
|
||||||
public ResponseEntity<ApiResponse<Void>> handleBusiness(BusinessException e) {
|
public ResponseEntity<ApiResponse<Void>> handleBusiness(BusinessException e) {
|
||||||
return ResponseEntity.status(resolveStatus(e.getCode()))
|
return ResponseEntity.status(resolveStatus(e.getCode()))
|
||||||
@ -40,8 +44,9 @@ public class GlobalExceptionHandler {
|
|||||||
|
|
||||||
@ExceptionHandler(Exception.class)
|
@ExceptionHandler(Exception.class)
|
||||||
public ResponseEntity<ApiResponse<Void>> handleException(Exception e) {
|
public ResponseEntity<ApiResponse<Void>> handleException(Exception e) {
|
||||||
|
log.error("Unhandled exception", e);
|
||||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||||
.body(ApiResponse.error(500, e.getMessage() == null ? "服务异常" : e.getMessage()));
|
.body(ApiResponse.error(500, "服务异常"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private HttpStatus resolveStatus(int code) {
|
private HttpStatus resolveStatus(int code) {
|
||||||
|
|||||||
@ -21,7 +21,7 @@ public class ImAppSecretClient {
|
|||||||
private final RestTemplate restTemplate = new RestTemplate();
|
private final RestTemplate restTemplate = new RestTemplate();
|
||||||
private final Map<String, String> cache = new ConcurrentHashMap<>();
|
private final Map<String, String> cache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
@Value("${im.tenant-service-url:http://192.168.116.9:8081}")
|
@Value("${im.tenant-service-url:http://127.0.0.1:8081}")
|
||||||
private String tenantServiceUrl;
|
private String tenantServiceUrl;
|
||||||
|
|
||||||
@Value("${im.internal-token:xuqm-internal-token}")
|
@Value("${im.internal-token:xuqm-internal-token}")
|
||||||
|
|||||||
@ -2,6 +2,8 @@ package com.xuqm.im.service;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.http.HttpEntity;
|
import org.springframework.http.HttpEntity;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
@ -14,11 +16,12 @@ import org.springframework.web.util.UriComponentsBuilder;
|
|||||||
@Component
|
@Component
|
||||||
public class ImFeatureConfigClient {
|
public class ImFeatureConfigClient {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(ImFeatureConfigClient.class);
|
||||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||||
|
|
||||||
private final RestTemplate restTemplate;
|
private final RestTemplate restTemplate;
|
||||||
|
|
||||||
@Value("${im.tenant-service-url:http://192.168.116.9:8081}")
|
@Value("${im.tenant-service-url:http://127.0.0.1:8081}")
|
||||||
private String tenantServiceUrl;
|
private String tenantServiceUrl;
|
||||||
|
|
||||||
@Value("${im.internal-token:xuqm-internal-token}")
|
@Value("${im.internal-token:xuqm-internal-token}")
|
||||||
@ -93,7 +96,7 @@ public class ImFeatureConfigClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// Fail closed: if config cannot be read, keep the feature disabled.
|
log.warn("Failed to read IM feature config for appId={}: {}", appId, e.getMessage());
|
||||||
}
|
}
|
||||||
return OBJECT_MAPPER.createObjectNode();
|
return OBJECT_MAPPER.createObjectNode();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
package com.xuqm.im.service;
|
package com.xuqm.im.service;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.client.RestClientException;
|
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
@ -16,10 +17,12 @@ import java.util.Map;
|
|||||||
@Component
|
@Component
|
||||||
public class ImPushBridgeClient {
|
public class ImPushBridgeClient {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(ImPushBridgeClient.class);
|
||||||
|
|
||||||
private final HttpClient httpClient = HttpClient.newHttpClient();
|
private final HttpClient httpClient = HttpClient.newHttpClient();
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
@Value("${im.push-service-url:http://192.168.116.9:8083}")
|
@Value("${im.push-service-url:http://127.0.0.1:8083}")
|
||||||
private String pushServiceUrl;
|
private String pushServiceUrl;
|
||||||
|
|
||||||
@Value("${im.internal-token:xuqm-internal-token}")
|
@Value("${im.internal-token:xuqm-internal-token}")
|
||||||
@ -48,7 +51,8 @@ public class ImPushBridgeClient {
|
|||||||
.POST(HttpRequest.BodyPublishers.ofString(json))
|
.POST(HttpRequest.BodyPublishers.ofString(json))
|
||||||
.build();
|
.build();
|
||||||
httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
} catch (Exception ignored) {
|
} catch (Exception e) {
|
||||||
|
log.warn("Failed to notify push-service for appId={}, userIds={}: {}", appId, userIds, e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,9 +44,9 @@ jwt:
|
|||||||
expiration: 86400000
|
expiration: 86400000
|
||||||
|
|
||||||
im:
|
im:
|
||||||
tenant-service-url: ${TENANT_SERVICE_URL:http://192.168.116.9:8081}
|
tenant-service-url: ${TENANT_SERVICE_URL:http://127.0.0.1:8081}
|
||||||
internal-token: ${SDK_INTERNAL_TOKEN:xuqm-internal-token}
|
internal-token: ${SDK_INTERNAL_TOKEN:xuqm-internal-token}
|
||||||
push-service-url: ${PUSH_SERVICE_URL:http://192.168.116.9:8083}
|
push-service-url: ${PUSH_SERVICE_URL:http://127.0.0.1:8083}
|
||||||
multi-login: true
|
multi-login: true
|
||||||
message-history-days: 30
|
message-history-days: 30
|
||||||
webhook-timeout-ms: 3000
|
webhook-timeout-ms: 3000
|
||||||
|
|||||||
@ -0,0 +1,27 @@
|
|||||||
|
package com.xuqm.tenant.controller;
|
||||||
|
|
||||||
|
import com.xuqm.common.model.ApiResponse;
|
||||||
|
import com.xuqm.tenant.service.DashboardService;
|
||||||
|
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.RestController;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/dashboard")
|
||||||
|
public class DashboardController {
|
||||||
|
|
||||||
|
private final DashboardService dashboardService;
|
||||||
|
|
||||||
|
public DashboardController(DashboardService dashboardService) {
|
||||||
|
this.dashboardService = dashboardService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/stats")
|
||||||
|
public ResponseEntity<ApiResponse<Map<String, Object>>> stats(@AuthenticationPrincipal String tenantId) {
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(dashboardService.stats(tenantId)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,7 +2,11 @@ package com.xuqm.tenant.controller;
|
|||||||
|
|
||||||
import com.xuqm.common.exception.BusinessException;
|
import com.xuqm.common.exception.BusinessException;
|
||||||
import com.xuqm.common.model.ApiResponse;
|
import com.xuqm.common.model.ApiResponse;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||||
import org.springframework.validation.FieldError;
|
import org.springframework.validation.FieldError;
|
||||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
@ -13,9 +17,11 @@ import java.util.stream.Collectors;
|
|||||||
@RestControllerAdvice
|
@RestControllerAdvice
|
||||||
public class GlobalExceptionHandler {
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
||||||
|
|
||||||
@ExceptionHandler(BusinessException.class)
|
@ExceptionHandler(BusinessException.class)
|
||||||
public ResponseEntity<ApiResponse<Void>> handle(BusinessException ex) {
|
public ResponseEntity<ApiResponse<Void>> handle(BusinessException ex) {
|
||||||
return ResponseEntity.status(ex.getCode())
|
return ResponseEntity.status(resolveStatus(ex.getCode()))
|
||||||
.body(ApiResponse.error(ex.getCode(), ex.getMessage()));
|
.body(ApiResponse.error(ex.getCode(), ex.getMessage()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,9 +33,31 @@ public class GlobalExceptionHandler {
|
|||||||
return ResponseEntity.badRequest().body(ApiResponse.badRequest(message));
|
return ResponseEntity.badRequest().body(ApiResponse.badRequest(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(IllegalArgumentException.class)
|
||||||
|
public ResponseEntity<ApiResponse<Void>> handle(IllegalArgumentException ex) {
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(ApiResponse.badRequest(ex.getMessage() == null ? "参数错误" : ex.getMessage()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(HttpMessageNotReadableException.class)
|
||||||
|
public ResponseEntity<ApiResponse<Void>> handle(HttpMessageNotReadableException ex) {
|
||||||
|
return ResponseEntity.badRequest().body(ApiResponse.badRequest("请求体格式错误"));
|
||||||
|
}
|
||||||
|
|
||||||
@ExceptionHandler(Exception.class)
|
@ExceptionHandler(Exception.class)
|
||||||
public ResponseEntity<ApiResponse<Void>> handle(Exception ex) {
|
public ResponseEntity<ApiResponse<Void>> handle(Exception ex) {
|
||||||
|
log.error("Unhandled exception", ex);
|
||||||
return ResponseEntity.internalServerError()
|
return ResponseEntity.internalServerError()
|
||||||
.body(ApiResponse.error(500, "服务器内部错误: " + ex.getMessage()));
|
.body(ApiResponse.error(500, "服务器内部错误"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpStatus resolveStatus(int code) {
|
||||||
|
return switch (code) {
|
||||||
|
case 400 -> HttpStatus.BAD_REQUEST;
|
||||||
|
case 401 -> HttpStatus.UNAUTHORIZED;
|
||||||
|
case 403 -> HttpStatus.FORBIDDEN;
|
||||||
|
case 404 -> HttpStatus.NOT_FOUND;
|
||||||
|
default -> HttpStatus.INTERNAL_SERVER_ERROR;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,7 +28,7 @@ public class SdkConfigController {
|
|||||||
@Value("${sdk.im-ws-url:wss://im.dev.xuqinmin.com/ws/im}")
|
@Value("${sdk.im-ws-url:wss://im.dev.xuqinmin.com/ws/im}")
|
||||||
private String imWsUrl;
|
private String imWsUrl;
|
||||||
|
|
||||||
@Value("${sdk.file-service-url:http://192.168.116.9:8086}")
|
@Value("${sdk.file-service-url:https://file.dev.xuqinmin.com}")
|
||||||
private String fileServiceUrl;
|
private String fileServiceUrl;
|
||||||
|
|
||||||
@Value("${sdk.im-api-url:https://im.dev.xuqinmin.com}")
|
@Value("${sdk.im-api-url:https://im.dev.xuqinmin.com}")
|
||||||
|
|||||||
@ -19,6 +19,7 @@ public interface TenantRepository extends JpaRepository<TenantEntity, String> {
|
|||||||
boolean existsByUsername(String username);
|
boolean existsByUsername(String username);
|
||||||
boolean existsByEmail(String email);
|
boolean existsByEmail(String email);
|
||||||
List<TenantEntity> findByParentId(String parentId);
|
List<TenantEntity> findByParentId(String parentId);
|
||||||
|
long countByParentId(String parentId);
|
||||||
|
|
||||||
@Query("SELECT t FROM TenantEntity t WHERE " +
|
@Query("SELECT t FROM TenantEntity t WHERE " +
|
||||||
"(:keyword IS NULL OR :keyword = '' OR t.username LIKE %:keyword% OR t.email LIKE %:keyword%)")
|
"(:keyword IS NULL OR :keyword = '' OR t.username LIKE %:keyword% OR t.email LIKE %:keyword%)")
|
||||||
|
|||||||
@ -0,0 +1,50 @@
|
|||||||
|
package com.xuqm.tenant.service;
|
||||||
|
|
||||||
|
import com.xuqm.tenant.entity.AppEntity;
|
||||||
|
import com.xuqm.tenant.entity.FeatureServiceEntity;
|
||||||
|
import com.xuqm.tenant.repository.AppRepository;
|
||||||
|
import com.xuqm.tenant.repository.FeatureServiceRepository;
|
||||||
|
import com.xuqm.tenant.repository.TenantRepository;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class DashboardService {
|
||||||
|
|
||||||
|
private final AppRepository appRepository;
|
||||||
|
private final FeatureServiceRepository featureServiceRepository;
|
||||||
|
private final TenantRepository tenantRepository;
|
||||||
|
private final OperationLogService operationLogService;
|
||||||
|
|
||||||
|
public DashboardService(AppRepository appRepository,
|
||||||
|
FeatureServiceRepository featureServiceRepository,
|
||||||
|
TenantRepository tenantRepository,
|
||||||
|
OperationLogService operationLogService) {
|
||||||
|
this.appRepository = appRepository;
|
||||||
|
this.featureServiceRepository = featureServiceRepository;
|
||||||
|
this.tenantRepository = tenantRepository;
|
||||||
|
this.operationLogService = operationLogService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> stats(String tenantId) {
|
||||||
|
List<AppEntity> apps = appRepository.findByTenantId(tenantId);
|
||||||
|
long serviceCount = 0;
|
||||||
|
for (AppEntity app : apps) {
|
||||||
|
serviceCount += featureServiceRepository.findByAppId(app.getId()).stream()
|
||||||
|
.filter(FeatureServiceEntity::isEnabled)
|
||||||
|
.count();
|
||||||
|
}
|
||||||
|
long subAccountCount = tenantRepository.countByParentId(tenantId);
|
||||||
|
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
result.put("appCount", apps.size());
|
||||||
|
result.put("serviceCount", serviceCount);
|
||||||
|
result.put("subAccountCount", subAccountCount);
|
||||||
|
|
||||||
|
operationLogService.record(tenantId, "CONSOLE", "DASHBOARD", tenantId, "VIEW_DASHBOARD", result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -86,5 +86,5 @@ sdk:
|
|||||||
bootstrap-app-package: ${SDK_BOOTSTRAP_APP_PACKAGE:com.xuqm.demo}
|
bootstrap-app-package: ${SDK_BOOTSTRAP_APP_PACKAGE:com.xuqm.demo}
|
||||||
bootstrap-app-description: ${SDK_BOOTSTRAP_APP_DESCRIPTION:XuqmGroup demo app}
|
bootstrap-app-description: ${SDK_BOOTSTRAP_APP_DESCRIPTION:XuqmGroup demo app}
|
||||||
im-ws-url: ${SDK_IM_WS_URL:wss://im.dev.xuqinmin.com/ws/im}
|
im-ws-url: ${SDK_IM_WS_URL:wss://im.dev.xuqinmin.com/ws/im}
|
||||||
file-service-url: ${SDK_FILE_SERVICE_URL:http://192.168.116.9:8086}
|
file-service-url: ${SDK_FILE_SERVICE_URL:https://file.dev.xuqinmin.com}
|
||||||
im-api-url: ${SDK_IM_API_URL:https://im.dev.xuqinmin.com}
|
im-api-url: ${SDK_IM_API_URL:https://im.dev.xuqinmin.com}
|
||||||
|
|||||||
@ -59,7 +59,6 @@ public class SecurityConfig {
|
|||||||
config.setAllowedOriginPatterns(List.of(
|
config.setAllowedOriginPatterns(List.of(
|
||||||
"http://localhost:*",
|
"http://localhost:*",
|
||||||
"http://127.0.0.1:*",
|
"http://127.0.0.1:*",
|
||||||
"http://192.168.116.9:*",
|
|
||||||
"http://*.xuqinmin.com",
|
"http://*.xuqinmin.com",
|
||||||
"https://*.xuqinmin.com"
|
"https://*.xuqinmin.com"
|
||||||
));
|
));
|
||||||
|
|||||||
@ -9,6 +9,9 @@ import com.xuqm.update.service.StoreSubmissionService;
|
|||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@ -91,8 +94,7 @@ public class AppStoreController {
|
|||||||
@PathVariable String versionId,
|
@PathVariable String versionId,
|
||||||
@RequestBody Map<String, Object> body) throws Exception {
|
@RequestBody Map<String, Object> body) throws Exception {
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
List<String> storeTypes = extractStringList(body, "storeTypes");
|
||||||
List<String> storeTypes = (List<String>) body.get("storeTypes");
|
|
||||||
return ResponseEntity.ok(ApiResponse.success(storeService.markSubmitted(versionId, storeTypes)));
|
return ResponseEntity.ok(ApiResponse.success(storeService.markSubmitted(versionId, storeTypes)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,8 +112,7 @@ public class AppStoreController {
|
|||||||
@PathVariable String versionId,
|
@PathVariable String versionId,
|
||||||
@RequestBody(required = false) Map<String, Object> body) throws Exception {
|
@RequestBody(required = false) Map<String, Object> body) throws Exception {
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
List<String> storeTypes = body != null ? extractStringList(body, "storeTypes") : null;
|
||||||
List<String> storeTypes = body != null ? (List<String>) body.get("storeTypes") : null;
|
|
||||||
String submitMode = body != null ? (String) body.get("submitMode") : null;
|
String submitMode = body != null ? (String) body.get("submitMode") : null;
|
||||||
String scheduledAtText = body != null ? (String) body.get("scheduledPublishAt") : null;
|
String scheduledAtText = body != null ? (String) body.get("scheduledPublishAt") : null;
|
||||||
Boolean autoPublishAfterReview = body != null && body.get("autoPublishAfterReview") != null
|
Boolean autoPublishAfterReview = body != null && body.get("autoPublishAfterReview") != null
|
||||||
@ -145,23 +146,44 @@ public class AppStoreController {
|
|||||||
@PathVariable String versionId,
|
@PathVariable String versionId,
|
||||||
@RequestBody Map<String, Object> body) throws Exception {
|
@RequestBody Map<String, Object> body) throws Exception {
|
||||||
|
|
||||||
String storeType = (String) body.get("storeType");
|
String storeType = body.get("storeType") instanceof String s ? s : null;
|
||||||
AppVersionEntity.StoreReviewState state =
|
if (storeType == null || storeType.isBlank()) throw new IllegalArgumentException("storeType 不能为空");
|
||||||
AppVersionEntity.StoreReviewState.valueOf((String) body.get("state"));
|
String stateStr = body.get("state") instanceof String s ? s : null;
|
||||||
|
if (stateStr == null) throw new IllegalArgumentException("state 不能为空");
|
||||||
|
AppVersionEntity.StoreReviewState state;
|
||||||
|
try {
|
||||||
|
state = AppVersionEntity.StoreReviewState.valueOf(stateStr);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
throw new IllegalArgumentException("无效的审核状态: " + stateStr);
|
||||||
|
}
|
||||||
String reason = body.get("reason") == null ? null : body.get("reason").toString();
|
String reason = body.get("reason") == null ? null : body.get("reason").toString();
|
||||||
return ResponseEntity.ok(ApiResponse.success(
|
return ResponseEntity.ok(ApiResponse.success(
|
||||||
storeService.updateStoreReview(versionId, storeType, state, reason)));
|
storeService.updateStoreReview(versionId, storeType, state, reason)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<String> extractStringList(Map<String, Object> body, String key) {
|
||||||
|
Object value = body.get(key);
|
||||||
|
if (value instanceof List<?> list) {
|
||||||
|
return list.stream()
|
||||||
|
.filter(item -> item instanceof String)
|
||||||
|
.map(item -> (String) item)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||||
|
private static final TypeReference<Map<String, Object>> MAP_TYPE = new TypeReference<>() {};
|
||||||
|
|
||||||
private void validateReviewWebhook(String configJson) {
|
private void validateReviewWebhook(String configJson) {
|
||||||
try {
|
try {
|
||||||
@SuppressWarnings("unchecked")
|
Map<String, Object> config = OBJECT_MAPPER.readValue(configJson, MAP_TYPE);
|
||||||
Map<String, Object> config = new com.fasterxml.jackson.databind.ObjectMapper()
|
|
||||||
.readValue(configJson, Map.class);
|
|
||||||
String webhookUrl = config.get("webhookUrl") == null ? "" : config.get("webhookUrl").toString().trim();
|
String webhookUrl = config.get("webhookUrl") == null ? "" : config.get("webhookUrl").toString().trim();
|
||||||
if (!webhookUrl.isBlank()) {
|
if (!webhookUrl.isBlank()) {
|
||||||
connectivityValidationService.validateCallbackUrl(webhookUrl, "审核通知");
|
connectivityValidationService.validateCallbackUrl(webhookUrl, "审核通知");
|
||||||
}
|
}
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
throw e;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new IllegalArgumentException("invalid review webhook config: " + e.getMessage(), e);
|
throw new IllegalArgumentException("invalid review webhook config: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
@ -172,16 +194,12 @@ public class AppStoreController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@SuppressWarnings("unchecked")
|
OBJECT_MAPPER.readValue(configJson, MAP_TYPE);
|
||||||
Map<String, Object> config = new com.fasterxml.jackson.databind.ObjectMapper()
|
|
||||||
.readValue(configJson, Map.class);
|
|
||||||
if (storeType == AppStoreConfigEntity.StoreType.REVIEW_WEBHOOK) {
|
|
||||||
validateReviewWebhook(configJson);
|
|
||||||
}
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
throw e;
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new IllegalArgumentException("invalid store config payload: " + e.getMessage(), e);
|
throw new IllegalArgumentException("invalid store config payload: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
|
if (storeType == AppStoreConfigEntity.StoreType.REVIEW_WEBHOOK) {
|
||||||
|
validateReviewWebhook(configJson);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeParseException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@ -139,7 +140,11 @@ public class AppVersionController {
|
|||||||
entity.setGrayMode("PERCENT");
|
entity.setGrayMode("PERCENT");
|
||||||
entity.setGrayMemberIds(null);
|
entity.setGrayMemberIds(null);
|
||||||
if (scheduledPublishAt != null && !scheduledPublishAt.isBlank()) {
|
if (scheduledPublishAt != null && !scheduledPublishAt.isBlank()) {
|
||||||
entity.setScheduledPublishAt(LocalDateTime.parse(scheduledPublishAt));
|
try {
|
||||||
|
entity.setScheduledPublishAt(LocalDateTime.parse(scheduledPublishAt));
|
||||||
|
} catch (DateTimeParseException e) {
|
||||||
|
throw new IllegalArgumentException("scheduledPublishAt 格式无效,应为 ISO-8601(如 2026-05-01T10:00:00)");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
entity.setWebhookUrl(webhookUrl);
|
entity.setWebhookUrl(webhookUrl);
|
||||||
entity.setStoreSubmitTargets(storeSubmitTargets);
|
entity.setStoreSubmitTargets(storeSubmitTargets);
|
||||||
@ -206,7 +211,11 @@ public class AppVersionController {
|
|||||||
} else {
|
} else {
|
||||||
entity.setPublishStatus(AppVersionEntity.PublishStatus.DRAFT);
|
entity.setPublishStatus(AppVersionEntity.PublishStatus.DRAFT);
|
||||||
if (scheduledPublishAt != null && !scheduledPublishAt.isBlank()) {
|
if (scheduledPublishAt != null && !scheduledPublishAt.isBlank()) {
|
||||||
entity.setScheduledPublishAt(LocalDateTime.parse(scheduledPublishAt));
|
try {
|
||||||
|
entity.setScheduledPublishAt(LocalDateTime.parse(scheduledPublishAt));
|
||||||
|
} catch (DateTimeParseException e) {
|
||||||
|
throw new IllegalArgumentException("scheduledPublishAt 格式无效,应为 ISO-8601(如 2026-05-01T10:00:00)");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
entity.setGrayEnabled(false);
|
entity.setGrayEnabled(false);
|
||||||
|
|||||||
@ -0,0 +1,62 @@
|
|||||||
|
package com.xuqm.update.controller;
|
||||||
|
|
||||||
|
import com.xuqm.common.exception.BusinessException;
|
||||||
|
import com.xuqm.common.model.ApiResponse;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||||
|
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@RestControllerAdvice
|
||||||
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
||||||
|
|
||||||
|
@ExceptionHandler(BusinessException.class)
|
||||||
|
public ResponseEntity<ApiResponse<Void>> handle(BusinessException ex) {
|
||||||
|
return ResponseEntity.status(resolveStatus(ex.getCode()))
|
||||||
|
.body(ApiResponse.error(ex.getCode(), ex.getMessage()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||||
|
public ResponseEntity<ApiResponse<Void>> handle(MethodArgumentNotValidException ex) {
|
||||||
|
String message = ex.getBindingResult().getFieldErrors().stream()
|
||||||
|
.map(f -> f.getDefaultMessage())
|
||||||
|
.collect(Collectors.joining("; "));
|
||||||
|
return ResponseEntity.badRequest().body(ApiResponse.badRequest(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(IllegalArgumentException.class)
|
||||||
|
public ResponseEntity<ApiResponse<Void>> handle(IllegalArgumentException ex) {
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(ApiResponse.badRequest(ex.getMessage() == null ? "参数错误" : ex.getMessage()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(HttpMessageNotReadableException.class)
|
||||||
|
public ResponseEntity<ApiResponse<Void>> handle(HttpMessageNotReadableException ex) {
|
||||||
|
return ResponseEntity.badRequest().body(ApiResponse.badRequest("请求体格式错误"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(Exception.class)
|
||||||
|
public ResponseEntity<ApiResponse<Void>> handle(Exception ex) {
|
||||||
|
log.error("Unhandled exception", ex);
|
||||||
|
return ResponseEntity.internalServerError()
|
||||||
|
.body(ApiResponse.error(500, "服务器内部错误"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpStatus resolveStatus(int code) {
|
||||||
|
return switch (code) {
|
||||||
|
case 400 -> HttpStatus.BAD_REQUEST;
|
||||||
|
case 401 -> HttpStatus.UNAUTHORIZED;
|
||||||
|
case 403 -> HttpStatus.FORBIDDEN;
|
||||||
|
case 404 -> HttpStatus.NOT_FOUND;
|
||||||
|
default -> HttpStatus.INTERNAL_SERVER_ERROR;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,7 +2,6 @@ package com.xuqm.update.controller;
|
|||||||
|
|
||||||
import com.xuqm.common.model.ApiResponse;
|
import com.xuqm.common.model.ApiResponse;
|
||||||
import com.xuqm.update.entity.AppPublishConfigEntity;
|
import com.xuqm.update.entity.AppPublishConfigEntity;
|
||||||
import com.xuqm.update.service.ConnectivityValidationService;
|
|
||||||
import com.xuqm.update.service.PublishConfigService;
|
import com.xuqm.update.service.PublishConfigService;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
@ -15,12 +14,9 @@ import java.util.Map;
|
|||||||
public class PublishConfigController {
|
public class PublishConfigController {
|
||||||
|
|
||||||
private final PublishConfigService publishConfigService;
|
private final PublishConfigService publishConfigService;
|
||||||
private final ConnectivityValidationService connectivityValidationService;
|
|
||||||
|
|
||||||
public PublishConfigController(PublishConfigService publishConfigService,
|
public PublishConfigController(PublishConfigService publishConfigService) {
|
||||||
ConnectivityValidationService connectivityValidationService) {
|
|
||||||
this.publishConfigService = publishConfigService;
|
this.publishConfigService = publishConfigService;
|
||||||
this.connectivityValidationService = connectivityValidationService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/publish/config")
|
@GetMapping("/publish/config")
|
||||||
@ -64,11 +60,15 @@ public class PublishConfigController {
|
|||||||
}
|
}
|
||||||
String selectCallback = body.get("graySelectCallbackUrl") == null ? "" : body.get("graySelectCallbackUrl").toString().trim();
|
String selectCallback = body.get("graySelectCallbackUrl") == null ? "" : body.get("graySelectCallbackUrl").toString().trim();
|
||||||
String syncCallback = body.get("grayDirectorySyncCallbackUrl") == null ? "" : body.get("grayDirectorySyncCallbackUrl").toString().trim();
|
String syncCallback = body.get("grayDirectorySyncCallbackUrl") == null ? "" : body.get("grayDirectorySyncCallbackUrl").toString().trim();
|
||||||
if (!selectCallback.isBlank()) {
|
if (!selectCallback.isBlank() && !isHttpUrl(selectCallback)) {
|
||||||
connectivityValidationService.validateCallbackUrl(selectCallback, "灰度成员选择回调");
|
throw new IllegalArgumentException("graySelectCallbackUrl must start with http:// or https://");
|
||||||
}
|
}
|
||||||
if (!syncCallback.isBlank()) {
|
if (!syncCallback.isBlank() && !isHttpUrl(syncCallback)) {
|
||||||
connectivityValidationService.validateCallbackUrl(syncCallback, "灰度成员同步回调");
|
throw new IllegalArgumentException("grayDirectorySyncCallbackUrl must start with http:// or https://");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isHttpUrl(String value) {
|
||||||
|
return value.startsWith("http://") || value.startsWith("https://");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,6 +24,10 @@ public class ConnectivityValidationService {
|
|||||||
if (url == null || url.isBlank()) {
|
if (url == null || url.isBlank()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
String normalized = url.trim();
|
||||||
|
if (!(normalized.startsWith("http://") || normalized.startsWith("https://"))) {
|
||||||
|
throw new IllegalArgumentException(label + " must start with http:// or https://");
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
Map<String, Object> body = new LinkedHashMap<>();
|
Map<String, Object> body = new LinkedHashMap<>();
|
||||||
body.put("probe", true);
|
body.put("probe", true);
|
||||||
@ -31,7 +35,7 @@ public class ConnectivityValidationService {
|
|||||||
body.put("timestamp", System.currentTimeMillis());
|
body.put("timestamp", System.currentTimeMillis());
|
||||||
String payload = objectMapper.writeValueAsString(body);
|
String payload = objectMapper.writeValueAsString(body);
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
.uri(URI.create(url))
|
.uri(URI.create(normalized))
|
||||||
.timeout(Duration.ofSeconds(5))
|
.timeout(Duration.ofSeconds(5))
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.header("X-Xuqm-Connectivity-Check", "true")
|
.header("X-Xuqm-Connectivity-Check", "true")
|
||||||
@ -43,7 +47,7 @@ public class ConnectivityValidationService {
|
|||||||
throw new IllegalStateException("connectivity check failed with HTTP " + status);
|
throw new IllegalStateException("connectivity check failed with HTTP " + status);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new IllegalArgumentException("cannot connect to " + label + ": " + url, e);
|
throw new IllegalArgumentException("cannot connect to " + label + ": " + normalized, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户