From 1567ca0cd811641ef315357eeb4edb3eb0a2c80f Mon Sep 17 00:00:00 2001 From: renshen <1005457301@qq.com> Date: Thu, 5 Nov 2026 11:18:46 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=97=A5=E5=BF=97=E7=BB=9F=E8=AE=A1?= =?UTF-8?q?=E7=BA=B3=E5=85=A5=E8=B4=A2=E5=8A=A1=E4=B8=AD=E5=BF=83=E7=A9=BA?= =?UTF-8?q?=E7=99=BD=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Console/Commands/SyncDingWorklogs.php | 88 +++++++++++++++++-- config/dingtalk.php | 10 +++ .../DingWorklogMirrorRepository.php | 5 +- .../View/DingWorklogStatisticsPageData.php | 2 +- tests/Feature/Business/PagesTest.php | 2 +- .../Feature/Console/SyncDingWorklogsTest.php | 77 ++++++++++++++++ 6 files changed, 172 insertions(+), 12 deletions(-) diff --git a/app/Console/Commands/SyncDingWorklogs.php b/app/Console/Commands/SyncDingWorklogs.php index 268e0043..19fa2a49 100644 --- a/app/Console/Commands/SyncDingWorklogs.php +++ b/app/Console/Commands/SyncDingWorklogs.php @@ -2,7 +2,9 @@ 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; @@ -19,6 +21,7 @@ class SyncDingWorklogs extends Command public function handle( DingTalkWorklogClient $client, DingWorklogMirrorRepository $repository, + FinanceRoster $financeRoster, ): int { if (! (bool) config('dingtalk.worklog.enabled', true)) { return self::SUCCESS; @@ -36,16 +39,41 @@ class SyncDingWorklogs extends Command } $days = max((int) ($this->option('days') ?: config('dingtalk.worklog.lookback_days', 35)), 1); - $template = trim((string) ($this->option('template') ?: config('dingtalk.worklog.template_name', '财务日报'))); + $templates = $this->templates(); $to = now(); $from = $to->copy()->subDays($days)->startOfDay(); try { - $result = $client->listReports($from, $to, $template); $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 ($result['reports'] as $report) { - if (! is_array($report)) { + 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'] @@ -63,9 +91,9 @@ class SyncDingWorklogs extends Command } $statisticsDate = DingWorklogMirrorRepository::statisticsDate($submittedAt); $rows[] = [ - 'report_id' => DingWorklogMirrorRepository::reportId($report), - 'template_name' => mb_substr(trim((string) ($report['template_name'] ?? $template)), 0, 191), - 'creator_userid' => mb_substr(trim((string) ($report['creator_userid'] ?? $report['creator_id'] ?? '')), 0, 128), + '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(), @@ -78,11 +106,11 @@ class SyncDingWorklogs extends Command } $upserted = $repository->upsertReports($rows); - $repository->recordSuccess($upserted, $template, $from, $to); + $repository->recordSuccess($upserted, implode(' / ', $templates), $from, $to); $this->info(sprintf( '钉钉日志同步完成:读取 %d 页,写入/更新 %d 条。', - $result['pages'], + $pages, $upserted, )); @@ -101,4 +129,46 @@ class SyncDingWorklogs extends Command return self::FAILURE; } } + + /** @return list */ + 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 $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']); + }, + ); + } } diff --git a/config/dingtalk.php b/config/dingtalk.php index 19b3b814..a103a70d 100644 --- a/config/dingtalk.php +++ b/config/dingtalk.php @@ -46,10 +46,20 @@ return [ 'worklog' => [ 'enabled' => filter_var(env('DINGTALK_WORKLOG_ENABLED', true), FILTER_VALIDATE_BOOL), 'template_name' => env('DINGTALK_WORKLOG_TEMPLATE_NAME', '财务日报'), + 'template_names' => array_values(array_filter(array_map( + 'trim', + explode(',', (string) env('DINGTALK_WORKLOG_TEMPLATE_NAMES', '财务日报,空白日志')), + ))), + 'finance_only_templates' => array_values(array_filter(array_map( + 'trim', + explode(',', (string) env('DINGTALK_WORKLOG_FINANCE_ONLY_TEMPLATES', '空白日志')), + ))), + 'display_name' => env('DINGTALK_WORKLOG_DISPLAY_NAME', '财务日报 / 日志'), 'userid' => env('DINGTALK_WORKLOG_USERID'), 'lookback_days' => (int) env('DINGTALK_WORKLOG_LOOKBACK_DAYS', 35), 'page_size' => min(max((int) env('DINGTALK_WORKLOG_PAGE_SIZE', 20), 1), 20), 'max_pages' => max((int) env('DINGTALK_WORKLOG_MAX_PAGES', 100), 1), + 'finance_cache_minutes' => max((int) env('DINGTALK_WORKLOG_FINANCE_CACHE_MINUTES', 720), 1), 'permission_scope' => 'qyapi_report_query', ], ]; diff --git a/modules/EcommerceFinance/Repositories/DingWorklogMirrorRepository.php b/modules/EcommerceFinance/Repositories/DingWorklogMirrorRepository.php index e1ae49a2..332860bc 100644 --- a/modules/EcommerceFinance/Repositories/DingWorklogMirrorRepository.php +++ b/modules/EcommerceFinance/Repositories/DingWorklogMirrorRepository.php @@ -117,7 +117,10 @@ class DingWorklogMirrorRepository $state = $this->state(); $values = [ 'source_key' => 'dingtalk_worklog', - 'source_object' => (string) config('dingtalk.worklog.template_name', '财务日报'), + 'source_object' => implode(' / ', (array) config( + 'dingtalk.worklog.template_names', + [config('dingtalk.worklog.template_name', '财务日报')], + )), 'target_table' => self::TABLE, 'status' => 'failed', 'run_mode' => 'incremental', diff --git a/modules/EcommerceFinance/View/DingWorklogStatisticsPageData.php b/modules/EcommerceFinance/View/DingWorklogStatisticsPageData.php index 2e481d4a..b24c209e 100644 --- a/modules/EcommerceFinance/View/DingWorklogStatisticsPageData.php +++ b/modules/EcommerceFinance/View/DingWorklogStatisticsPageData.php @@ -93,7 +93,7 @@ class DingWorklogStatisticsPageData 'worklogSubmissions' => $submissions, 'worklogDays' => (int) $from->diffInDays($to) + 1, 'worklogTotalSubmissions' => array_sum(array_column($daily, 'submission_count')), - 'worklogTemplateName' => (string) config('dingtalk.worklog.template_name', '财务日报'), + 'worklogTemplateName' => (string) config('dingtalk.worklog.display_name', '财务日报 / 日志'), 'worklogCutoffAt' => $latestStatisticsDate->copy()->addDay()->setTime(12, 0)->toDateTimeString(), ]; } diff --git a/tests/Feature/Business/PagesTest.php b/tests/Feature/Business/PagesTest.php index 9da3a360..a50534ee 100644 --- a/tests/Feature/Business/PagesTest.php +++ b/tests/Feature/Business/PagesTest.php @@ -139,7 +139,7 @@ class PagesTest extends FeatureTestCase ])) ->assertOk() ->assertSee('data-log-statistics', false) - ->assertSeeText('钉钉 财务日报 · 次日 12:00 截止') + ->assertSeeText('钉钉 财务日报 / 日志 · 次日 12:00 截止') ->assertSeeText('次日 12:00 前补交的日志,统一计入统计日') ->assertSeeText('qyapi_report_query') ->assertSeeText('前往钉钉开通') diff --git a/tests/Feature/Console/SyncDingWorklogsTest.php b/tests/Feature/Console/SyncDingWorklogsTest.php index 60bc8de1..d391b70f 100644 --- a/tests/Feature/Console/SyncDingWorklogsTest.php +++ b/tests/Feature/Console/SyncDingWorklogsTest.php @@ -2,8 +2,10 @@ namespace Tests\Feature\Console; +use App\Services\Auth\FinanceRoster; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; use Mockery; @@ -26,9 +28,12 @@ class SyncDingWorklogsTest extends TestCase 'app.timezone' => 'Asia/Shanghai', 'dingtalk.worklog.enabled' => true, 'dingtalk.worklog.template_name' => '财务日报', + 'dingtalk.worklog.template_names' => ['财务日报'], + 'dingtalk.worklog.finance_only_templates' => ['空白日志'], 'dingtalk.worklog.lookback_days' => 35, ]); DB::purge('business_mirror'); + Cache::flush(); Schema::connection('business_mirror')->create('business_mirror_table_states', function (Blueprint $table): void { $table->string('dataset_key', 191)->primary(); @@ -119,4 +124,76 @@ class SyncDingWorklogsTest extends TestCase Carbon::setTestNow(); } } + + public function test_blank_logs_are_included_only_for_finance_members(): void + { + Carbon::setTestNow(Carbon::parse('2026-11-05 13:00:00', 'Asia/Shanghai')); + config(['dingtalk.worklog.template_names' => ['财务日报', '空白日志']]); + + try { + $client = Mockery::mock(DingTalkWorklogClient::class); + $client->shouldReceive('isConfigured')->once()->andReturnTrue(); + $client->shouldReceive('listReports') + ->once() + ->withArgs(static fn ($from, $to, string $template): bool => $template === '财务日报') + ->andReturn([ + 'pages' => 1, + 'reports' => [[ + 'report_id' => 'finance-daily', + 'creator_id' => 'user-finance-daily', + 'creator_name' => '秋香', + 'template_name' => '财务日报', + 'create_time' => '2026-11-04 22:14:16', + ]], + ]); + $client->shouldReceive('listReports') + ->once() + ->withArgs(static fn ($from, $to, string $template): bool => $template === '空白日志') + ->andReturn([ + 'pages' => 1, + 'reports' => [ + [ + 'report_id' => 'finance-blank-log', + 'creator_id' => 'user-renshen', + 'creator_name' => '人参', + 'template_name' => '空白日志', + 'create_time' => '2026-11-04 23:00:00', + ], + [ + 'report_id' => 'other-department-blank-log', + 'creator_id' => 'user-other', + 'creator_name' => '其他人', + 'template_name' => '空白日志', + 'create_time' => '2026-11-04 23:10:00', + ], + ], + ]); + $this->app->instance(DingTalkWorklogClient::class, $client); + + $roster = Mockery::mock(FinanceRoster::class); + $roster->shouldReceive('findEligible')->once()->with('user-renshen') + ->andReturn(['restricted' => false]); + $roster->shouldReceive('findEligible')->once()->with('user-other') + ->andReturn(['restricted' => true]); + $this->app->instance(FinanceRoster::class, $roster); + + $this->artisan('business-mirror:sync-ding-worklogs') + ->expectsOutputToContain('写入/更新 2 条') + ->assertSuccessful(); + + $this->assertSame( + ['finance-blank-log', 'finance-daily'], + DB::connection('business_mirror')->table('ding_worklog_reports') + ->orderBy('report_id')->pluck('report_id')->all(), + ); + $this->assertSame( + '财务日报 / 空白日志', + DB::connection('business_mirror')->table('business_mirror_table_states') + ->where('dataset_key', 'dingtalk.worklog.finance_daily_reports') + ->value('source_object'), + ); + } finally { + Carbon::setTestNow(); + } + } } -- 2.47.2