比较提交
2 次代码提交
39367a57e1
...
4e54737e72
| 作者 | SHA1 | 提交日期 | |
|---|---|---|---|
|
|
4e54737e72 | ||
|
|
3bc8a39d0f |
@ -2,6 +2,7 @@ package com.xuqm.file.config;
|
|||||||
|
|
||||||
import com.xuqm.common.security.JwtAuthFilter;
|
import com.xuqm.common.security.JwtAuthFilter;
|
||||||
import com.xuqm.common.security.JwtUtil;
|
import com.xuqm.common.security.JwtUtil;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
@ -43,6 +44,9 @@ public class SecurityConfig {
|
|||||||
// Upload requires authentication
|
// Upload requires authentication
|
||||||
.anyRequest().authenticated()
|
.anyRequest().authenticated()
|
||||||
)
|
)
|
||||||
|
.exceptionHandling(ex -> ex
|
||||||
|
.authenticationEntryPoint((req, res, e) -> res.sendError(HttpServletResponse.SC_UNAUTHORIZED))
|
||||||
|
)
|
||||||
.addFilterBefore(new JwtAuthFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);
|
.addFilterBefore(new JwtAuthFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);
|
||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHt
|
|||||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
import org.springframework.web.cors.CorsConfiguration;
|
import org.springframework.web.cors.CorsConfiguration;
|
||||||
@ -42,6 +43,9 @@ public class SecurityConfig {
|
|||||||
.requestMatchers("/api/im/auth/**", "/api/im/internal/**", "/ws/**", "/actuator/**").permitAll()
|
.requestMatchers("/api/im/auth/**", "/api/im/internal/**", "/ws/**", "/actuator/**").permitAll()
|
||||||
.anyRequest().authenticated()
|
.anyRequest().authenticated()
|
||||||
)
|
)
|
||||||
|
.exceptionHandling(ex -> ex
|
||||||
|
.authenticationEntryPoint((req, res, e) -> res.sendError(HttpServletResponse.SC_UNAUTHORIZED))
|
||||||
|
)
|
||||||
.addFilterBefore(new JwtAuthFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);
|
.addFilterBefore(new JwtAuthFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);
|
||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
|||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
import org.springframework.web.cors.CorsConfiguration;
|
import org.springframework.web.cors.CorsConfiguration;
|
||||||
@ -39,6 +40,9 @@ public class SecurityConfig {
|
|||||||
.requestMatchers("/api/license/internal/**", "/actuator/health", "/actuator/info").permitAll()
|
.requestMatchers("/api/license/internal/**", "/actuator/health", "/actuator/info").permitAll()
|
||||||
.anyRequest().authenticated()
|
.anyRequest().authenticated()
|
||||||
)
|
)
|
||||||
|
.exceptionHandling(ex -> ex
|
||||||
|
.authenticationEntryPoint((req, res, e) -> res.sendError(HttpServletResponse.SC_UNAUTHORIZED))
|
||||||
|
)
|
||||||
.addFilterBefore(new JwtAuthFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);
|
.addFilterBefore(new JwtAuthFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);
|
||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
|||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
|
|
||||||
@ -30,6 +31,9 @@ public class SecurityConfig {
|
|||||||
.requestMatchers("/api/push/internal/**", "/api/push/auth/**", "/actuator/health", "/actuator/info").permitAll()
|
.requestMatchers("/api/push/internal/**", "/api/push/auth/**", "/actuator/health", "/actuator/info").permitAll()
|
||||||
.anyRequest().authenticated()
|
.anyRequest().authenticated()
|
||||||
)
|
)
|
||||||
|
.exceptionHandling(ex -> ex
|
||||||
|
.authenticationEntryPoint((req, res, e) -> res.sendError(HttpServletResponse.SC_UNAUTHORIZED))
|
||||||
|
)
|
||||||
.addFilterBefore(new JwtAuthFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);
|
.addFilterBefore(new JwtAuthFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);
|
||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package com.xuqm.tenant.config;
|
|||||||
|
|
||||||
import com.xuqm.common.security.JwtAuthFilter;
|
import com.xuqm.common.security.JwtAuthFilter;
|
||||||
import com.xuqm.common.security.JwtUtil;
|
import com.xuqm.common.security.JwtUtil;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||||
@ -41,6 +42,9 @@ public class SecurityConfig {
|
|||||||
).permitAll()
|
).permitAll()
|
||||||
.anyRequest().authenticated()
|
.anyRequest().authenticated()
|
||||||
)
|
)
|
||||||
|
.exceptionHandling(ex -> ex
|
||||||
|
.authenticationEntryPoint((req, res, e) -> res.sendError(HttpServletResponse.SC_UNAUTHORIZED))
|
||||||
|
)
|
||||||
.addFilterBefore(new JwtAuthFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);
|
.addFilterBefore(new JwtAuthFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);
|
||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -57,6 +57,7 @@ public class InternalSdkController {
|
|||||||
}
|
}
|
||||||
AppEntity app = provisioningService.resolveApp(appKey);
|
AppEntity app = provisioningService.resolveApp(appKey);
|
||||||
return ResponseEntity.ok(ApiResponse.success(Map.of(
|
return ResponseEntity.ok(ApiResponse.success(Map.of(
|
||||||
|
"name", app.getName() == null ? "" : app.getName(),
|
||||||
"androidPackageName", app.getPackageName() == null ? "" : app.getPackageName(),
|
"androidPackageName", app.getPackageName() == null ? "" : app.getPackageName(),
|
||||||
"iosBundleId", app.getIosBundleId() == null ? "" : app.getIosBundleId(),
|
"iosBundleId", app.getIosBundleId() == null ? "" : app.getIosBundleId(),
|
||||||
"harmonyBundleName", app.getHarmonyBundleName() == null ? "" : app.getHarmonyBundleName()
|
"harmonyBundleName", app.getHarmonyBundleName() == null ? "" : app.getHarmonyBundleName()
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
|||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
import org.springframework.web.cors.CorsConfiguration;
|
import org.springframework.web.cors.CorsConfiguration;
|
||||||
@ -46,6 +47,9 @@ public class SecurityConfig {
|
|||||||
).permitAll()
|
).permitAll()
|
||||||
.anyRequest().authenticated()
|
.anyRequest().authenticated()
|
||||||
)
|
)
|
||||||
|
.exceptionHandling(ex -> ex
|
||||||
|
.authenticationEntryPoint((req, res, e) -> res.sendError(HttpServletResponse.SC_UNAUTHORIZED))
|
||||||
|
)
|
||||||
.addFilterBefore(new JwtAuthFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class)
|
.addFilterBefore(new JwtAuthFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class)
|
||||||
.httpBasic(AbstractHttpConfigurer::disable)
|
.httpBasic(AbstractHttpConfigurer::disable)
|
||||||
.formLogin(AbstractHttpConfigurer::disable);
|
.formLogin(AbstractHttpConfigurer::disable);
|
||||||
|
|||||||
@ -35,4 +35,7 @@ public interface AppVersionRepository extends JpaRepository<AppVersionEntity, St
|
|||||||
|
|
||||||
@Query(value = "SELECT * FROM update_app_version WHERE store_review_status LIKE '%UNDER_REVIEW%'", nativeQuery = true)
|
@Query(value = "SELECT * FROM update_app_version WHERE store_review_status LIKE '%UNDER_REVIEW%'", nativeQuery = true)
|
||||||
List<AppVersionEntity> findAllWithUnderReviewStores();
|
List<AppVersionEntity> findAllWithUnderReviewStores();
|
||||||
|
|
||||||
|
List<AppVersionEntity> findByAppKeyAndPlatformAndVersionCodeAndIdNot(
|
||||||
|
String appKey, AppVersionEntity.Platform platform, int versionCode, String id);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package com.xuqm.update.service;
|
package com.xuqm.update.service;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.xuqm.update.entity.AppStoreConfigEntity;
|
import com.xuqm.update.entity.AppStoreConfigEntity;
|
||||||
import com.xuqm.update.entity.AppVersionEntity;
|
import com.xuqm.update.entity.AppVersionEntity;
|
||||||
@ -11,6 +12,7 @@ import com.xuqm.update.repository.RnBundleRepository;
|
|||||||
import com.xuqm.update.service.UpdateOperationLogService;
|
import com.xuqm.update.service.UpdateOperationLogService;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@ -30,6 +32,13 @@ public class AppStoreService {
|
|||||||
private static final ObjectMapper mapper = new ObjectMapper();
|
private static final ObjectMapper mapper = new ObjectMapper();
|
||||||
private static final Set<String> ACTIVE_REVIEW_STATES = Set.of("PENDING", "SUBMITTING", "UNDER_REVIEW");
|
private static final Set<String> ACTIVE_REVIEW_STATES = Set.of("PENDING", "SUBMITTING", "UNDER_REVIEW");
|
||||||
private final HttpClient http = HttpClient.newHttpClient();
|
private final HttpClient http = HttpClient.newHttpClient();
|
||||||
|
private final ConcurrentMap<String, String> appNameCache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@Value("${sdk.tenant-service-url:http://xuqm-tenant-service:9001}")
|
||||||
|
private String tenantServiceUrl;
|
||||||
|
|
||||||
|
@Value("${sdk.internal-token:xuqm-internal-token}")
|
||||||
|
private String internalToken;
|
||||||
|
|
||||||
private final AppStoreConfigRepository configRepo;
|
private final AppStoreConfigRepository configRepo;
|
||||||
private final AppVersionRepository versionRepo;
|
private final AppVersionRepository versionRepo;
|
||||||
@ -437,6 +446,28 @@ public class AppStoreService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String resolveAppName(String appKey) {
|
||||||
|
return appNameCache.computeIfAbsent(appKey, key -> {
|
||||||
|
try {
|
||||||
|
String url = tenantServiceUrl.replaceAll("/+$", "") + "/api/internal/sdk/apps/" + key + "/platform-info";
|
||||||
|
HttpRequest req = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(url))
|
||||||
|
.header("X-Internal-Token", internalToken)
|
||||||
|
.GET()
|
||||||
|
.build();
|
||||||
|
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString());
|
||||||
|
if (resp.statusCode() == 200) {
|
||||||
|
JsonNode body = mapper.readTree(resp.body());
|
||||||
|
String name = body.path("data").path("name").asText("");
|
||||||
|
if (!name.isBlank()) return name;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Failed to resolve app name for {}: {}", key, e.getMessage());
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private String buildWebhookBody(String notifyType, AppVersionEntity v,
|
private String buildWebhookBody(String notifyType, AppVersionEntity v,
|
||||||
String storeType, AppVersionEntity.StoreReviewState state, String reason) throws Exception {
|
String storeType, AppVersionEntity.StoreReviewState state, String reason) throws Exception {
|
||||||
String stateLabel = switch (state) {
|
String stateLabel = switch (state) {
|
||||||
@ -449,8 +480,9 @@ public class AppStoreService {
|
|||||||
};
|
};
|
||||||
String storeLabel = storeDisplayName(storeType);
|
String storeLabel = storeDisplayName(storeType);
|
||||||
String reasonSuffix = (reason != null && !reason.isBlank()) ? "\n原因:" + reason : "";
|
String reasonSuffix = (reason != null && !reason.isBlank()) ? "\n原因:" + reason : "";
|
||||||
|
String appName = resolveAppName(v.getAppKey());
|
||||||
String text = String.format("【应用审核通知】\n应用:%s\n版本:%s(%d)\n渠道:%s\n状态:%s%s",
|
String text = String.format("【应用审核通知】\n应用:%s\n版本:%s(%d)\n渠道:%s\n状态:%s%s",
|
||||||
v.getAppKey(), v.getVersionName(), v.getVersionCode(),
|
appName, v.getVersionName(), v.getVersionCode(),
|
||||||
storeLabel, stateLabel, reasonSuffix);
|
storeLabel, stateLabel, reasonSuffix);
|
||||||
|
|
||||||
return switch (notifyType.toUpperCase()) {
|
return switch (notifyType.toUpperCase()) {
|
||||||
|
|||||||
@ -134,6 +134,8 @@ public class StoreSubmissionService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
withdrawSupersededApprovedStores(v);
|
||||||
|
|
||||||
AtomicInteger successCount = new AtomicInteger();
|
AtomicInteger successCount = new AtomicInteger();
|
||||||
AtomicInteger rejectedCount = new AtomicInteger();
|
AtomicInteger rejectedCount = new AtomicInteger();
|
||||||
AtomicInteger skippedCount = new AtomicInteger();
|
AtomicInteger skippedCount = new AtomicInteger();
|
||||||
@ -1142,6 +1144,45 @@ public class StoreSubmissionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When a new version entity is submitted with the same versionCode as an existing one,
|
||||||
|
* mark APPROVED stores on the old entity as WITHDRAWN (DB-only, no store API call).
|
||||||
|
* This clears the superseded record without triggering notifications.
|
||||||
|
*/
|
||||||
|
private void withdrawSupersededApprovedStores(AppVersionEntity newVersion) {
|
||||||
|
List<AppVersionEntity> superseded = versionRepo.findByAppKeyAndPlatformAndVersionCodeAndIdNot(
|
||||||
|
newVersion.getAppKey(), newVersion.getPlatform(), newVersion.getVersionCode(), newVersion.getId());
|
||||||
|
for (AppVersionEntity old : superseded) {
|
||||||
|
if (old.getStoreReviewStatus() == null || !old.getStoreReviewStatus().contains("APPROVED")) continue;
|
||||||
|
try {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Map<String, Object>> reviewMap =
|
||||||
|
mapper.readValue(old.getStoreReviewStatus(), new TypeReference<>() {});
|
||||||
|
boolean changed = false;
|
||||||
|
for (Map.Entry<String, Map<String, Object>> entry : reviewMap.entrySet()) {
|
||||||
|
Object state = entry.getValue().get("state");
|
||||||
|
if ("APPROVED".equals(state != null ? state.toString() : "")) {
|
||||||
|
Map<String, Object> updated = new LinkedHashMap<>(entry.getValue());
|
||||||
|
updated.put("state", "WITHDRAWN");
|
||||||
|
updated.put("stage", "WITHDRAWN");
|
||||||
|
updated.put("reason", "已被新版本包替代");
|
||||||
|
updated.put("updatedAt", LocalDateTime.now().toString());
|
||||||
|
entry.setValue(updated);
|
||||||
|
changed = true;
|
||||||
|
log.info("Superseding APPROVED store {} on old version {} (versionCode={})",
|
||||||
|
entry.getKey(), old.getId(), old.getVersionCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changed) {
|
||||||
|
old.setStoreReviewStatus(mapper.writeValueAsString(reviewMap));
|
||||||
|
versionRepo.save(old);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Failed to supersede approved stores for old version {}: {}", old.getId(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private List<String> extractActiveReviewTargets(AppVersionEntity v) {
|
private List<String> extractActiveReviewTargets(AppVersionEntity v) {
|
||||||
if (v.getStoreReviewStatus() == null || v.getStoreReviewStatus().isBlank()) return List.of();
|
if (v.getStoreReviewStatus() == null || v.getStoreReviewStatus().isBlank()) return List.of();
|
||||||
try {
|
try {
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户