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

查看文件

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

查看文件

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

查看文件

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

查看文件

@ -14,20 +14,20 @@ public class LicenseAuthService {
this.jwtUtil = jwtUtil; 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( return jwtUtil.generate(deviceId, Map.of(
"companyId", companyId, "appKey", appKey,
"deviceId", deviceId, "deviceId", deviceId,
"recordId", recordId "recordId", recordId
)); ));
} }
public boolean verifyTokenPayload(String token, String companyId, String deviceId) { public boolean verifyTokenPayload(String token, String appKey, String deviceId) {
try { try {
var claims = jwtUtil.parse(token); 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)); 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) { } catch (Exception e) {
return false; 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) { if (req.getServiceType() == FeatureServiceEntity.ServiceType.LICENSE) {
appRepository.findByAppKey(normalizedAppId).ifPresent(app -> appRepository.findByAppKey(normalizedAppId).ifPresent(app ->
licenseServiceClient.syncCompany(app.getAppKey(), app.getName(), 1)); licenseServiceClient.syncAppLicense(app.getAppKey(), app.getName(), 1));
} }
return req; return req;
} }

查看文件

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

查看文件

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