diff --git a/backend/src/main/java/com/designhub/controller/OpenAnalyticsController.java b/backend/src/main/java/com/designhub/controller/OpenAnalyticsController.java index 49717d0..a09f0ba 100644 --- a/backend/src/main/java/com/designhub/controller/OpenAnalyticsController.java +++ b/backend/src/main/java/com/designhub/controller/OpenAnalyticsController.java @@ -21,6 +21,9 @@ import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.time.Instant; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.format.DateTimeParseException; import java.util.*; @Slf4j @@ -212,6 +215,644 @@ public class OpenAnalyticsController { } } + @Operation(summary = "开放接口查询需求统计", description = "供外部机器人获取需求统计维度数据") + @PostMapping("/demand-statistics") + public Result> getDemandStatistics( + @RequestBody OpenDemandStatisticsRequest 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 payload = String.join("|", + "analytics_demand_statistics", + requestBody.getAppKey().trim(), + dateRange, + month, + deptId, + userId, + 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 result = new LinkedHashMap<>(); + result.put("traceId", traceId); + result.put("time", Instant.now().toString()); + result.put("query", Map.of( + "dateRange", dateRange, + "month", month, + "deptId", deptId, + "userId", userId + )); + result.put("data", analyticsService.getDemandStatistics( + null, dateRange, requestBody.getDeptId(), requestBody.getUserId(), 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("/designer-performance") + public Result> getDesignerPerformance( + @RequestBody OpenDesignerPerformanceRequest 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 month = sanitizeNullableText(requestBody.getMonth()); + String deptId = requestBody.getDeptId() == null ? "" : String.valueOf(requestBody.getDeptId()); + String payload = String.join("|", + "analytics_designer_performance", + requestBody.getAppKey().trim(), + 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("签名校验失败"); + } + + Map result = new LinkedHashMap<>(); + result.put("traceId", traceId); + result.put("time", Instant.now().toString()); + result.put("query", Map.of( + "month", month, + "deptId", deptId + )); + result.put("data", analyticsService.getDesignerPerformance(null, month, requestBody.getDeptId())); + 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("/process-efficiency") + public Result> getProcessEfficiency( + @RequestBody OpenProcessEfficiencyRequest 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 month = sanitizeNullableText(requestBody.getMonth()); + String payload = String.join("|", + "analytics_process_efficiency", + requestBody.getAppKey().trim(), + month, + 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 result = new LinkedHashMap<>(); + result.put("traceId", traceId); + result.put("time", Instant.now().toString()); + result.put("query", Map.of("month", month)); + result.put("data", analyticsService.getProcessEfficiency(null, 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("/warnings") + public Result> getWarningTasks( + @RequestBody OpenWarningsRequest 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 payload = String.join("|", + "analytics_warnings", + requestBody.getAppKey().trim(), + 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 result = new LinkedHashMap<>(); + result.put("traceId", traceId); + result.put("time", Instant.now().toString()); + result.put("data", analyticsService.getWarningTasks(null)); + 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("/dimension-analysis") + public Result> getDimensionAnalysis( + @RequestBody OpenDimensionAnalysisRequest 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 payload = String.join("|", + "analytics_dimension_analysis", + requestBody.getAppKey().trim(), + dateRange, + month, + 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 result = new LinkedHashMap<>(); + result.put("traceId", traceId); + result.put("time", Instant.now().toString()); + result.put("query", Map.of("dateRange", dateRange, "month", month)); + result.put("data", analyticsService.getDimensionAnalysis(dateRange, 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("/designer-deduction-details") + public Result> getDesignerDeductionDetails( + @RequestBody OpenDesignerDeductionRequest 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 + || requestBody.getUserId() == 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 month = sanitizeNullableText(requestBody.getMonth()); + String payload = String.join("|", + "analytics_designer_deduction_details", + requestBody.getAppKey().trim(), + String.valueOf(requestBody.getUserId()), + month, + 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 result = new LinkedHashMap<>(); + result.put("traceId", traceId); + result.put("time", Instant.now().toString()); + result.put("query", Map.of("userId", requestBody.getUserId(), "month", month)); + result.put("data", analyticsService.getDesignerDeductionDetails(requestBody.getUserId(), 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("/designer-score-details") + public Result> getDesignerScoreDetails( + @RequestBody OpenDesignerScoreRequest 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 + || requestBody.getUserId() == 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 month = sanitizeNullableText(requestBody.getMonth()); + String payload = String.join("|", + "analytics_designer_score_details", + requestBody.getAppKey().trim(), + String.valueOf(requestBody.getUserId()), + month, + 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 result = new LinkedHashMap<>(); + result.put("traceId", traceId); + result.put("time", Instant.now().toString()); + result.put("query", Map.of("userId", requestBody.getUserId(), "month", month)); + result.put("data", analyticsService.getDesignerScoreDetails(requestBody.getUserId(), 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("/primary-dept/statistics") + public Result> getPrimaryDeptStatistics( + @RequestBody OpenPrimaryDeptStatisticsRequest 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.getStartTime()) + || !StringUtils.hasText(requestBody.getEndTime()) + || !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 deptId = requestBody.getDeptId() == null ? "" : String.valueOf(requestBody.getDeptId()); + String payload = String.join("|", + "analytics_primary_dept_statistics", + requestBody.getAppKey().trim(), + requestBody.getStartTime().trim(), + requestBody.getEndTime().trim(), + 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("签名校验失败"); + } + + LocalDateTime startTime = parseDateTime(requestBody.getStartTime()); + LocalDateTime endTime = parseDateTime(requestBody.getEndTime()); + Map result = new LinkedHashMap<>(); + result.put("traceId", traceId); + result.put("time", Instant.now().toString()); + result.put("query", Map.of( + "startTime", requestBody.getStartTime().trim(), + "endTime", requestBody.getEndTime().trim(), + "deptId", deptId + )); + result.put("data", analyticsService.getPrimaryDeptStatistics( + requestBody.getDeptId(), startTime, endTime)); + 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("/primary-dept/ranking") + public Result> getPrimaryDeptRanking( + @RequestBody OpenPrimaryRankingRequest requestBody, + HttpServletRequest request + ) { + return handlePrimaryRangeQuery(requestBody, request, "analytics_primary_dept_ranking", + (startTime, endTime) -> analyticsService.getPrimaryDeptRanking(startTime, endTime)); + } + + @Operation(summary = "开放接口查询一级需求流转效率", description = "供外部机器人查询一级需求流转效率") + @PostMapping("/primary-task/flow-efficiency") + public Result> getPrimaryTaskFlowEfficiency( + @RequestBody OpenPrimaryRankingRequest requestBody, + HttpServletRequest request + ) { + return handlePrimaryRangeQuery(requestBody, request, "analytics_primary_task_flow_efficiency", + (startTime, endTime) -> analyticsService.getPrimaryTaskFlowEfficiency(startTime, endTime)); + } + + @Operation(summary = "开放接口查询一级需求品牌分布", description = "供外部机器人查询一级需求品牌分布") + @PostMapping("/primary-task/brand-distribution") + public Result> getPrimaryTaskBrandDistribution( + @RequestBody OpenPrimaryRankingRequest requestBody, + HttpServletRequest request + ) { + return handlePrimaryRangeQuery(requestBody, request, "analytics_primary_task_brand_distribution", + (startTime, endTime) -> analyticsService.getPrimaryTaskBrandDistribution(startTime, endTime)); + } + + @Operation(summary = "开放接口查询一级需求用户排名", description = "供外部机器人查询一级需求用户排名") + @PostMapping("/primary-dept/user-ranking") + public Result> getPrimaryDeptUserRanking( + @RequestBody OpenPrimaryRankingRequest requestBody, + HttpServletRequest request + ) { + return handlePrimaryRangeQuery(requestBody, request, "analytics_primary_dept_user_ranking", + (startTime, endTime) -> analyticsService.getPrimaryDeptUserRanking(startTime, endTime)); + } + + private Result> handlePrimaryRangeQuery( + OpenPrimaryRankingRequest requestBody, + HttpServletRequest request, + String action, + PrimaryRangeQueryExecutor queryExecutor + ) { + 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.getStartTime()) + || !StringUtils.hasText(requestBody.getEndTime()) + || !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={}, action={}", traceId, clientIp, action); + return Result.error("鉴权失败"); + } + if (!isIpAllowed(clientIp, credential.getIpWhitelist())) { + log.warn("开放一级需求范围接口IP不在白名单: traceId={}, ip={}, action={}", traceId, clientIp, action); + return Result.error("IP未授权"); + } + verifyTimestamp(requestBody.getTimestamp(), credential.getSignExpireSeconds()); + + String payload = String.join("|", + action, + requestBody.getAppKey().trim(), + requestBody.getStartTime().trim(), + requestBody.getEndTime().trim(), + requestBody.getTimestamp().trim()); + String expectedSign = hmacSha256Hex(credential.getAppSecret(), payload); + if (!secureEquals(expectedSign, requestBody.getSign().trim().toLowerCase(Locale.ROOT))) { + log.warn("开放一级需求范围接口签名不匹配: traceId={}, ip={}, action={}", traceId, clientIp, action); + return Result.error("签名校验失败"); + } + + LocalDateTime startTime = parseDateTime(requestBody.getStartTime()); + LocalDateTime endTime = parseDateTime(requestBody.getEndTime()); + Object data = queryExecutor.execute(startTime, endTime); + + Map result = new LinkedHashMap<>(); + result.put("traceId", traceId); + result.put("time", Instant.now().toString()); + result.put("query", Map.of( + "startTime", requestBody.getStartTime().trim(), + "endTime", requestBody.getEndTime().trim() + )); + result.put("data", data); + return Result.success(result); + } catch (IllegalArgumentException e) { + return Result.error(e.getMessage()); + } catch (RuntimeException e) { + log.error("开放一级需求范围接口失败: traceId={}, ip={}, action={}, error={}", + traceId, clientIp, action, e.getMessage(), e); + return Result.error(e.getMessage()); + } catch (Exception e) { + log.error("开放一级需求范围接口系统异常: traceId={}, ip={}, action={}", traceId, clientIp, action, e); + return Result.error("系统异常,请联系管理员"); + } + } + private OpenApiCredential getCredential() { String appKey = systemConfigService.getConfigValue(KEY_APP_KEY, ""); if (!StringUtils.hasText(appKey)) { @@ -326,6 +967,22 @@ public class OpenAnalyticsController { return normalized; } + private LocalDateTime parseDateTime(String value) { + if (!StringUtils.hasText(value)) { + throw new IllegalArgumentException("时间参数不能为空"); + } + String text = value.trim(); + try { + return LocalDateTime.parse(text); + } catch (DateTimeParseException ignored) { + } + try { + return OffsetDateTime.parse(text).toLocalDateTime(); + } catch (DateTimeParseException ignored) { + } + throw new IllegalArgumentException("时间格式错误,需使用ISO时间格式"); + } + @Data public static class OpenAnalyticsOverviewRequest { private String appKey; @@ -348,6 +1005,87 @@ public class OpenAnalyticsController { private Long deptId; } + @Data + public static class OpenDemandStatisticsRequest { + private String appKey; + private String timestamp; + private String sign; + private String dateRange; + private String month; + private Long deptId; + private Long userId; + } + + @Data + public static class OpenDesignerPerformanceRequest { + private String appKey; + private String timestamp; + private String sign; + private String month; + private Long deptId; + } + + @Data + public static class OpenProcessEfficiencyRequest { + private String appKey; + private String timestamp; + private String sign; + private String month; + } + + @Data + public static class OpenWarningsRequest { + private String appKey; + private String timestamp; + private String sign; + } + + @Data + public static class OpenDimensionAnalysisRequest { + private String appKey; + private String timestamp; + private String sign; + private String dateRange; + private String month; + } + + @Data + public static class OpenDesignerDeductionRequest { + private String appKey; + private String timestamp; + private String sign; + private Long userId; + private String month; + } + + @Data + public static class OpenDesignerScoreRequest { + private String appKey; + private String timestamp; + private String sign; + private Long userId; + private String month; + } + + @Data + public static class OpenPrimaryDeptStatisticsRequest { + private String appKey; + private String timestamp; + private String sign; + private Long deptId; + private String startTime; + private String endTime; + } + + @Data + public static class OpenPrimaryRankingRequest { + private String appKey; + private String timestamp; + private String sign; + private String startTime; + private String endTime; + } + @Data private static class OpenApiCredential { private String appKey; @@ -355,4 +1093,9 @@ public class OpenAnalyticsController { private Integer signExpireSeconds; private String ipWhitelist; } + + @FunctionalInterface + private interface PrimaryRangeQueryExecutor { + Object execute(LocalDateTime startTime, LocalDateTime endTime); + } }