'sqlite', 'database.connections.sqlite.database' => ':memory:', 'audit.enabled' => false]); DB::purge('sqlite'); Carbon::setTestNow('2026-10-15 10:00:00'); Schema::create('users', function (Blueprint $table) { $table->id(); $table->string('name')->default('测试学员'); $table->string('mobile')->nullable(); $table->integer('is_schoolmate')->nullable(); $table->dateTime('schoolmate_time')->nullable(); $table->timestamps(); $table->softDeletes(); }); Schema::create('courses', function (Blueprint $table) { $table->id(); $table->string('name')->default('测试课程'); $table->date('start_date')->nullable(); $table->integer('auto_schoolmate')->default(0); $table->softDeletes(); }); Schema::create('course_signs', function (Blueprint $table) { $table->id(); $table->integer('course_id'); $table->integer('user_id'); $table->integer('status')->default(1); $table->softDeletes(); }); } protected function tearDown(): void { Carbon::setTestNow(); parent::tearDown(); } private function course(int $id, ?string $date, int $auto = 0): void { DB::table('courses')->insert(['id' => $id, 'start_date' => $date, 'auto_schoolmate' => $auto]); } private function sign(int $userId, int $courseId, int $status = 1, ?string $deleted = null): void { DB::table('course_signs')->insert(['user_id' => $userId, 'course_id' => $courseId, 'status' => $status, 'deleted_at' => $deleted]); } public function test_preview_does_not_write_and_execute_is_idempotent_and_preserves_existing_schoolmates(): void { $this->course(1, '2025-06-01'); $this->course(2, '2024-03-01'); DB::table('users')->insert([ ['id' => 1, 'is_schoolmate' => null, 'schoolmate_time' => null, 'deleted_at' => null], ['id' => 2, 'is_schoolmate' => 1, 'schoolmate_time' => '2020-01-01 00:00:00', 'deleted_at' => null], ['id' => 3, 'is_schoolmate' => 0, 'schoolmate_time' => null, 'deleted_at' => '2025-01-01 00:00:00'], ['id' => 4, 'is_schoolmate' => 0, 'schoolmate_time' => null, 'deleted_at' => null], ['id' => 5, 'is_schoolmate' => 0, 'schoolmate_time' => null, 'deleted_at' => null], ]); foreach ([1, 2, 3] as $id) $this->sign($id, 1); $this->sign(1, 2); $this->sign(1, 2); // 同人重复报名只补一次 $this->sign(4, 1, 0); $this->sign(5, 1, 1, '2025-01-01 00:00:00'); $this->artisan('schoolmate:backfill', ['course_ids' => '1,2'])->expectsOutputToContain('发现遗漏 1 人')->assertExitCode(0); $this->assertNull(DB::table('users')->where('id', 1)->value('is_schoolmate')); User::withoutEvents(function () { $this->artisan('schoolmate:backfill', ['course_ids' => '1,2', '--execute' => true])->expectsOutputToContain('实际补齐 1 人')->assertExitCode(0); }); $this->assertSame('2024-03-01 00:00:00', DB::table('users')->where('id', 1)->value('schoolmate_time')); $this->assertSame('2020-01-01 00:00:00', DB::table('users')->where('id', 2)->value('schoolmate_time')); foreach ([3, 4, 5] as $id) $this->assertEquals(0, DB::table('users')->where('id', $id)->value('is_schoolmate')); $this->artisan('schoolmate:backfill', ['course_ids' => '1,2', '--execute' => true])->expectsOutputToContain('实际补齐 0 人')->assertExitCode(0); } public function test_automatic_enrollment_catches_history_and_late_approval_but_respects_course_switch_and_date(): void { $this->course(1, '2024-01-01', 1); $this->course(2, '2026-10-16', 1); $this->course(3, '2026-10-17', 1); $this->course(4, '2024-01-01', 0); foreach ([1, 2, 3, 4, 5] as $id) { DB::table('users')->insert(['id' => $id, 'is_schoolmate' => 0]); $this->sign($id, min($id, 4), $id === 5 ? 0 : 1); } User::withoutEvents(function () { $this->artisan('auto_schoolmate')->expectsOutputToContain('共补齐 2 位')->assertExitCode(0); // 开课后才通过审核的历史课程报名,下次任务仍能处理。 $this->sign(5, 1); $this->artisan('auto_schoolmate')->expectsOutputToContain('共补齐 1 位')->assertExitCode(0); }); foreach ([1, 2, 5] as $id) $this->assertEquals(1, DB::table('users')->where('id', $id)->value('is_schoolmate')); foreach ([3, 4] as $id) $this->assertEquals(0, DB::table('users')->where('id', $id)->value('is_schoolmate')); } public function test_invalid_missing_deleted_or_undated_courses_abort_without_partial_updates(): void { $this->course(1, '2024-01-01'); $this->course(2, null); $this->course(3, '2024-01-01'); DB::table('courses')->where('id', 3)->update(['deleted_at' => '2025-01-01']); DB::table('users')->insert(['id' => 1, 'is_schoolmate' => 0]); $this->sign(1, 1); foreach (['1,0', '1,999', '1,2', '1,3'] as $ids) { $this->artisan('schoolmate:backfill', ['course_ids' => $ids, '--execute' => true])->assertExitCode(1); } $this->assertEquals(0, DB::table('users')->where('id', 1)->value('is_schoolmate')); } }