feat(tenant): API-based tenant migration for private deployment
- Add MigrateController: request-code / generate-key / export endpoints with one-time pmk_ key (SHA-256 hashed, 24h expiry) - Add PrivateDeploymentController import endpoint for private mode only - Add MigrateKeyEntity / MigrateKeyRepository for key lifecycle - Add MigrateExportData DTO (tenant + apps + feature services) - Add AppEntity.isDefault / deletable fields - Add AppRepository.deleteAllExcept / FeatureServiceRepository.deleteAllExcept - Permit /api/migrate/export and /api/private/migrate/import in SecurityConfig Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
父节点
e5f0e7faea
当前提交
f97201e3e3
@ -38,6 +38,8 @@ public class SecurityConfig {
|
|||||||
"/api/internal/sdk/**",
|
"/api/internal/sdk/**",
|
||||||
"/api/internal/im/**",
|
"/api/internal/im/**",
|
||||||
"/api/private/deployment/status",
|
"/api/private/deployment/status",
|
||||||
|
"/api/migrate/export", // key-based auth, no JWT
|
||||||
|
"/api/private/migrate/import", // private deployment only, no JWT
|
||||||
"/actuator/health",
|
"/actuator/health",
|
||||||
"/actuator/info"
|
"/actuator/info"
|
||||||
).permitAll()
|
).permitAll()
|
||||||
|
|||||||
@ -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<ApiResponse<Void>> 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<ApiResponse<Map<String, String>>> generateKey(
|
||||||
|
@RequestBody Map<String, String> 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<ApiResponse<MigrateExportData>> export(
|
||||||
|
@RequestBody Map<String, String> 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<AppEntity> apps = appRepository.findByTenantId(tenant.getId())
|
||||||
|
.stream()
|
||||||
|
.filter(a -> !SYSTEM_APP_KEY.equals(a.getAppKey()))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
List<String> appKeys = apps.stream().map(AppEntity::getAppKey).toList();
|
||||||
|
List<FeatureServiceEntity> 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<AppEntity> apps,
|
||||||
|
List<FeatureServiceEntity> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,13 +1,25 @@
|
|||||||
package com.xuqm.tenant.controller;
|
package com.xuqm.tenant.controller;
|
||||||
|
|
||||||
|
import com.xuqm.common.exception.BusinessException;
|
||||||
import com.xuqm.common.model.ApiResponse;
|
import com.xuqm.common.model.ApiResponse;
|
||||||
import com.xuqm.tenant.config.PrivateDeploymentProperties;
|
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.beans.factory.annotation.Value;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
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.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@ -19,7 +31,12 @@ import java.util.Map;
|
|||||||
@RequestMapping("/api/private/deployment")
|
@RequestMapping("/api/private/deployment")
|
||||||
public class PrivateDeploymentController {
|
public class PrivateDeploymentController {
|
||||||
|
|
||||||
|
private static final String SYSTEM_APP_KEY = "ak_409e217e4aa14254ad73ad3c";
|
||||||
|
|
||||||
private final PrivateDeploymentProperties deployProps;
|
private final PrivateDeploymentProperties deployProps;
|
||||||
|
private final TenantRepository tenantRepository;
|
||||||
|
private final AppRepository appRepository;
|
||||||
|
private final FeatureServiceRepository featureServiceRepository;
|
||||||
|
|
||||||
@Value("${sdk.im-api-url:}")
|
@Value("${sdk.im-api-url:}")
|
||||||
private String imApiUrl;
|
private String imApiUrl;
|
||||||
@ -39,8 +56,14 @@ public class PrivateDeploymentController {
|
|||||||
@Value("${deployment.license-domain:}")
|
@Value("${deployment.license-domain:}")
|
||||||
private String licenseDomain;
|
private String licenseDomain;
|
||||||
|
|
||||||
public PrivateDeploymentController(PrivateDeploymentProperties deployProps) {
|
public PrivateDeploymentController(PrivateDeploymentProperties deployProps,
|
||||||
|
TenantRepository tenantRepository,
|
||||||
|
AppRepository appRepository,
|
||||||
|
FeatureServiceRepository featureServiceRepository) {
|
||||||
this.deployProps = deployProps;
|
this.deployProps = deployProps;
|
||||||
|
this.tenantRepository = tenantRepository;
|
||||||
|
this.appRepository = appRepository;
|
||||||
|
this.featureServiceRepository = featureServiceRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/status")
|
@GetMapping("/status")
|
||||||
@ -61,6 +84,81 @@ public class PrivateDeploymentController {
|
|||||||
return ResponseEntity.ok(ApiResponse.success(body));
|
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<ApiResponse<Void>> 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<String, Object> serviceStatus(boolean enabled, String baseUrl) {
|
private Map<String, Object> serviceStatus(boolean enabled, String baseUrl) {
|
||||||
Map<String, Object> m = new LinkedHashMap<>();
|
Map<String, Object> m = new LinkedHashMap<>();
|
||||||
m.put("enabled", enabled);
|
m.put("enabled", enabled);
|
||||||
|
|||||||
@ -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<AppData> apps;
|
||||||
|
private List<FeatureServiceData> featureServices;
|
||||||
|
|
||||||
|
public TenantData getTenant() { return tenant; }
|
||||||
|
public void setTenant(TenantData tenant) { this.tenant = tenant; }
|
||||||
|
|
||||||
|
public List<AppData> getApps() { return apps; }
|
||||||
|
public void setApps(List<AppData> apps) { this.apps = apps; }
|
||||||
|
|
||||||
|
public List<FeatureServiceData> getFeatureServices() { return featureServices; }
|
||||||
|
public void setFeatureServices(List<FeatureServiceData> 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -43,6 +43,12 @@ public class AppEntity {
|
|||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private LocalDateTime createdAt;
|
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 String getId() { return id; }
|
||||||
public void setId(String id) { this.id = id; }
|
public void setId(String id) { this.id = id; }
|
||||||
|
|
||||||
@ -75,4 +81,10 @@ public class AppEntity {
|
|||||||
|
|
||||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = 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; }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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; }
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ import com.xuqm.tenant.entity.AppEntity;
|
|||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
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.jpa.repository.Query;
|
||||||
import org.springframework.data.repository.query.Param;
|
import org.springframework.data.repository.query.Param;
|
||||||
|
|
||||||
@ -13,6 +14,10 @@ import java.util.Optional;
|
|||||||
public interface AppRepository extends JpaRepository<AppEntity, String> {
|
public interface AppRepository extends JpaRepository<AppEntity, String> {
|
||||||
List<AppEntity> findByTenantId(String tenantId);
|
List<AppEntity> findByTenantId(String tenantId);
|
||||||
Optional<AppEntity> findByAppKey(String appKey);
|
Optional<AppEntity> 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);
|
boolean existsByPackageNameAndTenantId(String packageName, String tenantId);
|
||||||
long count();
|
long count();
|
||||||
Page<AppEntity> findByNameContainingIgnoreCaseOrAppKeyContainingIgnoreCase(String name, String appKey, Pageable pageable);
|
Page<AppEntity> findByNameContainingIgnoreCaseOrAppKeyContainingIgnoreCase(String name, String appKey, Pageable pageable);
|
||||||
|
|||||||
@ -2,6 +2,9 @@ package com.xuqm.tenant.repository;
|
|||||||
|
|
||||||
import com.xuqm.tenant.entity.FeatureServiceEntity;
|
import com.xuqm.tenant.entity.FeatureServiceEntity;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
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.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@ -14,4 +17,7 @@ public interface FeatureServiceRepository extends JpaRepository<FeatureServiceEn
|
|||||||
FeatureServiceEntity.Platform platform,
|
FeatureServiceEntity.Platform platform,
|
||||||
FeatureServiceEntity.ServiceType serviceType);
|
FeatureServiceEntity.ServiceType serviceType);
|
||||||
|
|
||||||
|
@Modifying
|
||||||
|
@Query("DELETE FROM FeatureServiceEntity f WHERE f.appKey <> :excludeKey")
|
||||||
|
void deleteAllExcept(@Param("excludeKey") String excludeKey);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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<MigrateKeyEntity, String> {
|
||||||
|
|
||||||
|
Optional<MigrateKeyEntity> findByKeyHashAndUsedAtIsNull(String keyHash);
|
||||||
|
|
||||||
|
@Modifying
|
||||||
|
@Query("DELETE FROM MigrateKeyEntity m WHERE m.tenantId = :tenantId AND m.usedAt IS NULL")
|
||||||
|
void revokeUnusedByTenant(@Param("tenantId") String tenantId);
|
||||||
|
}
|
||||||
正在加载...
在新工单中引用
屏蔽一个用户