diff --git a/tenant-service/src/main/resources/application.yml b/tenant-service/src/main/resources/application.yml index 3808819..24d40f9 100644 --- a/tenant-service/src/main/resources/application.yml +++ b/tenant-service/src/main/resources/application.yml @@ -101,6 +101,7 @@ sdk: im-ws-url: ${SDK_IM_WS_URL:wss://im.dev.xuqinmin.com/ws/im} file-service-url: ${SDK_FILE_SERVICE_URL:https://file.dev.xuqinmin.com} im-api-url: ${SDK_IM_API_URL:https://im.dev.xuqinmin.com} + bugcollect-api-url: ${SDK_BUGCOLLECT_API_URL:https://dev.xuqinmin.com/api/bugcollect} im-platform-events-recipient-user: ${SDK_IM_PLATFORM_EVENTS_RECIPIENT_USER:platform} im-platform-events-admin-user: ${SDK_IM_PLATFORM_EVENTS_ADMIN_USER:admin} im-platform-app-key: ${SDK_IM_PLATFORM_APP_KEY:ak_409e217e4aa14254ad73ad3c} diff --git a/xuqm-bugcollect-service/src/main/java/com/xuqm/bugcollect/controller/LogController.java b/xuqm-bugcollect-service/src/main/java/com/xuqm/bugcollect/controller/LogController.java index 7ff36ea..e3b1f38 100644 --- a/xuqm-bugcollect-service/src/main/java/com/xuqm/bugcollect/controller/LogController.java +++ b/xuqm-bugcollect-service/src/main/java/com/xuqm/bugcollect/controller/LogController.java @@ -197,10 +197,11 @@ public class LogController { @RequestParam String appKey, @RequestParam String platform, @RequestParam String appVersion, + @RequestParam(required = false) String buildId, @RequestParam(required = false) String bundleVersion, @RequestParam(required = false, defaultValue = "index") String bundleName, @RequestParam("file") MultipartFile file) throws IOException { - return ApiResponse.success(sourcemapService.upload(appKey, platform, appVersion, bundleVersion, bundleName, file)); + return ApiResponse.success(sourcemapService.upload(appKey, platform, appVersion, buildId, bundleVersion, bundleName, file)); } @GetMapping("/sourcemaps") diff --git a/xuqm-bugcollect-service/src/main/java/com/xuqm/bugcollect/dto/IssueBatchRequest.java b/xuqm-bugcollect-service/src/main/java/com/xuqm/bugcollect/dto/IssueBatchRequest.java index 557157c..9d0ea2d 100644 --- a/xuqm-bugcollect-service/src/main/java/com/xuqm/bugcollect/dto/IssueBatchRequest.java +++ b/xuqm-bugcollect-service/src/main/java/com/xuqm/bugcollect/dto/IssueBatchRequest.java @@ -28,6 +28,7 @@ public record IssueBatchRequest( @JsonProperty("user") UserInfo user, @JsonProperty("device") DeviceInfo device, String release, + String buildId, String environment, @JsonProperty("tags") Object tags ) {} diff --git a/xuqm-bugcollect-service/src/main/java/com/xuqm/bugcollect/dto/SourcemapInfo.java b/xuqm-bugcollect-service/src/main/java/com/xuqm/bugcollect/dto/SourcemapInfo.java index 3d3c757..ef29050 100644 --- a/xuqm-bugcollect-service/src/main/java/com/xuqm/bugcollect/dto/SourcemapInfo.java +++ b/xuqm-bugcollect-service/src/main/java/com/xuqm/bugcollect/dto/SourcemapInfo.java @@ -9,6 +9,7 @@ public record SourcemapInfo( @JsonProperty("platform") String platform, @JsonProperty("appVersion") String appVersion, @JsonProperty("bundleName") String bundleName, + @JsonProperty("buildId") String buildId, @JsonProperty("storageKey") String storageKey, @JsonProperty("uploadedAt") LocalDateTime uploadedAt ) {} diff --git a/xuqm-bugcollect-service/src/main/java/com/xuqm/bugcollect/dto/SourcemapUploadResponse.java b/xuqm-bugcollect-service/src/main/java/com/xuqm/bugcollect/dto/SourcemapUploadResponse.java index 6303945..36556ba 100644 --- a/xuqm-bugcollect-service/src/main/java/com/xuqm/bugcollect/dto/SourcemapUploadResponse.java +++ b/xuqm-bugcollect-service/src/main/java/com/xuqm/bugcollect/dto/SourcemapUploadResponse.java @@ -8,5 +8,6 @@ public record SourcemapUploadResponse( String platform, @JsonProperty("appVersion") String appVersion, @JsonProperty("bundleName") String bundleName, + @JsonProperty("buildId") String buildId, @JsonProperty("storageKey") String storageKey ) {} diff --git a/xuqm-bugcollect-service/src/main/java/com/xuqm/bugcollect/entity/LogIssueEventEntity.java b/xuqm-bugcollect-service/src/main/java/com/xuqm/bugcollect/entity/LogIssueEventEntity.java index a4f31b8..04828d4 100644 --- a/xuqm-bugcollect-service/src/main/java/com/xuqm/bugcollect/entity/LogIssueEventEntity.java +++ b/xuqm-bugcollect-service/src/main/java/com/xuqm/bugcollect/entity/LogIssueEventEntity.java @@ -56,6 +56,9 @@ public class LogIssueEventEntity { @Column(name = "app_version", length = 32) private String release; + @Column(length = 32) + private String buildId; + @Column(length = 50) private String environment; @@ -119,6 +122,9 @@ public class LogIssueEventEntity { public String getRelease() { return release; } public void setRelease(String release) { this.release = release; } + public String getBuildId() { return buildId; } + public void setBuildId(String buildId) { this.buildId = buildId; } + public String getEnvironment() { return environment; } public void setEnvironment(String environment) { this.environment = environment; } diff --git a/xuqm-bugcollect-service/src/main/java/com/xuqm/bugcollect/entity/LogSourcemapEntity.java b/xuqm-bugcollect-service/src/main/java/com/xuqm/bugcollect/entity/LogSourcemapEntity.java index d49bfdd..c4de050 100644 --- a/xuqm-bugcollect-service/src/main/java/com/xuqm/bugcollect/entity/LogSourcemapEntity.java +++ b/xuqm-bugcollect-service/src/main/java/com/xuqm/bugcollect/entity/LogSourcemapEntity.java @@ -23,6 +23,9 @@ public class LogSourcemapEntity { @Column(nullable = false, length = 128) private String bundleName = "index"; + @Column(length = 32) + private String buildId; + @Column(nullable = false, length = 512) private String storageKey; @@ -44,6 +47,9 @@ public class LogSourcemapEntity { public String getBundleName() { return bundleName; } public void setBundleName(String bundleName) { this.bundleName = bundleName; } + public String getBuildId() { return buildId; } + public void setBuildId(String buildId) { this.buildId = buildId; } + public String getStorageKey() { return storageKey; } public void setStorageKey(String storageKey) { this.storageKey = storageKey; } diff --git a/xuqm-bugcollect-service/src/main/java/com/xuqm/bugcollect/repository/LogSourcemapRepository.java b/xuqm-bugcollect-service/src/main/java/com/xuqm/bugcollect/repository/LogSourcemapRepository.java index f8663f3..79e61bb 100644 --- a/xuqm-bugcollect-service/src/main/java/com/xuqm/bugcollect/repository/LogSourcemapRepository.java +++ b/xuqm-bugcollect-service/src/main/java/com/xuqm/bugcollect/repository/LogSourcemapRepository.java @@ -8,7 +8,16 @@ import java.util.Optional; public interface LogSourcemapRepository extends JpaRepository { - Optional findByAppKeyAndPlatformAndAppVersionAndBundleName( + /** Exact match by buildId (for events that carry a buildId). */ + Optional findByAppKeyAndPlatformAndAppVersionAndBuildIdAndBundleName( + String appKey, String platform, String appVersion, String buildId, String bundleName); + + /** Backward-compat: exact match on (appKey, platform, appVersion, bundleName) where buildId is NULL. */ + Optional findByAppKeyAndPlatformAndAppVersionAndBuildIdIsNullAndBundleName( + String appKey, String platform, String appVersion, String bundleName); + + /** Fallback: latest upload for a version regardless of buildId. */ + Optional findFirstByAppKeyAndPlatformAndAppVersionAndBundleNameOrderByUploadedAtDesc( String appKey, String platform, String appVersion, String bundleName); List findByAppKey(String appKey); diff --git a/xuqm-bugcollect-service/src/main/java/com/xuqm/bugcollect/service/LogService.java b/xuqm-bugcollect-service/src/main/java/com/xuqm/bugcollect/service/LogService.java index 656f4e6..7b19078 100644 --- a/xuqm-bugcollect-service/src/main/java/com/xuqm/bugcollect/service/LogService.java +++ b/xuqm-bugcollect-service/src/main/java/com/xuqm/bugcollect/service/LogService.java @@ -130,6 +130,7 @@ public class LogService { eventEntity.setStack(stack); eventEntity.setPlatform(item.platform()); eventEntity.setRelease(item.release()); + eventEntity.setBuildId(item.buildId()); eventEntity.setEnvironment(item.environment() != null ? item.environment() : "production"); eventEntity.setDevice(item.device() != null ? toJson(item.device()) : null); eventEntity.setTags(item.tags() != null ? toJson(item.tags()) : null); @@ -137,10 +138,10 @@ public class LogService { eventEntity.setBreadcrumbs(toJson(item.breadcrumbs())); } eventEntity.setCreatedAt(eventTime); - issueEventRepository.save(eventEntity); + eventEntity = issueEventRepository.save(eventEntity); if (isNew) triggerWebhookAsync(issue); - triggerSymbolicationAsync(issue.getId(), item.appKey(), item.platform(), item.release()); + triggerSymbolicationAsync(eventEntity.getId(), item.appKey(), item.platform(), item.release(), item.buildId()); } @Async @@ -153,18 +154,19 @@ public class LogService { } @Async - void triggerSymbolicationAsync(Long issueId, String appKey, String platform, String release) { - log.debug("Symbolication triggered issueId={} appKey={} platform={} release={}", issueId, appKey, platform, release); + void triggerSymbolicationAsync(Long eventId, String appKey, String platform, String release, String buildId) { + log.debug("Symbolication triggered eventId={} appKey={} platform={} release={} buildId={}", + eventId, appKey, platform, release, buildId); - // 查找 sourcemap 文件 - String storageKey = sourcemapService.findSourceMap(appKey, platform, release, "index"); + // 查找 sourcemap 文件:精确 buildId 匹配,兜底取最新版本 + String storageKey = sourcemapService.findSourceMap(appKey, platform, release, buildId, "index"); if (storageKey == null) { - log.debug("No sourcemap found for appKey={} platform={} release={}", appKey, platform, release); + log.debug("No sourcemap found for appKey={} platform={} release={} buildId={}", appKey, platform, release, buildId); return; } - // 查找事件 - LogIssueEventEntity event = issueEventRepository.findById(issueId).orElse(null); + // 查找事件(用 eventId 精确定位,而非 issueId) + LogIssueEventEntity event = issueEventRepository.findById(eventId).orElse(null); if (event == null || event.getStack() == null || event.getStack().isBlank()) { return; } diff --git a/xuqm-bugcollect-service/src/main/java/com/xuqm/bugcollect/service/SourcemapService.java b/xuqm-bugcollect-service/src/main/java/com/xuqm/bugcollect/service/SourcemapService.java index 077ff8f..37c5770 100644 --- a/xuqm-bugcollect-service/src/main/java/com/xuqm/bugcollect/service/SourcemapService.java +++ b/xuqm-bugcollect-service/src/main/java/com/xuqm/bugcollect/service/SourcemapService.java @@ -37,13 +37,20 @@ public class SourcemapService { @Transactional public SourcemapUploadResponse upload(String appKey, String platform, String appVersion, - String bundleName, MultipartFile file) throws IOException { - return upload(appKey, platform, appVersion, null, bundleName, file); + String bundleVersion, String bundleName, MultipartFile file) throws IOException { + return upload(appKey, platform, appVersion, null, bundleVersion, bundleName, file); } + /** + * Upload a sourcemap file. + * When buildId is provided, each unique (appKey, platform, appVersion, buildId, bundleName) + * produces a separate record — multiple builds of the same version coexist. + * When buildId is null (backward compat), the legacy single-record-per-version behaviour is kept. + */ @Transactional public SourcemapUploadResponse upload(String appKey, String platform, String appVersion, - String bundleVersion, String bundleName, MultipartFile file) throws IOException { + String buildId, String bundleVersion, String bundleName, + MultipartFile file) throws IOException { if (bundleName == null || bundleName.isBlank()) { bundleName = "index"; } @@ -59,40 +66,63 @@ public class SourcemapService { String filename = resolveFilename(platform, bundleName); String versionKey = bundleVersion != null && !bundleVersion.isBlank() ? bundleVersion : appVersion; + // When buildId is present, store under a subdirectory to avoid file collisions across builds + String buildDir = buildId != null && !buildId.isBlank() ? buildId : "legacy"; - Path dir = Paths.get(storageDir, appKey, platform, versionKey); + Path dir = Paths.get(storageDir, appKey, platform, versionKey, buildDir); Files.createDirectories(dir); Path filePath = dir.resolve(filename); file.transferTo(filePath.toFile()); - var existing = sourcemapRepository.findByAppKeyAndPlatformAndAppVersionAndBundleName( - appKey, platform, appVersion, bundleName); - LogSourcemapEntity entity; - if (existing.isPresent()) { - entity = existing.get(); - entity.setStorageKey(filePath.toString()); - entity.setUploadedAt(LocalDateTime.now()); + if (buildId != null && !buildId.isBlank()) { + // New path: each buildId gets its own record; no overwrite + var existing = sourcemapRepository.findByAppKeyAndPlatformAndAppVersionAndBuildIdAndBundleName( + appKey, platform, appVersion, buildId, bundleName); + if (existing.isPresent()) { + entity = existing.get(); + entity.setStorageKey(filePath.toString()); + entity.setUploadedAt(LocalDateTime.now()); + } else { + entity = newEntity(appKey, platform, appVersion, buildId, bundleName, filePath.toString()); + } } else { - entity = new LogSourcemapEntity(); - entity.setAppKey(appKey); - entity.setPlatform(platform); - entity.setAppVersion(appVersion); - entity.setBundleName(bundleName); - entity.setStorageKey(filePath.toString()); - entity.setUploadedAt(LocalDateTime.now()); + // Legacy path: overwrite the single record for (appKey, platform, appVersion, bundleName) + var existing = sourcemapRepository.findByAppKeyAndPlatformAndAppVersionAndBuildIdIsNullAndBundleName( + appKey, platform, appVersion, bundleName); + if (existing.isPresent()) { + entity = existing.get(); + entity.setStorageKey(filePath.toString()); + entity.setUploadedAt(LocalDateTime.now()); + } else { + entity = newEntity(appKey, platform, appVersion, null, bundleName, filePath.toString()); + } } - entity = sourcemapRepository.save(entity); - log.info("SourceMap uploaded: appKey={}, platform={}, version={}, bundle={}", appKey, platform, versionKey, bundleName); + entity = sourcemapRepository.save(entity); + log.info("Sourcemap uploaded: appKey={} platform={} version={} buildId={} bundle={}", + appKey, platform, versionKey, buildId, bundleName); return new SourcemapUploadResponse( entity.getId(), entity.getAppKey(), entity.getPlatform(), - entity.getAppVersion(), entity.getBundleName(), entity.getStorageKey() + entity.getAppVersion(), entity.getBundleName(), entity.getBuildId(), entity.getStorageKey() ); } - /** Produce platform-appropriate filename, e.g. index.android.bundle.map / index.ios.bundle.map */ + private LogSourcemapEntity newEntity(String appKey, String platform, String appVersion, + String buildId, String bundleName, String storageKey) { + var e = new LogSourcemapEntity(); + e.setAppKey(appKey); + e.setPlatform(platform); + e.setAppVersion(appVersion); + e.setBuildId(buildId); + e.setBundleName(bundleName); + e.setStorageKey(storageKey); + e.setUploadedAt(LocalDateTime.now()); + return e; + } + + /** Produce platform-appropriate filename, e.g. index.android.bundle.map */ private String resolveFilename(String platform, String bundleName) { return switch (platform != null ? platform.toLowerCase() : "") { case "android" -> bundleName + ".android.bundle.map"; @@ -102,13 +132,31 @@ public class SourcemapService { }; } - public String findSourceMap(String appKey, String platform, String appVersion, String bundleName) { - return sourcemapRepository.findByAppKeyAndPlatformAndAppVersionAndBundleName( - appKey, platform, appVersion, bundleName) + /** + * Locate a sourcemap file. + * Priority: exact (appKey, platform, appVersion, buildId, bundleName) + * → version-only latest (appKey, platform, appVersion, bundleName) + */ + public String findSourceMap(String appKey, String platform, String appVersion, + String buildId, String bundleName) { + if (buildId != null && !buildId.isBlank()) { + var exact = sourcemapRepository.findByAppKeyAndPlatformAndAppVersionAndBuildIdAndBundleName( + appKey, platform, appVersion, buildId, bundleName); + if (exact.isPresent()) return exact.get().getStorageKey(); + } + // Fallback: newest upload for this version + return sourcemapRepository + .findFirstByAppKeyAndPlatformAndAppVersionAndBundleNameOrderByUploadedAtDesc( + appKey, platform, appVersion, bundleName) .map(LogSourcemapEntity::getStorageKey) .orElse(null); } + /** Backward-compat overload without buildId. */ + public String findSourceMap(String appKey, String platform, String appVersion, String bundleName) { + return findSourceMap(appKey, platform, appVersion, null, bundleName); + } + public String readSourceMapContent(String storageKey) throws IOException { return Files.readString(Paths.get(storageKey)); } @@ -116,14 +164,10 @@ public class SourcemapService { public List listByAppKey(String appKey) { return sourcemapRepository.findByAppKey(appKey) .stream() - .map(entity -> new SourcemapInfo( - entity.getId(), - entity.getAppKey(), - entity.getPlatform(), - entity.getAppVersion(), - entity.getBundleName(), - entity.getStorageKey(), - entity.getUploadedAt() + .map(e -> new SourcemapInfo( + e.getId(), e.getAppKey(), e.getPlatform(), + e.getAppVersion(), e.getBundleName(), e.getBuildId(), + e.getStorageKey(), e.getUploadedAt() )) .toList(); } diff --git a/xuqm-bugcollect-service/src/main/resources/db/migration/V6__build_id.sql b/xuqm-bugcollect-service/src/main/resources/db/migration/V6__build_id.sql new file mode 100644 index 0000000..e7c10e2 --- /dev/null +++ b/xuqm-bugcollect-service/src/main/resources/db/migration/V6__build_id.sql @@ -0,0 +1,27 @@ +-- V6: build_id support for multi-build-per-version sourcemap tracking +-- ───────────────────────────────────────────────────────────────────── +-- log_sourcemaps: +-- • Drop old unique key that prevented multiple builds of the same version +-- • Add build_id (nullable for backward compat with pre-V6 uploads) +-- • New unique key includes build_id; MySQL treats NULL as distinct in UNIQUE, +-- so old rows (build_id=NULL) are not affected and remain individually unique +-- • Add fallback index for version-only lookup (for events without buildId) +-- +-- log_issue_events: +-- • Add build_id so events can be matched to the exact sourcemap build + +ALTER TABLE log_sourcemaps + ADD COLUMN build_id VARCHAR(32) NULL COMMENT 'Gradle build timestamp yyyyMMddHHmmss; NULL = pre-V6 upload' + AFTER bundle_name; + +ALTER TABLE log_sourcemaps DROP KEY uk_map; + +ALTER TABLE log_sourcemaps + ADD UNIQUE KEY uk_map_build (app_key, platform, app_version, build_id, bundle_name); + +ALTER TABLE log_sourcemaps + ADD INDEX idx_sm_version_latest (app_key, platform, app_version, bundle_name, uploaded_at); + +ALTER TABLE log_issue_events + ADD COLUMN build_id VARCHAR(32) NULL COMMENT 'Matches log_sourcemaps.build_id for exact symbolication' + AFTER release;