175 lines
6.8 KiB
PHP
175 lines
6.8 KiB
PHP
<?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']);
|
||
},
|
||
);
|
||
}
|
||
}
|