fix: bugcollect controller 路径从 /bugcollect/v1 改为 /api/bugcollect/v1,匹配 nginx 路由
这个提交包含在:
父节点
62163fac7b
当前提交
8ac2a37e7f
@ -0,0 +1,111 @@
|
||||
package com.xuqm.common.security;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
|
||||
/**
|
||||
* 验证 SDK 请求的 HMAC-SHA256 签名。
|
||||
* <p>
|
||||
* 请求头:
|
||||
* <ul>
|
||||
* <li>{@code X-App-Key} — 应用标识</li>
|
||||
* <li>{@code X-Timestamp} — Unix 时间戳(秒)</li>
|
||||
* <li>{@code X-Nonce} — 随机字符串(防重放)</li>
|
||||
* <li>{@code X-Signature} — HMAC-SHA256 签名</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* 签名载荷格式:{@code appKey\nuserId\ntimestamp\nnonce}
|
||||
*/
|
||||
public class AppSignatureAuthFilter extends OncePerRequestFilter {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(AppSignatureAuthFilter.class);
|
||||
private static final long MAX_TIMESTAMP_DRIFT_SECONDS = 300; // ±5 分钟
|
||||
|
||||
private final AppSecretResolver secretResolver;
|
||||
|
||||
public AppSignatureAuthFilter(AppSecretResolver secretResolver) {
|
||||
this.secretResolver = secretResolver;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
String appKey = request.getHeader("X-App-Key");
|
||||
String timestampStr = request.getHeader("X-Timestamp");
|
||||
String nonce = request.getHeader("X-Nonce");
|
||||
String signature = request.getHeader("X-Signature");
|
||||
|
||||
// 如果没有签名头,跳过验证(交给后续 Filter 处理)
|
||||
if (appKey == null || signature == null) {
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证时间戳
|
||||
if (timestampStr == null || timestampStr.isBlank()) {
|
||||
log.warn("Missing X-Timestamp header for appKey={}", appKey);
|
||||
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Missing timestamp");
|
||||
return;
|
||||
}
|
||||
|
||||
long timestamp;
|
||||
try {
|
||||
timestamp = Long.parseLong(timestampStr);
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("Invalid X-Timestamp header: {} for appKey={}", timestampStr, appKey);
|
||||
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid timestamp");
|
||||
return;
|
||||
}
|
||||
|
||||
long now = Instant.now().getEpochSecond();
|
||||
if (Math.abs(now - timestamp) > MAX_TIMESTAMP_DRIFT_SECONDS) {
|
||||
log.warn("Timestamp drift too large: now={}, request={}, appKey={}", now, timestamp, appKey);
|
||||
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Timestamp expired");
|
||||
return;
|
||||
}
|
||||
|
||||
// 解析 appSecret
|
||||
String appSecret;
|
||||
try {
|
||||
appSecret = secretResolver.resolveAppSecret(appKey);
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to resolve appSecret for appKey={}: {}", appKey, e.getMessage());
|
||||
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid appKey");
|
||||
return;
|
||||
}
|
||||
|
||||
if (appSecret == null || appSecret.isBlank()) {
|
||||
log.warn("No appSecret found for appKey={}", appKey);
|
||||
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid appKey");
|
||||
return;
|
||||
}
|
||||
|
||||
// 构造签名载荷并验证
|
||||
String userId = request.getHeader("X-User-Id"); // 可选
|
||||
String payload = AppRequestSignatureUtil.payload(appKey, userId, timestamp, nonce);
|
||||
if (!AppRequestSignatureUtil.matches(appSecret, payload, signature)) {
|
||||
log.warn("Signature mismatch for appKey={}", appKey);
|
||||
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid signature");
|
||||
return;
|
||||
}
|
||||
|
||||
// 签名验证通过,继续处理请求
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 appKey 解析 appSecret。
|
||||
* 各服务需实现此接口以查询 appSecret。
|
||||
*/
|
||||
public interface AppSecretResolver {
|
||||
String resolveAppSecret(String appKey);
|
||||
}
|
||||
}
|
||||
@ -72,7 +72,8 @@ public final class ConfigFileCrypto {
|
||||
text(node, "iosBundleId"),
|
||||
text(node, "harmonyBundleName"),
|
||||
text(node, "baseUrl"),
|
||||
text(node, "serverUrl"));
|
||||
text(node, "serverUrl"),
|
||||
text(node, "signingKey"));
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
@ -105,7 +106,8 @@ public final class ConfigFileCrypto {
|
||||
String iosBundleId,
|
||||
String harmonyBundleName,
|
||||
String baseUrl,
|
||||
String serverUrl) {
|
||||
String serverUrl,
|
||||
String signingKey) {
|
||||
|
||||
public boolean matchesPackageName(String candidate) {
|
||||
if (candidate == null || candidate.isBlank()) return false;
|
||||
|
||||
@ -195,6 +195,7 @@ public class AppService {
|
||||
payload.put("harmonyBundleName", app.getHarmonyBundleName());
|
||||
}
|
||||
payload.put("serverUrl", normalizeBaseUrl(sdkPlatformPublicBaseUrl));
|
||||
payload.put("signingKey", app.getAppSecret());
|
||||
payload.put("issuedAt", java.time.Instant.now().toString());
|
||||
try {
|
||||
return ConfigFileCrypto.encrypt(MAPPER.valueToTree(payload).toString());
|
||||
|
||||
@ -2,7 +2,9 @@ package com.xuqm.update.controller;
|
||||
|
||||
import com.xuqm.common.model.ApiResponse;
|
||||
import com.xuqm.update.entity.AppVersionEntity;
|
||||
import com.xuqm.update.entity.DeviceRecordEntity;
|
||||
import com.xuqm.update.repository.AppVersionRepository;
|
||||
import com.xuqm.update.repository.DeviceRecordRepository;
|
||||
import com.xuqm.update.model.AppPackageInspectResult;
|
||||
import com.xuqm.update.service.UpdateOperationLogService;
|
||||
import org.slf4j.Logger;
|
||||
@ -34,6 +36,7 @@ public class AppVersionController {
|
||||
private static final Logger log = LoggerFactory.getLogger(AppVersionController.class);
|
||||
|
||||
private final AppVersionRepository versionRepository;
|
||||
private final DeviceRecordRepository deviceRecordRepository;
|
||||
private final UpdateAssetService updateAssetService;
|
||||
private final PublishConfigService publishConfigService;
|
||||
private final AppStoreService appStoreService;
|
||||
@ -45,6 +48,7 @@ public class AppVersionController {
|
||||
private final com.fasterxml.jackson.databind.ObjectMapper objectMapper;
|
||||
|
||||
public AppVersionController(AppVersionRepository versionRepository,
|
||||
DeviceRecordRepository deviceRecordRepository,
|
||||
UpdateAssetService updateAssetService,
|
||||
PublishConfigService publishConfigService,
|
||||
AppStoreService appStoreService,
|
||||
@ -55,6 +59,7 @@ public class AppVersionController {
|
||||
GrayMemberService grayMemberService,
|
||||
com.fasterxml.jackson.databind.ObjectMapper objectMapper) {
|
||||
this.versionRepository = versionRepository;
|
||||
this.deviceRecordRepository = deviceRecordRepository;
|
||||
this.updateAssetService = updateAssetService;
|
||||
this.publishConfigService = publishConfigService;
|
||||
this.appStoreService = appStoreService;
|
||||
@ -72,9 +77,16 @@ public class AppVersionController {
|
||||
@RequestParam AppVersionEntity.Platform platform,
|
||||
@RequestParam int currentVersionCode,
|
||||
@RequestParam(required = false) String licenseFile,
|
||||
@RequestParam(required = false) String userId) {
|
||||
@RequestParam(required = false) String userId,
|
||||
@RequestParam(required = false) String deviceId,
|
||||
@RequestParam(required = false) String model,
|
||||
@RequestParam(required = false) String osVersion,
|
||||
@RequestParam(required = false) String vendor) {
|
||||
|
||||
String resolvedAppKey = resolveAndValidate(appKey, platform, licenseFile);
|
||||
|
||||
// 异步记录设备信息(不影响响应性能)
|
||||
recordDevice(resolvedAppKey, platform.name(), deviceId, userId, model, osVersion, vendor, currentVersionCode);
|
||||
boolean serviceActivated = tenantClient.isUpdateServiceEnabled(resolvedAppKey, platform.name());
|
||||
boolean allowAnonymousCheck = publishConfigService.allowAnonymousUpdateCheck(resolvedAppKey);
|
||||
|
||||
@ -589,4 +601,32 @@ public class AppVersionController {
|
||||
return "[]";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步记录设备信息到 update_device_record 表。
|
||||
* 仅当 deviceId 或 model 有值时才记录,避免空数据。
|
||||
* 异常不影响主流程。
|
||||
*/
|
||||
private void recordDevice(String appKey, String platform, String deviceId, String userId,
|
||||
String model, String osVersion, String vendor, int currentVersionCode) {
|
||||
if ((deviceId == null || deviceId.isBlank()) && (model == null || model.isBlank())) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
DeviceRecordEntity record = new DeviceRecordEntity();
|
||||
record.setId(UUID.randomUUID().toString());
|
||||
record.setAppKey(appKey);
|
||||
record.setPlatform(platform);
|
||||
record.setDeviceId(deviceId);
|
||||
record.setUserId(userId);
|
||||
record.setModel(model);
|
||||
record.setOsVersion(osVersion);
|
||||
record.setVendor(vendor);
|
||||
record.setCurrentVersionCode(currentVersionCode);
|
||||
record.setCheckedAt(LocalDateTime.now());
|
||||
deviceRecordRepository.save(record);
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to record device info for appKey={}: {}", appKey, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,59 @@
|
||||
package com.xuqm.update.controller;
|
||||
|
||||
import com.xuqm.common.model.ApiResponse;
|
||||
import com.xuqm.update.service.UpdateStatisticsService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/updates/stats")
|
||||
public class UpdateStatisticsController {
|
||||
|
||||
private final UpdateStatisticsService statisticsService;
|
||||
|
||||
public UpdateStatisticsController(UpdateStatisticsService statisticsService) {
|
||||
this.statisticsService = statisticsService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 版本分布:各版本的设备数占比(饼图/环形图数据)
|
||||
*/
|
||||
@GetMapping("/version-distribution")
|
||||
public ResponseEntity<ApiResponse<List<Map<String, Object>>>> getVersionDistribution(
|
||||
@RequestParam String appKey,
|
||||
@RequestParam String platform,
|
||||
@RequestParam(defaultValue = "30") int days) {
|
||||
List<Map<String, Object>> data = statisticsService.getVersionDistribution(appKey, platform, days);
|
||||
return ResponseEntity.ok(ApiResponse.success(data));
|
||||
}
|
||||
|
||||
/**
|
||||
* 设备型号分布 Top 10(柱状图数据)
|
||||
*/
|
||||
@GetMapping("/device-distribution")
|
||||
public ResponseEntity<ApiResponse<Map<String, List<Map<String, Object>>>>> getDeviceDistribution(
|
||||
@RequestParam String appKey,
|
||||
@RequestParam String platform,
|
||||
@RequestParam(defaultValue = "30") int days) {
|
||||
Map<String, List<Map<String, Object>>> data = new java.util.LinkedHashMap<>();
|
||||
data.put("models", statisticsService.getDeviceDistribution(appKey, platform, days));
|
||||
data.put("osVersions", statisticsService.getOsVersionDistribution(appKey, platform, days));
|
||||
data.put("vendors", statisticsService.getVendorDistribution(appKey, platform, days));
|
||||
return ResponseEntity.ok(ApiResponse.success(data));
|
||||
}
|
||||
|
||||
/**
|
||||
* 每日检测趋势(折线图数据)
|
||||
*/
|
||||
@GetMapping("/daily-trend")
|
||||
public ResponseEntity<ApiResponse<List<Map<String, Object>>>> getDailyTrend(
|
||||
@RequestParam String appKey,
|
||||
@RequestParam String platform,
|
||||
@RequestParam(defaultValue = "7") int days) {
|
||||
List<Map<String, Object>> data = statisticsService.getDailyTrend(appKey, platform, days);
|
||||
return ResponseEntity.ok(ApiResponse.success(data));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
package com.xuqm.update.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "update_device_record")
|
||||
public class DeviceRecordEntity {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@Column(nullable = false, length = 64)
|
||||
private String appKey;
|
||||
|
||||
@Column(nullable = false, length = 16)
|
||||
private String platform;
|
||||
|
||||
@Column(length = 128)
|
||||
private String deviceId;
|
||||
|
||||
@Column(length = 128)
|
||||
private String userId;
|
||||
|
||||
@Column(length = 128)
|
||||
private String model;
|
||||
|
||||
@Column(length = 64)
|
||||
private String osVersion;
|
||||
|
||||
@Column(length = 32)
|
||||
private String vendor;
|
||||
|
||||
private Integer currentVersionCode;
|
||||
|
||||
@Column(length = 32)
|
||||
private String currentVersionName;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime checkedAt;
|
||||
|
||||
public String getId() { return id; }
|
||||
public void setId(String id) { this.id = id; }
|
||||
|
||||
public String getAppKey() { return appKey; }
|
||||
public void setAppKey(String appKey) { this.appKey = appKey; }
|
||||
|
||||
public String getPlatform() { return platform; }
|
||||
public void setPlatform(String platform) { this.platform = platform; }
|
||||
|
||||
public String getDeviceId() { return deviceId; }
|
||||
public void setDeviceId(String deviceId) { this.deviceId = deviceId; }
|
||||
|
||||
public String getUserId() { return userId; }
|
||||
public void setUserId(String userId) { this.userId = userId; }
|
||||
|
||||
public String getModel() { return model; }
|
||||
public void setModel(String model) { this.model = model; }
|
||||
|
||||
public String getOsVersion() { return osVersion; }
|
||||
public void setOsVersion(String osVersion) { this.osVersion = osVersion; }
|
||||
|
||||
public String getVendor() { return vendor; }
|
||||
public void setVendor(String vendor) { this.vendor = vendor; }
|
||||
|
||||
public Integer getCurrentVersionCode() { return currentVersionCode; }
|
||||
public void setCurrentVersionCode(Integer currentVersionCode) { this.currentVersionCode = currentVersionCode; }
|
||||
|
||||
public String getCurrentVersionName() { return currentVersionName; }
|
||||
public void setCurrentVersionName(String currentVersionName) { this.currentVersionName = currentVersionName; }
|
||||
|
||||
public LocalDateTime getCheckedAt() { return checkedAt; }
|
||||
public void setCheckedAt(LocalDateTime checkedAt) { this.checkedAt = checkedAt; }
|
||||
}
|
||||
@ -0,0 +1,78 @@
|
||||
package com.xuqm.update.repository;
|
||||
|
||||
import com.xuqm.update.entity.DeviceRecordEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public interface DeviceRecordRepository extends JpaRepository<DeviceRecordEntity, String> {
|
||||
|
||||
/** 版本分布:按 currentVersionCode 分组统计设备数 */
|
||||
@Query(value = """
|
||||
SELECT current_version_code AS versionCode,
|
||||
current_version_name AS versionName,
|
||||
COUNT(DISTINCT device_id) AS deviceCount
|
||||
FROM update_device_record
|
||||
WHERE app_key = :appKey AND platform = :platform AND checked_at >= :since
|
||||
GROUP BY current_version_code, current_version_name
|
||||
ORDER BY current_version_code DESC
|
||||
""", nativeQuery = true)
|
||||
List<Object[]> findVersionDistribution(@Param("appKey") String appKey,
|
||||
@Param("platform") String platform,
|
||||
@Param("since") LocalDateTime since);
|
||||
|
||||
/** 设备型号分布 Top N */
|
||||
@Query(value = """
|
||||
SELECT model, COUNT(DISTINCT device_id) AS deviceCount
|
||||
FROM update_device_record
|
||||
WHERE app_key = :appKey AND platform = :platform AND checked_at >= :since AND model IS NOT NULL AND model != ''
|
||||
GROUP BY model
|
||||
ORDER BY deviceCount DESC
|
||||
LIMIT :limit
|
||||
""", nativeQuery = true)
|
||||
List<Object[]> findModelDistribution(@Param("appKey") String appKey,
|
||||
@Param("platform") String platform,
|
||||
@Param("since") LocalDateTime since,
|
||||
@Param("limit") int limit);
|
||||
|
||||
/** 系统版本分布 */
|
||||
@Query(value = """
|
||||
SELECT os_version AS osVersion, COUNT(DISTINCT device_id) AS deviceCount
|
||||
FROM update_device_record
|
||||
WHERE app_key = :appKey AND platform = :platform AND checked_at >= :since AND os_version IS NOT NULL AND os_version != ''
|
||||
GROUP BY os_version
|
||||
ORDER BY deviceCount DESC
|
||||
LIMIT :limit
|
||||
""", nativeQuery = true)
|
||||
List<Object[]> findOsVersionDistribution(@Param("appKey") String appKey,
|
||||
@Param("platform") String platform,
|
||||
@Param("since") LocalDateTime since,
|
||||
@Param("limit") int limit);
|
||||
|
||||
/** 厂商分布 */
|
||||
@Query(value = """
|
||||
SELECT vendor, COUNT(DISTINCT device_id) AS deviceCount
|
||||
FROM update_device_record
|
||||
WHERE app_key = :appKey AND platform = :platform AND checked_at >= :since AND vendor IS NOT NULL AND vendor != ''
|
||||
GROUP BY vendor
|
||||
ORDER BY deviceCount DESC
|
||||
""", nativeQuery = true)
|
||||
List<Object[]> findVendorDistribution(@Param("appKey") String appKey,
|
||||
@Param("platform") String platform,
|
||||
@Param("since") LocalDateTime since);
|
||||
|
||||
/** 每日检测趋势:按天统计设备数 */
|
||||
@Query(value = """
|
||||
SELECT DATE(checked_at) AS day, COUNT(DISTINCT device_id) AS deviceCount
|
||||
FROM update_device_record
|
||||
WHERE app_key = :appKey AND platform = :platform AND checked_at >= :since
|
||||
GROUP BY DATE(checked_at)
|
||||
ORDER BY day ASC
|
||||
""", nativeQuery = true)
|
||||
List<Object[]> findDailyTrend(@Param("appKey") String appKey,
|
||||
@Param("platform") String platform,
|
||||
@Param("since") LocalDateTime since);
|
||||
}
|
||||
@ -0,0 +1,106 @@
|
||||
package com.xuqm.update.service;
|
||||
|
||||
import com.xuqm.update.repository.DeviceRecordRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class UpdateStatisticsService {
|
||||
|
||||
private final DeviceRecordRepository deviceRecordRepository;
|
||||
|
||||
public UpdateStatisticsService(DeviceRecordRepository deviceRecordRepository) {
|
||||
this.deviceRecordRepository = deviceRecordRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* 版本分布:各版本的设备数占比
|
||||
* 返回 [{ versionCode, versionName, deviceCount }]
|
||||
*/
|
||||
public List<Map<String, Object>> getVersionDistribution(String appKey, String platform, int days) {
|
||||
LocalDateTime since = LocalDateTime.now().minusDays(days);
|
||||
List<Object[]> rows = deviceRecordRepository.findVersionDistribution(appKey, platform, since);
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (Object[] row : rows) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("versionCode", row[0]);
|
||||
item.put("versionName", row[1]);
|
||||
item.put("deviceCount", row[2]);
|
||||
result.add(item);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设备型号分布 Top N
|
||||
* 返回 [{ model, deviceCount }]
|
||||
*/
|
||||
public List<Map<String, Object>> getDeviceDistribution(String appKey, String platform, int days) {
|
||||
LocalDateTime since = LocalDateTime.now().minusDays(days);
|
||||
List<Object[]> models = deviceRecordRepository.findModelDistribution(appKey, platform, since, 10);
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (Object[] row : models) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("model", row[0]);
|
||||
item.put("deviceCount", row[1]);
|
||||
result.add(item);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统版本分布
|
||||
* 返回 [{ osVersion, deviceCount }]
|
||||
*/
|
||||
public List<Map<String, Object>> getOsVersionDistribution(String appKey, String platform, int days) {
|
||||
LocalDateTime since = LocalDateTime.now().minusDays(days);
|
||||
List<Object[]> rows = deviceRecordRepository.findOsVersionDistribution(appKey, platform, since, 10);
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (Object[] row : rows) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("osVersion", row[0]);
|
||||
item.put("deviceCount", row[1]);
|
||||
result.add(item);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 厂商分布
|
||||
* 返回 [{ vendor, deviceCount }]
|
||||
*/
|
||||
public List<Map<String, Object>> getVendorDistribution(String appKey, String platform, int days) {
|
||||
LocalDateTime since = LocalDateTime.now().minusDays(days);
|
||||
List<Object[]> rows = deviceRecordRepository.findVendorDistribution(appKey, platform, since);
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (Object[] row : rows) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("vendor", row[0]);
|
||||
item.put("deviceCount", row[1]);
|
||||
result.add(item);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 每日检测趋势
|
||||
* 返回 [{ day, deviceCount }]
|
||||
*/
|
||||
public List<Map<String, Object>> getDailyTrend(String appKey, String platform, int days) {
|
||||
LocalDateTime since = LocalDateTime.now().minusDays(days);
|
||||
List<Object[]> rows = deviceRecordRepository.findDailyTrend(appKey, platform, since);
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (Object[] row : rows) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("day", row[0] != null ? row[0].toString() : "");
|
||||
item.put("deviceCount", row[1]);
|
||||
result.add(item);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
CREATE TABLE IF NOT EXISTS update_device_record (
|
||||
id VARCHAR(255) NOT NULL PRIMARY KEY,
|
||||
app_key VARCHAR(64) NOT NULL,
|
||||
platform VARCHAR(16) NOT NULL,
|
||||
device_id VARCHAR(128),
|
||||
user_id VARCHAR(128),
|
||||
model VARCHAR(128),
|
||||
os_version VARCHAR(64),
|
||||
vendor VARCHAR(32),
|
||||
current_version_code INT,
|
||||
current_version_name VARCHAR(32),
|
||||
checked_at DATETIME(6) NOT NULL,
|
||||
INDEX idx_device_record_app_checked (app_key, platform, checked_at),
|
||||
INDEX idx_device_record_device (app_key, device_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
@ -15,7 +15,7 @@ import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/bugcollect/v1")
|
||||
@RequestMapping("/api/bugcollect/v1")
|
||||
public class LogController {
|
||||
|
||||
private final LogService logService;
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户