GenerateStatsCommand.php 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. <?php
  2. namespace App\Module\OpenAPI\Commands;
  3. use App\Module\OpenAPI\Models\OpenApiLog;
  4. use App\Module\OpenAPI\Models\OpenApiStats;
  5. use Illuminate\Console\Command;
  6. use Carbon\Carbon;
  7. /**
  8. * 生成OpenAPI统计数据命令
  9. */
  10. class GenerateStatsCommand extends Command
  11. {
  12. /**
  13. * 命令签名
  14. *
  15. * @var string
  16. */
  17. protected $signature = 'openapi:generate-stats
  18. {--date= : 指定统计日期 (Y-m-d 格式)}
  19. {--hour= : 指定统计小时 (0-23)}
  20. {--force : 强制重新生成已存在的统计数据}';
  21. /**
  22. * 命令描述
  23. *
  24. * @var string
  25. */
  26. protected $description = '生成OpenAPI统计数据';
  27. /**
  28. * 执行命令
  29. *
  30. * @return int
  31. */
  32. public function handle(): int
  33. {
  34. $date = $this->option('date') ? Carbon::parse($this->option('date')) : Carbon::yesterday();
  35. $hour = $this->option('hour');
  36. $force = $this->option('force');
  37. $this->info("开始生成统计数据...");
  38. $this->info("统计日期: {$date->toDateString()}");
  39. if ($hour !== null) {
  40. $this->info("统计小时: {$hour}");
  41. $this->generateHourlyStats($date, (int)$hour, $force);
  42. } else {
  43. $this->info("生成全天统计数据");
  44. $this->generateDailyStats($date, $force);
  45. }
  46. $this->info("统计数据生成完成!");
  47. return 0;
  48. }
  49. /**
  50. * 生成每日统计数据
  51. *
  52. * @param Carbon $date
  53. * @param bool $force
  54. * @return void
  55. */
  56. protected function generateDailyStats(Carbon $date, bool $force): void
  57. {
  58. // 生成24小时的统计数据
  59. for ($hour = 0; $hour < 24; $hour++) {
  60. $this->generateHourlyStats($date, $hour, $force);
  61. }
  62. // 生成全天汇总统计
  63. $this->generateDailySummary($date, $force);
  64. }
  65. /**
  66. * 生成每小时统计数据
  67. *
  68. * @param Carbon $date
  69. * @param int $hour
  70. * @param bool $force
  71. * @return void
  72. */
  73. protected function generateHourlyStats(Carbon $date, int $hour, bool $force): void
  74. {
  75. $startTime = $date->copy()->hour($hour)->minute(0)->second(0);
  76. $endTime = $startTime->copy()->addHour();
  77. $this->line("处理时间段: {$startTime->format('Y-m-d H:i:s')} - {$endTime->format('Y-m-d H:i:s')}");
  78. // 获取该时间段的日志数据
  79. $logs = OpenApiLog::whereBetween('created_at', [$startTime, $endTime])->get();
  80. if ($logs->isEmpty()) {
  81. $this->line(" 无数据");
  82. return;
  83. }
  84. // 按应用和接口分组统计
  85. $groupedLogs = $logs->groupBy(function ($log) {
  86. return $log->app_id . '|' . $log->uri;
  87. });
  88. foreach ($groupedLogs as $key => $groupLogs) {
  89. [$appId, $endpoint] = explode('|', $key, 2);
  90. // 检查是否已存在统计记录
  91. $existingStats = OpenApiStats::where('app_id', $appId)
  92. ->where('date', $date->toDateString())
  93. ->where('hour', $hour)
  94. ->where('endpoint', $endpoint)
  95. ->first();
  96. if ($existingStats && !$force) {
  97. $this->line(" 跳过已存在的统计: {$appId} - {$endpoint}");
  98. continue;
  99. }
  100. // 计算统计数据
  101. $stats = $this->calculateStats($groupLogs);
  102. // 创建或更新统计记录
  103. $data = [
  104. 'app_id' => $appId,
  105. 'date' => $date->toDateString(),
  106. 'hour' => $hour,
  107. 'endpoint' => $endpoint,
  108. 'request_count' => $stats['request_count'],
  109. 'success_count' => $stats['success_count'],
  110. 'error_count' => $stats['error_count'],
  111. 'avg_response_time' => $stats['avg_response_time'],
  112. 'max_response_time' => $stats['max_response_time'],
  113. 'min_response_time' => $stats['min_response_time'],
  114. 'rate_limit_hits' => $stats['rate_limit_hits'],
  115. 'unique_ips' => $stats['unique_ips'],
  116. 'error_details' => $stats['error_details'],
  117. ];
  118. if ($existingStats) {
  119. $existingStats->update($data);
  120. $this->line(" 更新统计: {$appId} - {$endpoint} ({$stats['request_count']} 请求)");
  121. } else {
  122. OpenApiStats::create($data);
  123. $this->line(" 创建统计: {$appId} - {$endpoint} ({$stats['request_count']} 请求)");
  124. }
  125. }
  126. }
  127. /**
  128. * 生成每日汇总统计
  129. *
  130. * @param Carbon $date
  131. * @param bool $force
  132. * @return void
  133. */
  134. protected function generateDailySummary(Carbon $date, bool $force): void
  135. {
  136. $this->line("生成每日汇总统计...");
  137. // 获取该日期的所有小时统计数据
  138. $hourlyStats = OpenApiStats::where('date', $date->toDateString())
  139. ->whereNotNull('hour')
  140. ->get();
  141. if ($hourlyStats->isEmpty()) {
  142. $this->line(" 无小时统计数据");
  143. return;
  144. }
  145. // 按应用和接口分组
  146. $groupedStats = $hourlyStats->groupBy(function ($stat) {
  147. return $stat->app_id . '|' . $stat->endpoint;
  148. });
  149. foreach ($groupedStats as $key => $groupStats) {
  150. [$appId, $endpoint] = explode('|', $key, 2);
  151. // 检查是否已存在日汇总记录
  152. $existingSummary = OpenApiStats::where('app_id', $appId)
  153. ->where('date', $date->toDateString())
  154. ->whereNull('hour')
  155. ->where('endpoint', $endpoint)
  156. ->first();
  157. if ($existingSummary && !$force) {
  158. $this->line(" 跳过已存在的日汇总: {$appId} - {$endpoint}");
  159. continue;
  160. }
  161. // 计算汇总数据
  162. $summary = [
  163. 'app_id' => $appId,
  164. 'date' => $date->toDateString(),
  165. 'hour' => null,
  166. 'endpoint' => $endpoint,
  167. 'request_count' => $groupStats->sum('request_count'),
  168. 'success_count' => $groupStats->sum('success_count'),
  169. 'error_count' => $groupStats->sum('error_count'),
  170. 'avg_response_time' => $groupStats->avg('avg_response_time'),
  171. 'max_response_time' => $groupStats->max('max_response_time'),
  172. 'min_response_time' => $groupStats->min('min_response_time'),
  173. 'rate_limit_hits' => $groupStats->sum('rate_limit_hits'),
  174. 'unique_ips' => $groupStats->sum('unique_ips'), // 这里简化处理,实际应该去重
  175. 'error_details' => $this->mergeErrorDetails($groupStats),
  176. ];
  177. if ($existingSummary) {
  178. $existingSummary->update($summary);
  179. $this->line(" 更新日汇总: {$appId} - {$endpoint} ({$summary['request_count']} 请求)");
  180. } else {
  181. OpenApiStats::create($summary);
  182. $this->line(" 创建日汇总: {$appId} - {$endpoint} ({$summary['request_count']} 请求)");
  183. }
  184. }
  185. }
  186. /**
  187. * 计算统计数据
  188. *
  189. * @param \Illuminate\Support\Collection $logs
  190. * @return array
  191. */
  192. protected function calculateStats($logs): array
  193. {
  194. $requestCount = $logs->count();
  195. $successCount = $logs->where('response_code', '<', 400)->count();
  196. $errorCount = $requestCount - $successCount;
  197. $rateLimitHits = $logs->where('rate_limit_hit', true)->count();
  198. $uniqueIps = $logs->pluck('ip_address')->unique()->count();
  199. $responseTimes = $logs->pluck('response_time')->filter();
  200. $avgResponseTime = $responseTimes->avg() ?: 0;
  201. $maxResponseTime = $responseTimes->max() ?: 0;
  202. $minResponseTime = $responseTimes->min() ?: 0;
  203. // 统计错误详情
  204. $errorDetails = [];
  205. $errorLogs = $logs->where('response_code', '>=', 400);
  206. foreach ($errorLogs as $log) {
  207. $code = $log->response_code;
  208. if (!isset($errorDetails[$code])) {
  209. $errorDetails[$code] = [
  210. 'count' => 0,
  211. 'message' => $log->error_message ?: "HTTP {$code}",
  212. 'first_seen' => $log->created_at->toISOString(),
  213. ];
  214. }
  215. $errorDetails[$code]['count']++;
  216. $errorDetails[$code]['last_seen'] = $log->created_at->toISOString();
  217. }
  218. return [
  219. 'request_count' => $requestCount,
  220. 'success_count' => $successCount,
  221. 'error_count' => $errorCount,
  222. 'avg_response_time' => round($avgResponseTime, 2),
  223. 'max_response_time' => $maxResponseTime,
  224. 'min_response_time' => $minResponseTime,
  225. 'rate_limit_hits' => $rateLimitHits,
  226. 'unique_ips' => $uniqueIps,
  227. 'error_details' => $errorDetails,
  228. ];
  229. }
  230. /**
  231. * 合并错误详情
  232. *
  233. * @param \Illuminate\Support\Collection $stats
  234. * @return array
  235. */
  236. protected function mergeErrorDetails($stats): array
  237. {
  238. $mergedDetails = [];
  239. foreach ($stats as $stat) {
  240. $errorDetails = $stat->error_details ?? [];
  241. foreach ($errorDetails as $code => $details) {
  242. if (!isset($mergedDetails[$code])) {
  243. $mergedDetails[$code] = $details;
  244. } else {
  245. $mergedDetails[$code]['count'] += $details['count'];
  246. if (isset($details['last_seen'])) {
  247. $mergedDetails[$code]['last_seen'] = $details['last_seen'];
  248. }
  249. }
  250. }
  251. }
  252. return $mergedDetails;
  253. }
  254. }