From f6c06db04b0cefabe486cd9ec2880666556b2038 Mon Sep 17 00:00:00 2001 From: XuqmGroup Date: Thu, 30 Apr 2026 11:47:01 +0800 Subject: [PATCH] =?UTF-8?q?feat(deploy):=20=E6=B7=BB=E5=8A=A0=E7=94=9F?= =?UTF-8?q?=E4=BA=A7=E7=8E=AF=E5=A2=83=E9=83=A8=E7=BD=B2=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E5=92=8C=E8=81=94=E8=B0=83=E7=8E=AF=E5=A2=83=E5=88=87=E6=8D=A2?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 .env.production.example 配置文件,包含所有微服务的数据库和Redis配置 - 添加 compose.production.yaml Docker Compose部署文件,定义web和各服务容器 - 实现Android SDK环境切换功能,支持外部服务和本地联调模式切换 - 添加推送注册状态管理和接收开关设置界面 - 集成演示服务的应用密钥客户端和认证服务实现 - 完善文档说明各SDK模块的集成和使用方法 --- Jenkinsfile | 1 + .../demo/service/DemoAppSecretClient.java | 2 +- .../xuqm/demo/service/DemoAuthService.java | 2 +- .../src/main/resources/application.yml | 4 +- docs/API_ACCESS.md | 3 +- .../com/xuqm/file/config/SecurityConfig.java | 1 - .../src/main/resources/application.yml | 2 +- .../com/xuqm/im/config/SecurityConfig.java | 3 +- .../im/controller/GlobalExceptionHandler.java | 7 ++- .../xuqm/im/service/ImAppSecretClient.java | 2 +- .../im/service/ImFeatureConfigClient.java | 7 ++- .../xuqm/im/service/ImPushBridgeClient.java | 10 ++- im-service/src/main/resources/application.yml | 4 +- .../controller/DashboardController.java | 27 ++++++++ .../controller/GlobalExceptionHandler.java | 32 +++++++++- .../controller/SdkConfigController.java | 2 +- .../tenant/repository/TenantRepository.java | 1 + .../xuqm/tenant/service/DashboardService.java | 50 +++++++++++++++ .../src/main/resources/application.yml | 2 +- .../xuqm/update/config/SecurityConfig.java | 1 - .../update/controller/AppStoreController.java | 54 ++++++++++------ .../controller/AppVersionController.java | 13 +++- .../controller/GlobalExceptionHandler.java | 62 +++++++++++++++++++ .../controller/PublishConfigController.java | 18 +++--- .../ConnectivityValidationService.java | 8 ++- 25 files changed, 265 insertions(+), 53 deletions(-) create mode 100644 tenant-service/src/main/java/com/xuqm/tenant/controller/DashboardController.java create mode 100644 tenant-service/src/main/java/com/xuqm/tenant/service/DashboardService.java create mode 100644 update-service/src/main/java/com/xuqm/update/controller/GlobalExceptionHandler.java diff --git a/Jenkinsfile b/Jenkinsfile index 628391d..839be5d 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -2,6 +2,7 @@ pipeline { agent any 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: '要构建的服务模块') string(name: 'IMAGE_TAG', defaultValue: 'latest', description: '镜像 Tag(如 v1.2.3 或 latest)') booleanParam(name: 'DEPLOY', defaultValue: true, description: '构建后是否自动部署到生产服务器') diff --git a/demo-service/src/main/java/com/xuqm/demo/service/DemoAppSecretClient.java b/demo-service/src/main/java/com/xuqm/demo/service/DemoAppSecretClient.java index faa3101..cde1c68 100644 --- a/demo-service/src/main/java/com/xuqm/demo/service/DemoAppSecretClient.java +++ b/demo-service/src/main/java/com/xuqm/demo/service/DemoAppSecretClient.java @@ -21,7 +21,7 @@ public class DemoAppSecretClient { private final RestTemplate restTemplate; private final Map 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; @Value("${demo.internal-token:xuqm-internal-token}") diff --git a/demo-service/src/main/java/com/xuqm/demo/service/DemoAuthService.java b/demo-service/src/main/java/com/xuqm/demo/service/DemoAuthService.java index 6b00929..e956ce3 100644 --- a/demo-service/src/main/java/com/xuqm/demo/service/DemoAuthService.java +++ b/demo-service/src/main/java/com/xuqm/demo/service/DemoAuthService.java @@ -35,7 +35,7 @@ public class DemoAuthService { private final RestTemplate restTemplate; 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; public DemoAuthService(DemoUserRepository userRepository, diff --git a/demo-service/src/main/resources/application.yml b/demo-service/src/main/resources/application.yml index 7ec0728..3151cb9 100644 --- a/demo-service/src/main/resources/application.yml +++ b/demo-service/src/main/resources/application.yml @@ -35,9 +35,9 @@ jwt: expiration: 86400000 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} - 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: level: diff --git a/docs/API_ACCESS.md b/docs/API_ACCESS.md index 8e2d0e5..87a9157 100644 --- a/docs/API_ACCESS.md +++ b/docs/API_ACCESS.md @@ -88,7 +88,6 @@ | PUT | `/api/apps/{id}` | 是 | 更新应用 | | DELETE | `/api/apps/{id}` | 是 | 删除应用 | | GET | `/api/apps/{appId}/services` | 是 | 服务列表 | -| GET | `/api/apps/{appId}/services/item` | 是 | 按平台和服务类型查询单条服务配置 | | PUT | `/api/apps/{appId}/services/config` | 是 | 更新服务配置,IM 和 UPDATE 走各自的配置模型 | | POST | `/api/apps/{appId}/services/toggle` | 是 | 开关服务 | | POST | `/api/apps/{appId}/services/{id}/regenerate-key` | 是 | 重新生成服务密钥 | @@ -173,6 +172,8 @@ - 租户平台里的“发布配置”标签页保存灰度默认模式、成员目录同步回调和成员选择回调;当默认模式切到成员灰度时,至少要配置一个回调才允许保存,保存前也会做连通性校验。 - 上下架、上传、发布、灰度、市场提交、商店配置变更都会写入 `update_operation_log`,可通过 `GET /api/v1/updates/ops/logs?appId=...` 查询。 - 提交应用市场会真实调用已实现的厂商接口。小米、OPPO、vivo 和华为/荣耀当前支持服务端提交;App Store、Google Play、鸿蒙仍以跳转页和人工流程为主。 +- 租户平台控制台新增 `GET /api/dashboard/stats`,返回当前租户的应用数、已开通服务数和子账号数,同时会写一条 `CONSOLE / DASHBOARD / VIEW_DASHBOARD` 操作日志。 +- 租户平台“操作日志”菜单现在集中查看租户平台与版本管理两类日志;版本管理日志继续按 `appId` 查询,控制台访问日志则落在 `t_operation_log`。 ## update-sdk 自动发版 diff --git a/file-service/src/main/java/com/xuqm/file/config/SecurityConfig.java b/file-service/src/main/java/com/xuqm/file/config/SecurityConfig.java index 5a5ee67..3dda731 100644 --- a/file-service/src/main/java/com/xuqm/file/config/SecurityConfig.java +++ b/file-service/src/main/java/com/xuqm/file/config/SecurityConfig.java @@ -54,7 +54,6 @@ public class SecurityConfig { config.setAllowedOriginPatterns(List.of( "http://localhost:*", "http://127.0.0.1:*", - "http://192.168.116.9:*", "http://*.xuqinmin.com", "https://*.xuqinmin.com" )); diff --git a/file-service/src/main/resources/application.yml b/file-service/src/main/resources/application.yml index 1201fd3..0693cb0 100644 --- a/file-service/src/main/resources/application.yml +++ b/file-service/src/main/resources/application.yml @@ -41,7 +41,7 @@ jwt: file: 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: level: diff --git a/im-service/src/main/java/com/xuqm/im/config/SecurityConfig.java b/im-service/src/main/java/com/xuqm/im/config/SecurityConfig.java index 0130064..27665b4 100644 --- a/im-service/src/main/java/com/xuqm/im/config/SecurityConfig.java +++ b/im-service/src/main/java/com/xuqm/im/config/SecurityConfig.java @@ -56,7 +56,8 @@ public class SecurityConfig { config.setAllowedOriginPatterns(List.of( "http://localhost:*", "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.setAllowedHeaders(List.of("*")); diff --git a/im-service/src/main/java/com/xuqm/im/controller/GlobalExceptionHandler.java b/im-service/src/main/java/com/xuqm/im/controller/GlobalExceptionHandler.java index b5c522d..f250dce 100644 --- a/im-service/src/main/java/com/xuqm/im/controller/GlobalExceptionHandler.java +++ b/im-service/src/main/java/com/xuqm/im/controller/GlobalExceptionHandler.java @@ -2,6 +2,8 @@ package com.xuqm.im.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; @@ -12,6 +14,8 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; @RestControllerAdvice public class GlobalExceptionHandler { + private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); + @ExceptionHandler(BusinessException.class) public ResponseEntity> handleBusiness(BusinessException e) { return ResponseEntity.status(resolveStatus(e.getCode())) @@ -40,8 +44,9 @@ public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) public ResponseEntity> handleException(Exception e) { + log.error("Unhandled exception", e); 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) { diff --git a/im-service/src/main/java/com/xuqm/im/service/ImAppSecretClient.java b/im-service/src/main/java/com/xuqm/im/service/ImAppSecretClient.java index 4ddb733..4c71861 100644 --- a/im-service/src/main/java/com/xuqm/im/service/ImAppSecretClient.java +++ b/im-service/src/main/java/com/xuqm/im/service/ImAppSecretClient.java @@ -21,7 +21,7 @@ public class ImAppSecretClient { private final RestTemplate restTemplate = new RestTemplate(); private final Map 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; @Value("${im.internal-token:xuqm-internal-token}") diff --git a/im-service/src/main/java/com/xuqm/im/service/ImFeatureConfigClient.java b/im-service/src/main/java/com/xuqm/im/service/ImFeatureConfigClient.java index ca9b4e8..6f95b1e 100644 --- a/im-service/src/main/java/com/xuqm/im/service/ImFeatureConfigClient.java +++ b/im-service/src/main/java/com/xuqm/im/service/ImFeatureConfigClient.java @@ -2,6 +2,8 @@ package com.xuqm.im.service; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; @@ -14,11 +16,12 @@ import org.springframework.web.util.UriComponentsBuilder; @Component public class ImFeatureConfigClient { + private static final Logger log = LoggerFactory.getLogger(ImFeatureConfigClient.class); private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); 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; @Value("${im.internal-token:xuqm-internal-token}") @@ -93,7 +96,7 @@ public class ImFeatureConfigClient { } } } 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(); } diff --git a/im-service/src/main/java/com/xuqm/im/service/ImPushBridgeClient.java b/im-service/src/main/java/com/xuqm/im/service/ImPushBridgeClient.java index 4d9346a..2f0cca6 100644 --- a/im-service/src/main/java/com/xuqm/im/service/ImPushBridgeClient.java +++ b/im-service/src/main/java/com/xuqm/im/service/ImPushBridgeClient.java @@ -1,9 +1,10 @@ package com.xuqm.im.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.Component; -import org.springframework.web.client.RestClientException; import java.net.URI; import java.net.http.HttpClient; @@ -16,10 +17,12 @@ import java.util.Map; @Component public class ImPushBridgeClient { + private static final Logger log = LoggerFactory.getLogger(ImPushBridgeClient.class); + private final HttpClient httpClient = HttpClient.newHttpClient(); 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; @Value("${im.internal-token:xuqm-internal-token}") @@ -48,7 +51,8 @@ public class ImPushBridgeClient { .POST(HttpRequest.BodyPublishers.ofString(json)) .build(); 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()); } } } diff --git a/im-service/src/main/resources/application.yml b/im-service/src/main/resources/application.yml index cfa4025..bce75d3 100644 --- a/im-service/src/main/resources/application.yml +++ b/im-service/src/main/resources/application.yml @@ -44,9 +44,9 @@ jwt: expiration: 86400000 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} - 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 message-history-days: 30 webhook-timeout-ms: 3000 diff --git a/tenant-service/src/main/java/com/xuqm/tenant/controller/DashboardController.java b/tenant-service/src/main/java/com/xuqm/tenant/controller/DashboardController.java new file mode 100644 index 0000000..e8f93ba --- /dev/null +++ b/tenant-service/src/main/java/com/xuqm/tenant/controller/DashboardController.java @@ -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>> stats(@AuthenticationPrincipal String tenantId) { + return ResponseEntity.ok(ApiResponse.success(dashboardService.stats(tenantId))); + } +} diff --git a/tenant-service/src/main/java/com/xuqm/tenant/controller/GlobalExceptionHandler.java b/tenant-service/src/main/java/com/xuqm/tenant/controller/GlobalExceptionHandler.java index e1abfab..5adfd90 100644 --- a/tenant-service/src/main/java/com/xuqm/tenant/controller/GlobalExceptionHandler.java +++ b/tenant-service/src/main/java/com/xuqm/tenant/controller/GlobalExceptionHandler.java @@ -2,7 +2,11 @@ package com.xuqm.tenant.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.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -13,9 +17,11 @@ import java.util.stream.Collectors; @RestControllerAdvice public class GlobalExceptionHandler { + private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); + @ExceptionHandler(BusinessException.class) public ResponseEntity> handle(BusinessException ex) { - return ResponseEntity.status(ex.getCode()) + return ResponseEntity.status(resolveStatus(ex.getCode())) .body(ApiResponse.error(ex.getCode(), ex.getMessage())); } @@ -27,9 +33,31 @@ public class GlobalExceptionHandler { return ResponseEntity.badRequest().body(ApiResponse.badRequest(message)); } + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity> handle(IllegalArgumentException ex) { + return ResponseEntity.badRequest() + .body(ApiResponse.badRequest(ex.getMessage() == null ? "参数错误" : ex.getMessage())); + } + + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity> handle(HttpMessageNotReadableException ex) { + return ResponseEntity.badRequest().body(ApiResponse.badRequest("请求体格式错误")); + } + @ExceptionHandler(Exception.class) public ResponseEntity> handle(Exception ex) { + log.error("Unhandled exception", ex); 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; + }; } } diff --git a/tenant-service/src/main/java/com/xuqm/tenant/controller/SdkConfigController.java b/tenant-service/src/main/java/com/xuqm/tenant/controller/SdkConfigController.java index d9c250d..56987f3 100644 --- a/tenant-service/src/main/java/com/xuqm/tenant/controller/SdkConfigController.java +++ b/tenant-service/src/main/java/com/xuqm/tenant/controller/SdkConfigController.java @@ -28,7 +28,7 @@ public class SdkConfigController { @Value("${sdk.im-ws-url:wss://im.dev.xuqinmin.com/ws/im}") 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; @Value("${sdk.im-api-url:https://im.dev.xuqinmin.com}") diff --git a/tenant-service/src/main/java/com/xuqm/tenant/repository/TenantRepository.java b/tenant-service/src/main/java/com/xuqm/tenant/repository/TenantRepository.java index f552df4..58d8692 100644 --- a/tenant-service/src/main/java/com/xuqm/tenant/repository/TenantRepository.java +++ b/tenant-service/src/main/java/com/xuqm/tenant/repository/TenantRepository.java @@ -19,6 +19,7 @@ public interface TenantRepository extends JpaRepository { boolean existsByUsername(String username); boolean existsByEmail(String email); List findByParentId(String parentId); + long countByParentId(String parentId); @Query("SELECT t FROM TenantEntity t WHERE " + "(:keyword IS NULL OR :keyword = '' OR t.username LIKE %:keyword% OR t.email LIKE %:keyword%)") diff --git a/tenant-service/src/main/java/com/xuqm/tenant/service/DashboardService.java b/tenant-service/src/main/java/com/xuqm/tenant/service/DashboardService.java new file mode 100644 index 0000000..8293fc6 --- /dev/null +++ b/tenant-service/src/main/java/com/xuqm/tenant/service/DashboardService.java @@ -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 stats(String tenantId) { + List 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 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; + } +} diff --git a/tenant-service/src/main/resources/application.yml b/tenant-service/src/main/resources/application.yml index 75a2605..e56a2c1 100644 --- a/tenant-service/src/main/resources/application.yml +++ b/tenant-service/src/main/resources/application.yml @@ -86,5 +86,5 @@ sdk: bootstrap-app-package: ${SDK_BOOTSTRAP_APP_PACKAGE:com.xuqm.demo} bootstrap-app-description: ${SDK_BOOTSTRAP_APP_DESCRIPTION:XuqmGroup demo app} 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} diff --git a/update-service/src/main/java/com/xuqm/update/config/SecurityConfig.java b/update-service/src/main/java/com/xuqm/update/config/SecurityConfig.java index d685be7..ea20c95 100644 --- a/update-service/src/main/java/com/xuqm/update/config/SecurityConfig.java +++ b/update-service/src/main/java/com/xuqm/update/config/SecurityConfig.java @@ -59,7 +59,6 @@ public class SecurityConfig { config.setAllowedOriginPatterns(List.of( "http://localhost:*", "http://127.0.0.1:*", - "http://192.168.116.9:*", "http://*.xuqinmin.com", "https://*.xuqinmin.com" )); diff --git a/update-service/src/main/java/com/xuqm/update/controller/AppStoreController.java b/update-service/src/main/java/com/xuqm/update/controller/AppStoreController.java index 33c4f13..7280504 100644 --- a/update-service/src/main/java/com/xuqm/update/controller/AppStoreController.java +++ b/update-service/src/main/java/com/xuqm/update/controller/AppStoreController.java @@ -9,6 +9,9 @@ import com.xuqm.update.service.StoreSubmissionService; import org.springframework.http.ResponseEntity; 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.Map; @@ -91,8 +94,7 @@ public class AppStoreController { @PathVariable String versionId, @RequestBody Map body) throws Exception { - @SuppressWarnings("unchecked") - List storeTypes = (List) body.get("storeTypes"); + List storeTypes = extractStringList(body, "storeTypes"); return ResponseEntity.ok(ApiResponse.success(storeService.markSubmitted(versionId, storeTypes))); } @@ -110,8 +112,7 @@ public class AppStoreController { @PathVariable String versionId, @RequestBody(required = false) Map body) throws Exception { - @SuppressWarnings("unchecked") - List storeTypes = body != null ? (List) body.get("storeTypes") : null; + List storeTypes = body != null ? extractStringList(body, "storeTypes") : null; String submitMode = body != null ? (String) body.get("submitMode") : null; String scheduledAtText = body != null ? (String) body.get("scheduledPublishAt") : null; Boolean autoPublishAfterReview = body != null && body.get("autoPublishAfterReview") != null @@ -145,23 +146,44 @@ public class AppStoreController { @PathVariable String versionId, @RequestBody Map body) throws Exception { - String storeType = (String) body.get("storeType"); - AppVersionEntity.StoreReviewState state = - AppVersionEntity.StoreReviewState.valueOf((String) body.get("state")); + String storeType = body.get("storeType") instanceof String s ? s : null; + if (storeType == null || storeType.isBlank()) throw new IllegalArgumentException("storeType 不能为空"); + 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(); return ResponseEntity.ok(ApiResponse.success( storeService.updateStoreReview(versionId, storeType, state, reason))); } + private List extractStringList(Map 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_TYPE = new TypeReference<>() {}; + private void validateReviewWebhook(String configJson) { try { - @SuppressWarnings("unchecked") - Map config = new com.fasterxml.jackson.databind.ObjectMapper() - .readValue(configJson, Map.class); + Map config = OBJECT_MAPPER.readValue(configJson, MAP_TYPE); String webhookUrl = config.get("webhookUrl") == null ? "" : config.get("webhookUrl").toString().trim(); if (!webhookUrl.isBlank()) { connectivityValidationService.validateCallbackUrl(webhookUrl, "审核通知"); } + } catch (IllegalArgumentException e) { + throw e; } catch (Exception e) { throw new IllegalArgumentException("invalid review webhook config: " + e.getMessage(), e); } @@ -172,16 +194,12 @@ public class AppStoreController { return; } try { - @SuppressWarnings("unchecked") - Map config = new com.fasterxml.jackson.databind.ObjectMapper() - .readValue(configJson, Map.class); - if (storeType == AppStoreConfigEntity.StoreType.REVIEW_WEBHOOK) { - validateReviewWebhook(configJson); - } - } catch (IllegalArgumentException e) { - throw e; + OBJECT_MAPPER.readValue(configJson, MAP_TYPE); } catch (Exception e) { throw new IllegalArgumentException("invalid store config payload: " + e.getMessage(), e); } + if (storeType == AppStoreConfigEntity.StoreType.REVIEW_WEBHOOK) { + validateReviewWebhook(configJson); + } } } diff --git a/update-service/src/main/java/com/xuqm/update/controller/AppVersionController.java b/update-service/src/main/java/com/xuqm/update/controller/AppVersionController.java index ff097d7..c1960ff 100644 --- a/update-service/src/main/java/com/xuqm/update/controller/AppVersionController.java +++ b/update-service/src/main/java/com/xuqm/update/controller/AppVersionController.java @@ -12,6 +12,7 @@ import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.time.LocalDateTime; +import java.time.format.DateTimeParseException; import java.util.List; import java.util.Map; import java.util.Optional; @@ -139,7 +140,11 @@ public class AppVersionController { entity.setGrayMode("PERCENT"); entity.setGrayMemberIds(null); 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.setStoreSubmitTargets(storeSubmitTargets); @@ -206,7 +211,11 @@ public class AppVersionController { } else { entity.setPublishStatus(AppVersionEntity.PublishStatus.DRAFT); 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); diff --git a/update-service/src/main/java/com/xuqm/update/controller/GlobalExceptionHandler.java b/update-service/src/main/java/com/xuqm/update/controller/GlobalExceptionHandler.java new file mode 100644 index 0000000..6238145 --- /dev/null +++ b/update-service/src/main/java/com/xuqm/update/controller/GlobalExceptionHandler.java @@ -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> handle(BusinessException ex) { + return ResponseEntity.status(resolveStatus(ex.getCode())) + .body(ApiResponse.error(ex.getCode(), ex.getMessage())); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> 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> handle(IllegalArgumentException ex) { + return ResponseEntity.badRequest() + .body(ApiResponse.badRequest(ex.getMessage() == null ? "参数错误" : ex.getMessage())); + } + + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity> handle(HttpMessageNotReadableException ex) { + return ResponseEntity.badRequest().body(ApiResponse.badRequest("请求体格式错误")); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity> 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; + }; + } +} diff --git a/update-service/src/main/java/com/xuqm/update/controller/PublishConfigController.java b/update-service/src/main/java/com/xuqm/update/controller/PublishConfigController.java index 82b6f0e..77ed982 100644 --- a/update-service/src/main/java/com/xuqm/update/controller/PublishConfigController.java +++ b/update-service/src/main/java/com/xuqm/update/controller/PublishConfigController.java @@ -2,7 +2,6 @@ package com.xuqm.update.controller; import com.xuqm.common.model.ApiResponse; import com.xuqm.update.entity.AppPublishConfigEntity; -import com.xuqm.update.service.ConnectivityValidationService; import com.xuqm.update.service.PublishConfigService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -15,12 +14,9 @@ import java.util.Map; public class PublishConfigController { private final PublishConfigService publishConfigService; - private final ConnectivityValidationService connectivityValidationService; - public PublishConfigController(PublishConfigService publishConfigService, - ConnectivityValidationService connectivityValidationService) { + public PublishConfigController(PublishConfigService publishConfigService) { this.publishConfigService = publishConfigService; - this.connectivityValidationService = connectivityValidationService; } @GetMapping("/publish/config") @@ -64,11 +60,15 @@ public class PublishConfigController { } String selectCallback = body.get("graySelectCallbackUrl") == null ? "" : body.get("graySelectCallbackUrl").toString().trim(); String syncCallback = body.get("grayDirectorySyncCallbackUrl") == null ? "" : body.get("grayDirectorySyncCallbackUrl").toString().trim(); - if (!selectCallback.isBlank()) { - connectivityValidationService.validateCallbackUrl(selectCallback, "灰度成员选择回调"); + if (!selectCallback.isBlank() && !isHttpUrl(selectCallback)) { + throw new IllegalArgumentException("graySelectCallbackUrl must start with http:// or https://"); } - if (!syncCallback.isBlank()) { - connectivityValidationService.validateCallbackUrl(syncCallback, "灰度成员同步回调"); + if (!syncCallback.isBlank() && !isHttpUrl(syncCallback)) { + throw new IllegalArgumentException("grayDirectorySyncCallbackUrl must start with http:// or https://"); } } + + private boolean isHttpUrl(String value) { + return value.startsWith("http://") || value.startsWith("https://"); + } } diff --git a/update-service/src/main/java/com/xuqm/update/service/ConnectivityValidationService.java b/update-service/src/main/java/com/xuqm/update/service/ConnectivityValidationService.java index b03c3ba..2014473 100644 --- a/update-service/src/main/java/com/xuqm/update/service/ConnectivityValidationService.java +++ b/update-service/src/main/java/com/xuqm/update/service/ConnectivityValidationService.java @@ -24,6 +24,10 @@ public class ConnectivityValidationService { if (url == null || url.isBlank()) { return; } + String normalized = url.trim(); + if (!(normalized.startsWith("http://") || normalized.startsWith("https://"))) { + throw new IllegalArgumentException(label + " must start with http:// or https://"); + } try { Map body = new LinkedHashMap<>(); body.put("probe", true); @@ -31,7 +35,7 @@ public class ConnectivityValidationService { body.put("timestamp", System.currentTimeMillis()); String payload = objectMapper.writeValueAsString(body); HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(url)) + .uri(URI.create(normalized)) .timeout(Duration.ofSeconds(5)) .header("Content-Type", "application/json") .header("X-Xuqm-Connectivity-Check", "true") @@ -43,7 +47,7 @@ public class ConnectivityValidationService { throw new IllegalStateException("connectivity check failed with HTTP " + status); } } catch (Exception e) { - throw new IllegalArgumentException("cannot connect to " + label + ": " + url, e); + throw new IllegalArgumentException("cannot connect to " + label + ": " + normalized, e); } } }