- 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>
169 行
6.9 KiB
Java
169 行
6.9 KiB
Java
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;
|
|
|
|
/**
|
|
* Public endpoint — returns deployment mode and enabled service capabilities.
|
|
* Safe to expose without authentication: contains no credentials.
|
|
*/
|
|
@RestController
|
|
@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;
|
|
|
|
@Value("${sdk.file-service-url:}")
|
|
private String fileServiceUrl;
|
|
|
|
@Value("${deployment.im-domain:}")
|
|
private String imDomain;
|
|
|
|
@Value("${deployment.push-domain:}")
|
|
private String pushDomain;
|
|
|
|
@Value("${deployment.update-domain:}")
|
|
private String updateDomain;
|
|
|
|
@Value("${deployment.license-domain:}")
|
|
private String licenseDomain;
|
|
|
|
public PrivateDeploymentController(PrivateDeploymentProperties deployProps,
|
|
TenantRepository tenantRepository,
|
|
AppRepository appRepository,
|
|
FeatureServiceRepository featureServiceRepository) {
|
|
this.deployProps = deployProps;
|
|
this.tenantRepository = tenantRepository;
|
|
this.appRepository = appRepository;
|
|
this.featureServiceRepository = featureServiceRepository;
|
|
}
|
|
|
|
@GetMapping("/status")
|
|
public ResponseEntity<ApiResponse<Map<String, Object>>> status() {
|
|
Map<String, Object> services = new LinkedHashMap<>();
|
|
|
|
services.put("file", serviceStatus(deployProps.isEnableFile(), fileServiceUrl));
|
|
services.put("im", serviceStatus(deployProps.isEnableIm(), imDomain));
|
|
services.put("push", serviceStatus(deployProps.isEnablePush(), pushDomain));
|
|
services.put("update", serviceStatus(deployProps.isEnableUpdate(), updateDomain));
|
|
services.put("license", serviceStatus(deployProps.isEnableLicense(), licenseDomain));
|
|
|
|
Map<String, Object> body = new LinkedHashMap<>();
|
|
body.put("mode", deployProps.getMode());
|
|
body.put("tenantRegisterEnabled", deployProps.isTenantRegisterEnabled());
|
|
body.put("services", services);
|
|
|
|
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) {
|
|
Map<String, Object> m = new LinkedHashMap<>();
|
|
m.put("enabled", enabled);
|
|
m.put("baseUrl", enabled ? baseUrl : null);
|
|
return m;
|
|
}
|
|
}
|