Align license service with app model

这个提交包含在:
XuqmGroup 2026-05-15 21:42:10 +08:00
父节点 4f59fead0a
当前提交 bc1165d22e
共有 16 个文件被更改,包括 175 次插入382 次删除

查看文件

@ -1,16 +1,13 @@
package com.xuqm.license.controller;
import com.xuqm.common.model.ApiResponse;
import com.xuqm.license.entity.CompanyEntity;
import com.xuqm.license.entity.AppLicenseEntity;
import com.xuqm.license.entity.DeviceEntity;
import com.xuqm.license.service.CompanyService;
import com.xuqm.license.service.AppLicenseService;
import com.xuqm.license.service.DeviceService;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
@ -18,47 +15,24 @@ import java.util.Map;
@RequestMapping("/api/license/admin")
public class LicenseAdminController {
private final CompanyService companyService;
private final AppLicenseService appLicenseService;
private final DeviceService deviceService;
public LicenseAdminController(CompanyService companyService, DeviceService deviceService) {
this.companyService = companyService;
public LicenseAdminController(AppLicenseService appLicenseService, DeviceService deviceService) {
this.appLicenseService = appLicenseService;
this.deviceService = deviceService;
}
@GetMapping("/companies")
public ResponseEntity<ApiResponse<List<CompanyEntity>>> listCompanies() {
return ResponseEntity.ok(ApiResponse.success(companyService.listAll()));
}
@PostMapping("/companies")
public ResponseEntity<ApiResponse<CompanyEntity>> createCompany(@RequestBody CreateCompanyRequest req) {
CompanyEntity company = companyService.create(req.name(), req.maxDevices(), req.expiresAt(), req.remark());
return ResponseEntity.ok(ApiResponse.success(company));
}
@GetMapping("/companies/{id}")
public ResponseEntity<ApiResponse<Map<String, Object>>> getCompany(@PathVariable String id) {
CompanyEntity company = companyService.getById(id);
List<DeviceEntity> devices = deviceService.listByCompany(id);
@GetMapping("/apps/{appKey}")
public ResponseEntity<ApiResponse<Map<String, Object>>> getAppLicense(@PathVariable String appKey) {
AppLicenseEntity license = appLicenseService.getByAppKey(appKey);
List<DeviceEntity> devices = deviceService.listByApp(appKey);
Map<String, Object> data = new java.util.LinkedHashMap<>();
data.put("company", company);
data.put("license", license);
data.put("devices", devices);
return ResponseEntity.ok(ApiResponse.success(data));
}
@PutMapping("/companies/{id}")
public ResponseEntity<ApiResponse<CompanyEntity>> updateCompany(@PathVariable String id, @RequestBody UpdateCompanyRequest req) {
CompanyEntity company = companyService.update(id, req.name(), req.maxDevices(), req.expiresAt(), req.isActive(), req.remark());
return ResponseEntity.ok(ApiResponse.success(company));
}
@DeleteMapping("/companies/{id}")
public ResponseEntity<ApiResponse<Void>> deleteCompany(@PathVariable String id) {
companyService.delete(id);
return ResponseEntity.ok(ApiResponse.ok());
}
@DeleteMapping("/devices/{id}")
public ResponseEntity<ApiResponse<Void>> revokeDevice(@PathVariable String id) {
deviceService.revoke(id);
@ -71,18 +45,4 @@ public class LicenseAdminController {
return ResponseEntity.ok(ApiResponse.ok());
}
public record CreateCompanyRequest(
@NotBlank String name,
@NotNull Integer maxDevices,
LocalDateTime expiresAt,
String remark
) {}
public record UpdateCompanyRequest(
String name,
Integer maxDevices,
LocalDateTime expiresAt,
Boolean isActive,
String remark
) {}
}

查看文件

@ -1,9 +1,9 @@
package com.xuqm.license.controller;
import com.xuqm.common.model.ApiResponse;
import com.xuqm.license.entity.CompanyEntity;
import com.xuqm.license.entity.AppLicenseEntity;
import com.xuqm.license.entity.DeviceEntity;
import com.xuqm.license.service.CompanyService;
import com.xuqm.license.service.AppLicenseService;
import com.xuqm.license.service.DeviceService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
@ -18,63 +18,63 @@ import java.util.Map;
@RequestMapping("/api/license/internal")
public class LicenseInternalController {
private final CompanyService companyService;
private final AppLicenseService appLicenseService;
private final DeviceService deviceService;
@Value("${license.internal-token:xuqm-license-internal-token}")
private String internalToken;
public LicenseInternalController(CompanyService companyService, DeviceService deviceService) {
this.companyService = companyService;
public LicenseInternalController(AppLicenseService appLicenseService, DeviceService deviceService) {
this.appLicenseService = appLicenseService;
this.deviceService = deviceService;
}
@GetMapping("/companies/{appKey}/status")
public ResponseEntity<ApiResponse<Map<String, Object>>> getCompanyStatus(
@GetMapping("/apps/{appKey}/status")
public ResponseEntity<ApiResponse<Map<String, Object>>> getAppLicenseStatus(
@RequestHeader(value = "X-Internal-Token", required = false) String token,
@PathVariable String appKey) {
if (!isAllowed(token)) {
return ResponseEntity.status(403).body(ApiResponse.error(403, "Forbidden"));
}
try {
CompanyEntity company = companyService.getById(appKey);
AppLicenseEntity license = appLicenseService.getByAppKey(appKey);
Map<String, Object> data = new HashMap<>();
data.put("exists", true);
data.put("active", companyService.isCompanyValid(company));
data.put("maxDevices", company.getMaxDevices());
data.put("registeredDevices", company.getRegisteredDevices());
data.put("expiresAt", company.getExpiresAt());
data.put("active", appLicenseService.isValid(license));
data.put("maxDevices", license.getMaxDevices());
data.put("registeredDevices", license.getRegisteredDevices());
data.put("expiresAt", license.getExpiresAt());
return ResponseEntity.ok(ApiResponse.success(data));
} catch (Exception e) {
return ResponseEntity.ok(ApiResponse.success(Map.of("exists", false)));
}
}
@GetMapping("/companies/{appKey}/devices")
public ResponseEntity<ApiResponse<List<DeviceEntity>>> listDevicesByCompany(
@GetMapping("/apps/{appKey}/devices")
public ResponseEntity<ApiResponse<List<DeviceEntity>>> listDevicesByApp(
@RequestHeader(value = "X-Internal-Token", required = false) String token,
@PathVariable String appKey) {
if (!isAllowed(token)) {
return ResponseEntity.status(403).body(ApiResponse.error(403, "Forbidden"));
}
return ResponseEntity.ok(ApiResponse.success(deviceService.listByCompany(appKey)));
return ResponseEntity.ok(ApiResponse.success(deviceService.listByApp(appKey)));
}
@PostMapping("/companies")
public ResponseEntity<ApiResponse<CompanyEntity>> upsertCompany(
@PostMapping("/apps")
public ResponseEntity<ApiResponse<AppLicenseEntity>> upsertAppLicense(
@RequestHeader(value = "X-Internal-Token", required = false) String token,
@RequestBody UpsertCompanyRequest req) {
@RequestBody UpsertAppLicenseRequest req) {
if (!isAllowed(token)) {
return ResponseEntity.status(403).body(ApiResponse.error(403, "Forbidden"));
}
CompanyEntity company = companyService.upsert(
AppLicenseEntity license = appLicenseService.upsert(
req.id(),
req.name(),
req.maxDevices(),
req.expiresAt(),
req.isActive(),
req.remark());
return ResponseEntity.ok(ApiResponse.success(company));
return ResponseEntity.ok(ApiResponse.success(license));
}
@GetMapping("/devices/{deviceId}")
@ -93,7 +93,7 @@ public class LicenseInternalController {
return token != null && internalToken.equals(token);
}
public record UpsertCompanyRequest(
public record UpsertAppLicenseRequest(
String id,
String name,
Integer maxDevices,

查看文件

@ -28,7 +28,7 @@ public class LicensePublicController {
@PostMapping("/register")
public ResponseEntity<ApiResponse<Map<String, Object>>> register(@Valid @RequestBody RegisterRequest req) {
DeviceService.RegisterResult result = deviceService.register(
req.companyId(),
req.appKey(),
req.deviceId(),
req.deviceName(),
req.deviceModel(),
@ -46,7 +46,7 @@ public class LicensePublicController {
@PostMapping("/verify")
public ResponseEntity<ApiResponse<Map<String, Object>>> verify(@Valid @RequestBody VerifyRequest req) {
DeviceService.VerifyResult result = deviceService.verify(req.companyId(), req.deviceId(), req.token(), req.userInfo());
DeviceService.VerifyResult result = deviceService.verify(req.appKey(), req.deviceId(), req.token(), req.userInfo());
Map<String, Object> data = new java.util.LinkedHashMap<>();
data.put("valid", result.valid());
if (result.error() != null) {
@ -56,7 +56,7 @@ public class LicensePublicController {
}
public record RegisterRequest(
@NotBlank @JsonProperty("companyId") @JsonAlias("company_id") String companyId,
@NotBlank String appKey,
@NotBlank @JsonProperty("deviceId") @JsonAlias("device_id") String deviceId,
@JsonProperty("deviceName") @JsonAlias("device_name") String deviceName,
@JsonProperty("deviceModel") @JsonAlias("device_model") String deviceModel,
@ -66,7 +66,7 @@ public class LicensePublicController {
) {}
public record VerifyRequest(
@NotBlank @JsonProperty("companyId") @JsonAlias("company_id") String companyId,
@NotBlank String appKey,
@NotBlank @JsonProperty("deviceId") @JsonAlias("device_id") String deviceId,
@NotBlank String token,
@JsonProperty("userInfo") @JsonAlias("user_info") JsonNode userInfo

查看文件

@ -7,12 +7,12 @@ import jakarta.persistence.Table;
import java.time.LocalDateTime;
@Entity
@Table(name = "companies")
public class CompanyEntity {
@Table(name = "app_licenses")
public class AppLicenseEntity {
@Id
@Column(length = 36)
private String id;
@Column(name = "app_key", length = 64)
private String appKey;
@Column(nullable = false, length = 255)
private String name;
@ -38,8 +38,8 @@ public class CompanyEntity {
@Column(nullable = false, name = "updated_at")
private LocalDateTime updatedAt;
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getAppKey() { return appKey; }
public void setAppKey(String appKey) { this.appKey = appKey; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }

查看文件

@ -14,8 +14,8 @@ public class DeviceEntity {
@Column(length = 36)
private String id;
@Column(nullable = false, name = "company_id", length = 36)
private String companyId;
@Column(nullable = false, name = "app_key", length = 64)
private String appKey;
@Column(nullable = false, name = "device_id", length = 255, unique = true)
private String deviceId;
@ -68,8 +68,8 @@ public class DeviceEntity {
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getCompanyId() { return companyId; }
public void setCompanyId(String companyId) { this.companyId = companyId; }
public String getAppKey() { return appKey; }
public void setAppKey(String appKey) { this.appKey = appKey; }
public String getDeviceId() { return deviceId; }
public void setDeviceId(String deviceId) { this.deviceId = deviceId; }

查看文件

@ -0,0 +1,9 @@
package com.xuqm.license.repository;
import com.xuqm.license.entity.AppLicenseEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface AppLicenseRepository extends JpaRepository<AppLicenseEntity, String> {
}

查看文件

@ -1,12 +0,0 @@
package com.xuqm.license.repository;
import com.xuqm.license.entity.CompanyEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface CompanyRepository extends JpaRepository<CompanyEntity, String> {
List<CompanyEntity> findAllByOrderByCreatedAtDesc();
}

查看文件

@ -10,6 +10,6 @@ import java.util.Optional;
@Repository
public interface DeviceRepository extends JpaRepository<DeviceEntity, String> {
Optional<DeviceEntity> findByDeviceId(String deviceId);
List<DeviceEntity> findByCompanyIdOrderByRegisteredAtDesc(String companyId);
long countByCompanyIdAndIsActiveTrue(String companyId);
List<DeviceEntity> findByAppKeyOrderByRegisteredAtDesc(String appKey);
long countByAppKeyAndIsActiveTrue(String appKey);
}

查看文件

@ -0,0 +1,85 @@
package com.xuqm.license.service;
import com.xuqm.common.exception.BusinessException;
import com.xuqm.license.entity.AppLicenseEntity;
import com.xuqm.license.repository.AppLicenseRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
@Service
public class AppLicenseService {
private final AppLicenseRepository repository;
public AppLicenseService(AppLicenseRepository repository) {
this.repository = repository;
}
public AppLicenseEntity getByAppKey(String appKey) {
return repository.findById(appKey)
.orElseThrow(() -> new BusinessException(404, "App license not found"));
}
@Transactional
public AppLicenseEntity upsert(String appKey, String name, Integer maxDevices,
LocalDateTime expiresAt, Boolean isActive, String remark) {
return repository.findById(appKey)
.map(license -> update(appKey, name, maxDevices, expiresAt, isActive, remark))
.orElseGet(() -> create(appKey, name, maxDevices, expiresAt, isActive, remark));
}
@Transactional
public AppLicenseEntity update(String appKey, String name, Integer maxDevices,
LocalDateTime expiresAt, Boolean isActive, String remark) {
AppLicenseEntity license = getByAppKey(appKey);
if (name != null) license.setName(name);
if (maxDevices != null) license.setMaxDevices(maxDevices);
if (expiresAt != null) license.setExpiresAt(expiresAt);
if (isActive != null) license.setIsActive(isActive);
if (remark != null) license.setRemark(remark);
license.setUpdatedAt(LocalDateTime.now());
return repository.save(license);
}
@Transactional
public void incrementRegisteredDevices(String appKey) {
AppLicenseEntity license = getByAppKey(appKey);
license.setRegisteredDevices(license.getRegisteredDevices() + 1);
license.setUpdatedAt(LocalDateTime.now());
repository.save(license);
}
@Transactional
public void decrementRegisteredDevices(String appKey) {
AppLicenseEntity license = getByAppKey(appKey);
if (license.getRegisteredDevices() > 0) {
license.setRegisteredDevices(license.getRegisteredDevices() - 1);
license.setUpdatedAt(LocalDateTime.now());
repository.save(license);
}
}
public boolean isValid(AppLicenseEntity license) {
if (license == null || !Boolean.TRUE.equals(license.getIsActive())) {
return false;
}
return license.getExpiresAt() == null || !LocalDateTime.now().isAfter(license.getExpiresAt());
}
private AppLicenseEntity create(String appKey, String name, Integer maxDevices,
LocalDateTime expiresAt, Boolean isActive, String remark) {
AppLicenseEntity license = new AppLicenseEntity();
license.setAppKey(appKey);
license.setName(name);
license.setMaxDevices(maxDevices != null ? maxDevices : 1);
license.setRegisteredDevices(0);
license.setExpiresAt(expiresAt);
license.setIsActive(isActive != null ? isActive : true);
license.setRemark(remark);
license.setCreatedAt(LocalDateTime.now());
license.setUpdatedAt(LocalDateTime.now());
return repository.save(license);
}
}

查看文件

@ -1,115 +0,0 @@
package com.xuqm.license.service;
import com.xuqm.common.exception.BusinessException;
import com.xuqm.license.entity.CompanyEntity;
import com.xuqm.license.repository.CompanyRepository;
import com.xuqm.license.repository.DeviceRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
@Service
public class CompanyService {
private final CompanyRepository companyRepository;
private final DeviceRepository deviceRepository;
public CompanyService(CompanyRepository companyRepository, DeviceRepository deviceRepository) {
this.companyRepository = companyRepository;
this.deviceRepository = deviceRepository;
}
public List<CompanyEntity> listAll() {
return companyRepository.findAllByOrderByCreatedAtDesc();
}
public CompanyEntity getById(String id) {
return companyRepository.findById(id)
.orElseThrow(() -> new BusinessException(404, "Company not found"));
}
@Transactional
public CompanyEntity create(String name, Integer maxDevices, LocalDateTime expiresAt, String remark) {
return createWithId(UUID.randomUUID().toString(), name, maxDevices, expiresAt, remark);
}
@Transactional
public CompanyEntity createWithId(String id, String name, Integer maxDevices, LocalDateTime expiresAt, String remark) {
CompanyEntity company = new CompanyEntity();
company.setId(id);
company.setName(name);
company.setMaxDevices(maxDevices != null ? maxDevices : 1);
company.setRegisteredDevices(0);
company.setExpiresAt(expiresAt);
company.setIsActive(true);
company.setRemark(remark);
company.setCreatedAt(LocalDateTime.now());
company.setUpdatedAt(LocalDateTime.now());
return companyRepository.save(company);
}
@Transactional
public CompanyEntity upsert(String id, String name, Integer maxDevices, LocalDateTime expiresAt, Boolean isActive, String remark) {
return companyRepository.findById(id)
.map(company -> update(id, name, maxDevices, expiresAt, isActive, remark))
.orElseGet(() -> {
CompanyEntity created = createWithId(id, name, maxDevices, expiresAt, remark);
if (isActive != null) {
created.setIsActive(isActive);
created.setUpdatedAt(LocalDateTime.now());
return companyRepository.save(created);
}
return created;
});
}
@Transactional
public CompanyEntity update(String id, String name, Integer maxDevices, LocalDateTime expiresAt, Boolean isActive, String remark) {
CompanyEntity company = getById(id);
if (name != null) company.setName(name);
if (maxDevices != null) company.setMaxDevices(maxDevices);
if (expiresAt != null) company.setExpiresAt(expiresAt);
if (isActive != null) company.setIsActive(isActive);
if (remark != null) company.setRemark(remark);
company.setUpdatedAt(LocalDateTime.now());
return companyRepository.save(company);
}
@Transactional
public void delete(String id) {
CompanyEntity company = getById(id);
deviceRepository.deleteAll(deviceRepository.findByCompanyIdOrderByRegisteredAtDesc(id));
companyRepository.delete(company);
}
@Transactional
public void incrementRegisteredDevices(String companyId) {
CompanyEntity company = getById(companyId);
company.setRegisteredDevices(company.getRegisteredDevices() + 1);
company.setUpdatedAt(LocalDateTime.now());
companyRepository.save(company);
}
@Transactional
public void decrementRegisteredDevices(String companyId) {
CompanyEntity company = getById(companyId);
if (company.getRegisteredDevices() > 0) {
company.setRegisteredDevices(company.getRegisteredDevices() - 1);
company.setUpdatedAt(LocalDateTime.now());
companyRepository.save(company);
}
}
public boolean isCompanyValid(CompanyEntity company) {
if (company == null || !Boolean.TRUE.equals(company.getIsActive())) {
return false;
}
if (company.getExpiresAt() != null && LocalDateTime.now().isAfter(company.getExpiresAt())) {
return false;
}
return true;
}
}

查看文件

@ -3,7 +3,7 @@ package com.xuqm.license.service;
import com.xuqm.common.exception.BusinessException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xuqm.license.entity.CompanyEntity;
import com.xuqm.license.entity.AppLicenseEntity;
import com.xuqm.license.entity.DeviceEntity;
import com.xuqm.license.repository.DeviceRepository;
import org.springframework.stereotype.Service;
@ -22,14 +22,14 @@ import java.util.UUID;
public class DeviceService {
private final DeviceRepository deviceRepository;
private final CompanyService companyService;
private final AppLicenseService appLicenseService;
private final LicenseAuthService licenseAuthService;
private final ObjectMapper objectMapper;
public DeviceService(DeviceRepository deviceRepository, CompanyService companyService,
public DeviceService(DeviceRepository deviceRepository, AppLicenseService appLicenseService,
LicenseAuthService licenseAuthService, ObjectMapper objectMapper) {
this.deviceRepository = deviceRepository;
this.companyService = companyService;
this.appLicenseService = appLicenseService;
this.licenseAuthService = licenseAuthService;
this.objectMapper = objectMapper;
}
@ -38,12 +38,12 @@ public class DeviceService {
return deviceRepository.findByDeviceId(deviceId);
}
public List<DeviceEntity> listByCompany(String companyId) {
return deviceRepository.findByCompanyIdOrderByRegisteredAtDesc(companyId);
public List<DeviceEntity> listByApp(String appKey) {
return deviceRepository.findByAppKeyOrderByRegisteredAtDesc(appKey);
}
@Transactional
public RegisterResult register(String companyId, String deviceId, String deviceName,
public RegisterResult register(String appKey, String deviceId, String deviceName,
String deviceModel, String deviceVendor, String osVersion,
JsonNode userInfo) {
// Check if device already registered
@ -54,7 +54,7 @@ public class DeviceService {
throw new BusinessException(403, "Device has been deactivated");
}
// Re-issue token
String token = licenseAuthService.generateToken(companyId, deviceId, existing.getId());
String token = licenseAuthService.generateToken(appKey, deviceId, existing.getId());
String tokenHash = hashToken(token);
existing.setDeviceName(firstNonBlank(deviceName, existing.getDeviceName()));
existing.setDeviceModel(firstNonBlank(deviceModel, existing.getDeviceModel()));
@ -69,22 +69,22 @@ public class DeviceService {
}
// Validate company
CompanyEntity company = companyService.getById(companyId);
if (!companyService.isCompanyValid(company)) {
throw new BusinessException(403, "Company license is inactive or expired");
AppLicenseEntity license = appLicenseService.getByAppKey(appKey);
if (!appLicenseService.isValid(license)) {
throw new BusinessException(403, "App license is inactive or expired");
}
if (company.getRegisteredDevices() >= company.getMaxDevices()) {
throw new BusinessException(403, "Device limit reached. Max allowed: " + company.getMaxDevices());
if (license.getRegisteredDevices() >= license.getMaxDevices()) {
throw new BusinessException(403, "Device limit reached. Max allowed: " + license.getMaxDevices());
}
// Create device record
String recordId = UUID.randomUUID().toString();
String token = licenseAuthService.generateToken(companyId, deviceId, recordId);
String token = licenseAuthService.generateToken(appKey, deviceId, recordId);
String tokenHash = hashToken(token);
DeviceEntity device = new DeviceEntity();
device.setId(recordId);
device.setCompanyId(companyId);
device.setAppKey(appKey);
device.setDeviceId(deviceId);
device.setDeviceName(deviceName);
device.setDeviceModel(deviceModel);
@ -98,14 +98,14 @@ public class DeviceService {
device.setUpdatedAt(LocalDateTime.now());
deviceRepository.save(device);
companyService.incrementRegisteredDevices(companyId);
appLicenseService.incrementRegisteredDevices(appKey);
return new RegisterResult(true, token, null);
}
@Transactional
public VerifyResult verify(String companyId, String deviceId, String token, JsonNode userInfo) {
if (!licenseAuthService.verifyTokenPayload(token, companyId, deviceId)) {
public VerifyResult verify(String appKey, String deviceId, String token, JsonNode userInfo) {
if (!licenseAuthService.verifyTokenPayload(token, appKey, deviceId)) {
return new VerifyResult(false, "Token mismatch");
}
@ -118,9 +118,9 @@ public class DeviceService {
return new VerifyResult(false, "Token revoked");
}
CompanyEntity company = companyService.getById(companyId);
if (!companyService.isCompanyValid(company)) {
return new VerifyResult(false, "Company license inactive or expired");
AppLicenseEntity license = appLicenseService.getByAppKey(appKey);
if (!appLicenseService.isValid(license)) {
return new VerifyResult(false, "App license inactive or expired");
}
device.setLastVerifiedAt(LocalDateTime.now());
@ -138,7 +138,7 @@ public class DeviceService {
device.setIsActive(false);
device.setUpdatedAt(LocalDateTime.now());
deviceRepository.save(device);
companyService.decrementRegisteredDevices(device.getCompanyId());
appLicenseService.decrementRegisteredDevices(device.getAppKey());
}
@Transactional
@ -148,7 +148,7 @@ public class DeviceService {
device.setIsActive(true);
device.setUpdatedAt(LocalDateTime.now());
deviceRepository.save(device);
companyService.incrementRegisteredDevices(device.getCompanyId());
appLicenseService.incrementRegisteredDevices(device.getAppKey());
}
public static String hashToken(String token) {

查看文件

@ -14,20 +14,20 @@ public class LicenseAuthService {
this.jwtUtil = jwtUtil;
}
public String generateToken(String companyId, String deviceId, String recordId) {
public String generateToken(String appKey, String deviceId, String recordId) {
return jwtUtil.generate(deviceId, Map.of(
"companyId", companyId,
"appKey", appKey,
"deviceId", deviceId,
"recordId", recordId
));
}
public boolean verifyTokenPayload(String token, String companyId, String deviceId) {
public boolean verifyTokenPayload(String token, String appKey, String deviceId) {
try {
var claims = jwtUtil.parse(token);
String claimCompanyId = firstNonNull(claims.get("companyId", String.class), claims.get("company_id", String.class));
String claimAppKey = claims.get("appKey", String.class);
String claimDeviceId = firstNonNull(claims.get("deviceId", String.class), claims.get("device_id", String.class));
return companyId.equals(claimCompanyId) && deviceId.equals(claimDeviceId);
return appKey.equals(claimAppKey) && deviceId.equals(claimDeviceId);
} catch (Exception e) {
return false;
}

查看文件

@ -1,131 +0,0 @@
package com.xuqm.tenant.config;
import com.xuqm.tenant.entity.AppEntity;
import com.xuqm.tenant.entity.FeatureServiceEntity;
import com.xuqm.tenant.entity.TenantEntity;
import com.xuqm.tenant.repository.AppRepository;
import com.xuqm.tenant.repository.FeatureServiceRepository;
import com.xuqm.tenant.repository.TenantRepository;
import com.xuqm.tenant.service.LicenseServiceClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.security.SecureRandom;
import java.time.LocalDateTime;
import java.util.Base64;
import java.util.List;
import java.util.UUID;
@Component
public class LicenseMigrationRunner implements ApplicationRunner {
private final LicenseServiceClient licenseClient;
private final AppRepository appRepository;
private final FeatureServiceRepository featureServiceRepository;
private final TenantRepository tenantRepository;
@Value("${license.migration.enabled:true}")
private boolean migrationEnabled;
@Value("${license.migration.app-name:临床知识库}")
private String migrationAppName;
private static final SecureRandom RANDOM = new SecureRandom();
public LicenseMigrationRunner(LicenseServiceClient licenseClient,
AppRepository appRepository,
FeatureServiceRepository featureServiceRepository,
TenantRepository tenantRepository) {
this.licenseClient = licenseClient;
this.appRepository = appRepository;
this.featureServiceRepository = featureServiceRepository;
this.tenantRepository = tenantRepository;
}
@Override
@Transactional
public void run(ApplicationArguments args) {
if (!migrationEnabled) {
return;
}
// 检查是否已有 LICENSE 类型的 FeatureService如果有说明已经迁移过
List<FeatureServiceEntity> allServices = featureServiceRepository.findAll();
boolean hasLicense = allServices.stream()
.anyMatch(s -> s.getServiceType() == FeatureServiceEntity.ServiceType.LICENSE);
if (hasLicense) {
return;
}
// 获取系统租户第一个创建的租户
TenantEntity systemTenant = tenantRepository.findFirstByOrderByCreatedAtAsc()
.orElse(null);
if (systemTenant == null) {
return;
}
// 目前只有一个公司 f713d051-0fbe-4f2d-bec9-bf7b96fc9ce4
// company_id 直接作为 appKey
String companyId = "f713d051-0fbe-4f2d-bec9-bf7b96fc9ce4";
// 检查是否已存在该 appKey 的应用
if (appRepository.findByAppKey(companyId).isPresent()) {
// 应用已存在只需确保 LICENSE 服务已开通
ensureLicenseFeatureService(companyId);
return;
}
// 创建应用appKey = company_id直接复用
AppEntity app = new AppEntity();
app.setId(UUID.randomUUID().toString());
app.setTenantId(systemTenant.getId());
app.setName(migrationAppName);
app.setPackageName("com.xuqm.clinical");
app.setAppKey(companyId); // 直接复用 company_id 作为 appKey
app.setAppSecret(generateSecret());
app.setCreatedAt(LocalDateTime.now());
appRepository.save(app);
// 自动开通 LICENSE 服务所有平台
ensureLicenseFeatureService(companyId);
// 自动开通 FILE 服务与创建应用时一致
for (FeatureServiceEntity.Platform platform : FeatureServiceEntity.Platform.values()) {
FeatureServiceEntity entity = new FeatureServiceEntity();
entity.setId(UUID.randomUUID().toString());
entity.setAppKey(companyId);
entity.setPlatform(platform);
entity.setServiceType(FeatureServiceEntity.ServiceType.FILE);
entity.setEnabled(true);
entity.setCreatedAt(LocalDateTime.now());
featureServiceRepository.save(entity);
}
}
private void ensureLicenseFeatureService(String appKey) {
for (FeatureServiceEntity.Platform platform : FeatureServiceEntity.Platform.values()) {
featureServiceRepository
.findByAppKeyAndPlatformAndServiceType(appKey, platform, FeatureServiceEntity.ServiceType.LICENSE)
.orElseGet(() -> {
FeatureServiceEntity feature = new FeatureServiceEntity();
feature.setId(UUID.randomUUID().toString());
feature.setAppKey(appKey);
feature.setPlatform(platform);
feature.setServiceType(FeatureServiceEntity.ServiceType.LICENSE);
feature.setEnabled(true);
feature.setConfig("{\"maxDevices\":1}");
feature.setCreatedAt(LocalDateTime.now());
return featureServiceRepository.save(feature);
});
}
}
private String generateSecret() {
byte[] bytes = new byte[32];
RANDOM.nextBytes(bytes);
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
}
}

查看文件

@ -154,7 +154,7 @@ public class FeatureServiceManager {
}
if (req.getServiceType() == FeatureServiceEntity.ServiceType.LICENSE) {
appRepository.findByAppKey(normalizedAppId).ifPresent(app ->
licenseServiceClient.syncCompany(app.getAppKey(), app.getName(), 1));
licenseServiceClient.syncAppLicense(app.getAppKey(), app.getName(), 1));
}
return req;
}

查看文件

@ -23,9 +23,9 @@ public class LicenseServiceClient {
private final RestTemplate restTemplate = new RestTemplate();
private final ObjectMapper objectMapper = new ObjectMapper();
public boolean isCompanyExists(String appKey) {
public boolean isAppLicenseExists(String appKey) {
try {
ResponseEntity<String> response = callInternal("/api/license/internal/companies/" + appKey + "/status", HttpMethod.GET, null);
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 node.path("data").path("exists").asBoolean(false);
@ -38,7 +38,7 @@ public class LicenseServiceClient {
public List<Map<String, Object>> listDevices(String appKey) {
try {
ResponseEntity<String> response = callInternal("/api/license/internal/companies/" + appKey + "/devices", HttpMethod.GET, null);
ResponseEntity<String> response = callInternal("/api/license/internal/apps/" + appKey + "/devices", 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<>() {});
@ -49,14 +49,14 @@ public class LicenseServiceClient {
return List.of();
}
public void syncCompany(String appKey, String name, Integer maxDevices) {
public void syncAppLicense(String appKey, String name, Integer maxDevices) {
try {
Map<String, Object> body = Map.of(
"id", appKey,
"name", name,
"maxDevices", maxDevices != null ? maxDevices : 1
);
callInternal("/api/license/internal/companies", HttpMethod.POST, body);
callInternal("/api/license/internal/apps", HttpMethod.POST, body);
} catch (Exception e) {
// ignore
}

查看文件

@ -62,9 +62,6 @@ license:
base-url: ${LICENSE_SERVICE_BASE_URL:http://license-service:8085}
public-base-url: ${LICENSE_PUBLIC_BASE_URL:https://auth.dev.xuqinmin.com/}
internal-token: ${LICENSE_INTERNAL_TOKEN:xuqm-license-internal-token}
migration:
enabled: true
app-name: 临床知识库
captcha:
expire-seconds: 300