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) {
|
if (app == null) {
|
||||||
provisioningService.ensureApp(bootstrapAppKey, true);
|
provisioningService.ensureApp(bootstrapAppKey, true);
|
||||||
log.info("[PRIVATE] Bootstrap app created: {}", bootstrapAppKey);
|
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;
|
package com.xuqm.tenant.config;
|
||||||
|
|
||||||
|
import com.xuqm.tenant.entity.AppEntity;
|
||||||
|
import com.xuqm.tenant.repository.AppRepository;
|
||||||
import com.xuqm.tenant.service.SdkAppProvisioningService;
|
import com.xuqm.tenant.service.SdkAppProvisioningService;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.boot.ApplicationArguments;
|
import org.springframework.boot.ApplicationArguments;
|
||||||
import org.springframework.boot.ApplicationRunner;
|
import org.springframework.boot.ApplicationRunner;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
@ -9,13 +12,42 @@ import org.springframework.stereotype.Component;
|
|||||||
public class SdkAppInitializer implements ApplicationRunner {
|
public class SdkAppInitializer implements ApplicationRunner {
|
||||||
|
|
||||||
private final SdkAppProvisioningService provisioningService;
|
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.provisioningService = provisioningService;
|
||||||
|
this.deployProps = deployProps;
|
||||||
|
this.appRepository = appRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run(ApplicationArguments args) {
|
public void run(ApplicationArguments args) {
|
||||||
|
migrateExistingSystemApps();
|
||||||
|
if (!deployProps.isPrivate()) {
|
||||||
provisioningService.ensureBootstrapApp();
|
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) {
|
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) {
|
public AppEntity getByAppKey(String appKey, String tenantId) {
|
||||||
|
|||||||
@ -88,6 +88,8 @@ public class SdkAppProvisioningService {
|
|||||||
app.setAppKey(appKey);
|
app.setAppKey(appKey);
|
||||||
app.setAppSecret(generateSecret());
|
app.setAppSecret(generateSecret());
|
||||||
app.setCreatedAt(LocalDateTime.now());
|
app.setCreatedAt(LocalDateTime.now());
|
||||||
|
app.setDefault(true);
|
||||||
|
app.setDeletable(false);
|
||||||
app = appRepository.save(app);
|
app = appRepository.save(app);
|
||||||
|
|
||||||
if (bootstrapDefaults) {
|
if (bootstrapDefaults) {
|
||||||
|
|||||||
@ -46,13 +46,8 @@ public class GlobalExceptionHandler {
|
|||||||
@ExceptionHandler(Exception.class)
|
@ExceptionHandler(Exception.class)
|
||||||
public ResponseEntity<ApiResponse<Void>> handle(Exception ex) {
|
public ResponseEntity<ApiResponse<Void>> handle(Exception ex) {
|
||||||
log.error("Unhandled 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()
|
return ResponseEntity.internalServerError()
|
||||||
.body(ApiResponse.error(500, detail));
|
.body(ApiResponse.error(500, "服务器内部错误"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private HttpStatus resolveStatus(int code) {
|
private HttpStatus resolveStatus(int code) {
|
||||||
|
|||||||
@ -6,5 +6,5 @@ import org.springframework.data.jpa.repository.JpaRepository;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface AppPublishConfigRepository extends JpaRepository<AppPublishConfigEntity, String> {
|
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);
|
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,
|
AppStoreConfigEntity.StoreType storeType,
|
||||||
String configJson,
|
String configJson,
|
||||||
boolean enabled) {
|
boolean enabled) {
|
||||||
boolean isCreate = configRepo.findByAppKeyAndStoreType(appKey, storeType).isEmpty();
|
boolean isCreate = configRepo.findTopByAppKeyAndStoreTypeOrderByUpdatedAtDesc(appKey, storeType).isEmpty();
|
||||||
AppStoreConfigEntity entity = configRepo
|
AppStoreConfigEntity entity = configRepo
|
||||||
.findByAppKeyAndStoreType(appKey, storeType)
|
.findByAppKeyAndStoreType(appKey, storeType)
|
||||||
.orElseGet(AppStoreConfigEntity::new);
|
.orElseGet(AppStoreConfigEntity::new);
|
||||||
@ -97,7 +97,7 @@ public class AppStoreService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void deleteConfig(String appKey, AppStoreConfigEntity.StoreType storeType) {
|
public void deleteConfig(String appKey, AppStoreConfigEntity.StoreType storeType) {
|
||||||
configRepo.findByAppKeyAndStoreType(appKey, storeType).ifPresent(cfg -> {
|
configRepo.findTopByAppKeyAndStoreTypeOrderByUpdatedAtDesc(appKey, storeType).ifPresent(cfg -> {
|
||||||
configRepo.delete(cfg);
|
configRepo.delete(cfg);
|
||||||
operationLogService.record(
|
operationLogService.record(
|
||||||
appKey,
|
appKey,
|
||||||
@ -245,7 +245,7 @@ public class AppStoreService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Map<String, String> getReviewWebhookConfig(String appKey) throws Exception {
|
public Map<String, String> getReviewWebhookConfig(String appKey) throws Exception {
|
||||||
AppStoreConfigEntity cfg = configRepo.findByAppKeyAndStoreType(
|
AppStoreConfigEntity cfg = configRepo.findTopByAppKeyAndStoreTypeOrderByUpdatedAtDesc(
|
||||||
appKey, AppStoreConfigEntity.StoreType.REVIEW_WEBHOOK).orElse(null);
|
appKey, AppStoreConfigEntity.StoreType.REVIEW_WEBHOOK).orElse(null);
|
||||||
if (cfg == null || !cfg.isEnabled() || cfg.getConfigJson() == null || cfg.getConfigJson().isBlank()) {
|
if (cfg == null || !cfg.isEnabled() || cfg.getConfigJson() == null || cfg.getConfigJson().isBlank()) {
|
||||||
return Map.of();
|
return Map.of();
|
||||||
@ -257,7 +257,7 @@ public class AppStoreService {
|
|||||||
if (storeType == null || storeType == AppStoreConfigEntity.StoreType.REVIEW_WEBHOOK) {
|
if (storeType == null || storeType == AppStoreConfigEntity.StoreType.REVIEW_WEBHOOK) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
return configRepo.findByAppKeyAndStoreType(appKey, storeType)
|
return configRepo.findTopByAppKeyAndStoreTypeOrderByUpdatedAtDesc(appKey, storeType)
|
||||||
.filter(AppStoreConfigEntity::isEnabled)
|
.filter(AppStoreConfigEntity::isEnabled)
|
||||||
.map(AppStoreConfigEntity::getConfigJson)
|
.map(AppStoreConfigEntity::getConfigJson)
|
||||||
.map(this::extractJumpUrl)
|
.map(this::extractJumpUrl)
|
||||||
|
|||||||
@ -45,7 +45,7 @@ public class PublishConfigService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public AppPublishConfigEntity getConfig(String appKey) {
|
public AppPublishConfigEntity getConfig(String appKey) {
|
||||||
return configRepository.findByAppKey(appKey).orElseGet(() -> {
|
return configRepository.findTopByAppKeyOrderByUpdatedAtDesc(appKey).orElseGet(() -> {
|
||||||
AppPublishConfigEntity entity = new AppPublishConfigEntity();
|
AppPublishConfigEntity entity = new AppPublishConfigEntity();
|
||||||
entity.setId(UUID.randomUUID().toString());
|
entity.setId(UUID.randomUUID().toString());
|
||||||
entity.setAppKey(appKey);
|
entity.setAppKey(appKey);
|
||||||
@ -56,7 +56,7 @@ public class PublishConfigService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public AppPublishConfigEntity saveConfig(String appKey, Map<String, Object> body) {
|
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) {
|
if (entity.getId() == null) {
|
||||||
entity.setId(UUID.randomUUID().toString());
|
entity.setId(UUID.randomUUID().toString());
|
||||||
entity.setAppKey(appKey);
|
entity.setAppKey(appKey);
|
||||||
@ -71,7 +71,7 @@ public class PublishConfigService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public JsonNode getConfigNode(String appKey) {
|
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()) {
|
if (entity == null || entity.getConfigJson() == null || entity.getConfigJson().isBlank()) {
|
||||||
try {
|
try {
|
||||||
return objectMapper.readTree(defaultConfigJson());
|
return objectMapper.readTree(defaultConfigJson());
|
||||||
|
|||||||
@ -398,7 +398,7 @@ public class StoreSubmissionService {
|
|||||||
for (String storeType : targets) {
|
for (String storeType : targets) {
|
||||||
AppStoreConfigEntity cfg;
|
AppStoreConfigEntity cfg;
|
||||||
try {
|
try {
|
||||||
cfg = configRepo.findByAppKeyAndStoreType(v.getAppKey(),
|
cfg = configRepo.findTopByAppKeyAndStoreTypeOrderByUpdatedAtDesc(v.getAppKey(),
|
||||||
AppStoreConfigEntity.StoreType.valueOf(storeType)).orElse(null);
|
AppStoreConfigEntity.StoreType.valueOf(storeType)).orElse(null);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
results.add(StoreRemoteState.failed(
|
results.add(StoreRemoteState.failed(
|
||||||
@ -694,7 +694,7 @@ public class StoreSubmissionService {
|
|||||||
if (!isUnderReview && !isRejected) continue;
|
if (!isUnderReview && !isRejected) continue;
|
||||||
AppStoreConfigEntity cfg;
|
AppStoreConfigEntity cfg;
|
||||||
try {
|
try {
|
||||||
cfg = configRepo.findByAppKeyAndStoreType(v.getAppKey(),
|
cfg = configRepo.findTopByAppKeyAndStoreTypeOrderByUpdatedAtDesc(v.getAppKey(),
|
||||||
AppStoreConfigEntity.StoreType.valueOf(storeType)).orElse(null);
|
AppStoreConfigEntity.StoreType.valueOf(storeType)).orElse(null);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
continue;
|
continue;
|
||||||
@ -779,7 +779,7 @@ public class StoreSubmissionService {
|
|||||||
for (String storeType : targets) {
|
for (String storeType : targets) {
|
||||||
AppStoreConfigEntity cfg;
|
AppStoreConfigEntity cfg;
|
||||||
try {
|
try {
|
||||||
cfg = configRepo.findByAppKeyAndStoreType(v.getAppKey(),
|
cfg = configRepo.findTopByAppKeyAndStoreTypeOrderByUpdatedAtDesc(v.getAppKey(),
|
||||||
AppStoreConfigEntity.StoreType.valueOf(storeType)).orElse(null);
|
AppStoreConfigEntity.StoreType.valueOf(storeType)).orElse(null);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
results.add(StoreRemoteState.failed(
|
results.add(StoreRemoteState.failed(
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户