359 lines
16 KiB
Java
359 lines
16 KiB
Java
package com.designhub.controller;
|
|
|
|
import com.designhub.common.Result;
|
|
import com.designhub.service.AnalyticsService;
|
|
import com.designhub.service.SystemConfigService;
|
|
import com.designhub.vo.*;
|
|
import io.swagger.v3.oas.annotations.Operation;
|
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
import jakarta.servlet.http.HttpServletRequest;
|
|
import lombok.Data;
|
|
import lombok.RequiredArgsConstructor;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import org.springframework.util.StringUtils;
|
|
import org.springframework.web.bind.annotation.PostMapping;
|
|
import org.springframework.web.bind.annotation.RequestBody;
|
|
import org.springframework.web.bind.annotation.RequestMapping;
|
|
import org.springframework.web.bind.annotation.RestController;
|
|
|
|
import javax.crypto.Mac;
|
|
import javax.crypto.spec.SecretKeySpec;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.security.MessageDigest;
|
|
import java.time.Instant;
|
|
import java.util.*;
|
|
|
|
@Slf4j
|
|
@Tag(name = "开放数据分析接口", description = "对外机器人查询数据分析数据")
|
|
@RestController
|
|
@RequestMapping("/api/open/analytics")
|
|
@RequiredArgsConstructor
|
|
public class OpenAnalyticsController {
|
|
|
|
private static final String KEY_ENABLED = "openapi.analytics_query_enabled";
|
|
private static final String KEY_APP_KEY = "openapi.analytics_query_app_key";
|
|
private static final String KEY_APP_SECRET = "openapi.analytics_query_app_secret";
|
|
private static final String KEY_SIGN_EXPIRE_SECONDS = "openapi.analytics_query_sign_expire_seconds";
|
|
private static final String KEY_IP_WHITELIST = "openapi.analytics_query_ip_whitelist";
|
|
|
|
private static final String FALLBACK_KEY_APP_KEY = "openapi.task_delete_app_key";
|
|
private static final String FALLBACK_KEY_APP_SECRET = "openapi.task_delete_app_secret";
|
|
private static final String FALLBACK_KEY_SIGN_EXPIRE_SECONDS = "openapi.task_delete_sign_expire_seconds";
|
|
private static final String FALLBACK_KEY_IP_WHITELIST = "openapi.task_delete_ip_whitelist";
|
|
|
|
private final AnalyticsService analyticsService;
|
|
private final SystemConfigService systemConfigService;
|
|
|
|
@Operation(summary = "开放接口查询数据分析总览", description = "供外部机器人获取数据分析页面核心数据")
|
|
@PostMapping("/overview")
|
|
public Result<Map<String, Object>> getOverview(
|
|
@RequestBody OpenAnalyticsOverviewRequest requestBody,
|
|
HttpServletRequest request
|
|
) {
|
|
String traceId = UUID.randomUUID().toString().replace("-", "").substring(0, 16);
|
|
String clientIp = resolveClientIp(request);
|
|
try {
|
|
if (!systemConfigService.getBooleanValue(KEY_ENABLED, false)) {
|
|
return Result.error("开放数据分析查询接口未启用");
|
|
}
|
|
if (requestBody == null
|
|
|| !StringUtils.hasText(requestBody.getAppKey())
|
|
|| !StringUtils.hasText(requestBody.getTimestamp())
|
|
|| !StringUtils.hasText(requestBody.getSign())) {
|
|
return Result.error("参数不完整");
|
|
}
|
|
|
|
OpenApiCredential credential = getCredential();
|
|
if (!StringUtils.hasText(credential.getAppKey()) || !StringUtils.hasText(credential.getAppSecret())) {
|
|
return Result.error("开放接口密钥未配置");
|
|
}
|
|
if (!credential.getAppKey().equals(requestBody.getAppKey().trim())) {
|
|
log.warn("开放数据分析接口appKey不匹配: traceId={}, ip={}", traceId, clientIp);
|
|
return Result.error("鉴权失败");
|
|
}
|
|
if (!isIpAllowed(clientIp, credential.getIpWhitelist())) {
|
|
log.warn("开放数据分析接口IP不在白名单: traceId={}, ip={}", traceId, clientIp);
|
|
return Result.error("IP未授权");
|
|
}
|
|
|
|
verifyTimestamp(requestBody.getTimestamp(), credential.getSignExpireSeconds());
|
|
|
|
String dateRange = normalizeDateRange(requestBody.getDateRange());
|
|
String month = sanitizeNullableText(requestBody.getMonth());
|
|
String deptId = requestBody.getDeptId() == null ? "" : String.valueOf(requestBody.getDeptId());
|
|
String userId = requestBody.getUserId() == null ? "" : String.valueOf(requestBody.getUserId());
|
|
String includeTaskProgress = String.valueOf(Boolean.TRUE.equals(requestBody.getIncludeTaskProgress()));
|
|
String payload = String.join("|",
|
|
"analytics_overview",
|
|
requestBody.getAppKey().trim(),
|
|
dateRange,
|
|
month,
|
|
deptId,
|
|
userId,
|
|
includeTaskProgress,
|
|
requestBody.getTimestamp().trim());
|
|
|
|
String expectedSign = hmacSha256Hex(credential.getAppSecret(), payload);
|
|
if (!secureEquals(expectedSign, requestBody.getSign().trim().toLowerCase(Locale.ROOT))) {
|
|
log.warn("开放数据分析接口签名不匹配: traceId={}, ip={}", traceId, clientIp);
|
|
return Result.error("签名校验失败");
|
|
}
|
|
|
|
Map<String, Object> data = new LinkedHashMap<>();
|
|
data.put("demandStatistics", analyticsService.getDemandStatistics(
|
|
null, dateRange, requestBody.getDeptId(), requestBody.getUserId(), month));
|
|
data.put("designerPerformance", analyticsService.getDesignerPerformance(
|
|
null, month, requestBody.getDeptId()));
|
|
data.put("processEfficiency", analyticsService.getProcessEfficiency(null, month));
|
|
data.put("warningTasks", analyticsService.getWarningTasks(null));
|
|
if (Boolean.TRUE.equals(requestBody.getIncludeTaskProgress())) {
|
|
data.put("taskProgressOverview", analyticsService.getTaskProgressOverview(
|
|
dateRange, month, requestBody.getDeptId()));
|
|
}
|
|
|
|
Map<String, Object> result = new LinkedHashMap<>();
|
|
result.put("traceId", traceId);
|
|
result.put("time", Instant.now().toString());
|
|
result.put("query", Map.of(
|
|
"dateRange", dateRange,
|
|
"month", month == null ? "" : month,
|
|
"deptId", requestBody.getDeptId() == null ? "" : requestBody.getDeptId(),
|
|
"userId", requestBody.getUserId() == null ? "" : requestBody.getUserId()
|
|
));
|
|
result.put("data", data);
|
|
log.info("开放数据分析总览接口成功: traceId={}, ip={}, deptId={}, month={}",
|
|
traceId, clientIp, requestBody.getDeptId(), month);
|
|
return Result.success(result);
|
|
} catch (IllegalArgumentException e) {
|
|
return Result.error(e.getMessage());
|
|
} catch (RuntimeException e) {
|
|
log.error("开放数据分析总览接口失败: traceId={}, ip={}, error={}", traceId, clientIp, e.getMessage(), e);
|
|
return Result.error(e.getMessage());
|
|
} catch (Exception e) {
|
|
log.error("开放数据分析总览接口系统异常: traceId={}, ip={}", traceId, clientIp, e);
|
|
return Result.error("系统异常,请联系管理员");
|
|
}
|
|
}
|
|
|
|
@Operation(summary = "开放接口查询任务进度总览", description = "供外部机器人获取任务进度总览页面数据")
|
|
@PostMapping("/task-progress/overview")
|
|
public Result<Map<String, Object>> getTaskProgressOverview(
|
|
@RequestBody OpenTaskProgressOverviewRequest requestBody,
|
|
HttpServletRequest request
|
|
) {
|
|
String traceId = UUID.randomUUID().toString().replace("-", "").substring(0, 16);
|
|
String clientIp = resolveClientIp(request);
|
|
try {
|
|
if (!systemConfigService.getBooleanValue(KEY_ENABLED, false)) {
|
|
return Result.error("开放数据分析查询接口未启用");
|
|
}
|
|
if (requestBody == null
|
|
|| !StringUtils.hasText(requestBody.getAppKey())
|
|
|| !StringUtils.hasText(requestBody.getTimestamp())
|
|
|| !StringUtils.hasText(requestBody.getSign())) {
|
|
return Result.error("参数不完整");
|
|
}
|
|
|
|
OpenApiCredential credential = getCredential();
|
|
if (!StringUtils.hasText(credential.getAppKey()) || !StringUtils.hasText(credential.getAppSecret())) {
|
|
return Result.error("开放接口密钥未配置");
|
|
}
|
|
if (!credential.getAppKey().equals(requestBody.getAppKey().trim())) {
|
|
log.warn("开放任务进度接口appKey不匹配: traceId={}, ip={}", traceId, clientIp);
|
|
return Result.error("鉴权失败");
|
|
}
|
|
if (!isIpAllowed(clientIp, credential.getIpWhitelist())) {
|
|
log.warn("开放任务进度接口IP不在白名单: traceId={}, ip={}", traceId, clientIp);
|
|
return Result.error("IP未授权");
|
|
}
|
|
|
|
verifyTimestamp(requestBody.getTimestamp(), credential.getSignExpireSeconds());
|
|
|
|
String dateRange = normalizeDateRange(requestBody.getDateRange());
|
|
String month = sanitizeNullableText(requestBody.getMonth());
|
|
String deptId = requestBody.getDeptId() == null ? "" : String.valueOf(requestBody.getDeptId());
|
|
String payload = String.join("|",
|
|
"analytics_task_progress_overview",
|
|
requestBody.getAppKey().trim(),
|
|
dateRange,
|
|
month,
|
|
deptId,
|
|
requestBody.getTimestamp().trim());
|
|
|
|
String expectedSign = hmacSha256Hex(credential.getAppSecret(), payload);
|
|
if (!secureEquals(expectedSign, requestBody.getSign().trim().toLowerCase(Locale.ROOT))) {
|
|
log.warn("开放任务进度接口签名不匹配: traceId={}, ip={}", traceId, clientIp);
|
|
return Result.error("签名校验失败");
|
|
}
|
|
|
|
TaskProgressOverviewVO overview = analyticsService.getTaskProgressOverview(
|
|
dateRange, month, requestBody.getDeptId());
|
|
|
|
Map<String, Object> result = new LinkedHashMap<>();
|
|
result.put("traceId", traceId);
|
|
result.put("time", Instant.now().toString());
|
|
result.put("query", Map.of(
|
|
"dateRange", dateRange,
|
|
"month", month == null ? "" : month,
|
|
"deptId", requestBody.getDeptId() == null ? "" : requestBody.getDeptId()
|
|
));
|
|
result.put("data", overview);
|
|
log.info("开放任务进度接口成功: traceId={}, ip={}, deptId={}, month={}",
|
|
traceId, clientIp, requestBody.getDeptId(), month);
|
|
return Result.success(result);
|
|
} catch (IllegalArgumentException e) {
|
|
return Result.error(e.getMessage());
|
|
} catch (RuntimeException e) {
|
|
log.error("开放任务进度接口失败: traceId={}, ip={}, error={}", traceId, clientIp, e.getMessage(), e);
|
|
return Result.error(e.getMessage());
|
|
} catch (Exception e) {
|
|
log.error("开放任务进度接口系统异常: traceId={}, ip={}", traceId, clientIp, e);
|
|
return Result.error("系统异常,请联系管理员");
|
|
}
|
|
}
|
|
|
|
private OpenApiCredential getCredential() {
|
|
String appKey = systemConfigService.getConfigValue(KEY_APP_KEY, "");
|
|
if (!StringUtils.hasText(appKey)) {
|
|
appKey = systemConfigService.getConfigValue(FALLBACK_KEY_APP_KEY, "");
|
|
}
|
|
|
|
String appSecret = systemConfigService.getConfigValue(KEY_APP_SECRET, "");
|
|
if (!StringUtils.hasText(appSecret)) {
|
|
appSecret = systemConfigService.getConfigValue(FALLBACK_KEY_APP_SECRET, "");
|
|
}
|
|
|
|
Integer signExpireSeconds = systemConfigService.getIntValue(KEY_SIGN_EXPIRE_SECONDS, null);
|
|
if (signExpireSeconds == null || signExpireSeconds <= 0) {
|
|
signExpireSeconds = systemConfigService.getIntValue(FALLBACK_KEY_SIGN_EXPIRE_SECONDS, 300);
|
|
}
|
|
|
|
String ipWhitelist = systemConfigService.getConfigValue(KEY_IP_WHITELIST, "");
|
|
if (!StringUtils.hasText(ipWhitelist)) {
|
|
ipWhitelist = systemConfigService.getConfigValue(FALLBACK_KEY_IP_WHITELIST, "");
|
|
}
|
|
|
|
OpenApiCredential credential = new OpenApiCredential();
|
|
credential.setAppKey(appKey);
|
|
credential.setAppSecret(appSecret);
|
|
credential.setSignExpireSeconds(signExpireSeconds == null ? 300 : signExpireSeconds);
|
|
credential.setIpWhitelist(ipWhitelist);
|
|
return credential;
|
|
}
|
|
|
|
private boolean isIpAllowed(String ip, String whitelist) {
|
|
if (!StringUtils.hasText(whitelist)) {
|
|
return true;
|
|
}
|
|
String normalizedIp = ip == null ? "" : ip.trim();
|
|
for (String item : whitelist.split(",")) {
|
|
if (normalizedIp.equals(item.trim())) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private void verifyTimestamp(String timestamp, int expireSeconds) {
|
|
long nowMillis = Instant.now().toEpochMilli();
|
|
long requestMillis = parseTimestampMillis(timestamp);
|
|
long allowedSkew = Math.max(30, expireSeconds) * 1000L;
|
|
if (Math.abs(nowMillis - requestMillis) > allowedSkew) {
|
|
throw new IllegalArgumentException("签名已过期");
|
|
}
|
|
}
|
|
|
|
private long parseTimestampMillis(String timestamp) {
|
|
String value = timestamp == null ? "" : timestamp.trim();
|
|
if (!value.matches("\\d+")) {
|
|
throw new IllegalArgumentException("timestamp格式错误");
|
|
}
|
|
long ts = Long.parseLong(value);
|
|
if (value.length() <= 10) {
|
|
return ts * 1000L;
|
|
}
|
|
if (value.length() == 13) {
|
|
return ts;
|
|
}
|
|
throw new IllegalArgumentException("timestamp长度错误");
|
|
}
|
|
|
|
private String resolveClientIp(HttpServletRequest request) {
|
|
String xForwardedFor = request.getHeader("X-Forwarded-For");
|
|
if (StringUtils.hasText(xForwardedFor)) {
|
|
int idx = xForwardedFor.indexOf(',');
|
|
return idx > 0 ? xForwardedFor.substring(0, idx).trim() : xForwardedFor.trim();
|
|
}
|
|
String realIp = request.getHeader("X-Real-IP");
|
|
if (StringUtils.hasText(realIp)) {
|
|
return realIp.trim();
|
|
}
|
|
return request.getRemoteAddr();
|
|
}
|
|
|
|
private String hmacSha256Hex(String secret, String payload) throws Exception {
|
|
Mac mac = Mac.getInstance("HmacSHA256");
|
|
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
|
|
byte[] raw = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
|
|
StringBuilder sb = new StringBuilder(raw.length * 2);
|
|
for (byte b : raw) {
|
|
sb.append(String.format("%02x", b));
|
|
}
|
|
return sb.toString();
|
|
}
|
|
|
|
private boolean secureEquals(String a, String b) {
|
|
if (a == null || b == null) {
|
|
return false;
|
|
}
|
|
byte[] aa = a.getBytes(StandardCharsets.UTF_8);
|
|
byte[] bb = b.getBytes(StandardCharsets.UTF_8);
|
|
return MessageDigest.isEqual(aa, bb);
|
|
}
|
|
|
|
private String sanitizeNullableText(String value) {
|
|
return StringUtils.hasText(value) ? value.trim() : "";
|
|
}
|
|
|
|
private String normalizeDateRange(String dateRange) {
|
|
if (!StringUtils.hasText(dateRange)) {
|
|
return "month";
|
|
}
|
|
String normalized = dateRange.trim().toLowerCase(Locale.ROOT);
|
|
if (!Arrays.asList("week", "month", "quarter", "all").contains(normalized)) {
|
|
return "month";
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
@Data
|
|
public static class OpenAnalyticsOverviewRequest {
|
|
private String appKey;
|
|
private String timestamp;
|
|
private String sign;
|
|
private String dateRange;
|
|
private String month;
|
|
private Long deptId;
|
|
private Long userId;
|
|
private Boolean includeTaskProgress;
|
|
}
|
|
|
|
@Data
|
|
public static class OpenTaskProgressOverviewRequest {
|
|
private String appKey;
|
|
private String timestamp;
|
|
private String sign;
|
|
private String dateRange;
|
|
private String month;
|
|
private Long deptId;
|
|
}
|
|
|
|
@Data
|
|
private static class OpenApiCredential {
|
|
private String appKey;
|
|
private String appSecret;
|
|
private Integer signExpireSeconds;
|
|
private String ipWhitelist;
|
|
}
|
|
}
|