'sqlite','database.connections.sqlite.database'=>':memory:','audit.enabled'=>false]); DB::purge('sqlite'); $this->withoutMiddleware(); Schema::create('users',function(Blueprint $t) { $t->id(); foreach(['name','nickname','headimgurl','company_name','mobile','openid'] as $f) $t->string($f)->nullable(); $t->timestamps();$t->softDeletes(); }); Schema::create('supply_demands',function(Blueprint $t) { $t->id();$t->integer('user_id')->default(0);$t->integer('type')->default(2);$t->integer('status')->default(0);$t->integer('public_way')->default(3);$t->integer('view_count')->default(0);$t->integer('contact_count')->default(0);foreach(['title','content','tag','mobile','wechat','email','expire_time','file_ids','contact_name'] as $f)$t->text($f)->nullable();$t->timestamps();$t->softDeletes(); }); foreach(['dialogues','messages'] as $table) Schema::create($table,function(Blueprint $t) use($table) { $t->id();$t->integer('user_id');$t->integer('to_user_id');$t->integer('supply_demand_id')->nullable();if($table==='messages') { $t->integer('dialogue_id');$t->text('content');$t->integer('is_read')->default(0); }else { $t->text('last_content')->nullable();$t->text('last_datetime')->nullable(); }$t->timestamps();$t->softDeletes(); }); Schema::create('configs',function(Blueprint $t) {$t->id();$t->string('key');$t->text('value')->nullable();$t->timestamps();$t->softDeletes();}); Schema::create('course_types',function(Blueprint $t) {$t->id();$t->string('name');$t->softDeletes();}); Schema::create('courses',function(Blueprint $t) {$t->id();$t->string('name');$t->string('class')->nullable();$t->integer('type');$t->softDeletes();}); Schema::create('course_signs',function(Blueprint $t) {$t->id();$t->integer('user_id');$t->integer('course_id');$t->integer('status');$t->timestamps();$t->softDeletes();}); (require database_path('migrations/2026_10_16_160000_add_supply_demand_handover.php'))->up(); foreach([1,2,3] as $id) User::create(['id'=>$id,'name'=>'学员'.$id,'mobile'=>'1380000000'.$id,'company_name'=>'公司'.$id]); $admin=new Admin();$admin->forceFill(['id'=>1,'department_id'=>1]);Auth::guard('admin')->setUser($admin); Auth::guard('mobile')->setUser(User::find(2)); Carbon::setTestNow(Carbon::parse('2026-10-16 15:00:00')); } protected function tearDown(): void { Carbon::setTestNow();Auth::guard('mobile')->forgetUser();Auth::guard('admin')->forgetUser();DB::disconnect('sqlite');parent::tearDown(); } private function demand($owner=0,$status=1) { return SupplyDemand::create(['user_id'=>$owner,'status'=>$status,'is_proxy'=>1,'publisher_name'=>'学员1','publisher_company'=>'公司1','publisher_mobile'=>'13800000001','title'=>'外部需求','content'=>'采购设备','public_way'=>3]); } private function actAs($id) { Auth::guard('mobile')->setUser(User::find($id)); } public function test_managed_messages_transfer_without_copying_and_owner_can_reply() { $d=$this->demand();$sent=Conversations::send(2,['supply_demand_id'=>$d->id,'content'=>'我可以供应']); $this->assertSame(0,Message::first()->to_user_id);$this->assertSame(1,Message::count()); Handover::verified(User::find(1)); $this->assertSame(1,Message::first()->to_user_id);$this->assertSame(0,Message::first()->is_read);$this->assertSame(1,Message::count()); $this->assertSame(1,(int)$d->fresh()->user_id); Conversations::send(1,['dialogue_id'=>$sent['dialogue_id'],'content'=>'您好,可以进一步沟通']); $this->assertSame(2,Message::count());$this->assertSame(2,Message::latest('id')->first()->to_user_id); $this->assertSame(1,DB::table('supply_demand_sms_notices')->count()); } public function test_auto_binding_requires_matching_name_company_and_preserves_pending_messages() { $matching=$this->demand();$name=$this->demand();$company=$this->demand();$incomplete=$this->demand(); $name->publisher_name='其他人';$name->save(); $company->publisher_company='其他公司';$company->save(); $incomplete->publisher_company='';$incomplete->save(); Conversations::send(2,['supply_demand_id'=>$name->id,'content'=>'先保存咨询']); $this->assertSame(1,Handover::verified(User::find(1))); $this->assertSame(1,(int)$matching->fresh()->user_id); foreach ([$name,$company,$incomplete] as $d) $this->assertSame(0,(int)$d->fresh()->user_id); $this->assertStringContainsString('姓名不一致',$name->fresh()->binding_note); $this->assertStringContainsString('公司不一致',$company->fresh()->binding_note); $this->assertStringContainsString('公司信息不完整',$incomplete->fresh()->binding_note); $this->assertSame(0,Message::first()->to_user_id); $this->assertSame(0,DB::table('supply_demand_sms_notices')->count()); $this->postJson('/api/admin/supply-demand/bind-user',['id'=>$name->id,'user_id'=>1,'identity_confirmed'=>true])->assertJsonPath('user_id',1); $this->assertSame(1,Message::first()->to_user_id);$this->assertSame(1,Message::count()); } public function test_profile_completion_retries_verified_account_binding_and_ignores_edge_whitespace() { $d=$this->demand();$user=User::find(1);$user->name=null;$user->company_name=null;$user->save(); $this->assertSame(0,Handover::verified($user)); $this->assertStringContainsString('信息不完整',$d->fresh()->binding_note); $user->name=' 学员1 ';$user->company_name=' 公司1 ';$user->save(); $this->assertSame(1,Handover::autoBind($user)); $this->assertNull($d->fresh()->binding_note); } public function test_unverified_and_duplicate_phones_do_not_auto_bind() { $d=$this->demand();$this->assertSame(0,Handover::autoBind(User::find(1))); User::create(['name'=>'重复手机号','mobile'=>'13800000001']);Handover::verified(User::find(1)); $this->assertSame(0,(int)$d->fresh()->user_id);$this->assertNotEmpty($d->fresh()->binding_note); } public function test_one_consultant_two_demands_have_separate_conversations() { $a=$this->demand();$b=$this->demand(); $one=Conversations::send(2,['supply_demand_id'=>$a->id,'content'=>'咨询A']);$two=Conversations::send(2,['supply_demand_id'=>$b->id,'content'=>'咨询B']); $this->assertNotSame($one['dialogue_id'],$two['dialogue_id']); Handover::verified(User::find(1));$this->assertSame(2,Dialogue::where('to_user_id',1)->count()); } public function test_read_only_current_conversation_and_deny_other_user() { $d=$this->demand(1);$a=Conversations::send(2,['supply_demand_id'=>$d->id,'content'=>'咨询']); $b=Conversations::send(3,['supply_demand_id'=>$d->id,'content'=>'另一位咨询']); $this->actAs(1);$this->getJson('/api/mobile/supply-demand/unread-count')->assertOk()->assertJsonPath('unread_count',2); $this->getJson('/api/mobile/supply-demand/message-list?dialogue_id='.$a['dialogue_id'])->assertOk()->assertJsonPath('message.total',1); $this->assertSame(1,Message::where('to_user_id',1)->where('is_read',0)->count()); $this->actAs(3);$this->getJson('/api/mobile/supply-demand/message-list?dialogue_id='.$a['dialogue_id'])->assertOk()->assertJsonPath('errcode',10002); } public function test_self_send_is_rejected_even_existing_conversation() { $d=$this->demand(1);Dialogue::create(['user_id'=>1,'to_user_id'=>1,'supply_demand_id'=>$d->id]); $this->expectException(\InvalidArgumentException::class);Conversations::send(1,['supply_demand_id'=>$d->id,'to_user_id'=>1,'content'=>'自己']); } public function test_missing_limit_is_unlimited_but_positive_limit_is_exact() { $a=$this->demand();$b=$this->demand();Conversations::send(2,['supply_demand_id'=>$a->id,'content'=>'一']); Config::create(['key'=>'message_limit','value'=>'1']);$this->expectException(\InvalidArgumentException::class);Conversations::send(2,['supply_demand_id'=>$b->id,'content'=>'二']); } public function test_customer_uploaded_template_previews_three_valid_rows() { $path=base_path('../../需求文档/需求导入模板 (1).xlsx'); if (!is_file($path)) { $this->markTestSkipped('客户附件仅用于本地回归'); } $file=new \Illuminate\Http\UploadedFile($path,'需求导入模板.xlsx','application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',null,true); $this->post('/api/admin/supply-demand/import-preview',['file'=>$file])->assertOk()->assertJsonPath('total',3)->assertJsonCount(3,'rows')->assertJsonCount(0,'errors'); } public function test_import_types_contacts_tags_and_long_content() { $row=['type'=>'供应','title'=>'设备供应','content'=>str_repeat('详细内容',100),'tag'=>'新能源、新材料,制造,新能源','wechat'=>'wx-test','email'=>'test@example.com','publisher_name'=>'张同学','publisher_company'=>'公司','publisher_mobile'=>'13900000001','expire_time'=>'2027-01-31']; $this->postJson('/api/admin/supply-demand/import-rows',['rows'=>[$row]])->assertJsonPath('imported',1); $d=SupplyDemand::first();$this->assertSame(1,(int)$d->type);$this->assertSame('新能源,新材料,制造',$d->tag);$this->assertSame($row['content'],$d->content);$this->assertSame('wx-test',$d->wechat);$this->assertSame('test@example.com',$d->email); $row['type']='未知';$this->assertCount(1,ImportRows::validate([$row])['errors']); $export=new \App\Exports\SupplyDemandImportTemplate([],ImportRows::HEADERS); $bytes=\Maatwebsite\Excel\Facades\Excel::raw($export,\Maatwebsite\Excel\Excel::XLSX); $file=tempnam(sys_get_temp_dir(),'supply-template');file_put_contents($file,$bytes); try { $sheet=\PhpOffice\PhpSpreadsheet\IOFactory::load($file)->getActiveSheet();$this->assertSame('"供应,需求,投融资"',$sheet->getCell('A2')->getDataValidation()->getFormula1());$this->assertSame('详细内容',$sheet->getCell('C1')->getValue());$this->assertStringContainsString('、',$sheet->getCell('D2')->getDataValidation()->getPrompt()); } finally { unlink($file); } } public function test_import_validation_duplicate_and_pending_publish() { $row=['title'=>'设备采购','content'=>'一批设备','publisher_name'=>'张同学','publisher_company'=>'公司','publisher_mobile'=>'13900000001']; $v=ImportRows::validate([$row,$row]);$this->assertCount(1,$v['errors']); $this->postJson('/api/admin/supply-demand/import-rows',['rows'=>[$row]])->assertOk()->assertJsonPath('imported',1); $d=SupplyDemand::first();$this->assertSame(0,(int)$d->status);$this->assertSame(0,(int)$d->user_id); $this->getJson('/api/mobile/supply-demand/index')->assertJsonPath('supplyDemands.total',0); $this->postJson('/api/admin/supply-demand/batch-publish',['ids'=>[$d->id]])->assertOk()->assertJsonPath('count',1); $this->getJson('/api/mobile/supply-demand/index')->assertJsonPath('supplyDemands.total',1); $this->postJson('/api/admin/supply-demand/import-rows',['rows'=>[$row]])->assertJsonPath('imported',0); } public function test_contact_details_are_not_leaked_for_proxy_demand() { $d=$this->demand();$d->mobile='13800000001';$d->wechat='private';$d->email='private@example.com';$d->save(); $res=$this->getJson('/api/mobile/supply-demand/detail?id='.$d->id)->assertOk(); $res->assertJsonPath('mobile',null)->assertJsonPath('wechat',null)->assertJsonMissingPath('publisher_mobile')->assertJsonMissingPath('publisher.mobile'); } public function test_deleted_demand_keeps_history_but_has_no_detail_link() { $d=$this->demand();$a=Conversations::send(2,['supply_demand_id'=>$d->id,'content'=>'咨询']); $this->getJson('/api/admin/supply-demand/destroy?id='.$d->id)->assertOk(); $res=$this->getJson('/api/mobile/supply-demand/message-list?dialogue_id='.$a['dialogue_id'])->assertOk(); $res->assertJsonPath('supplyDemand',null)->assertJsonPath('message.data.0.supply_demand',null); $this->getJson('/api/mobile/supply-demand/detail?id='.$d->id)->assertOk()->assertJsonPath('errcode',10002); } public function test_no_enrollment_never_inherits_another_users_class() { DB::table('course_types')->insert(['id'=>1,'name'=>'高研班']); DB::table('courses')->insert(['id'=>1,'type'=>1,'name'=>'第十期高研班','class'=>'第十期']); DB::table('course_signs')->insert(['id'=>1,'user_id'=>1,'course_id'=>1,'status'=>1,'created_at'=>now()]); $user=User::find(2);$profile=Profiles::user($user); $this->assertSame('',$profile['class_name']);$this->assertSame('',$profile['course_name']); $user->setRelation('courseSigns',User::find(1)->courseSigns); $this->assertSame('',Profiles::user($user)['class_name']); $d=$this->demand(2);$this->getJson('/api/mobile/supply-demand/index')->assertJsonPath('supplyDemands.data.0.publisher.class_name',''); } public function test_unbound_publisher_hides_matched_courses_and_keeps_imported_company_until_binding() { DB::table('course_types')->insert(['id'=>1,'name'=>'高研班']); DB::table('courses')->insert(['id'=>1,'type'=>1,'name'=>'高研课程','class'=>'高研一班']); DB::table('course_signs')->insert(['id'=>1,'user_id'=>1,'course_id'=>1,'status'=>1,'created_at'=>now()]); $user=User::find(1);$user->company_name='账号公司';$user->save(); $d=$this->demand();$d->publisher_mobile=$user->getRawOriginal('mobile');$d->publisher_company='导入公司';$d->save(); foreach ([false,true] as $private) { $profile=Profiles::publisher($d,$private); $this->assertSame('',$profile['course_name']);$this->assertSame('',$profile['class_name']);$this->assertSame('导入公司',$profile['company_name']); } $this->postJson('/api/admin/supply-demand/bind-user',['id'=>$d->id,'user_id'=>1,'identity_confirmed'=>true])->assertJsonPath('publisher.class_name','高研一班')->assertJsonPath('publisher.company_name','账号公司'); $this->getJson('/api/mobile/supply-demand/detail?id='.$d->id)->assertJsonPath('publisher.class_name','高研一班')->assertJsonPath('publisher.company_name','账号公司'); $this->assertSame('导入公司',$d->fresh()->publisher_company); $this->assertSame('账号公司',$user->fresh()->company_name); $user->company_name=null;$user->save(); $this->assertSame('导入公司',Profiles::publisher($d->fresh())['company_name']); } public function test_class_priority_approved_only_and_latest_within_system() { foreach([[1,'初创班'],[2,'高研班'],[3,'其他课程']] as [$id,$name]) DB::table('course_types')->insert(['id'=>$id,'name'=>$name]); foreach([[1,1,'初创一班'],[2,2,'高研旧班'],[3,2,'高研新班'],[4,2,'待审核班'],[5,3,'其他新班']] as [$id,$type,$name]) DB::table('courses')->insert(['id'=>$id,'type'=>$type,'name'=>$name,'class'=>$name]); foreach([[1,1,1,'2026-01-01'],[2,2,1,'2026-01-02'],[3,3,1,'2026-02-01'],[4,4,0,'2026-10-16'],[5,5,1,'2026-10-15']] as [$id,$course,$status,$date]) DB::table('course_signs')->insert(['id'=>$id,'user_id'=>1,'course_id'=>$course,'status'=>$status,'created_at'=>$date]); $this->assertSame('高研新班',Profiles::user(User::find(1))['class_name']); DB::table('course_signs')->whereIn('id',[1,2,3])->update(['status'=>0]); $this->assertSame('其他新班',Profiles::user(User::find(1))['class_name']); DB::table('course_signs')->update(['status'=>0]);$this->assertSame('',Profiles::user(User::find(1))['class_name']); } public function test_sms_body_can_be_configured_without_sending_on_save() { $settings=\App\Services\SupplyDemand\Settings::all(); $this->assertStringContainsString('我的私信',$settings['sms_content']); $settings['sms_content']='您有新的供需对接,请登录查看。'; $this->postJson('/api/admin/supply-demand/settings',$settings)->assertJsonPath('sms_content',$settings['sms_content']); $this->assertSame(0,DB::table('supply_demand_sms_notices')->count()); Config::create(['key'=>'sms_sign','value'=>'【测试签名】']); $d=$this->demand(1);Conversations::send(2,['supply_demand_id'=>$d->id,'content'=>'咨询']); $calls=0; $this->assertTrue(Notices::deliver(1,function($mobile,$text)use(&$calls){$calls++;$this->assertSame('【测试签名】您有新的供需对接,请登录查看。',$text);return true;})); $this->assertSame(1,$calls); $settings['sms_content']=' '; $this->postJson('/api/admin/supply-demand/settings',$settings)->assertJsonPath('errcode',10002); $this->assertSame('您有新的供需对接,请登录查看。',\App\Services\SupplyDemand\Settings::all()['sms_content']); } public function test_sms_uses_existing_channel_contract_cooldown_and_failure_is_retryable() { $d=$this->demand(1);Conversations::send(2,['supply_demand_id'=>$d->id,'content'=>'咨询']); $calls=0;$sender=function($mobile,$text) use(&$calls) {$calls++;$this->assertSame('13800000001',$mobile);$this->assertStringContainsString('校友权益—供需对接—我的私信',$text);return true;}; $this->assertTrue(Notices::deliver(1,$sender));Notices::queue(1);$this->assertFalse(Notices::deliver(1,$sender));$this->assertSame(1,$calls); Carbon::setTestNow(now()->addMinutes(31));$this->assertFalse(Notices::deliver(1,function(){return false;}));$this->assertSame(1,DB::table('supply_demand_sms_notices')->value('attempts'));$this->assertSame(1,Message::count()); } public function test_today_old_demand_new_messages_and_two_way_stats() { $d=$this->demand(1);$d->created_at='2026-01-01 00:00:00';$d->save(); $a=Conversations::send(2,['supply_demand_id'=>$d->id,'content'=>'今天咨询']);Conversations::send(1,['dialogue_id'=>$a['dialogue_id'],'content'=>'今天回复']); $this->getJson('/api/admin/supply-demand/chart?start_date=2026-10-16&end_date=2026-10-16')->assertOk()->assertJsonPath('message_count',2)->assertJsonPath('interaction_count',1)->assertJsonPath('list.total',1)->assertJsonPath('supply_demand_count',0); } public function test_manual_binding_links_same_imported_identity_demands_and_moves_all_their_messages() { $a=$this->demand();$b=$this->demand();$otherName=$this->demand();$otherCompany=$this->demand(); $otherName->publisher_name='其他人';$otherName->save();$otherCompany->publisher_company='其他公司';$otherCompany->save(); Conversations::send(2,['supply_demand_id'=>$a->id,'content'=>'咨询A']); Conversations::send(3,['supply_demand_id'=>$b->id,'content'=>'咨询B']); Conversations::send(2,['supply_demand_id'=>$otherName->id,'content'=>'其他人咨询']); $this->postJson('/api/admin/supply-demand/bind-user',['id'=>$a->id,'user_id'=>1,'identity_confirmed'=>true])->assertJsonPath('bound_count',2); $this->assertSame(1,(int)$a->fresh()->user_id);$this->assertSame(1,(int)$b->fresh()->user_id); $this->assertSame(0,(int)$otherName->fresh()->user_id);$this->assertSame(0,(int)$otherCompany->fresh()->user_id); $this->assertSame(2,Message::where('to_user_id',1)->count());$this->assertSame(1,Message::where('to_user_id',0)->count()); $this->assertSame(3,Message::count());$this->assertSame(2,Dialogue::where('to_user_id',1)->count()); } public function test_batch_unpublish_keeps_bindings_and_messages_and_excludes_public_listing() { $published=$this->demand(1);$pending=$this->demand(0,0);$rejected=$this->demand(0,2); Conversations::send(2,['supply_demand_id'=>$published->id,'content'=>'历史咨询']); $this->postJson('/api/admin/supply-demand/batch-unpublish',['ids'=>[$published->id,$pending->id,$rejected->id]])->assertJsonPath('count',2)->assertJsonPath('skipped_count',1); $this->assertSame(4,(int)$published->fresh()->status);$this->assertSame(4,(int)$pending->fresh()->status);$this->assertSame(2,(int)$rejected->fresh()->status); $this->assertSame(1,(int)$published->fresh()->user_id);$this->assertSame(1,Message::count()); $this->getJson('/api/mobile/supply-demand/index')->assertJsonPath('supplyDemands.total',0); $other=$this->demand(); $this->postJson('/api/admin/supply-demand/batch-unpublish',['ids'=>[$other->id,99999]])->assertJsonPath('errcode',10002); $this->assertSame(1,(int)$other->fresh()->status); $this->postJson('/api/admin/supply-demand/batch-unpublish',['ids'=>[]])->assertJsonPath('errcode',10002); } public function test_manual_review_filter_only_includes_unbound_proxy_rows_needing_review() { $review=$this->demand();$review->binding_note='姓名不一致,请人工核实';$review->save(); $plain=$this->demand();$bound=$this->demand(1);$bound->binding_note='管理员人工核实绑定:1';$bound->save(); $this->getJson('/api/admin/supply-demand/index?binding=manual_review')->assertJsonPath('total',1)->assertJsonPath('data.0.id',$review->id); $this->getJson('/api/admin/supply-demand/index?binding=unbound')->assertJsonPath('total',2); $this->getJson('/api/admin/supply-demand/index?binding=bound')->assertJsonPath('total',1); $this->getJson('/api/admin/supply-demand/index?binding=manual_review&filter[0][key]=title&filter[0][op]=like&filter[0][value]=不存在')->assertJsonPath('total',0); } public function test_manual_search_and_confirmed_binding_include_legacy_unverified_students() { $d=$this->demand();$user=User::find(1); $this->getJson('/api/admin/supply-demand/binding-users?keyword='.$user->getRawOriginal('mobile'))->assertJsonPath('0.id',1)->assertJsonPath('0.mobile_verified',false); $this->assertSame(0,Handover::autoBind($user)); $this->postJson('/api/admin/supply-demand/bind-user',['id'=>$d->id,'user_id'=>1])->assertJsonPath('errcode',10002); $this->postJson('/api/admin/supply-demand/bind-user',['id'=>$d->id,'user_id'=>1,'identity_confirmed'=>true])->assertJsonPath('user_id',1); $this->assertNull($user->fresh()->supply_mobile_verified_at); $this->assertStringContainsString('管理员人工核实绑定',$d->fresh()->binding_note); } public function test_settings_and_manual_binding_validation() { $this->postJson('/api/admin/supply-demand/settings',['detail_tip'=>'详情提示','chat_tip'=>'顶部提示','sent_tip'=>'成功提示','sms_enabled'=>'0','sms_interval_minutes'=>15,'sms_daily_limit'=>2])->assertOk()->assertJsonPath('chat_tip','顶部提示'); $d=$this->demand();$this->postJson('/api/admin/supply-demand/bind-user',['id'=>$d->id,'user_id'=>1])->assertJsonPath('errcode',10002); Handover::verified(User::find(3));$this->postJson('/api/admin/supply-demand/bind-user',['id'=>$d->id,'user_id'=>3,'identity_confirmed'=>true])->assertOk()->assertJsonPath('user_id',3); } public function test_provider_calls_stop_at_daily_cap_and_retry_next_day() { $d=$this->demand(1);Conversations::send(2,['supply_demand_id'=>$d->id,'content'=>'咨询']); Config::create(['key'=>'supply_demand_sms_daily_limit','value'=>'1']); $calls=0;$sender=function() use(&$calls) {$calls++;return true;}; $this->assertTrue(Notices::deliver(1,$sender));Notices::queue(1); Carbon::setTestNow(now()->addHours(2));$this->assertFalse(Notices::deliver(1,$sender)); Carbon::setTestNow(now()->addDay());$this->assertTrue(Notices::deliver(1,$sender));$this->assertSame(2,$calls); } public function test_export_includes_both_profiles_and_selected_course() { $d=$this->demand(1);Conversations::send(2,['supply_demand_id'=>$d->id,'content'=>'今天咨询']); \Maatwebsite\Excel\Facades\Excel::fake(); $this->getJson('/api/admin/supply-demand/export-interactions?start_date=2026-10-16&end_date=2026-10-16')->assertOk(); \Maatwebsite\Excel\Facades\Excel::assertDownloaded('供需交互统计.xlsx',function($export) { $row=$export->array()[0];$headers=$export->headings(); $this->assertSame('公司1',$row[array_search('发布人公司',$headers)]); $this->assertSame('公司2',$row[array_search('对接人公司',$headers)]); $this->assertSame('13800000002',$row[array_search('对接人联系方式',$headers)]); $this->assertContains('对接人报名课程',$headers);$this->assertContains('发布人所在班级',$headers);return true; }); } public function test_import_with_invalid_row_is_atomic_and_expired_publish_rejected() { $row=['title'=>'设备采购','content'=>'设备','publisher_name'=>'张同学','publisher_company'=>'公司','publisher_mobile'=>'13900000001']; $bad=$row;$bad['publisher_mobile']='错误'; $this->postJson('/api/admin/supply-demand/import-rows',['rows'=>[$row,$bad]])->assertJsonPath('imported',0);$this->assertSame(0,SupplyDemand::count()); $d=$this->demand(0,0);$d->expire_time='2026-10-15 23:59:59';$d->save(); $this->postJson('/api/admin/supply-demand/batch-publish',['ids'=>[$d->id]])->assertJsonPath('errcode',10002);$this->assertSame(0,(int)$d->fresh()->status); } public function test_spoofed_receiver_is_ignored_and_direct_access_is_denied() { $d=$this->demand(1);$sent=Conversations::send(2,['supply_demand_id'=>$d->id,'to_user_id'=>3,'content'=>'咨询']); $this->assertSame(1,Message::first()->to_user_id); $this->expectException(\Illuminate\Database\Eloquent\ModelNotFoundException::class); Conversations::send(3,['dialogue_id'=>$sent['dialogue_id'],'content'=>'越权发送']); } public function test_repeated_binding_is_rejected_without_duplicating_messages() { $d=$this->demand();Conversations::send(2,['supply_demand_id'=>$d->id,'content'=>'咨询']);Handover::verified(User::find(1)); $this->assertSame(0,Handover::autoBind(User::find(1)));$this->assertSame(1,Message::count()); $this->postJson('/api/admin/supply-demand/bind-user',['id'=>$d->id,'user_id'=>1])->assertJsonPath('errcode',10002); } public function test_normalization_can_preview_and_apply_idempotently() { $d=$this->demand(1);$c=Dialogue::create(['user_id'=>2,'to_user_id'=>1,'supply_demand_id'=>$d->id]); Message::create(['dialogue_id'=>$c->id,'user_id'=>2,'to_user_id'=>1,'supply_demand_id'=>$d->id,'content'=>'历史咨询','is_read'=>0]); $this->artisan('supply-demand:normalize-conversations')->assertExitCode(0);$this->assertSame(1,Dialogue::count()); $this->artisan('supply-demand:normalize-conversations',['--apply'=>true])->assertExitCode(0);$id=Message::first()->dialogue_id; $this->artisan('supply-demand:normalize-conversations',['--apply'=>true])->assertExitCode(0);$this->assertSame($id,Message::first()->dialogue_id);$this->assertSame(1,Message::count()); } public function test_real_excel_preview_preserves_mobile_and_imports_valid_date() { $sheet=new \PhpOffice\PhpSpreadsheet\Spreadsheet(); $sheet->getActiveSheet()->fromArray([ImportRows::HEADERS,['需求','采购设备','一批设备','制造','张同学','某公司','13900000001','2027-01-01','','']],null,'A1'); $path=tempnam(sys_get_temp_dir(),'demand'); try { (new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($sheet))->save($path); $file=new \Illuminate\Http\UploadedFile($path,'需求导入.xlsx',null,null,true); $res=$this->post('/api/admin/supply-demand/import-preview',['file'=>$file])->assertOk()->assertJsonCount(0,'errors')->assertJsonPath('rows.0.publisher_mobile','13900000001')->assertJsonPath('rows.0.expire_time','2027-01-01 23:59:59'); $rows=$res->json('rows');$rows[0]['expire_time']=substr($rows[0]['expire_time'],0,10); $this->postJson('/api/admin/supply-demand/import-rows',['rows'=>$rows])->assertJsonPath('imported',1); } finally { unlink($path); } } public function test_excel_export_stores_formula_like_input_as_literal_text() { $export=new \App\Exports\SupplyDemandExport([],[]);$sheet=new \PhpOffice\PhpSpreadsheet\Spreadsheet();$cell=$sheet->getActiveSheet()->getCell('A1'); $export->bindValue($cell,'=HYPERLINK("example.com")');$this->assertSame('s',$cell->getDataType()); } public function test_owner_cannot_publish_unreviewed_import_through_mobile_edit() { $d=$this->demand(1,0);$this->actAs(1); $this->postJson('/api/mobile/supply-demand/save',['id'=>$d->id,'status'=>1,'title'=>'更新标题'])->assertOk(); $this->assertSame(0,(int)$d->fresh()->status); $this->actAs(2);$this->getJson('/api/mobile/supply-demand/detail?id='.$d->id)->assertJsonPath('errcode',10002); } public function test_verified_login_account_merge_retains_and_unifies_consultations() { $d=$this->demand(1);$first=Conversations::send(2,['supply_demand_id'=>$d->id,'content'=>'临时账号咨询']); Conversations::send(3,['supply_demand_id'=>$d->id,'content'=>'已有账号咨询']); Handover::mergeAccount(2,3); $this->assertSame(0,Message::where('user_id',2)->count());$this->assertSame(2,Message::where('user_id',3)->count()); $this->assertSame(1,Dialogue::whereHas('messages')->count());$this->assertSame(2,Message::where('is_read',0)->count()); } private function enableGuestRequests() { Schema::create('personal_access_tokens',function(Blueprint $t) { $t->id();$t->morphs('tokenable');$t->string('name');$t->string('token',64)->unique();$t->text('abilities')->nullable();$t->timestamp('last_used_at')->nullable();$t->timestamp('expires_at')->nullable();$t->timestamps(); }); $this->withMiddleware();config(['app.debug'=>false]);Auth::guard('mobile')->forgetUser(); } public function test_keyword_search_matches_tags_without_bypassing_public_or_owner_filters() { $published=$this->demand(1);$published->tag='新能源,人工智能';$published->save(); $draft=$this->demand(2,0);$draft->tag='人工智能';$draft->save(); $this->getJson('/api/mobile/supply-demand/index?keyword='.urlencode('智能'))->assertJsonPath('supplyDemands.total',1)->assertJsonPath('supplyDemands.data.0.id',$published->id); $this->getJson('/api/mobile/supply-demand/index?myself=1&keyword='.urlencode('智能'))->assertJsonPath('supplyDemands.total',1)->assertJsonPath('supplyDemands.data.0.id',$draft->id); $this->getJson('/api/mobile/supply-demand/index?type=1&keyword='.urlencode('智能'))->assertJsonPath('supplyDemands.total',0); $this->getJson('/api/mobile/supply-demand/index?keyword='.urlencode('外部需求'))->assertJsonPath('supplyDemands.total',1); $this->getJson('/api/mobile/supply-demand/index?keyword='.urlencode('采购设备'))->assertJsonPath('supplyDemands.total',1); $this->enableGuestRequests(); $this->getJson('/api/mobile/supply-demand/index?keyword='.urlencode('新能源'))->assertJsonPath('supplyDemands.total',1)->assertJsonPath('supplyDemands.data.0.id',$published->id); } public function test_library_read_endpoints_allow_guests_but_booking_submission_remains_protected() { Schema::create('uploads',function(Blueprint $t) { $t->id();$t->softDeletes(); }); Schema::create('books',function(Blueprint $t) { $t->id();$t->string('title');$t->string('author')->nullable();$t->string('isbn')->nullable();$t->string('category');$t->integer('cover_id')->nullable();$t->timestamps();$t->softDeletes(); }); DB::table('books')->insert(['id'=>1,'title'=>'游客查询图书','category'=>'科技']); $this->enableGuestRequests(); $this->getJson('/api/mobile/book/other')->assertOk()->assertJsonPath('category.0','科技'); $this->getJson('/api/mobile/book/index')->assertOk()->assertJsonPath('data.0.title','游客查询图书'); $this->getJson('/api/mobile/book/detail?id=1')->assertOk()->assertJsonPath('title','游客查询图书'); $this->postJson('/api/mobile/schedule/save',[])->assertJsonPath('errcode',40001); } public function test_shared_supply_pages_allow_guests_and_expired_tokens_but_not_drafts() { $published=$this->demand();$draft=$this->demand(0,0);$this->enableGuestRequests(); $this->getJson('/api/mobile/supply-demand/index')->assertOk()->assertJsonPath('supplyDemands.total',1); $this->getJson('/api/mobile/supply-demand/detail?id='.$published->id)->assertOk()->assertJsonPath('title','外部需求'); $this->getJson('/api/mobile/supply-demand/detail?id='.$draft->id)->assertOk()->assertJsonPath('errcode',10002); $this->withToken('expired-token')->getJson('/api/mobile/supply-demand/index')->assertOk()->assertJsonPath('supplyDemands.total',1); $this->withToken('expired-token')->getJson('/api/mobile/supply-demand/session')->assertJsonPath('registered',false)->assertJsonPath('user',null); $this->getJson('/api/mobile/supply-demand/index?myself=1')->assertJsonPath('errcode',10003); } public function test_guest_or_wechat_only_account_cannot_use_member_actions() { $d=$this->demand();$this->enableGuestRequests(); $this->postJson('/api/mobile/supply-demand/send-message',['supply_demand_id'=>$d->id,'content'=>'游客发送'])->assertJsonPath('errcode',40001); $bare=User::create(['name'=>null,'mobile'=>null]);$token=$bare->createToken('mobile-token')->plainTextToken; $this->withToken($token)->getJson('/api/mobile/supply-demand/session')->assertJsonPath('registered',false); $this->withToken($token)->postJson('/api/mobile/supply-demand/send-message',['supply_demand_id'=>$d->id,'content'=>'未注册发送'])->assertJsonPath('errcode',10003); $this->withToken($token)->postJson('/api/mobile/supply-demand/save',['title'=>'未注册发布'])->assertJsonPath('errcode',10003); $this->assertSame(0,Message::count()); } public function test_registered_user_without_any_course_can_be_searched_and_publish() { $this->assertSame(0,DB::table('course_signs')->where('user_id',2)->count()); $this->getJson('/api/admin/supply-demand/binding-users?keyword=13800000002')->assertJsonPath('0.id',2)->assertJsonPath('0.course_name',''); $this->enableGuestRequests();$token=User::find(2)->createToken('mobile-token')->plainTextToken; $this->withToken($token)->postJson('/api/mobile/supply-demand/save',['title'=>'未报名用户发布','type'=>2,'content'=>'需求详情','status'=>0])->assertOk()->assertJsonPath('user_id',2); $this->assertSame(1,SupplyDemand::where('user_id',2)->where('title','未报名用户发布')->count()); } public function test_registered_member_keeps_private_access_after_optional_browse() { $d=$this->demand(1,0);$this->enableGuestRequests();$token=User::find(1)->createToken('mobile-token')->plainTextToken; $this->withToken($token)->getJson('/api/mobile/supply-demand/session')->assertJsonPath('registered',true)->assertJsonPath('user.id',1); $this->withToken($token)->getJson('/api/mobile/supply-demand/detail?id='.$d->id)->assertJsonPath('id',$d->id); $this->withToken($token)->getJson('/api/mobile/supply-demand/index?myself=1')->assertJsonPath('supplyDemands.total',1); $this->withToken('invalid')->getJson('/api/mobile/supply-demand/detail?id='.$d->id)->assertJsonPath('errcode',10002); } public function test_existing_mobile_account_with_older_token_does_not_need_login_again() { $this->enableGuestRequests();config(['sanctum.expiration'=>1440]); $user=User::find(2);$user->name=null;$user->save(); $token=$user->createToken('mobile-token')->plainTextToken; DB::table('personal_access_tokens')->update(['created_at'=>now()->subDays(7)]); $this->withToken($token)->getJson('/api/mobile/supply-demand/session')->assertJsonPath('registered',true)->assertJsonPath('user.id',2); $this->withToken($token)->getJson('/api/mobile/supply-demand/unread-count')->assertJsonMissing(['errcode'=>40001])->assertJsonMissing(['errcode'=>10003]); } public function test_expired_real_token_can_browse_but_cannot_send() { $d=$this->demand();$this->enableGuestRequests();$token=User::find(2)->createToken('mobile-token')->plainTextToken; DB::table('personal_access_tokens')->update(['expires_at'=>now()->subMinute()]); $this->withToken($token)->getJson('/api/mobile/supply-demand/index')->assertJsonPath('supplyDemands.total',1); $this->withToken($token)->postJson('/api/mobile/supply-demand/send-message',['supply_demand_id'=>$d->id,'content'=>'已过期登录'])->assertJsonPath('errcode',40001); $this->assertSame(0,Message::count()); } }