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>
这个提交包含在:
XuqmGroup 2026-05-15 22:38:46 +08:00
父节点 0ed09a8229
当前提交 10f0043f15
共有 4 个文件被更改,包括 100 次插入12 次删除

查看文件

@ -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) {