#feature: some update
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
<?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}次";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user