StatisticCommonService.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. <?php
  2. namespace App\Service\Statistic;
  3. use App\Model\DailyDwOrderDetails;
  4. use App\Model\DailyPwOrderDetails;
  5. use App\Model\MonthlyDdOrder;
  6. use App\Model\MonthlyDdOrderDetails;
  7. use App\Model\MonthlyPsOrder;
  8. use App\Model\MonthlyPsOrderDetails;
  9. use App\Service\Service;
  10. use Illuminate\Support\Facades\DB;
  11. class StatisticCommonService extends Service
  12. {
  13. /**
  14. * 传参相关、时间数据自动拼接
  15. * @param $data
  16. * @return array
  17. */
  18. public function commonRule($data)
  19. {
  20. if (!empty($data['year'])) {
  21. $return = $this->getYearRangeInfo($data['year']);
  22. if (is_null($return)) return [false, '年度格式错误'];
  23. list($data['month_start'], $data['month_end']) = $return;
  24. } else {
  25. if (isset($data['time']) && !empty($data['time'])) {
  26. $start = $this->changeDateToDate($data['time'][0]);
  27. $end = $this->changeDateToDate($data['time'][1], true);
  28. $data['month_start'] = date("Y-m-d", $start);
  29. $data['month_end'] = date("Y-m-d", $end);
  30. }
  31. }
  32. if (!isset($data['month_start'])) $month_start = date('Y-01-01');
  33. else $month_start = date('Y-m-01', strtotime($data['month_start']));
  34. if (!isset($data['month_end'])) $month_end = date('Y-01-01', strtotime('+1 year', strtotime($month_start)));
  35. else {
  36. $start_year = date('Y', strtotime($month_start));
  37. $end_year = date('Y', strtotime($data['month_end']));
  38. if ($start_year != $end_year) return [false, "查询不得跨年!", ""];
  39. $month_end = date('Y-m-01', strtotime($data['month_end'] . ' +1 month'));
  40. }
  41. return [true, strtotime($month_start), strtotime($month_end)];
  42. }
  43. /**
  44. * 根据前端 ISO 时间字符串获取该年份的起止日期和时间戳
  45. * 适配:2019-12-31T16:00:00.000Z 这种带时区的数据
  46. *
  47. * @param string $isoStr 前端传来的时间字符串
  48. * @return array|null
  49. */
  50. public function getYearRangeInfo($isoStr)
  51. {
  52. if (empty($isoStr)) return null;
  53. try {
  54. // 1. 解析 ISO 8601 字符串
  55. $date = new \DateTime($isoStr);
  56. // 2. 强制转为中国时区(PRC),处理 16:00:00Z 这种 UTC 偏移
  57. $date->setTimezone(new \DateTimeZone('PRC'));
  58. // 3. 提取年份
  59. $year = $date->format('Y');
  60. // 4. 构造日期字符串
  61. $startDate = $year . "-01-01";
  62. $endDate = $year . "-12-31";
  63. return [
  64. $startDate,
  65. $endDate,
  66. ];
  67. } catch (\Exception $e) {
  68. // var_dump($e->getMessage());die;
  69. return null;
  70. }
  71. }
  72. /**
  73. * 用于拉取对应项目人员日维度的对应工时信息
  74. * @param $user
  75. * @param $data
  76. * @param $month_start
  77. * @param $month_end
  78. * @return array
  79. */
  80. public function getItemEmployeeDayWorkList($user, $data, $month_start, $month_end)
  81. {
  82. $month_employee = DailyPwOrderDetails::Clear($user, $data);
  83. return $month_employee->where("order_time", ">=", $month_start)
  84. ->where("order_time", "<", $month_end)
  85. ->where('del_time', 0)
  86. ->select(
  87. "item_id",
  88. "employee_id",
  89. // 保持别名输出
  90. DB::raw("FROM_UNIXTIME(order_time, '%Y-%m-%d') as order_date"),
  91. DB::raw("SUM(total_work_min) as total_work")
  92. )
  93. // 2. 优化:不加索引时,把离散度高的字段(id)放前面,
  94. // 让 MySQL 在临时表里 hash 归类时能快一点点
  95. ->groupBy("item_id", "employee_id", DB::raw("FROM_UNIXTIME(order_time, '%Y-%m-%d')"))
  96. ->orderBy("order_date", "asc")
  97. ->get()
  98. ->toArray();
  99. }
  100. /**
  101. * 用于拉取对应项目设备日维度的对应工时信息
  102. * @param $user
  103. * @param $data
  104. * @param $month_start
  105. * @param $month_end
  106. * @return array
  107. */
  108. public function getItemDeviceMonthWorkList($user, $data, $month_start, $month_end)
  109. {
  110. $month_device = DailyDwOrderDetails::Clear($user, $data);
  111. return $month_device->where("order_time", ">=", $month_start)->where("order_time", "<", $month_end)
  112. ->where('del_time', 0)
  113. ->select(
  114. "item_id",
  115. "device_id",
  116. // 将时间戳转为 Y-m-d 格式并起别名
  117. DB::raw("FROM_UNIXTIME(order_time, '%Y-%m') as order_month"),
  118. // 聚合求和
  119. DB::raw("SUM(total_work_min) as total_work")
  120. )
  121. ->groupBy(DB::raw("FROM_UNIXTIME(order_time, '%Y-%m')"), "item_id", "device_id")->get()->toArray();
  122. }
  123. /**
  124. * 用于拉取对应项目人员月维度的对应工时信息
  125. * @param $user
  126. * @param $data
  127. * @param $month_start
  128. * @param $month_end
  129. * @return array
  130. */
  131. public function getItemEmployeeMonthWorkList($user, $data, $month_start, $month_end)
  132. {
  133. $month_employee = DailyPwOrderDetails::Clear($user, $data);
  134. return $month_employee->where("order_time", ">=", $month_start)->where("order_time", "<", $month_end)
  135. ->where('del_time', 0)
  136. ->select(
  137. "item_id",
  138. "employee_id",
  139. // 将时间戳转为 Y-m-d 格式并起别名
  140. DB::raw("FROM_UNIXTIME(order_time, '%Y-%m') as order_month"),
  141. // 聚合求和
  142. DB::raw("SUM(total_work_min) as total_work")
  143. )
  144. ->groupBy("order_month", "item_id", "employee_id")->get()->toArray();
  145. }
  146. /**
  147. * 统计对应条件的人员月度工资
  148. * @param $user
  149. * @param $data
  150. * @param $month_start
  151. * @param $month_end
  152. * @return array
  153. */
  154. public function getEmployeeSalary($user, $data, $month_start, $month_end){
  155. $monthly_ps_order_ids = MonthlyPsOrder::Clear($user, $data)
  156. ->where('del_time', 0)->where("month", ">=", $month_start)->where("month", "<", $month_end)
  157. ->pluck('id')->toArray();
  158. $monthly_ps_order_key_list = MonthlyPsOrder::Clear($user, $data)
  159. ->where('del_time', 0)
  160. ->where("month", ">=", $month_start)
  161. ->where("month", "<", $month_end)
  162. ->select('id', DB::raw("FROM_UNIXTIME(month, '%Y-%m') as month_str"))
  163. ->pluck('month_str', 'id')
  164. ->toArray();
  165. $month_employee_salary = MonthlyPsOrderDetails::wherein('main_id', $monthly_ps_order_ids)
  166. ->where('del_time',0)->select("employee_id",DB::raw("(base_salary + performance_salary + bonus + other) as salary"), "main_id")
  167. ->get()->toArray();
  168. return collect($month_employee_salary)->mapWithKeys(function ($val) use ($monthly_ps_order_key_list) {
  169. $month = $monthly_ps_order_key_list[$val['main_id']] ?? null;
  170. // 如果没有找到月份,返回空数组,mapWithKeys 会自动忽略它
  171. if (!$month) {
  172. return [];
  173. }
  174. return [$val['employee_id'] . '_' . $month => (int)round($val['salary'] * 100)];
  175. })->toArray();
  176. }
  177. /**
  178. * 统计对应条件的设备月度费用
  179. * @param $user
  180. * @param $data
  181. * @param $month_start
  182. * @param $month_end
  183. * @return array
  184. */
  185. public function getDeviceAmount($user, $data, $month_start, $month_end){
  186. $monthly_dd_order_ids = MonthlyDdOrder::Clear($user, $data)->where('del_time', 0)->where("month", ">=", $month_start)->where("month", "<", $month_end)
  187. ->pluck('id')->toArray();
  188. $monthly_dd_order_key_list = MonthlyDdOrder::Clear($user, $data)->where('del_time', 0)->where("month", ">=", $month_start)->where("month", "<", $month_end)
  189. ->select('id', DB::raw("FROM_UNIXTIME(month, '%Y-%m') as month_str"))
  190. ->pluck("month_str", 'id')->toArray();
  191. $month_device_salary = MonthlyDdOrderDetails::wherein('main_id', $monthly_dd_order_ids)
  192. ->select("device_id", "depreciation_amount", "main_id")
  193. ->get()->toArray();
  194. return collect($month_device_salary)->mapWithKeys(function ($val) use ($monthly_dd_order_key_list) {
  195. $month = $monthly_dd_order_key_list[$val['main_id']] ?? null;
  196. // 如果没有找到月份,返回空数组,mapWithKeys 会自动忽略它
  197. if (!$month) {
  198. return [];
  199. }
  200. return [$val['device_id'] . '_' . $month => (int)round($val['depreciation_amount'] * 100)];
  201. })->toArray();
  202. }
  203. /**
  204. * 基于key用于统计信息的总数量
  205. * @param $list
  206. * @param $key
  207. * @return array
  208. */
  209. public function calculateCount($list, $key)
  210. {
  211. //统计人员相关的合计信息,用于补全计算差额
  212. $collect = collect($list);
  213. return $collect->groupBy(function ($item) use ($key) {
  214. return collect($key)->map(function ($k) use ($item) {
  215. return $item[$k] ?? '';
  216. })->implode('_');
  217. })->map(function ($group) {
  218. return $group->count();
  219. })->toArray();
  220. }
  221. /**
  222. * 基于用于统计信息的汇总信息(时)
  223. * @param $list
  224. * @param $key
  225. * @param $sum
  226. * @return array
  227. */
  228. public function calculateSumForHour($list, $key, $sum)
  229. {
  230. //统计人员相关的合计信息,用于补全计算差额
  231. $collect = collect($list);
  232. return $collect->groupBy(function ($item) use ($key) {
  233. // 动态拼接分组 Key:例如 "101_2023-10-01"
  234. return collect($key)->map(fn($k) => $item[$k] ?? '')->implode('_');
  235. })->map(function ($group) use ($sum) {
  236. // 1. 动态对指定字段求和,算出小时并保留两位小数
  237. $hours = round($group->sum($sum) / 60, 2);
  238. // 2. 乘以 100 并转为整数,存储为分位(解决浮点数精度问题)
  239. return (int)round($hours * 100);
  240. })->toArray();
  241. }
  242. /**
  243. * 基于用于统计信息的汇总信息
  244. * @param $list
  245. * @param $key
  246. * @param $sum
  247. * @return array
  248. */
  249. public function calculateSum($list, $key, $sum)
  250. {
  251. //统计人员相关的合计信息,用于补全计算差额
  252. $collect = collect($list);
  253. return $collect->groupBy(function ($item) use ($key) {
  254. // 动态拼接分组 Key:例如 "101_2023-10-01"
  255. return collect($key)->map(fn($k) => $item[$k] ?? '')->implode('_');
  256. })->map(function ($group) use ($sum) {
  257. return $group->sum($sum);
  258. })->toArray();
  259. }
  260. /**
  261. * 计算key的比例和统计分种维度的总计和平均工资
  262. * @param $month_employee_list
  263. * @return array
  264. */
  265. public function calculateRatioForMonth($month_employee_list,$employee_monthly_total_min,$salary_map,$key1,$key2){
  266. $item_month_list = [];
  267. $all_salary = [];
  268. $all_key_salary = [];
  269. foreach ($month_employee_list as $item) {
  270. $key = collect($key1)->map(fn($k) => $item[$k] ?? '')->implode('_');
  271. $item_key = collect($key2)->map(fn($k) => $item[$k] ?? '')->implode('_');
  272. if (!isset($item_month_list[$item_key])) {
  273. $item_month_list[$item_key] = [
  274. "month" => $item['order_month'],
  275. "total_work" => $item['total_work'],
  276. "work_minutes" => 0,
  277. "ratio" => 0,
  278. "total_min" => 0,
  279. "total_salary" => 0,
  280. "item_id" => $item['item_id'],
  281. "employee_id" => $item['employee_id'],
  282. "allocated_salary" => 0,
  283. ];
  284. }
  285. $total_min = $employee_monthly_total_min[$key] ?? 0;
  286. $total_salary = $salary_map[$key] ?? 0;
  287. if(!isset($all_key_salary[$key])) {
  288. if(!isset($all_salary[$item['order_month']])) $all_salary[$item['order_month']] = 0;
  289. $all_salary[$item['order_month']] += $total_salary;
  290. $all_key_salary[$key] = 1;
  291. }
  292. $item_month_list[$item_key]['total_salary'] = $total_salary;
  293. $item_month_list[$item_key]['total_hours'] = round($total_min/60,2);
  294. $item_month_list[$item_key]['total_min'] += $total_salary;
  295. // B. 计算工资分摊:(项目工时 / 月总工时) * 月工资
  296. if ($total_min > 0) {
  297. // var_dump(round($item['total_work']/60,2));
  298. // var_dump( $item_month_list[$item_key]['total_hours']);die;
  299. // var_dump();
  300. $ratio = round(round($item['total_work']/60,2) / $item_month_list[$item_key]['total_hours'],6);
  301. $allocated_salary = round($ratio * $total_salary);
  302. } else {
  303. $allocated_salary = 0;
  304. $ratio = 0;
  305. }
  306. $item_month_list[$item_key]['ratio'] += round($ratio,2);
  307. $item_month_list[$item_key]['allocated_salary'] += $allocated_salary;
  308. $item_month_list[$item_key]['work_minutes'] += $item['total_work'];
  309. }
  310. return [$item_month_list,$all_salary];
  311. }
  312. /**
  313. * 计算设备的的比例和统计分种维度的总计和平均工资
  314. * 并且返回总计金额和总计工时
  315. * @param $month_employee_list
  316. * @return array
  317. */
  318. public function calculateDeviceRatioForMonth($month_device_list,$device_monthly_total_min,$salary_map,$key1,$key2){
  319. $item_month_list = [];
  320. $device_total_depreciation = [];
  321. foreach ($month_device_list as $item) {
  322. $key = collect($key1)->map(fn($k) => $item[$k] ?? '')->implode('_');
  323. $item_key = collect($key2)->map(fn($k) => $item[$k] ?? '')->implode('_');
  324. $total_depreciation = $salary_map[$key] ?? 0;
  325. $total_min = $device_monthly_total_min[$key] ?? 0;
  326. if (!isset($item_month_list[$item_key])) {
  327. $item_month_list[$item_key] = [
  328. "month" => $item['order_month'],
  329. "allocated_depreciation" => 0,
  330. "work_minutes" => 0,
  331. "total_min" => $total_min,
  332. "item_id" => $item['item_id'],
  333. "device_id" => $item['device_id'],
  334. "total_depreciation" => $total_depreciation,
  335. ];
  336. $device_total_depreciation['total_hours'][$key] = round($total_min/60,2)*100;
  337. $device_total_depreciation['total_depreciation'][$key] = $total_depreciation;
  338. }
  339. // B. 计算工资分摊:(项目工时 / 月总工时) * 月工资
  340. if ($total_min > 0) {
  341. $ratio = round((round($item['total_work']/60,2) / round($total_min/60,2)), 4);
  342. $allocated_salary = round($ratio * $total_depreciation);
  343. } else {
  344. $ratio = 0;
  345. $allocated_salary = 0;
  346. }
  347. $item_month_list[$item_key]['allocated_depreciation'] += $allocated_salary;
  348. $item_month_list[$item_key]['work_minutes'] += $item['total_work'];
  349. $item_month_list[$item_key]['ratio'] = $ratio;
  350. }
  351. return [$item_month_list,$device_total_depreciation];
  352. }
  353. /**
  354. * 闭包中调用计算余数方法
  355. * @param $key
  356. * @param $item
  357. * @param $count
  358. * @param $sum
  359. * @param $word_keys
  360. * @return void
  361. */
  362. public function calculateClosure($key, &$item, &$count, &$sum, $word_keys)
  363. {
  364. // 数据格式
  365. // $word_keys = [
  366. // "employee_work_count" =>
  367. // [
  368. // "key" => "total_work",
  369. // "value" => "total_work_hours",
  370. // "type" => "hour",
  371. // ]
  372. // ];
  373. foreach ($word_keys as $k => $v) {
  374. if($v['type'] == 'ratio'&&!isset($sum[$k][$key])) $sum[$k][$key] = 100;
  375. if( !isset($sum[$k][$key])) $sum[$k][$key] = 0;
  376. if (--$count[$k][$key] > 0) {
  377. if($v['type'] == "hour") $current_hours = round($item[$v['key']] / 60, 2);
  378. elseif($v['type'] == "money") {
  379. $current_hours = round($item[$v['key']] / 100, 2);
  380. }
  381. else $current_hours = $item[$v['key']];
  382. if($v['type'] == "100b") {
  383. $sum[$k][$key] -= $current_hours ;
  384. $item[$v['value']] = $current_hours/100;
  385. }
  386. else{
  387. $sum[$k][$key] -= ($current_hours * 100);
  388. $item[$v['value']] = $current_hours;
  389. }
  390. } else {
  391. if($v['type'] == "day") $item[$v['value']] = $sum[$k][$key];
  392. if($v['type'] == "ratio") $item[$v['value']] =round($sum[$k][$key] / 100, 4);
  393. else $item[$v['value']] = round($sum[$k][$key] / 100, 2);
  394. }
  395. }
  396. }
  397. }