License服务改造:平台无关、ops管理最大设备数、有效期不可变
- LICENSE审批只创建1条FeatureServiceEntity记录(不分平台) - FeatureServiceManager扩展平台无关查询到LICENSE - LicenseServiceClient新增getAppLicenseStatus/updateMaxDevices方法 - OpsController新增license管理接口(GET状态、PUT最大设备数) - AppLicenseService.update中expiresAt一旦设置不可修改 - 审批流程支持传入expiresAt参数 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
这个提交包含在:
父节点
0ed09a8229
当前提交
10f0043f15
@ -36,7 +36,8 @@ public class AppLicenseService {
|
|||||||
AppLicenseEntity license = getByAppKey(appKey);
|
AppLicenseEntity license = getByAppKey(appKey);
|
||||||
if (name != null) license.setName(name);
|
if (name != null) license.setName(name);
|
||||||
if (maxDevices != null) license.setMaxDevices(maxDevices);
|
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 (isActive != null) license.setIsActive(isActive);
|
||||||
if (remark != null) license.setRemark(remark);
|
if (remark != null) license.setRemark(remark);
|
||||||
license.setUpdatedAt(LocalDateTime.now());
|
license.setUpdatedAt(LocalDateTime.now());
|
||||||
|
|||||||
@ -12,6 +12,7 @@ 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;
|
||||||
@ -37,14 +38,17 @@ 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;
|
||||||
|
|
||||||
public OpsController(OpsService opsService, FeatureServiceManager featureServiceManager,
|
public OpsController(OpsService opsService, FeatureServiceManager featureServiceManager,
|
||||||
RiskControlService riskControlService,
|
RiskControlService riskControlService,
|
||||||
OpsPushDiagnosticsClient pushDiagnosticsClient) {
|
OpsPushDiagnosticsClient pushDiagnosticsClient,
|
||||||
|
LicenseServiceClient licenseServiceClient) {
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/api/auth/ops/login")
|
@PostMapping("/api/auth/ops/login")
|
||||||
@ -112,8 +116,9 @@ public class OpsController {
|
|||||||
@PathVariable String requestId,
|
@PathVariable String requestId,
|
||||||
@RequestBody(required = false) Map<String, String> body) {
|
@RequestBody(required = false) Map<String, String> body) {
|
||||||
String reviewNote = body != null ? body.getOrDefault("reviewNote", "") : "";
|
String reviewNote = body != null ? body.getOrDefault("reviewNote", "") : "";
|
||||||
|
String expiresAt = body != null ? body.get("expiresAt") : null;
|
||||||
return ResponseEntity.ok(ApiResponse.success(
|
return ResponseEntity.ok(ApiResponse.success(
|
||||||
featureServiceManager.approveRequest(requestId, reviewNote)));
|
featureServiceManager.approveRequest(requestId, reviewNote, expiresAt)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/api/ops/service-requests/{requestId}/reject")
|
@PostMapping("/api/ops/service-requests/{requestId}/reject")
|
||||||
@ -194,6 +199,35 @@ 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')")
|
||||||
|
|||||||
@ -120,6 +120,11 @@ public class FeatureServiceManager {
|
|||||||
*/
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public ServiceActivationRequestEntity approveRequest(String requestId, String reviewNote) {
|
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)
|
ServiceActivationRequestEntity req = requestRepository.findById(requestId)
|
||||||
.orElseThrow(() -> new BusinessException(404, "申请不存在"));
|
.orElseThrow(() -> new BusinessException(404, "申请不存在"));
|
||||||
if (req.getStatus() != Status.PENDING) {
|
if (req.getStatus() != Status.PENDING) {
|
||||||
@ -138,15 +143,27 @@ public class FeatureServiceManager {
|
|||||||
if (isAppWideService(req.getServiceType())) {
|
if (isAppWideService(req.getServiceType())) {
|
||||||
List<FeatureServiceEntity> services = repository.findByAppKeyAndServiceType(normalizedAppId, req.getServiceType());
|
List<FeatureServiceEntity> services = repository.findByAppKeyAndServiceType(normalizedAppId, req.getServiceType());
|
||||||
if (services.isEmpty()) {
|
if (services.isEmpty()) {
|
||||||
for (FeatureServiceEntity.Platform platform : FeatureServiceEntity.Platform.values()) {
|
if (req.getServiceType() == FeatureServiceEntity.ServiceType.LICENSE) {
|
||||||
|
// LICENSE 不分平台,只创建一条记录
|
||||||
FeatureServiceEntity created = new FeatureServiceEntity();
|
FeatureServiceEntity created = new FeatureServiceEntity();
|
||||||
created.setId(UUID.randomUUID().toString());
|
created.setId(UUID.randomUUID().toString());
|
||||||
created.setAppKey(normalizedAppId);
|
created.setAppKey(normalizedAppId);
|
||||||
created.setPlatform(platform);
|
created.setPlatform(FeatureServiceEntity.Platform.ANDROID);
|
||||||
created.setServiceType(req.getServiceType());
|
created.setServiceType(FeatureServiceEntity.ServiceType.LICENSE);
|
||||||
created.setEnabled(true);
|
created.setEnabled(true);
|
||||||
created.setCreatedAt(LocalDateTime.now());
|
created.setCreatedAt(LocalDateTime.now());
|
||||||
repository.save(created);
|
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 {
|
} else {
|
||||||
services.forEach(service -> service.setEnabled(true));
|
services.forEach(service -> service.setEnabled(true));
|
||||||
@ -154,7 +171,7 @@ public class FeatureServiceManager {
|
|||||||
}
|
}
|
||||||
if (req.getServiceType() == FeatureServiceEntity.ServiceType.LICENSE) {
|
if (req.getServiceType() == FeatureServiceEntity.ServiceType.LICENSE) {
|
||||||
appRepository.findByAppKey(normalizedAppId).ifPresent(app ->
|
appRepository.findByAppKey(normalizedAppId).ifPresent(app ->
|
||||||
licenseServiceClient.syncAppLicense(app.getAppKey(), app.getName(), 1));
|
licenseServiceClient.syncAppLicense(app.getAppKey(), app.getName(), 1, expiresAt));
|
||||||
}
|
}
|
||||||
return req;
|
return req;
|
||||||
}
|
}
|
||||||
@ -197,7 +214,8 @@ public class FeatureServiceManager {
|
|||||||
|
|
||||||
public FeatureServiceEntity getOrFail(String appKey, FeatureServiceEntity.Platform platform,
|
public FeatureServiceEntity getOrFail(String appKey, FeatureServiceEntity.Platform platform,
|
||||||
FeatureServiceEntity.ServiceType serviceType) {
|
FeatureServiceEntity.ServiceType serviceType) {
|
||||||
if (serviceType == FeatureServiceEntity.ServiceType.IM) {
|
if (serviceType == FeatureServiceEntity.ServiceType.IM
|
||||||
|
|| serviceType == FeatureServiceEntity.ServiceType.LICENSE) {
|
||||||
return repository.findByAppKeyAndServiceType(appKey, serviceType)
|
return repository.findByAppKeyAndServiceType(appKey, serviceType)
|
||||||
.stream()
|
.stream()
|
||||||
.findFirst()
|
.findFirst()
|
||||||
@ -212,7 +230,8 @@ public class FeatureServiceManager {
|
|||||||
FeatureServiceEntity.Platform platform,
|
FeatureServiceEntity.Platform platform,
|
||||||
FeatureServiceEntity.ServiceType serviceType,
|
FeatureServiceEntity.ServiceType serviceType,
|
||||||
String config) {
|
String config) {
|
||||||
if (serviceType == FeatureServiceEntity.ServiceType.IM) {
|
if (serviceType == FeatureServiceEntity.ServiceType.IM
|
||||||
|
|| serviceType == FeatureServiceEntity.ServiceType.LICENSE) {
|
||||||
List<FeatureServiceEntity> services = repository.findByAppKeyAndServiceType(appKey, serviceType);
|
List<FeatureServiceEntity> services = repository.findByAppKeyAndServiceType(appKey, serviceType);
|
||||||
if (services.isEmpty()) {
|
if (services.isEmpty()) {
|
||||||
throw new BusinessException(404, "服务未配置");
|
throw new BusinessException(404, "服务未配置");
|
||||||
@ -568,7 +587,8 @@ public class FeatureServiceManager {
|
|||||||
FeatureServiceEntity.Platform platform,
|
FeatureServiceEntity.Platform platform,
|
||||||
FeatureServiceEntity.ServiceType serviceType) {
|
FeatureServiceEntity.ServiceType serviceType) {
|
||||||
FeatureServiceEntity entity;
|
FeatureServiceEntity entity;
|
||||||
if (serviceType == FeatureServiceEntity.ServiceType.IM) {
|
if (serviceType == FeatureServiceEntity.ServiceType.IM
|
||||||
|
|| serviceType == FeatureServiceEntity.ServiceType.LICENSE) {
|
||||||
entity = repository.findByAppKeyAndServiceType(appKey, serviceType)
|
entity = repository.findByAppKeyAndServiceType(appKey, serviceType)
|
||||||
.stream()
|
.stream()
|
||||||
.findFirst()
|
.findFirst()
|
||||||
|
|||||||
@ -50,11 +50,44 @@ public class LicenseServiceClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void syncAppLicense(String appKey, String name, Integer maxDevices) {
|
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<String, Object> 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<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 {
|
try {
|
||||||
Map<String, Object> body = Map.of(
|
Map<String, Object> body = Map.of(
|
||||||
"id", appKey,
|
"id", appKey,
|
||||||
"name", name,
|
"maxDevices", maxDevices
|
||||||
"maxDevices", maxDevices != null ? maxDevices : 1
|
|
||||||
);
|
);
|
||||||
callInternal("/api/license/internal/apps", HttpMethod.POST, body);
|
callInternal("/api/license/internal/apps", HttpMethod.POST, body);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户