feat(license): 租户自主管理最大设备数,ops 彻底移除 license 管理

license-service:
- LicenseAdminController: 新增 PATCH /api/license/admin/apps/{appKey},
  租户可直接修改 maxDevices / isActive / remark

tenant-service:
- OpsController: 移除 GET /api/ops/apps/{appKey}/license 和
  PUT /api/ops/apps/{appKey}/license/max-devices 两个端点,
  同时移除 licenseServiceClient 字段注入
- LicenseServiceClient: 移除 updateMaxDevices() 和 getAppLicenseStatus()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
XuqmGroup 2026-05-21 12:45:33 +08:00
父节点 af922ae420
当前提交 8a3c41d5ff
共有 3 个文件被更改,包括 18 次插入61 次删除

查看文件

@ -33,6 +33,18 @@ public class LicenseAdminController {
return ResponseEntity.ok(ApiResponse.success(data)); return ResponseEntity.ok(ApiResponse.success(data));
} }
@PatchMapping("/apps/{appKey}")
public ResponseEntity<ApiResponse<AppLicenseEntity>> updateAppLicense(
@PathVariable String appKey,
@RequestBody UpdateAppLicenseRequest req) {
if (req.maxDevices() != null && req.maxDevices() < 1) {
throw new com.xuqm.common.exception.BusinessException(400, "最大设备数必须大于0");
}
AppLicenseEntity updated = appLicenseService.update(
appKey, null, req.maxDevices(), null, req.isActive(), req.remark());
return ResponseEntity.ok(ApiResponse.success(updated));
}
@DeleteMapping("/devices/{id}") @DeleteMapping("/devices/{id}")
public ResponseEntity<ApiResponse<Void>> revokeDevice(@PathVariable String id) { public ResponseEntity<ApiResponse<Void>> revokeDevice(@PathVariable String id) {
deviceService.revoke(id); deviceService.revoke(id);
@ -45,4 +57,10 @@ public class LicenseAdminController {
return ResponseEntity.ok(ApiResponse.ok()); return ResponseEntity.ok(ApiResponse.ok());
} }
public record UpdateAppLicenseRequest(
Integer maxDevices,
Boolean isActive,
String remark
) {}
} }

查看文件

@ -13,7 +13,6 @@ import com.xuqm.tenant.entity.RiskConfigEntity;
import com.xuqm.tenant.entity.SensitiveWordEntity; import com.xuqm.tenant.entity.SensitiveWordEntity;
import com.xuqm.tenant.service.OpsService; import com.xuqm.tenant.service.OpsService;
import com.xuqm.tenant.service.OpsPushDiagnosticsClient; import com.xuqm.tenant.service.OpsPushDiagnosticsClient;
import com.xuqm.tenant.service.LicenseServiceClient;
import com.xuqm.tenant.service.RiskControlService; import com.xuqm.tenant.service.RiskControlService;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@ -39,19 +38,16 @@ public class OpsController {
private final FeatureServiceManager featureServiceManager; private final FeatureServiceManager featureServiceManager;
private final RiskControlService riskControlService; private final RiskControlService riskControlService;
private final OpsPushDiagnosticsClient pushDiagnosticsClient; private final OpsPushDiagnosticsClient pushDiagnosticsClient;
private final LicenseServiceClient licenseServiceClient;
private final PrivateDeploymentProperties deploymentProperties; private final PrivateDeploymentProperties deploymentProperties;
public OpsController(OpsService opsService, FeatureServiceManager featureServiceManager, public OpsController(OpsService opsService, FeatureServiceManager featureServiceManager,
RiskControlService riskControlService, RiskControlService riskControlService,
OpsPushDiagnosticsClient pushDiagnosticsClient, OpsPushDiagnosticsClient pushDiagnosticsClient,
LicenseServiceClient licenseServiceClient,
PrivateDeploymentProperties deploymentProperties) { PrivateDeploymentProperties deploymentProperties) {
this.opsService = opsService; this.opsService = opsService;
this.featureServiceManager = featureServiceManager; this.featureServiceManager = featureServiceManager;
this.riskControlService = riskControlService; this.riskControlService = riskControlService;
this.pushDiagnosticsClient = pushDiagnosticsClient; this.pushDiagnosticsClient = pushDiagnosticsClient;
this.licenseServiceClient = licenseServiceClient;
this.deploymentProperties = deploymentProperties; this.deploymentProperties = deploymentProperties;
} }
@ -220,35 +216,6 @@ public class OpsController {
body.getOrDefault("payload", "{}")))); body.getOrDefault("payload", "{}"))));
} }
/* ---------- License 管理 ---------- */
@GetMapping("/api/ops/apps/{appKey}/license")
@PreAuthorize("hasAuthority('ROLE_OPS')")
public ResponseEntity<ApiResponse<Map<String, Object>>> 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<ApiResponse<Void>> updateMaxDevices(
@PathVariable String appKey,
@RequestBody Map<String, Object> 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") @GetMapping("/api/ops/risk/rules")
@PreAuthorize("hasAuthority('ROLE_OPS')") @PreAuthorize("hasAuthority('ROLE_OPS')")

查看文件

@ -2,7 +2,6 @@ package com.xuqm.tenant.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 com.xuqm.common.model.ApiResponse;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*; import org.springframework.http.*;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -68,33 +67,6 @@ public class LicenseServiceClient {
} }
} }
public Map<String, Object> getAppLicenseStatus(String appKey) {
try {
ResponseEntity<String> 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<String, Object> body = Map.of(
"id", appKey,
"maxDevices", maxDevices
);
callInternal("/api/license/internal/apps", HttpMethod.POST, body);
} catch (Exception e) {
// ignore
}
}
private ResponseEntity<String> callInternal(String path, HttpMethod method, Object body) { private ResponseEntity<String> callInternal(String path, HttpMethod method, Object body) {
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
headers.set("X-Internal-Token", internalToken); headers.set("X-Internal-Token", internalToken);