CaiWuXiTong/app/Console/Commands/SyncDingWorklogs.php

175 lines
6.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Console\Commands;
use App\Services\Auth\FinanceRoster;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
use Modules\EcommerceFinance\Exceptions\DingTalkWorklogException;
use Modules\EcommerceFinance\Repositories\DingWorklogMirrorRepository;
use Modules\EcommerceFinance\Services\DingTalkWorklogClient;
use Throwable;
class SyncDingWorklogs extends Command
{
protected $signature = 'business-mirror:sync-ding-worklogs
{--days= : 回看天数,默认使用配置值}
{--template= : 日志模板名称,默认使用配置值}';
protected $description = '同步钉钉财务日报到日志统计镜像';
public function handle(
DingTalkWorklogClient $client,
DingWorklogMirrorRepository $repository,
FinanceRoster $financeRoster,
): int {
if (! (bool) config('dingtalk.worklog.enabled', true)) {
return self::SUCCESS;
}
if (! $repository->available()) {
$this->error('日志统计镜像表尚未建立,请先执行数据库迁移。');
return self::FAILURE;
}
if (! $client->isConfigured()) {
$repository->recordFailure('dingtalk_credentials_missing');
$this->error('钉钉应用凭据未配置。');
return self::FAILURE;
}
$days = max((int) ($this->option('days') ?: config('dingtalk.worklog.lookback_days', 35)), 1);
$templates = $this->templates();
$to = now();
$from = $to->copy()->subDays($days)->startOfDay();
try {
$now = now();
$pages = 0;
$reports = [];
foreach ($templates as $template) {
$result = $client->listReports($from, $to, $template);
$pages += (int) $result['pages'];
foreach ($result['reports'] as $report) {
if (! is_array($report)) {
continue;
}
$reportId = DingWorklogMirrorRepository::reportId($report);
$reports[$reportId] = [
'requested_template' => $template,
'report' => $report,
];
}
}
$financeOnlyTemplates = array_values(array_filter(array_map(
'trim',
(array) config('dingtalk.worklog.finance_only_templates', ['空白日志']),
)));
$financeMembers = [];
$rows = [];
foreach ($reports as $reportId => $entry) {
$report = $entry['report'];
$templateName = trim((string) ($report['template_name'] ?? $entry['requested_template']));
$creatorUserId = mb_substr(trim((string) ($report['creator_userid'] ?? $report['creator_id'] ?? '')), 0, 128);
if (in_array($templateName, $financeOnlyTemplates, true)
&& ! $this->isFinanceMember($creatorUserId, $financeRoster, $financeMembers)) {
continue;
}
$submittedRaw = $report['create_time']
?? $report['created_at']
?? $report['submit_time']
?? null;
if ($submittedRaw === null || $submittedRaw === '') {
continue;
}
try {
$submittedAt = DingWorklogMirrorRepository::submittedAt($submittedRaw);
} catch (Throwable) {
continue;
}
$statisticsDate = DingWorklogMirrorRepository::statisticsDate($submittedAt);
$rows[] = [
'report_id' => $reportId,
'template_name' => mb_substr($templateName, 0, 191),
'creator_userid' => $creatorUserId,
'creator_name' => mb_substr(trim((string) ($report['creator_name'] ?? $report['creator_nick'] ?? '')), 0, 191),
'submitted_at' => $submittedAt->toDateTimeString(),
'statistics_date' => $statisticsDate->toDateString(),
'remark' => trim((string) ($report['remark'] ?? '')),
'raw_payload' => json_encode($report, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
'synced_at' => $now,
'created_at' => $now,
'updated_at' => $now,
];
}
$upserted = $repository->upsertReports($rows);
$repository->recordSuccess($upserted, implode(' / ', $templates), $from, $to);
$this->info(sprintf(
'钉钉日志同步完成:读取 %d 页,写入/更新 %d 条。',
$pages,
$upserted,
));
return self::SUCCESS;
} catch (DingTalkWorklogException $exception) {
$repository->recordFailure($exception->errorCode);
report($exception);
$this->error($exception->getMessage());
return self::FAILURE;
} catch (Throwable $exception) {
$repository->recordFailure('dingtalk_worklog_sync_failed');
report($exception);
$this->error('钉钉日志同步失败,详细原因已记录到受保护日志。');
return self::FAILURE;
}
}
/** @return list<string> */
private function templates(): array
{
$configured = trim((string) $this->option('template'));
$templates = $configured !== ''
? preg_split('/[,,]/u', $configured, -1, PREG_SPLIT_NO_EMPTY)
: (array) config('dingtalk.worklog.template_names', [
config('dingtalk.worklog.template_name', '财务日报'),
]);
$templates = array_values(array_unique(array_filter(array_map(
static fn (mixed $template): string => trim((string) $template),
$templates,
))));
return $templates !== [] ? $templates : ['财务日报'];
}
/**
* “空白日志”是全公司共用模板,只统计财务中心成员的记录。
*
* @param array<string,bool> $members
*/
private function isFinanceMember(string $userId, FinanceRoster $roster, array &$members): bool
{
if ($userId === '') {
return false;
}
if (array_key_exists($userId, $members)) {
return $members[$userId];
}
return $members[$userId] = (bool) Cache::remember(
'dingtalk:worklog:finance-member:' . hash('sha256', $userId),
now()->addMinutes(max((int) config('dingtalk.worklog.finance_cache_minutes', 720), 1)),
static function () use ($roster, $userId): bool {
$member = $roster->findEligible($userId);
return $member !== null && empty($member['restricted']);
},
);
}
}