WaterInventory.php 4.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
  1. <?php
  2. declare(strict_types=1);
  3. namespace plugin\saiadmin\app\service;
  4. use plugin\saiadmin\app\model\water\{WaterModel,WaterSku,WaterAccount,WaterDelivery,WaterLedger};
  5. use plugin\saiadmin\exception\ApiException;
  6. use OpenSpout\Common\Entity\Row;
  7. use OpenSpout\Writer\XLSX\Writer;
  8. /** Read model for the inventory dashboard; all mutations retain the original stock service. */
  9. final class WaterInventory
  10. {
  11. private function query(array $filters)
  12. {
  13. $keyword=trim((string)($filters['keyword']??''));
  14. if (mb_strlen($keyword)>120) throw new ApiException('搜索内容不能超过120个字符');
  15. $query=WaterSku::query();
  16. if ($keyword!=='') $query->where('name','like','%'.$keyword.'%');
  17. return $query->selectRaw("*, CASE WHEN status<>1 THEN 'disabled' WHEN available<=safety_stock THEN 'alert' ELSE 'healthy' END AS stock_status");
  18. }
  19. public function dashboard(array $filters): array
  20. {
  21. return WaterModel::transaction(function () use ($filters) {
  22. $summary=WaterSku::query()->selectRaw('COUNT(*) AS specs, COALESCE(SUM(available),0) AS available,
  23. COALESCE(SUM(locked),0) AS locked, COALESCE(SUM(transit),0) AS transit, COALESCE(SUM(empty_qty),0) AS warehouse_empty,
  24. COALESCE(SUM(damaged_qty),0) AS damaged, COALESCE(SUM(empty_lost_qty),0) AS empty_lost,
  25. COALESCE(SUM(CASE WHEN status=1 AND available<=safety_stock THEN 1 ELSE 0 END),0) AS alerts')->first()->toArray();
  26. $summary=array_map('intval',$summary);
  27. $summary['site_empty']=(int)WaterAccount::query()->sum('empty_qty');
  28. $summary['vehicle_empty']=(int)WaterDelivery::query()->sum('empty_in_vehicle');
  29. $summary['empty_assets']=$summary['warehouse_empty']+$summary['site_empty']+$summary['vehicle_empty'];
  30. $summary['total']=$summary['available']+$summary['locked']+$summary['transit'];
  31. $start=date('Y-m-d',strtotime('-6 days'));
  32. $movements=WaterLedger::query()->whereIn('kind',['inbound','delivery_return','depart'])
  33. ->whereBetween('created_at',[$start.' 00:00:00',date('Y-m-d').' 23:59:59'])
  34. ->selectRaw('DATE(created_at) AS day,kind,SUM(quantity) AS quantity')->groupByRaw('DATE(created_at),kind')->get();
  35. $trend=[];
  36. for ($i=0;$i<7;$i++) { $day=date('Y-m-d',strtotime($start.' +'.$i.' days')); $trend[$day]=['day'=>$day,'inbound'=>0,'outbound'=>0]; }
  37. foreach ($movements as $row) $trend[$row->day][$row->kind==='depart'?'outbound':'inbound']+=(int)$row->quantity;
  38. $list=$this->query($filters)->orderBy('id','desc')->pageResult(max(1,(int)($filters['page']??1)),max(1,min(100,(int)($filters['limit']??10))));
  39. return ['summary'=>$summary,'trend'=>array_values($trend),'list'=>$list,'updated_at'=>date('Y-m-d H:i:s')];
  40. });
  41. }
  42. public function export(array $filters): string
  43. {
  44. $rows=$this->query($filters)->orderBy('id','desc')->limit(50001)->get();
  45. if (count($rows)>50000) throw new ApiException('导出超过50000条,请缩小搜索范围');
  46. $path=tempnam(sys_get_temp_dir(),'water-stock-');
  47. if ($path===false) throw new ApiException('无法创建导出文件');
  48. $writer=new Writer(); $opened=false;
  49. try {
  50. $writer->openToFile($path); $opened=true;
  51. $writer->addRow(Row::fromValues(['商品规格','容量/L','可用满桶','锁定','在途','仓库空桶','安全库存','破损隔离满桶','累计空桶损失','状态']));
  52. foreach ($rows as $row) {
  53. $name=(string)$row->name;
  54. if (preg_match('/^[=+@\-\t\r\n]/u',$name)) $name="'".$name;
  55. $writer->addRow(Row::fromValues([$name,(float)$row->liters,(int)$row->available,(int)$row->locked,(int)$row->transit,
  56. (int)$row->empty_qty,(int)$row->safety_stock,(int)$row->damaged_qty,(int)$row->empty_lost_qty,
  57. ['disabled'=>'停用','alert'=>'预警','healthy'=>'充足'][$row->stock_status]]));
  58. }
  59. $writer->close(); $opened=false;
  60. $content=file_get_contents($path);
  61. if ($content===false) throw new ApiException('读取导出文件失败');
  62. return $content;
  63. } finally { try { if ($opened) $writer->close(); } finally { if (is_file($path)) unlink($path); } }
  64. }
  65. }