You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
wx.sstbc.com/tests/Feature/SupplyDemandHandoverTest.php

414 lines
37 KiB

3 days ago
<?php
namespace Tests\Feature;
use Tests\TestCase;
use App\Models\User;
use App\Models\Admin;
use App\Models\SupplyDemand;
use App\Models\Message;
use App\Models\Dialogue;
use App\Models\Config;
use App\Services\SupplyDemand\Conversations;
use App\Services\SupplyDemand\Handover;
use App\Services\SupplyDemand\Profiles;
use App\Services\SupplyDemand\ImportRows;
use App\Services\SupplyDemand\Notices;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Carbon;
class SupplyDemandHandoverTest extends TestCase
{
protected function setUp(): void {
parent::setUp();
config(['database.default'=>'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(); }
2 days ago
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]); }
3 days ago
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());
}
2 days ago
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);
}
3 days ago
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'=>'二']);
}
2 days ago
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); }
}
3 days ago
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);
}
2 days ago
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']);
}
3 days ago
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']);
}
2 days ago
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']);
}
3 days ago
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);
}
2 days ago
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);
}
3 days ago
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);
2 days ago
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);
3 days ago
}
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();
2 days ago
$sheet->getActiveSheet()->fromArray([ImportRows::HEADERS,['需求','采购设备','一批设备','制造','张同学','某公司','13900000001','2027-01-01','','']],null,'A1');
3 days ago
$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();
}
2 days ago
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);
}
3 days ago
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());
}
2 days ago
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());
}
3 days ago
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);
}
3 days ago
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]);
}
3 days ago
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());
}
}