diff --git a/tenant-service/src/main/java/com/xuqm/tenant/config/SecurityConfig.java b/tenant-service/src/main/java/com/xuqm/tenant/config/SecurityConfig.java index 9aed72d..3502eef 100644 --- a/tenant-service/src/main/java/com/xuqm/tenant/config/SecurityConfig.java +++ b/tenant-service/src/main/java/com/xuqm/tenant/config/SecurityConfig.java @@ -38,6 +38,8 @@ public class SecurityConfig { "/api/internal/sdk/**", "/api/internal/im/**", "/api/private/deployment/status", + "/api/migrate/export", // key-based auth, no JWT + "/api/private/migrate/import", // private deployment only, no JWT "/actuator/health", "/actuator/info" ).permitAll() diff --git a/tenant-service/src/main/java/com/xuqm/tenant/controller/MigrateController.java b/tenant-service/src/main/java/com/xuqm/tenant/controller/MigrateController.java new file mode 100644 index 0000000..4e19181 --- /dev/null +++ b/tenant-service/src/main/java/com/xuqm/tenant/controller/MigrateController.java @@ -0,0 +1,215 @@ +package com.xuqm.tenant.controller; + +import com.xuqm.common.exception.BusinessException; +import com.xuqm.common.model.ApiResponse; +import com.xuqm.tenant.config.PrivateDeploymentProperties; +import com.xuqm.tenant.dto.MigrateExportData; +import com.xuqm.tenant.entity.AppEntity; +import com.xuqm.tenant.entity.FeatureServiceEntity; +import com.xuqm.tenant.entity.MigrateKeyEntity; +import com.xuqm.tenant.entity.TenantEntity; +import com.xuqm.tenant.repository.AppRepository; +import com.xuqm.tenant.repository.FeatureServiceRepository; +import com.xuqm.tenant.repository.MigrateKeyRepository; +import com.xuqm.tenant.repository.TenantRepository; +import com.xuqm.tenant.service.EmailService; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.time.LocalDateTime; +import java.util.Base64; +import java.util.HexFormat; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * Public platform side: generates migration keys and exports tenant data. + * Only active when DEPLOYMENT_MODE != PRIVATE. + */ +@RestController +@RequestMapping("/api/migrate") +public class MigrateController { + + private static final String SYSTEM_APP_KEY = "ak_409e217e4aa14254ad73ad3c"; + private static final String PURPOSE = "PRIVATE_MIGRATE"; + private static final int KEY_EXPIRE_HOURS = 24; + private static final SecureRandom RANDOM = new SecureRandom(); + + private final PrivateDeploymentProperties deployProps; + private final EmailService emailService; + private final MigrateKeyRepository migrateKeyRepository; + private final TenantRepository tenantRepository; + private final AppRepository appRepository; + private final FeatureServiceRepository featureServiceRepository; + + public MigrateController(PrivateDeploymentProperties deployProps, + EmailService emailService, + MigrateKeyRepository migrateKeyRepository, + TenantRepository tenantRepository, + AppRepository appRepository, + FeatureServiceRepository featureServiceRepository) { + this.deployProps = deployProps; + this.emailService = emailService; + this.migrateKeyRepository = migrateKeyRepository; + this.tenantRepository = tenantRepository; + this.appRepository = appRepository; + this.featureServiceRepository = featureServiceRepository; + } + + /** Step 1: tenant requests email verification code. Requires login. */ + @PostMapping("/request-code") + public ResponseEntity> requestCode(@AuthenticationPrincipal String tenantId) { + requirePublicMode(); + TenantEntity tenant = tenantRepository.findById(tenantId) + .orElseThrow(() -> new BusinessException("租户不存在")); + emailService.sendVerificationCode(tenant.getEmail(), PURPOSE); + return ResponseEntity.ok(ApiResponse.ok()); + } + + /** Step 2: verify email code → generate a one-time migration key. Requires login. */ + @PostMapping("/generate-key") + @Transactional + public ResponseEntity>> generateKey( + @RequestBody Map body, + @AuthenticationPrincipal String tenantId) { + requirePublicMode(); + TenantEntity tenant = tenantRepository.findById(tenantId) + .orElseThrow(() -> new BusinessException("租户不存在")); + + emailService.verify(tenant.getEmail(), body.get("code"), PURPOSE); + + // Revoke previous unused keys for this tenant + migrateKeyRepository.revokeUnusedByTenant(tenantId); + + // Generate plaintext key: pmk_ + 32 Base64URL chars + byte[] raw = new byte[24]; + RANDOM.nextBytes(raw); + String plainKey = "pmk_" + Base64.getUrlEncoder().withoutPadding().encodeToString(raw); + + MigrateKeyEntity entity = new MigrateKeyEntity(); + entity.setId(UUID.randomUUID().toString()); + entity.setTenantId(tenantId); + entity.setKeyHash(sha256(plainKey)); + entity.setCreatedAt(LocalDateTime.now()); + entity.setExpiresAt(LocalDateTime.now().plusHours(KEY_EXPIRE_HOURS)); + migrateKeyRepository.save(entity); + + return ResponseEntity.ok(ApiResponse.success(Map.of("migrationKey", plainKey))); + } + + /** Step 3: deploy script calls this with the migration key to export tenant data. No JWT required. */ + @PostMapping("/export") + @Transactional + public ResponseEntity> export( + @RequestBody Map body) { + requirePublicMode(); + + String key = body.get("migrationKey"); + if (key == null || key.isBlank()) { + throw new BusinessException("迁移密钥不能为空"); + } + + String hash = sha256(key); + MigrateKeyEntity keyEntity = migrateKeyRepository.findByKeyHashAndUsedAtIsNull(hash) + .orElseThrow(() -> new BusinessException("迁移密钥无效或已使用")); + + if (keyEntity.getExpiresAt().isBefore(LocalDateTime.now())) { + throw new BusinessException("迁移密钥已过期,请重新生成"); + } + + // Mark key as used + keyEntity.setUsedAt(LocalDateTime.now()); + migrateKeyRepository.save(keyEntity); + + TenantEntity tenant = tenantRepository.findById(keyEntity.getTenantId()) + .orElseThrow(() -> new BusinessException("租户不存在")); + + List apps = appRepository.findByTenantId(tenant.getId()) + .stream() + .filter(a -> !SYSTEM_APP_KEY.equals(a.getAppKey())) + .toList(); + + List appKeys = apps.stream().map(AppEntity::getAppKey).toList(); + List featureServices = appKeys.isEmpty() + ? List.of() + : featureServiceRepository.findAll().stream() + .filter(f -> appKeys.contains(f.getAppKey())) + .toList(); + + MigrateExportData data = buildExport(tenant, apps, featureServices); + return ResponseEntity.ok(ApiResponse.success(data)); + } + + // ── Helpers ──────────────────────────────────────────────────────────────── + + private void requirePublicMode() { + if (deployProps.isPrivate()) { + throw new BusinessException(403, "此接口仅在公有化平台可用"); + } + } + + private MigrateExportData buildExport(TenantEntity tenant, + List apps, + List featureServices) { + MigrateExportData data = new MigrateExportData(); + + MigrateExportData.TenantData td = new MigrateExportData.TenantData(); + td.setId(tenant.getId()); + td.setUsername(tenant.getUsername()); + td.setEmail(tenant.getEmail()); + td.setNickname(tenant.getNickname()); + td.setPhone(tenant.getPhone()); + td.setPasswordHash(tenant.getPassword()); + td.setCreatedAt(tenant.getCreatedAt().toString()); + data.setTenant(td); + + data.setApps(apps.stream().map(a -> { + MigrateExportData.AppData ad = new MigrateExportData.AppData(); + ad.setId(a.getId()); + ad.setAppKey(a.getAppKey()); + ad.setAppSecret(a.getAppSecret()); + ad.setName(a.getName()); + ad.setPackageName(a.getPackageName()); + ad.setIosBundleId(a.getIosBundleId()); + ad.setHarmonyBundleName(a.getHarmonyBundleName()); + ad.setDescription(a.getDescription()); + ad.setIconUrl(a.getIconUrl()); + ad.setCreatedAt(a.getCreatedAt().toString()); + return ad; + }).toList()); + + data.setFeatureServices(featureServices.stream().map(f -> { + MigrateExportData.FeatureServiceData fd = new MigrateExportData.FeatureServiceData(); + fd.setId(f.getId()); + fd.setAppKey(f.getAppKey()); + fd.setPlatform(f.getPlatform().name()); + fd.setServiceType(f.getServiceType().name()); + fd.setEnabled(f.isEnabled()); + fd.setSecretKey(f.getSecretKey()); + fd.setConfig(f.getConfig()); + fd.setCreatedAt(f.getCreatedAt().toString()); + return fd; + }).toList()); + + return data; + } + + private static String sha256(String input) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8)); + return HexFormat.of().formatHex(hash); + } catch (Exception e) { + throw new RuntimeException("SHA-256 failed", e); + } + } +} diff --git a/tenant-service/src/main/java/com/xuqm/tenant/controller/PrivateDeploymentController.java b/tenant-service/src/main/java/com/xuqm/tenant/controller/PrivateDeploymentController.java index 65d99fa..48ca904 100644 --- a/tenant-service/src/main/java/com/xuqm/tenant/controller/PrivateDeploymentController.java +++ b/tenant-service/src/main/java/com/xuqm/tenant/controller/PrivateDeploymentController.java @@ -1,13 +1,25 @@ package com.xuqm.tenant.controller; +import com.xuqm.common.exception.BusinessException; import com.xuqm.common.model.ApiResponse; import com.xuqm.tenant.config.PrivateDeploymentProperties; +import com.xuqm.tenant.dto.MigrateExportData; +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 org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.time.LocalDateTime; import java.util.LinkedHashMap; import java.util.Map; @@ -19,7 +31,12 @@ import java.util.Map; @RequestMapping("/api/private/deployment") public class PrivateDeploymentController { + private static final String SYSTEM_APP_KEY = "ak_409e217e4aa14254ad73ad3c"; + private final PrivateDeploymentProperties deployProps; + private final TenantRepository tenantRepository; + private final AppRepository appRepository; + private final FeatureServiceRepository featureServiceRepository; @Value("${sdk.im-api-url:}") private String imApiUrl; @@ -39,8 +56,14 @@ public class PrivateDeploymentController { @Value("${deployment.license-domain:}") private String licenseDomain; - public PrivateDeploymentController(PrivateDeploymentProperties deployProps) { + public PrivateDeploymentController(PrivateDeploymentProperties deployProps, + TenantRepository tenantRepository, + AppRepository appRepository, + FeatureServiceRepository featureServiceRepository) { this.deployProps = deployProps; + this.tenantRepository = tenantRepository; + this.appRepository = appRepository; + this.featureServiceRepository = featureServiceRepository; } @GetMapping("/status") @@ -61,6 +84,81 @@ public class PrivateDeploymentController { return ResponseEntity.ok(ApiResponse.success(body)); } + /** Import tenant data exported from the public platform. PRIVATE mode only, no JWT required. */ + @PostMapping("/migrate/import") + @Transactional + public ResponseEntity> importData(@RequestBody MigrateExportData data) { + if (!deployProps.isPrivate()) { + throw new BusinessException(403, "此接口仅在私有化部署可用"); + } + + MigrateExportData.TenantData td = data.getTenant(); + if (td == null) { + throw new BusinessException("迁移数据不完整:缺少租户信息"); + } + + // Clear existing tenant data, preserving the system app + featureServiceRepository.deleteAllExcept(SYSTEM_APP_KEY); + appRepository.deleteAllExcept(SYSTEM_APP_KEY); + tenantRepository.deleteAll(); + + // Import tenant + TenantEntity tenant = new TenantEntity(); + tenant.setId(td.getId()); + tenant.setUsername(td.getUsername()); + tenant.setEmail(td.getEmail()); + tenant.setNickname(td.getNickname()); + tenant.setPhone(td.getPhone()); + tenant.setPassword(td.getPasswordHash()); + tenant.setType(TenantEntity.Type.MAIN); + tenant.setStatus(TenantEntity.Status.ACTIVE); + tenant.setCreatedAt(LocalDateTime.parse(td.getCreatedAt())); + tenantRepository.save(tenant); + + // Bind system app to the imported tenant + appRepository.findByAppKey(SYSTEM_APP_KEY).ifPresent(app -> { + app.setTenantId(td.getId()); + appRepository.save(app); + }); + + // Import apps + if (data.getApps() != null) { + for (MigrateExportData.AppData ad : data.getApps()) { + AppEntity app = new AppEntity(); + app.setId(ad.getId()); + app.setTenantId(td.getId()); + app.setAppKey(ad.getAppKey()); + app.setAppSecret(ad.getAppSecret()); + app.setName(ad.getName()); + app.setPackageName(ad.getPackageName()); + app.setIosBundleId(ad.getIosBundleId()); + app.setHarmonyBundleName(ad.getHarmonyBundleName()); + app.setDescription(ad.getDescription()); + app.setIconUrl(ad.getIconUrl()); + app.setCreatedAt(LocalDateTime.parse(ad.getCreatedAt())); + appRepository.save(app); + } + } + + // Import feature services + if (data.getFeatureServices() != null) { + for (MigrateExportData.FeatureServiceData fd : data.getFeatureServices()) { + FeatureServiceEntity fs = new FeatureServiceEntity(); + fs.setId(fd.getId()); + fs.setAppKey(fd.getAppKey()); + fs.setPlatform(FeatureServiceEntity.Platform.valueOf(fd.getPlatform())); + fs.setServiceType(FeatureServiceEntity.ServiceType.valueOf(fd.getServiceType())); + fs.setEnabled(fd.isEnabled()); + fs.setSecretKey(fd.getSecretKey()); + fs.setConfig(fd.getConfig()); + fs.setCreatedAt(LocalDateTime.parse(fd.getCreatedAt())); + featureServiceRepository.save(fs); + } + } + + return ResponseEntity.ok(ApiResponse.ok()); + } + private Map serviceStatus(boolean enabled, String baseUrl) { Map m = new LinkedHashMap<>(); m.put("enabled", enabled); diff --git a/tenant-service/src/main/java/com/xuqm/tenant/dto/MigrateExportData.java b/tenant-service/src/main/java/com/xuqm/tenant/dto/MigrateExportData.java new file mode 100644 index 0000000..149fb79 --- /dev/null +++ b/tenant-service/src/main/java/com/xuqm/tenant/dto/MigrateExportData.java @@ -0,0 +1,107 @@ +package com.xuqm.tenant.dto; + +import java.util.List; + +/** Full tenant snapshot transferred from public platform to private deployment. */ +public class MigrateExportData { + + private TenantData tenant; + private List apps; + private List featureServices; + + public TenantData getTenant() { return tenant; } + public void setTenant(TenantData tenant) { this.tenant = tenant; } + + public List getApps() { return apps; } + public void setApps(List apps) { this.apps = apps; } + + public List getFeatureServices() { return featureServices; } + public void setFeatureServices(List featureServices) { this.featureServices = featureServices; } + + public static class TenantData { + private String id; + private String username; + private String email; + private String nickname; + private String phone; + private String passwordHash; + private String createdAt; + + public String getId() { return id; } + public void setId(String id) { this.id = id; } + public String getUsername() { return username; } + public void setUsername(String username) { this.username = username; } + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + public String getNickname() { return nickname; } + public void setNickname(String nickname) { this.nickname = nickname; } + public String getPhone() { return phone; } + public void setPhone(String phone) { this.phone = phone; } + public String getPasswordHash() { return passwordHash; } + public void setPasswordHash(String passwordHash) { this.passwordHash = passwordHash; } + public String getCreatedAt() { return createdAt; } + public void setCreatedAt(String createdAt) { this.createdAt = createdAt; } + } + + public static class AppData { + private String id; + private String appKey; + private String appSecret; + private String name; + private String packageName; + private String iosBundleId; + private String harmonyBundleName; + private String description; + private String iconUrl; + private String createdAt; + + 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 getAppSecret() { return appSecret; } + public void setAppSecret(String appSecret) { this.appSecret = appSecret; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public String getPackageName() { return packageName; } + public void setPackageName(String packageName) { this.packageName = packageName; } + public String getIosBundleId() { return iosBundleId; } + public void setIosBundleId(String iosBundleId) { this.iosBundleId = iosBundleId; } + public String getHarmonyBundleName() { return harmonyBundleName; } + public void setHarmonyBundleName(String harmonyBundleName) { this.harmonyBundleName = harmonyBundleName; } + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + public String getIconUrl() { return iconUrl; } + public void setIconUrl(String iconUrl) { this.iconUrl = iconUrl; } + public String getCreatedAt() { return createdAt; } + public void setCreatedAt(String createdAt) { this.createdAt = createdAt; } + } + + public static class FeatureServiceData { + private String id; + private String appKey; + private String platform; + private String serviceType; + private boolean enabled; + private String secretKey; + private String config; + private String createdAt; + + 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 getPlatform() { return platform; } + public void setPlatform(String platform) { this.platform = platform; } + public String getServiceType() { return serviceType; } + public void setServiceType(String serviceType) { this.serviceType = serviceType; } + public boolean isEnabled() { return enabled; } + public void setEnabled(boolean enabled) { this.enabled = enabled; } + public String getSecretKey() { return secretKey; } + public void setSecretKey(String secretKey) { this.secretKey = secretKey; } + public String getConfig() { return config; } + public void setConfig(String config) { this.config = config; } + public String getCreatedAt() { return createdAt; } + public void setCreatedAt(String createdAt) { this.createdAt = createdAt; } + } +} diff --git a/tenant-service/src/main/java/com/xuqm/tenant/entity/AppEntity.java b/tenant-service/src/main/java/com/xuqm/tenant/entity/AppEntity.java index 4733590..9dc26c3 100644 --- a/tenant-service/src/main/java/com/xuqm/tenant/entity/AppEntity.java +++ b/tenant-service/src/main/java/com/xuqm/tenant/entity/AppEntity.java @@ -43,6 +43,12 @@ public class AppEntity { @Column(nullable = false) private LocalDateTime createdAt; + @Column(name = "is_default", columnDefinition = "BIT(1) DEFAULT 0") + private boolean isDefault; + + @Column(columnDefinition = "BIT(1) DEFAULT 1") + private boolean deletable = true; + public String getId() { return id; } public void setId(String id) { this.id = id; } @@ -75,4 +81,10 @@ public class AppEntity { public LocalDateTime getCreatedAt() { return createdAt; } public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } + + public boolean isDefault() { return isDefault; } + public void setDefault(boolean isDefault) { this.isDefault = isDefault; } + + public boolean isDeletable() { return deletable; } + public void setDeletable(boolean deletable) { this.deletable = deletable; } } diff --git a/tenant-service/src/main/java/com/xuqm/tenant/entity/MigrateKeyEntity.java b/tenant-service/src/main/java/com/xuqm/tenant/entity/MigrateKeyEntity.java new file mode 100644 index 0000000..a91d45f --- /dev/null +++ b/tenant-service/src/main/java/com/xuqm/tenant/entity/MigrateKeyEntity.java @@ -0,0 +1,49 @@ +package com.xuqm.tenant.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.time.LocalDateTime; + +@Entity +@Table(name = "t_migrate_key") +public class MigrateKeyEntity { + + @Id + private String id; + + @Column(nullable = false, length = 64) + private String tenantId; + + /** SHA-256 hex of the plaintext migration key. */ + @Column(nullable = false, length = 128) + private String keyHash; + + @Column(nullable = false) + private LocalDateTime createdAt; + + @Column(nullable = false) + private LocalDateTime expiresAt; + + @Column + private LocalDateTime usedAt; + + public String getId() { return id; } + public void setId(String id) { this.id = id; } + + public String getTenantId() { return tenantId; } + public void setTenantId(String tenantId) { this.tenantId = tenantId; } + + public String getKeyHash() { return keyHash; } + public void setKeyHash(String keyHash) { this.keyHash = keyHash; } + + public LocalDateTime getCreatedAt() { return createdAt; } + public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } + + public LocalDateTime getExpiresAt() { return expiresAt; } + public void setExpiresAt(LocalDateTime expiresAt) { this.expiresAt = expiresAt; } + + public LocalDateTime getUsedAt() { return usedAt; } + public void setUsedAt(LocalDateTime usedAt) { this.usedAt = usedAt; } +} diff --git a/tenant-service/src/main/java/com/xuqm/tenant/repository/AppRepository.java b/tenant-service/src/main/java/com/xuqm/tenant/repository/AppRepository.java index b2b0103..2a3c138 100644 --- a/tenant-service/src/main/java/com/xuqm/tenant/repository/AppRepository.java +++ b/tenant-service/src/main/java/com/xuqm/tenant/repository/AppRepository.java @@ -4,6 +4,7 @@ import com.xuqm.tenant.entity.AppEntity; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -13,6 +14,10 @@ import java.util.Optional; public interface AppRepository extends JpaRepository { List findByTenantId(String tenantId); Optional findByAppKey(String appKey); + + @Modifying + @Query("DELETE FROM AppEntity a WHERE a.appKey <> :excludeKey") + void deleteAllExcept(@Param("excludeKey") String excludeKey); boolean existsByPackageNameAndTenantId(String packageName, String tenantId); long count(); Page findByNameContainingIgnoreCaseOrAppKeyContainingIgnoreCase(String name, String appKey, Pageable pageable); diff --git a/tenant-service/src/main/java/com/xuqm/tenant/repository/FeatureServiceRepository.java b/tenant-service/src/main/java/com/xuqm/tenant/repository/FeatureServiceRepository.java index 3b571c9..e2ac6ae 100644 --- a/tenant-service/src/main/java/com/xuqm/tenant/repository/FeatureServiceRepository.java +++ b/tenant-service/src/main/java/com/xuqm/tenant/repository/FeatureServiceRepository.java @@ -2,6 +2,9 @@ package com.xuqm.tenant.repository; import com.xuqm.tenant.entity.FeatureServiceEntity; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.List; import java.util.Optional; @@ -14,4 +17,7 @@ public interface FeatureServiceRepository extends JpaRepository :excludeKey") + void deleteAllExcept(@Param("excludeKey") String excludeKey); } diff --git a/tenant-service/src/main/java/com/xuqm/tenant/repository/MigrateKeyRepository.java b/tenant-service/src/main/java/com/xuqm/tenant/repository/MigrateKeyRepository.java new file mode 100644 index 0000000..25171a1 --- /dev/null +++ b/tenant-service/src/main/java/com/xuqm/tenant/repository/MigrateKeyRepository.java @@ -0,0 +1,18 @@ +package com.xuqm.tenant.repository; + +import com.xuqm.tenant.entity.MigrateKeyEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Optional; + +public interface MigrateKeyRepository extends JpaRepository { + + Optional findByKeyHashAndUsedAtIsNull(String keyHash); + + @Modifying + @Query("DELETE FROM MigrateKeyEntity m WHERE m.tenantId = :tenantId AND m.usedAt IS NULL") + void revokeUnusedByTenant(@Param("tenantId") String tenantId); +}