[ 'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '', ], 'upstream_mirrors.connection' => 'business_mirror', 'app.timezone' => 'Asia/Shanghai', 'dingtalk.attendance.enabled' => true, ]); DB::purge('business_mirror'); 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_06_090000_create_ding_attendance_records.php'))->up(); } public function test_command_mirrors_leave_and_rest_attendance_records(): void { Carbon::setTestNow(Carbon::parse('2026-11-05 13:00:00', 'Asia/Shanghai')); try { $client = Mockery::mock(DingTalkAttendanceClient::class); $client->shouldReceive('isConfigured')->once()->andReturnTrue(); $client->shouldReceive('listRecords') ->once() ->withArgs(static fn (array $userIds, Carbon $from, Carbon $to): bool => $userIds === ['changyi', 'yaoyao'] && $from->toDateString() === '2026-11-04' && $to->toDateString() === '2026-11-04' ) ->andReturn([ 'pages' => 1, 'records' => [ [ 'id' => 444806944496, 'userId' => 'changyi', 'workDate' => 1793721600000, 'checkType' => 'OnDuty', 'timeResult' => 'NotSigned', 'approveTagName' => '请假', 'approveId' => 9645537212, 'procInstId' => 'proc-changyi', 'sourceType' => 'SYSTEM', 'baseCheckTime' => 1793754000000, 'userCheckTime' => 1793754000000, ], [ 'id' => 446291047276, 'userId' => 'yaoyao', 'workDate' => 1793721600000, 'checkType' => 'OffDuty', 'timeResult' => 'NotSigned', 'approveTagName' => '请假', 'approveId' => 9444644919, 'procInstId' => 'proc-yaoyao', 'sourceType' => 'SYSTEM', 'baseCheckTime' => 1793786400000, 'userCheckTime' => 1793786400000, ], ], ]); $this->app->instance(DingTalkAttendanceClient::class, $client); $roster = Mockery::mock(FinanceRoster::class); $roster->shouldReceive('cachedFinanceMembers')->once()->andReturn([ ['userid' => 'changyi', 'name' => '常仪'], ['userid' => 'yaoyao', 'name' => '杳杳'], ]); $this->app->instance(FinanceRoster::class, $roster); $this->artisan('business-mirror:sync-ding-attendance', [ '--from' => '2026-11-04', '--to' => '2026-11-04', ])->expectsOutputToContain('写入/更新 2 条')->assertSuccessful(); $rows = DB::connection('business_mirror') ->table('ding_attendance_records') ->orderBy('record_id') ->get(['record_id', 'user_id', 'user_name', 'work_date', 'approve_tag_name']) ->map(static fn (object $row): array => (array) $row) ->all(); $this->assertSame([ [ 'record_id' => '444806944496', 'user_id' => 'changyi', 'user_name' => '常仪', 'work_date' => '2026-11-04', 'approve_tag_name' => '请假', ], [ 'record_id' => '446291047276', 'user_id' => 'yaoyao', 'user_name' => '杳杳', 'work_date' => '2026-11-04', 'approve_tag_name' => '请假', ], ], $rows); $this->assertSame( 'succeeded', DB::connection('business_mirror')->table('business_mirror_table_states') ->where('dataset_key', 'dingtalk.attendance.finance_daily_records') ->value('status'), ); } finally { Carbon::setTestNow(); } } }