221 lines
7.8 KiB
PHP
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}次";
|
|
}
|
|
}
|