configService->get(self::DINGTALK_TOKEN_CONFIG_KEY)); if ($token === '') { throw new RuntimeException('未配置 ERP 请求日报的钉钉机器人 Token'); } [$start, $end] = $this->resolvePeriod($date, $from, $to); $periodLabel = $this->formatPeriodLabel($start, $end); $records = $this->database->connection('agentslave') ->table('request_records') ->selectRaw("agents.name as agent_name, agents.code as agent_code, SUBSTRING_INDEX(request_records.request_uri, '?', 1) as request_uri, COUNT(*) as request_count") ->leftJoin('agents', 'agents.id', '=', 'request_records.user_id') ->where('request_records.created', '>=', $start->toDateTimeString()) ->where('request_records.created', '<', $end->toDateTimeString()) ->where('request_records.request_uri', 'like', '/openapi/erp/%') ->groupByRaw("agents.id, agents.name, agents.code, SUBSTRING_INDEX(request_records.request_uri, '?', 1)") ->orderBy('agents.name') ->orderBy('agents.code') ->orderByRaw("SUBSTRING_INDEX(request_records.request_uri, '?', 1)") ->get(); $messages = $this->formatMessages($periodLabel, $records); $messageCount = count($messages); foreach ($messages as $index => $message) { if (! $this->dingTalkService->sendTextToToken($token, $message)) { throw new RuntimeException(sprintf('ERP 请求日报第 %d/%d 条发送到钉钉失败', $index + 1, $messageCount)); } } return [ 'date' => $periodLabel, 'from' => $start->toDateTimeString(), 'to' => $end->subSecond()->toDateTimeString(), 'company_count' => $records->groupBy(fn ($record) => $record->agent_name."\0".$record->agent_code)->count(), 'request_count' => $records->sum('request_count'), ]; } /** * @return array{0: CarbonImmutable, 1: CarbonImmutable} half-open interval [start, end) */ private function resolvePeriod(?string $date, ?string $from, ?string $to): array { $date = $this->normalizeOption($date); $from = $this->normalizeOption($from); $to = $this->normalizeOption($to); if ($date !== null && ($from !== null || $to !== null)) { throw new InvalidArgumentException('--date 不能与 --from/--to 同时使用'); } if ($date !== null) { if (! $this->isDateOnly($date)) { throw new InvalidArgumentException('--date 仅支持 Y-m-d 格式'); } $start = CarbonImmutable::parse($date, self::REPORT_TIMEZONE)->startOfDay(); return [$start, $start->addDay()]; } if ($from === null && $to === null) { $start = CarbonImmutable::now(self::REPORT_TIMEZONE)->subDay()->startOfDay(); return [$start, $start->addDay()]; } if ($from === null || $to === null) { throw new InvalidArgumentException('--from 与 --to 需要同时指定'); } $start = $this->parseBound($from, isStart: true); $end = $this->parseBound($to, isStart: false); if ($end->lessThanOrEqualTo($start)) { throw new InvalidArgumentException('结束时间必须晚于开始时间'); } return [$start, $end]; } private function parseBound(string $value, bool $isStart): CarbonImmutable { $parsed = CarbonImmutable::parse($value, self::REPORT_TIMEZONE); if ($this->isDateOnly($value)) { return $isStart ? $parsed->startOfDay() : $parsed->startOfDay()->addDay(); } return $isStart ? $parsed : $parsed->addSecond(); } private function isDateOnly(string $value): bool { return (bool) preg_match('/^\d{4}-\d{2}-\d{2}$/', $value); } private function normalizeOption(?string $value): ?string { if ($value === null) { return null; } $value = trim($value); return $value === '' ? null : $value; } private function formatPeriodLabel(CarbonImmutable $start, CarbonImmutable $end): string { $inclusiveEnd = $end->subSecond(); $isFullDayStart = $start->format('H:i:s') === '00:00:00'; $isFullDayEnd = $inclusiveEnd->format('H:i:s') === '23:59:59'; $spansSingleDay = $start->toDateString() === $inclusiveEnd->toDateString(); if ($isFullDayStart && $isFullDayEnd && $spansSingleDay) { return $start->toDateString(); } if ($isFullDayStart && $isFullDayEnd) { return $start->toDateString().' ~ '.$inclusiveEnd->toDateString(); } return $start->format('Y-m-d H:i:s').' ~ '.$inclusiveEnd->format('Y-m-d H:i:s'); } /** * @return array */ private function formatMessages(string $periodLabel, Collection $records): array { $title = "{$periodLabel} ERP OpenAPI 请求统计"; if ($records->isEmpty()) { return ["{$title}\n无请求记录"]; } $messages = [$title]; foreach ($records->groupBy(fn ($record) => $record->agent_name."\0".$record->agent_code) as $companyRecords) { $first = $companyRecords->first(); $company = trim(($first->agent_name ?: '未知机构').' '.($first->agent_code ?: '')); $this->appendCompanyRecords($messages, $title, $company, $companyRecords); } if (count($messages) === 1) { return $messages; } return array_map( fn (string $message, int $index) => "{$message}(第 ".($index + 1).'/'.count($messages).' 条)', $messages, array_keys($messages) ); } private function appendCompanyRecords(array &$messages, string $title, string $company, Collection $records): void { foreach ($records->values() as $index => $record) { $line = $this->formatRequestLine($record); $messageIndex = array_key_last($messages); $isFirstRecord = $index === 0; $addition = $isFirstRecord ? "\n\n{$company}\n{$line}" : "\n{$line}"; if (strlen($messages[$messageIndex].$addition) <= self::MAX_MESSAGE_BYTES) { $messages[$messageIndex] .= $addition; continue; } $messages[] = "{$title}\n\n{$company}\n{$line}"; } } private function formatRequestLine(object $record): string { $uri = explode('?', (string) $record->request_uri, 2)[0]; $uri = preg_replace('/[\\x00-\\x1F\\x7F]/u', '', $uri) ?? ''; return Str::limit($uri, 1_000, '…')." {$record->request_count}次"; } }