diff --git a/license-service/src/main/java/com/xuqm/license/service/AppLicenseService.java b/license-service/src/main/java/com/xuqm/license/service/AppLicenseService.java index c9efe51..0cc9104 100644 --- a/license-service/src/main/java/com/xuqm/license/service/AppLicenseService.java +++ b/license-service/src/main/java/com/xuqm/license/service/AppLicenseService.java @@ -36,7 +36,8 @@ public class AppLicenseService { AppLicenseEntity license = getByAppKey(appKey); if (name != null) license.setName(name); if (maxDevices != null) license.setMaxDevices(maxDevices); - if (expiresAt != null) license.setExpiresAt(expiresAt); + // expiresAt 一旦设置不可修改 + if (expiresAt != null && license.getExpiresAt() == null) license.setExpiresAt(expiresAt); if (isActive != null) license.setIsActive(isActive); if (remark != null) license.setRemark(remark); license.setUpdatedAt(LocalDateTime.now()); diff --git a/tenant-service/src/main/java/com/xuqm/tenant/controller/OpsController.java b/tenant-service/src/main/java/com/xuqm/tenant/controller/OpsController.java index 02f2ba1..6a1823e 100644 --- a/tenant-service/src/main/java/com/xuqm/tenant/controller/OpsController.java +++ b/tenant-service/src/main/java/com/xuqm/tenant/controller/OpsController.java @@ -12,6 +12,7 @@ import com.xuqm.tenant.entity.RiskConfigEntity; import com.xuqm.tenant.entity.SensitiveWordEntity; import com.xuqm.tenant.service.OpsService; import com.xuqm.tenant.service.OpsPushDiagnosticsClient; +import com.xuqm.tenant.service.LicenseServiceClient; import com.xuqm.tenant.service.RiskControlService; import org.springframework.data.domain.Page; import org.springframework.http.ResponseEntity; @@ -37,14 +38,17 @@ public class OpsController { private final FeatureServiceManager featureServiceManager; private final RiskControlService riskControlService; private final OpsPushDiagnosticsClient pushDiagnosticsClient; + private final LicenseServiceClient licenseServiceClient; public OpsController(OpsService opsService, FeatureServiceManager featureServiceManager, RiskControlService riskControlService, - OpsPushDiagnosticsClient pushDiagnosticsClient) { + OpsPushDiagnosticsClient pushDiagnosticsClient, + LicenseServiceClient licenseServiceClient) { this.opsService = opsService; this.featureServiceManager = featureServiceManager; this.riskControlService = riskControlService; this.pushDiagnosticsClient = pushDiagnosticsClient; + this.licenseServiceClient = licenseServiceClient; } @PostMapping("/api/auth/ops/login") @@ -112,8 +116,9 @@ public class OpsController { @PathVariable String requestId, @RequestBody(required = false) Map body) { String reviewNote = body != null ? body.getOrDefault("reviewNote", "") : ""; + String expiresAt = body != null ? body.get("expiresAt") : null; return ResponseEntity.ok(ApiResponse.success( - featureServiceManager.approveRequest(requestId, reviewNote))); + featureServiceManager.approveRequest(requestId, reviewNote, expiresAt))); } @PostMapping("/api/ops/service-requests/{requestId}/reject") @@ -194,6 +199,35 @@ public class OpsController { body.getOrDefault("payload", "{}")))); } + /* ---------- License 管理 ---------- */ + @GetMapping("/api/ops/apps/{appKey}/license") + @PreAuthorize("hasAuthority('ROLE_OPS')") + public ResponseEntity>> getAppLicense(@PathVariable String appKey) { + return ResponseEntity.ok(ApiResponse.success(licenseServiceClient.getAppLicenseStatus(appKey))); + } + + @PutMapping("/api/ops/apps/{appKey}/license/max-devices") + @PreAuthorize("hasAuthority('ROLE_OPS')") + public ResponseEntity> updateMaxDevices( + @PathVariable String appKey, + @RequestBody Map body) { + Object maxDevicesObj = body.get("maxDevices"); + if (maxDevicesObj == null) { + throw new com.xuqm.common.exception.BusinessException(400, "最大设备数不能为空"); + } + int maxDevices; + if (maxDevicesObj instanceof Number n) { + maxDevices = n.intValue(); + } else { + maxDevices = Integer.parseInt(maxDevicesObj.toString()); + } + if (maxDevices < 1) { + throw new com.xuqm.common.exception.BusinessException(400, "最大设备数必须大于0"); + } + licenseServiceClient.updateMaxDevices(appKey, maxDevices); + return ResponseEntity.ok(ApiResponse.ok()); + } + /* ---------- 风控配置 ---------- */ @GetMapping("/api/ops/risk/rules") @PreAuthorize("hasAuthority('ROLE_OPS')") diff --git a/tenant-service/src/main/java/com/xuqm/tenant/service/FeatureServiceManager.java b/tenant-service/src/main/java/com/xuqm/tenant/service/FeatureServiceManager.java index 2978174..a0759ba 100644 --- a/tenant-service/src/main/java/com/xuqm/tenant/service/FeatureServiceManager.java +++ b/tenant-service/src/main/java/com/xuqm/tenant/service/FeatureServiceManager.java @@ -120,6 +120,11 @@ public class FeatureServiceManager { */ @Transactional public ServiceActivationRequestEntity approveRequest(String requestId, String reviewNote) { + return approveRequest(requestId, reviewNote, null); + } + + @Transactional + public ServiceActivationRequestEntity approveRequest(String requestId, String reviewNote, String expiresAt) { ServiceActivationRequestEntity req = requestRepository.findById(requestId) .orElseThrow(() -> new BusinessException(404, "申请不存在")); if (req.getStatus() != Status.PENDING) { @@ -138,15 +143,27 @@ public class FeatureServiceManager { if (isAppWideService(req.getServiceType())) { List services = repository.findByAppKeyAndServiceType(normalizedAppId, req.getServiceType()); if (services.isEmpty()) { - for (FeatureServiceEntity.Platform platform : FeatureServiceEntity.Platform.values()) { + if (req.getServiceType() == FeatureServiceEntity.ServiceType.LICENSE) { + // LICENSE 不分平台,只创建一条记录 FeatureServiceEntity created = new FeatureServiceEntity(); created.setId(UUID.randomUUID().toString()); created.setAppKey(normalizedAppId); - created.setPlatform(platform); - created.setServiceType(req.getServiceType()); + created.setPlatform(FeatureServiceEntity.Platform.ANDROID); + created.setServiceType(FeatureServiceEntity.ServiceType.LICENSE); created.setEnabled(true); created.setCreatedAt(LocalDateTime.now()); repository.save(created); + } else { + for (FeatureServiceEntity.Platform platform : FeatureServiceEntity.Platform.values()) { + FeatureServiceEntity created = new FeatureServiceEntity(); + created.setId(UUID.randomUUID().toString()); + created.setAppKey(normalizedAppId); + created.setPlatform(platform); + created.setServiceType(req.getServiceType()); + created.setEnabled(true); + created.setCreatedAt(LocalDateTime.now()); + repository.save(created); + } } } else { services.forEach(service -> service.setEnabled(true)); @@ -154,7 +171,7 @@ public class FeatureServiceManager { } if (req.getServiceType() == FeatureServiceEntity.ServiceType.LICENSE) { appRepository.findByAppKey(normalizedAppId).ifPresent(app -> - licenseServiceClient.syncAppLicense(app.getAppKey(), app.getName(), 1)); + licenseServiceClient.syncAppLicense(app.getAppKey(), app.getName(), 1, expiresAt)); } return req; } @@ -197,7 +214,8 @@ public class FeatureServiceManager { public FeatureServiceEntity getOrFail(String appKey, FeatureServiceEntity.Platform platform, FeatureServiceEntity.ServiceType serviceType) { - if (serviceType == FeatureServiceEntity.ServiceType.IM) { + if (serviceType == FeatureServiceEntity.ServiceType.IM + || serviceType == FeatureServiceEntity.ServiceType.LICENSE) { return repository.findByAppKeyAndServiceType(appKey, serviceType) .stream() .findFirst() @@ -212,7 +230,8 @@ public class FeatureServiceManager { FeatureServiceEntity.Platform platform, FeatureServiceEntity.ServiceType serviceType, String config) { - if (serviceType == FeatureServiceEntity.ServiceType.IM) { + if (serviceType == FeatureServiceEntity.ServiceType.IM + || serviceType == FeatureServiceEntity.ServiceType.LICENSE) { List services = repository.findByAppKeyAndServiceType(appKey, serviceType); if (services.isEmpty()) { throw new BusinessException(404, "服务未配置"); @@ -568,7 +587,8 @@ public class FeatureServiceManager { FeatureServiceEntity.Platform platform, FeatureServiceEntity.ServiceType serviceType) { FeatureServiceEntity entity; - if (serviceType == FeatureServiceEntity.ServiceType.IM) { + if (serviceType == FeatureServiceEntity.ServiceType.IM + || serviceType == FeatureServiceEntity.ServiceType.LICENSE) { entity = repository.findByAppKeyAndServiceType(appKey, serviceType) .stream() .findFirst() diff --git a/tenant-service/src/main/java/com/xuqm/tenant/service/LicenseServiceClient.java b/tenant-service/src/main/java/com/xuqm/tenant/service/LicenseServiceClient.java index dad7cfb..f10c117 100644 --- a/tenant-service/src/main/java/com/xuqm/tenant/service/LicenseServiceClient.java +++ b/tenant-service/src/main/java/com/xuqm/tenant/service/LicenseServiceClient.java @@ -50,11 +50,44 @@ public class LicenseServiceClient { } public void syncAppLicense(String appKey, String name, Integer maxDevices) { + syncAppLicense(appKey, name, maxDevices, null); + } + + public void syncAppLicense(String appKey, String name, Integer maxDevices, String expiresAt) { + try { + Map body = new java.util.HashMap<>(); + body.put("id", appKey); + body.put("name", name); + body.put("maxDevices", maxDevices != null ? maxDevices : 1); + if (expiresAt != null && !expiresAt.isBlank()) { + body.put("expiresAt", expiresAt); + } + callInternal("/api/license/internal/apps", HttpMethod.POST, body); + } catch (Exception e) { + // ignore + } + } + + public Map getAppLicenseStatus(String appKey) { + try { + ResponseEntity response = callInternal( + "/api/license/internal/apps/" + appKey + "/status", HttpMethod.GET, null); + if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { + JsonNode node = objectMapper.readTree(response.getBody()); + return objectMapper.convertValue(node.path("data"), + new com.fasterxml.jackson.core.type.TypeReference<>() {}); + } + } catch (Exception e) { + // ignore + } + return Map.of("exists", false); + } + + public void updateMaxDevices(String appKey, int maxDevices) { try { Map body = Map.of( "id", appKey, - "name", name, - "maxDevices", maxDevices != null ? maxDevices : 1 + "maxDevices", maxDevices ); callInternal("/api/license/internal/apps", HttpMethod.POST, body); } catch (Exception e) {