[ 'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '', ], 'upstream_mirrors.connection' => 'business_mirror', '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(); $table->string('source_key', 64); $table->string('source_object', 191); $table->string('target_table', 64); $table->string('status', 24)->default('pending'); $table->string('run_mode', 24)->nullable(); $table->json('cursor')->nullable(); $table->json('high_watermark')->nullable(); $table->dateTime('window_started_at')->nullable(); $table->dateTime('window_ends_at')->nullable(); $table->dateTime('initial_completed_at')->nullable(); $table->dateTime('last_attempted_at')->nullable(); $table->dateTime('last_succeeded_at')->nullable(); $table->unsignedBigInteger('last_upserted_rows')->default(0); $table->unsignedBigInteger('mirror_row_count')->default(0); $table->string('last_error_code', 120)->nullable(); $table->timestamps(); }); (require base_path('database/migrations/2026_11_04_230000_create_ding_worklog_reports.php'))->up(); } public function test_command_stores_reports_under_the_correct_statistics_date(): void { Carbon::setTestNow(Carbon::parse('2026-11-05 13:00:00', 'Asia/Shanghai')); try { $client = Mockery::mock(DingTalkWorklogClient::class); $client->shouldReceive('isConfigured')->once()->andReturnTrue(); $client->shouldReceive('listReports')->once()->andReturn([ 'pages' => 1, 'reports' => [ [ 'report_id' => 'before-noon', 'creator_id' => 'user-a', 'creator_name' => '秋香', 'template_name' => '财务日报', 'create_time' => '2026-11-04 10:26:00', ], [ 'report_id' => 'last-minute', 'creator_id' => 'user-b', 'creator_name' => '李斯', 'template_name' => '财务日报', 'create_time' => '2026-11-05 11:59:59', ], [ 'report_id' => 'noon', 'creator_id' => 'user-c', 'creator_name' => '晨曦', 'template_name' => '财务日报', 'create_time' => '2026-11-05 12:00:00', ], ], ]); $this->app->instance(DingTalkWorklogClient::class, $client); $this->artisan('business-mirror:sync-ding-worklogs') ->expectsOutputToContain('写入/更新 3 条') ->assertSuccessful(); $rows = DB::connection('business_mirror') ->table('ding_worklog_reports') ->orderBy('report_id') ->get(['report_id', 'creator_userid', 'statistics_date']) ->map(static fn (object $row): array => (array) $row) ->all(); $this->assertSame([ [ 'report_id' => 'before-noon', 'creator_userid' => 'user-a', 'statistics_date' => '2026-11-03', ], [ 'report_id' => 'last-minute', 'creator_userid' => 'user-b', 'statistics_date' => '2026-11-04', ], [ 'report_id' => 'noon', 'creator_userid' => 'user-c', 'statistics_date' => '2026-11-05', ], ], $rows); } finally { 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('cachedFinanceMembers')->once()->andReturn([ ['userid' => 'user-renshen', 'name' => '人参'], ]); $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(); } } }