Files
toolbox/app/Services/ErpRequestReportService.php
T
2026-08-12 18:00:09 +08:00

221 lines
7.8 KiB
PHP

<?php
namespace App\Services;
use Carbon\CarbonImmutable;
use Illuminate\Database\DatabaseManager;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use InvalidArgumentException;
use RuntimeException;
class ErpRequestReportService
{
public const DINGTALK_TOKEN_CONFIG_KEY = 'erp_request_report.dingtalk_token';
private const REPORT_TIMEZONE = 'Asia/Shanghai';
// Keep a margin below DingTalk's text-message limit for transport overhead.
private const MAX_MESSAGE_BYTES = 18_000;
public function __construct(
private DatabaseManager $database,
private DingTalkService $dingTalkService,
private ConfigService $configService
) {}
/**
* @param string|null $date 单日(Y-m-d),与 from/to 互斥
* @param string|null $from 开始时间(Y-m-d 或 Y-m-d H:i:s),含
* @param string|null $to 结束时间(Y-m-d 或 Y-m-d H:i:s);仅日期时含整天,含时分秒时含该时刻
*/
public function sendReport(?string $date = null, ?string $from = null, ?string $to = null): array
{
$token = trim((string) $this->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<int, string>
*/
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}次";
}
}