| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384 |
- <?php
- require dirname(__DIR__).'/vendor/autoload.php';
- \Dotenv\Dotenv::createUnsafeMutable(dirname(__DIR__))->load();
- \support\App::loadAllConfig(['route']);
- date_default_timezone_set('Asia/Shanghai');
- use plugin\saiadmin\app\service\routing\RouteOptimizer;
- use plugin\saiadmin\app\service\{WaterRoutePlanning,WaterDispatch,WaterBusiness};
- use plugin\saiadmin\app\model\water\{WaterDelivery,WaterRoute,WaterAccount,WaterSku,WaterRoutePreview};
- use plugin\saiadmin\exception\ApiException;
- use support\Db;
- $checks=0;
- function check(bool $ok,string $message): void { global $checks; $checks++; if (!$ok) throw new RuntimeException($message); }
- function rejects(callable $fn,string $message): void { try { $fn(); } catch (ApiException $e) { check(true,$message); return; } throw new RuntimeException($message); }
- $date=date('Y-m-d'); $departure=$date.' 08:00:00';
- $tasks=[['id'=>1],['id'=>2],['id'=>3,'deadline_at'=>$date.' 08:00:02']];
- $matrix=array_fill(0,4,array_fill(0,4,['distance'=>9,'time'=>9]));
- foreach ([[0,1,1,2],[1,2,1,2],[2,3,1,2],[0,2,3,1],[2,1,3,1],[1,3,3,1],[0,3,4,1],[3,2,4,2],[3,1,4,3]] as [$a,$b,$distance,$time]) $matrix[$a][$b]=compact('distance','time');
- $engine=new RouteOptimizer();
- $short=$engine->optimize($tasks,$matrix,'shortest',$departure,0);
- $fast=$engine->optimize($tasks,$matrix,'fastest',$departure,0);
- $deadline=$engine->optimize($tasks,$matrix,'deadline',$departure,0);
- check($short['optimized']['task_ids']===[1,2,3],'shortest distance objective');
- check($fast['optimized']['task_ids']===[2,1,3],'fastest completion objective');
- check($deadline['optimized']['task_ids']===[3,2,1],'deadline objective differs from distance and time');
- check($deadline['optimized']['late_count']===0 && $fast['optimized']['late_count']===1,'deadline lateness uses actual path duration');
- check($engine->optimize($tasks,$matrix,'deadline',$departure,1)['optimized']['late_count']===1,'service time included in deadline calculation');
- $blocked=$matrix; foreach ($blocked as &$row) $row[3]=null; unset($row);
- rejects(fn()=>$engine->optimize($tasks,$blocked,'shortest',$departure,0),'unreachable station fails complete route');
- $bad=$matrix; $bad[0][1]['time']=-1;
- rejects(fn()=>$engine->optimize($tasks,$bad,'fastest',$departure,0),'negative costs rejected');
- rejects(fn()=>$engine->optimize($tasks,$matrix,'unknown',$departure,0),'strategy allowlist');
- rejects(fn()=>$engine->optimize($tasks,[],'shortest',$departure,0),'matrix dimensions');
- rejects(fn()=>$engine->optimize(array_fill(0,9,['id'=>1]),[],'shortest',$departure,0),'bounded exact optimization');
- $zero=$engine->optimize([['id'=>1]],[[null,['distance'=>0,'time'=>0]],[null,null]],'shortest',$departure,0);
- check($zero['optimized']['distance']==0 && $zero['optimized']['task_ids']===[1],'same-coordinate station retained');
- $before=WaterDelivery::query()->count(); $previews=WaterRoutePreview::query()->count();
- Db::beginTransaction();
- try {
- $tag='ROUTING-'.bin2hex(random_bytes(5)); $business=new WaterBusiness(); $dispatch=new WaterDispatch(); $planner=new WaterRoutePlanning();
- $sku=$business->save('sku',['name'=>$tag,'liters'=>18.9],1)['id'];
- $business->stock(['sku_id'=>$sku,'mode'=>'inbound','quantity'=>30,'reference'=>$tag,'note'=>'rollback fixture'],1);
- $ids=[]; $points=[[113.92,22.52]];
- foreach ([1,2,3] as $i) {
- $point=[113.93+$i/1000,22.53]; $points[]=$point;
- $enterprise=$business->save('enterprise',['name'=>$tag.$i,'contact'=>'测试','phone'=>'TEST','region'=>$tag,'address'=>'测试','longitude'=>$point[0],'latitude'=>$point[1]],1)['id'];
- WaterAccount::query()->insert(['enterprise_id'=>$enterprise,'sku_id'=>$sku,'entitlement'=>10,'updated_at'=>date('Y-m-d H:i:s')]);
- $id=$business->createDelivery(['enterprise_id'=>$enterprise,'sku_id'=>$sku,'quantity'=>2],1)['id'];
- $dispatch->schedule(['id'=>$id,'date'=>$date,'longitude'=>$point[0],'latitude'=>$point[1],'origin_longitude'=>113.92,'origin_latitude'=>22.52,'origin_address'=>'共同出发点'],1);
- $ids[]=$id;
- }
- $payload=['task_ids'=>$ids,'points'=>$points,'matrix'=>$matrix,'strategy'=>'fastest','departure_at'=>$departure,'service_minutes'=>0];
- $plan=$planner->preview($payload,1);
- check($plan['optimized']['task_ids']===[$ids[1],$ids[0],$ids[2]],'model records mapped to optimizer');
- $rows=WaterDelivery::query()->whereIn('id',$ids)->get()->toArray();
- rejects(fn()=>$planner->validated($plan['token'],$rows,2),'preview bound to actor');
- $badPoints=$payload; $badPoints['points'][1][0]+=0.01;
- rejects(fn()=>$planner->preview($badPoints,1),'stale coordinates rejected');
- WaterRoutePreview::query()->where('token',$plan['token'])->update(['expires_at'=>date('Y-m-d H:i:s',time()-1)]);
- rejects(fn()=>$planner->validated($plan['token'],$rows,1),'expired preview rejected');
- $plan=$planner->preview($payload,1);
- WaterDelivery::query()->where('id',$ids[0])->update(['deadline_at'=>$date.' 09:00:00']);
- rejects(fn()=>$planner->validated($plan['token'],WaterDelivery::query()->whereIn('id',$ids)->get()->toArray(),1),'deadline change invalidates preview');
- WaterDelivery::query()->where('id',$ids[0])->update(['deadline_at'=>null]);
- $driver=$dispatch->saveDriver(['name'=>$tag,'phone'=>$tag,'region'=>$tag,'capacity'=>20],1)['id'];
- $dispatch->shift(['driver_id'=>$driver,'date'=>$date,'state'=>'on'],1);
- $request=['date'=>$date,'request_key'=>$tag,'routes'=>[['driver_id'=>$driver,'task_ids'=>$plan['optimized']['task_ids'],'optimization_token'=>$plan['token']]]];
- $routeId=$dispatch->commit($request,1)['route_ids'][0];
- check($dispatch->commit($request,1)['replayed']===true,'optimized commit replay');
- check(WaterDelivery::query()->where('route_id',$routeId)->orderBy('stop_order')->pluck('id')->all()===$plan['optimized']['task_ids'],'optimized order persisted');
- $payload['strategy']='shortest'; $reorder=$planner->preview($payload,1);
- $stock=WaterSku::query()->find($sku)->toArray();
- $accounts=WaterAccount::query()->where('sku_id',$sku)->get()->toArray();
- $planner->apply(['route_id'=>$routeId,'token'=>$reorder['token']],1);
- check(WaterDelivery::query()->where('route_id',$routeId)->orderBy('stop_order')->pluck('id')->all()===$ids,'existing route reordered');
- check($planner->apply(['route_id'=>$routeId,'token'=>$reorder['token']],1)['replayed']===true,'reorder replay safe');
- check(WaterSku::query()->find($sku)->toArray()===$stock && WaterAccount::query()->where('sku_id',$sku)->get()->toArray()===$accounts,'reorder preserves stock and entitlement');
- check(json_decode(WaterRoute::query()->find($routeId)->optimization,true)['strategy']==='shortest','strategy metadata persisted');
- $next=$planner->preview($payload,1);
- $business->transition(['id'=>$ids[0],'action'=>'depart'],1);
- rejects(fn()=>$planner->apply(['route_id'=>$routeId,'token'=>$next['token']],1),'in-transit route cannot reorder');
- } finally { Db::rollBack(); }
- check(WaterDelivery::query()->count()===$before && WaterRoutePreview::query()->count()===$previews,'all fixtures and previews rolled back');
- echo "PASS: $checks routing strategy and model integration checks; fixtures rolled back.\n";
|