seed_water_district_dispatch_demo.php 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. <?php
  2. /** 罗湖/福田/宝安今日配送演示;默认只读,--apply 原子写入,固定批次防重。 */
  3. declare(strict_types=1);
  4. require dirname(__DIR__).'/vendor/autoload.php';
  5. \Dotenv\Dotenv::createUnsafeMutable(dirname(__DIR__))->load();
  6. \support\App::loadAllConfig(['route']);
  7. date_default_timezone_set('Asia/Shanghai');
  8. use plugin\saiadmin\app\model\water\WaterModel;
  9. use plugin\saiadmin\app\model\water\WaterSku;
  10. use plugin\saiadmin\app\model\water\WaterAccount;
  11. use plugin\saiadmin\app\model\water\WaterEnterprise;
  12. use plugin\saiadmin\app\model\water\WaterDelivery;
  13. use plugin\saiadmin\app\model\water\WaterOrder;
  14. use plugin\saiadmin\app\model\water\WaterLedger;
  15. use plugin\saiadmin\app\service\WaterBusiness;
  16. use plugin\saiadmin\app\service\WaterDispatch;
  17. $batch = 'TEST-WATER-DISTRICTS-20260911';
  18. $date = '2026-09-11';
  19. $apply = in_array('--apply',$argv,true);
  20. $origin = ['origin_address'=>'深圳市南山区南油第四工业区4栋(测试出发点)',
  21. 'origin_longitude'=>113.924140,'origin_latitude'=>22.522806];
  22. // 各区内人工模拟选点,仅供地图和调度测试,不代表真实企业或已核实的门牌位置。
  23. $districts = [
  24. ['name'=>'罗湖','code'=>'LH','stops'=>[
  25. ['name'=>'东门商贸','quantity'=>4,'longitude'=>114.117000,'latitude'=>22.547000],
  26. ['name'=>'翠竹办公','quantity'=>6,'longitude'=>114.130000,'latitude'=>22.563000],
  27. ]],
  28. ['name'=>'福田','code'=>'FT','stops'=>[
  29. ['name'=>'中心商务','quantity'=>5,'longitude'=>114.057000,'latitude'=>22.543000],
  30. ['name'=>'车公庙研发','quantity'=>7,'longitude'=>114.025000,'latitude'=>22.536000],
  31. ]],
  32. ['name'=>'宝安','code'=>'BA','stops'=>[
  33. ['name'=>'新安科技','quantity'=>6,'longitude'=>113.883000,'latitude'=>22.554000],
  34. ['name'=>'西乡创意','quantity'=>8,'longitude'=>113.863000,'latitude'=>22.578000],
  35. ]],
  36. ];
  37. $dispatch = new WaterDispatch();
  38. $before = $dispatch->board(['date'=>$date])['summary'];
  39. $replayed = false;
  40. if ($apply) {
  41. $replayed = WaterModel::transaction(function () use ($batch,$date,$origin,$districts,$dispatch) {
  42. $sku = WaterSku::query()->where('name','【测试0910】18.9L桶装水')->lock(true)->first()?->toArray();
  43. if (!$sku || (int)$sku['status'] !== 1) throw new RuntimeException('缺少启用的指定测试商品');
  44. if (WaterLedger::query()->where(['kind'=>'demo_seed','reference'=>$batch])->exists()) return true;
  45. if (date('Y-m-d') !== $date) throw new RuntimeException('固定日期测试批次仅允许当天首次执行');
  46. $business = new WaterBusiness();
  47. $skuId = (int)$sku['id'];
  48. $oldTasks = WaterDelivery::query()->orderBy('id')->get()->toArray();
  49. $oldAccounts = WaterAccount::query()->orderBy('id')->get()->toArray();
  50. $oldOrders = WaterOrder::query()->orderBy('id')->get()->toArray();
  51. $total = array_sum(array_map(fn($d)=>array_sum(array_column($d['stops'],'quantity')),$districts));
  52. $business->stock(['sku_id'=>$skuId,'mode'=>'inbound','quantity'=>$total,'reference'=>$batch.'-STOCK',
  53. 'note'=>'【测试数据】三区配送模拟入库36桶,未发生真实入库'],1);
  54. $groups = []; $taskIds = [];
  55. foreach ($districts as $district) {
  56. $region = '深圳市'.$district['name'].'区';
  57. $ids = [];
  58. foreach ($district['stops'] as $index=>$stop) {
  59. $name = '【测试0911】'.$district['name'].$stop['name'];
  60. if (WaterEnterprise::query()->where('name',$name)->exists()) throw new RuntimeException('同名测试企业已存在,停止避免覆盖');
  61. $enterpriseId = $business->save('enterprise',[
  62. 'name'=>$name,'contact'=>'片区测试联系人','phone'=>'TEST-0911-'.$district['code'].'-'.($index+1),
  63. 'region'=>$region,'address'=>$region.$stop['name'].'测试点(虚构企业、模拟坐标,请勿实际配送)',
  64. 'longitude'=>$stop['longitude'],'latitude'=>$stop['latitude'],'status'=>1,
  65. ],1)['id'];
  66. WaterAccount::query()->insert(['enterprise_id'=>$enterpriseId,'sku_id'=>$skuId,
  67. 'entitlement'=>$stop['quantity'],'threshold_qty'=>2,'updated_at'=>date('Y-m-d H:i:s')]);
  68. WaterLedger::query()->insert(['kind'=>'demo_seed','enterprise_id'=>$enterpriseId,'sku_id'=>$skuId,
  69. 'quantity'=>$stop['quantity'],'reference'=>$batch.'-RIGHTS-'.$district['code'].'-'.$index,
  70. 'note'=>'【测试数据】三区调度模拟权益,无真实收款','created_by'=>1,'created_at'=>date('Y-m-d H:i:s')]);
  71. $id = $business->createDelivery(['enterprise_id'=>$enterpriseId,'sku_id'=>$skuId,'quantity'=>$stop['quantity'],'date'=>$date],1)['id'];
  72. $dispatch->schedule($origin+['id'=>$id,'date'=>$date,'deadline_at'=>$date.($index===0?' 17:00:00':' 18:00:00'),
  73. 'longitude'=>$stop['longitude'],'latitude'=>$stop['latitude']],1);
  74. $ids[] = $id; $taskIds[] = $id;
  75. }
  76. $driverId = $dispatch->saveDriver(['name'=>'【测试0911】'.$district['name'].'配送员',
  77. 'phone'=>'TEST-0911-DRIVER-'.$district['code'],'region'=>$region,'capacity'=>30,
  78. 'vehicle'=>'测试车辆-'.$district['code']],1)['id'];
  79. $dispatch->shift(['driver_id'=>$driverId,'date'=>$date,'state'=>'on'],1);
  80. $groups[] = ['driver_id'=>$driverId,'task_ids'=>$ids];
  81. }
  82. $result = $dispatch->commit(['date'=>$date,'request_key'=>$batch,'routes'=>$groups],1);
  83. if (count($result['route_ids']) !== 3) throw new RuntimeException('路线数量不正确');
  84. $afterSku = WaterSku::query()->find($skuId)->toArray();
  85. if ((int)$afterSku['available'] !== (int)$sku['available'] || (int)$afterSku['locked'] !== (int)$sku['locked']+$total
  86. || (int)$afterSku['transit'] !== (int)$sku['transit']) throw new RuntimeException('库存核对失败');
  87. if (WaterOrder::query()->orderBy('id')->get()->toArray() !== $oldOrders) throw new RuntimeException('原收款订单发生变化');
  88. foreach ($oldTasks as $row) {
  89. if (WaterDelivery::query()->find($row['id'])->toArray() !== $row) throw new RuntimeException('原任务发生变化');
  90. }
  91. foreach ($oldAccounts as $row) {
  92. if (WaterAccount::query()->find($row['id'])->toArray() !== $row) throw new RuntimeException('原账户发生变化');
  93. }
  94. foreach ($groups as $index=>$group) {
  95. $route = $dispatch->route(['id'=>$result['route_ids'][$index]]);
  96. if (count($route['tasks']) !== 2) throw new RuntimeException('每条测试路线必须包含两个站点');
  97. foreach ($route['tasks'] as $task) {
  98. $account = WaterAccount::query()->where(['enterprise_id'=>$task['enterprise_id'],'sku_id'=>$skuId])->first()->toArray();
  99. if ($task['status'] !== 'assigned' || $task['scheduled_date'] !== $date
  100. || $task['region'] !== '深圳市'.$districts[$index]['name'].'区'
  101. || (int)$account['frozen'] !== (int)$task['quantity']) throw new RuntimeException('片区、任务状态或冻结权益不一致');
  102. }
  103. }
  104. WaterLedger::query()->insert(['kind'=>'demo_seed','sku_id'=>$skuId,'quantity'=>0,'reference'=>$batch,
  105. 'note'=>'【测试数据】罗湖、福田、宝安各2单,合计36桶及3条两站路线;模拟坐标;任务ID:'.implode(',',$taskIds),
  106. 'created_by'=>1,'created_at'=>date('Y-m-d H:i:s')]);
  107. return false;
  108. });
  109. }
  110. $board = $dispatch->board(['date'=>$date]);
  111. $filters = [];
  112. foreach ($districts as $district) {
  113. $region = '深圳市'.$district['name'].'区';
  114. $filtered = $dispatch->board(['date'=>$date,'region'=>$region]);
  115. $filters[$region] = ['summary'=>$filtered['summary'],'route_ids'=>array_values(array_unique(array_column($filtered['tasks'],'route_id'))),
  116. 'tasks'=>array_map(fn($row)=>array_intersect_key($row,array_flip(['id','enterprise_name','quantity','status','route_id','stop_order'])),$filtered['tasks'])];
  117. }
  118. echo json_encode(['mode'=>$apply?'applied':'inspect','batch'=>$batch,'replayed'=>$replayed,'before'=>$before,
  119. 'after'=>$board['summary'],'areas'=>$board['areas'],'district_filters'=>$filters],JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT).PHP_EOL;