fix: suppress duplicate-result errors and hide system apps from private deployment
update-service: - AppPublishConfigRepository/AppStoreConfigRepository: change Optional-returning findBy methods to findTopBy...OrderByUpdatedAtDesc to tolerate duplicate rows in public DB and avoid IncorrectResultSizeDataAccessException - Revert GlobalExceptionHandler to safe "服务器内部错误" (debug details removed) tenant-service: - SdkAppInitializer: skip Demo Chat creation on DEPLOYMENT_MODE=PRIVATE; migrate existing system apps (ak_demo_chat, IM platform app) to is_default=true - SdkAppProvisioningService.ensureApp: mark all platform-provisioned apps as is_default=true, deletable=false so they don't appear in user's app list - PrivateTenantBootstrapInitializer: migrate existing private bootstrap apps to is_default=true on upgrade - AppService.listByTenant: filter out is_default=true system apps from the list Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
父节点
855b17ef0e
当前提交
9728dbb002
@ -89,6 +89,11 @@ public class PrivateTenantBootstrapInitializer implements ApplicationRunner {
|
||||
if (app == null) {
|
||||
provisioningService.ensureApp(bootstrapAppKey, true);
|
||||
log.info("[PRIVATE] Bootstrap app created: {}", bootstrapAppKey);
|
||||
} else if (!app.isDefault() || app.isDeletable()) {
|
||||
app.setDefault(true);
|
||||
app.setDeletable(false);
|
||||
appRepository.save(app);
|
||||
log.info("[PRIVATE] Bootstrap app marked as system app: {}", bootstrapAppKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
package com.xuqm.tenant.config;
|
||||
|
||||
import com.xuqm.tenant.entity.AppEntity;
|
||||
import com.xuqm.tenant.repository.AppRepository;
|
||||
import com.xuqm.tenant.service.SdkAppProvisioningService;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
@ -9,13 +12,42 @@ import org.springframework.stereotype.Component;
|
||||
public class SdkAppInitializer implements ApplicationRunner {
|
||||
|
||||
private final SdkAppProvisioningService provisioningService;
|
||||
private final PrivateDeploymentProperties deployProps;
|
||||
private final AppRepository appRepository;
|
||||
|
||||
public SdkAppInitializer(SdkAppProvisioningService provisioningService) {
|
||||
@Value("${sdk.bootstrap-app-key:ak_demo_chat}")
|
||||
private String bootstrapAppKey;
|
||||
|
||||
@Value("${sdk.im-platform-app-key:ak_409e217e4aa14254ad73ad3c}")
|
||||
private String imPlatformAppKey;
|
||||
|
||||
public SdkAppInitializer(SdkAppProvisioningService provisioningService,
|
||||
PrivateDeploymentProperties deployProps,
|
||||
AppRepository appRepository) {
|
||||
this.provisioningService = provisioningService;
|
||||
this.deployProps = deployProps;
|
||||
this.appRepository = appRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) {
|
||||
provisioningService.ensureBootstrapApp();
|
||||
migrateExistingSystemApps();
|
||||
if (!deployProps.isPrivate()) {
|
||||
provisioningService.ensureBootstrapApp();
|
||||
}
|
||||
}
|
||||
|
||||
private void migrateExistingSystemApps() {
|
||||
markSystemApp(bootstrapAppKey);
|
||||
markSystemApp(imPlatformAppKey);
|
||||
}
|
||||
|
||||
private void markSystemApp(String appKey) {
|
||||
AppEntity app = appRepository.findByAppKey(appKey).orElse(null);
|
||||
if (app != null && (!app.isDefault() || app.isDeletable())) {
|
||||
app.setDefault(true);
|
||||
app.setDeletable(false);
|
||||
appRepository.save(app);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,7 +33,9 @@ public class AppService {
|
||||
}
|
||||
|
||||
public List<AppEntity> listByTenant(String tenantId) {
|
||||
return appRepository.findByTenantId(tenantId);
|
||||
return appRepository.findByTenantId(tenantId).stream()
|
||||
.filter(app -> !app.isDefault())
|
||||
.toList();
|
||||
}
|
||||
|
||||
public AppEntity getByAppKey(String appKey, String tenantId) {
|
||||
|
||||
@ -88,6 +88,8 @@ public class SdkAppProvisioningService {
|
||||
app.setAppKey(appKey);
|
||||
app.setAppSecret(generateSecret());
|
||||
app.setCreatedAt(LocalDateTime.now());
|
||||
app.setDefault(true);
|
||||
app.setDeletable(false);
|
||||
app = appRepository.save(app);
|
||||
|
||||
if (bootstrapDefaults) {
|
||||
|
||||
@ -46,13 +46,8 @@ public class GlobalExceptionHandler {
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<ApiResponse<Void>> handle(Exception ex) {
|
||||
log.error("Unhandled exception", ex);
|
||||
String detail = ex.getClass().getSimpleName() + ": " + ex.getMessage();
|
||||
Throwable cause = ex.getCause();
|
||||
if (cause != null) {
|
||||
detail += " | caused by: " + cause.getClass().getSimpleName() + ": " + cause.getMessage();
|
||||
}
|
||||
return ResponseEntity.internalServerError()
|
||||
.body(ApiResponse.error(500, detail));
|
||||
.body(ApiResponse.error(500, "服务器内部错误"));
|
||||
}
|
||||
|
||||
private HttpStatus resolveStatus(int code) {
|
||||
|
||||
@ -6,5 +6,5 @@ import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface AppPublishConfigRepository extends JpaRepository<AppPublishConfigEntity, String> {
|
||||
Optional<AppPublishConfigEntity> findByAppKey(String appKey);
|
||||
Optional<AppPublishConfigEntity> findTopByAppKeyOrderByUpdatedAtDesc(String appKey);
|
||||
}
|
||||
|
||||
@ -12,5 +12,5 @@ public interface AppStoreConfigRepository extends JpaRepository<AppStoreConfigEn
|
||||
|
||||
List<AppStoreConfigEntity> findByAppKeyAndEnabled(String appKey, boolean enabled);
|
||||
|
||||
Optional<AppStoreConfigEntity> findByAppKeyAndStoreType(String appKey, AppStoreConfigEntity.StoreType storeType);
|
||||
Optional<AppStoreConfigEntity> findTopByAppKeyAndStoreTypeOrderByUpdatedAtDesc(String appKey, AppStoreConfigEntity.StoreType storeType);
|
||||
}
|
||||
|
||||
@ -69,7 +69,7 @@ public class AppStoreService {
|
||||
AppStoreConfigEntity.StoreType storeType,
|
||||
String configJson,
|
||||
boolean enabled) {
|
||||
boolean isCreate = configRepo.findByAppKeyAndStoreType(appKey, storeType).isEmpty();
|
||||
boolean isCreate = configRepo.findTopByAppKeyAndStoreTypeOrderByUpdatedAtDesc(appKey, storeType).isEmpty();
|
||||
AppStoreConfigEntity entity = configRepo
|
||||
.findByAppKeyAndStoreType(appKey, storeType)
|
||||
.orElseGet(AppStoreConfigEntity::new);
|
||||
@ -97,7 +97,7 @@ public class AppStoreService {
|
||||
}
|
||||
|
||||
public void deleteConfig(String appKey, AppStoreConfigEntity.StoreType storeType) {
|
||||
configRepo.findByAppKeyAndStoreType(appKey, storeType).ifPresent(cfg -> {
|
||||
configRepo.findTopByAppKeyAndStoreTypeOrderByUpdatedAtDesc(appKey, storeType).ifPresent(cfg -> {
|
||||
configRepo.delete(cfg);
|
||||
operationLogService.record(
|
||||
appKey,
|
||||
@ -245,7 +245,7 @@ public class AppStoreService {
|
||||
}
|
||||
|
||||
public Map<String, String> getReviewWebhookConfig(String appKey) throws Exception {
|
||||
AppStoreConfigEntity cfg = configRepo.findByAppKeyAndStoreType(
|
||||
AppStoreConfigEntity cfg = configRepo.findTopByAppKeyAndStoreTypeOrderByUpdatedAtDesc(
|
||||
appKey, AppStoreConfigEntity.StoreType.REVIEW_WEBHOOK).orElse(null);
|
||||
if (cfg == null || !cfg.isEnabled() || cfg.getConfigJson() == null || cfg.getConfigJson().isBlank()) {
|
||||
return Map.of();
|
||||
@ -257,7 +257,7 @@ public class AppStoreService {
|
||||
if (storeType == null || storeType == AppStoreConfigEntity.StoreType.REVIEW_WEBHOOK) {
|
||||
return "";
|
||||
}
|
||||
return configRepo.findByAppKeyAndStoreType(appKey, storeType)
|
||||
return configRepo.findTopByAppKeyAndStoreTypeOrderByUpdatedAtDesc(appKey, storeType)
|
||||
.filter(AppStoreConfigEntity::isEnabled)
|
||||
.map(AppStoreConfigEntity::getConfigJson)
|
||||
.map(this::extractJumpUrl)
|
||||
|
||||
@ -45,7 +45,7 @@ public class PublishConfigService {
|
||||
}
|
||||
|
||||
public AppPublishConfigEntity getConfig(String appKey) {
|
||||
return configRepository.findByAppKey(appKey).orElseGet(() -> {
|
||||
return configRepository.findTopByAppKeyOrderByUpdatedAtDesc(appKey).orElseGet(() -> {
|
||||
AppPublishConfigEntity entity = new AppPublishConfigEntity();
|
||||
entity.setId(UUID.randomUUID().toString());
|
||||
entity.setAppKey(appKey);
|
||||
@ -56,7 +56,7 @@ public class PublishConfigService {
|
||||
}
|
||||
|
||||
public AppPublishConfigEntity saveConfig(String appKey, Map<String, Object> body) {
|
||||
AppPublishConfigEntity entity = configRepository.findByAppKey(appKey).orElseGet(AppPublishConfigEntity::new);
|
||||
AppPublishConfigEntity entity = configRepository.findTopByAppKeyOrderByUpdatedAtDesc(appKey).orElseGet(AppPublishConfigEntity::new);
|
||||
if (entity.getId() == null) {
|
||||
entity.setId(UUID.randomUUID().toString());
|
||||
entity.setAppKey(appKey);
|
||||
@ -71,7 +71,7 @@ public class PublishConfigService {
|
||||
}
|
||||
|
||||
public JsonNode getConfigNode(String appKey) {
|
||||
AppPublishConfigEntity entity = configRepository.findByAppKey(appKey).orElse(null);
|
||||
AppPublishConfigEntity entity = configRepository.findTopByAppKeyOrderByUpdatedAtDesc(appKey).orElse(null);
|
||||
if (entity == null || entity.getConfigJson() == null || entity.getConfigJson().isBlank()) {
|
||||
try {
|
||||
return objectMapper.readTree(defaultConfigJson());
|
||||
|
||||
@ -398,7 +398,7 @@ public class StoreSubmissionService {
|
||||
for (String storeType : targets) {
|
||||
AppStoreConfigEntity cfg;
|
||||
try {
|
||||
cfg = configRepo.findByAppKeyAndStoreType(v.getAppKey(),
|
||||
cfg = configRepo.findTopByAppKeyAndStoreTypeOrderByUpdatedAtDesc(v.getAppKey(),
|
||||
AppStoreConfigEntity.StoreType.valueOf(storeType)).orElse(null);
|
||||
} catch (Exception e) {
|
||||
results.add(StoreRemoteState.failed(
|
||||
@ -694,7 +694,7 @@ public class StoreSubmissionService {
|
||||
if (!isUnderReview && !isRejected) continue;
|
||||
AppStoreConfigEntity cfg;
|
||||
try {
|
||||
cfg = configRepo.findByAppKeyAndStoreType(v.getAppKey(),
|
||||
cfg = configRepo.findTopByAppKeyAndStoreTypeOrderByUpdatedAtDesc(v.getAppKey(),
|
||||
AppStoreConfigEntity.StoreType.valueOf(storeType)).orElse(null);
|
||||
} catch (Exception e) {
|
||||
continue;
|
||||
@ -779,7 +779,7 @@ public class StoreSubmissionService {
|
||||
for (String storeType : targets) {
|
||||
AppStoreConfigEntity cfg;
|
||||
try {
|
||||
cfg = configRepo.findByAppKeyAndStoreType(v.getAppKey(),
|
||||
cfg = configRepo.findTopByAppKeyAndStoreTypeOrderByUpdatedAtDesc(v.getAppKey(),
|
||||
AppStoreConfigEntity.StoreType.valueOf(storeType)).orElse(null);
|
||||
} catch (Exception e) {
|
||||
results.add(StoreRemoteState.failed(
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户