StatisticCommonService.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  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)->where("order_time", "<", $month_end)
  84. ->where('del_time', 0)
  85. ->select(
  86. "item_id",
  87. "employee_id",
  88. // 将时间戳转为 Y-m-d 格式并起别名
  89. DB::raw("FROM_UNIXTIME(order_time, '%Y-%m-%d') as order_date"),
  90. // 聚合求和
  91. DB::raw("SUM(total_work_min) as total_work")
  92. )
  93. ->groupBy(DB::raw("FROM_UNIXTIME(order_time, '%Y-%m-%d')"), "item_id", "employee_id")
  94. ->orderby("order_date", "asc")->get()->toArray();
  95. }
  96. /**
  97. * 用于拉取对应项目设备日维度的对应工时信息
  98. * @param $user
  99. * @param $data
  100. * @param $month_start
  101. * @param $month_end
  102. * @return array
  103. */
  104. public function getItemDeviceMonthWorkList($user, $data, $month_start, $month_end)
  105. {
  106. $month_device = DailyDwOrderDetails::Clear($user, $data);
  107. return $month_device->where("order_time", ">=", $month_start)->where("order_time", "<", $month_end)
  108. ->where('del_time', 0)
  109. ->select(
  110. "item_id",
  111. "device_id",
  112. // 将时间戳转为 Y-m-d 格式并起别名
  113. DB::raw("FROM_UNIXTIME(order_time, '%Y-%m') as order_month"),
  114. // 聚合求和
  115. DB::raw("SUM(total_work_min) as total_work")
  116. )
  117. ->groupBy(DB::raw("FROM_UNIXTIME(order_time, '%Y-%m')"), "item_id", "device_id")->get()->toArray();
  118. }
  119. /**
  120. * 用于拉取对应项目人员月维度的对应工时信息
  121. * @param $user
  122. * @param $data
  123. * @param $month_start
  124. * @param $month_end
  125. * @return array
  126. */
  127. public function getItemEmployeeMonthWorkList($user, $data, $month_start, $month_end)
  128. {
  129. $month_employee = DailyPwOrderDetails::Clear($user, $data);
  130. return $month_employee->where("order_time", ">=", $month_start)->where("order_time", "<", $month_end)
  131. ->where('del_time', 0)
  132. ->select(
  133. "item_id",
  134. "employee_id",
  135. // 将时间戳转为 Y-m-d 格式并起别名
  136. DB::raw("FROM_UNIXTIME(order_time, '%Y-%m') as order_month"),
  137. // 聚合求和
  138. DB::raw("SUM(total_work_min) as total_work")
  139. )
  140. ->groupBy("order_month", "item_id", "employee_id")->get()->toArray();
  141. }
  142. /**
  143. * 统计对应条件的人员月度工资
  144. * @param $user
  145. * @param $data
  146. * @param $month_start
  147. * @param $month_end
  148. * @return array
  149. */
  150. public function getEmployeeSalary($user, $data, $month_start, $month_end){
  151. $monthly_ps_order_ids = MonthlyPsOrder::Clear($user, $data)
  152. ->where('del_time', 0)->where("month", ">=", $month_start)->where("month", "<", $month_end)
  153. ->pluck('id')->toArray();
  154. $monthly_ps_order_key_list = MonthlyPsOrder::Clear($user, $data)
  155. ->where('del_time', 0)
  156. ->where("month", ">=", $month_start)
  157. ->where("month", "<", $month_end)
  158. ->select('id', DB::raw("FROM_UNIXTIME(month, '%Y-%m') as month_str"))
  159. ->pluck('month_str', 'id')
  160. ->toArray();
  161. $month_employee_salary = MonthlyPsOrderDetails::wherein('main_id', $monthly_ps_order_ids)
  162. ->where('del_time',0)->select("employee_id",DB::raw("(base_salary + performance_salary + bonus + other) as salary"), "main_id")
  163. ->get()->toArray();
  164. return collect($month_employee_salary)->mapWithKeys(function ($val) use ($monthly_ps_order_key_list) {
  165. $month = $monthly_ps_order_key_list[$val['main_id']] ?? null;
  166. // 如果没有找到月份,返回空数组,mapWithKeys 会自动忽略它
  167. if (!$month) {
  168. return [];
  169. }
  170. return [$val['employee_id'] . '_' . $month => (int)round($val['salary'] * 100)];
  171. })->toArray();
  172. }
  173. /**
  174. * 统计对应条件的设备月度费用
  175. * @param $user
  176. * @param $data
  177. * @param $month_start
  178. * @param $month_end
  179. * @return array
  180. */
  181. public function getDeviceAmount($user, $data, $month_start, $month_end){
  182. $monthly_dd_order_ids = MonthlyDdOrder::Clear($user, $data)->where('del_time', 0)->where("month", ">=", $month_start)->where("month", "<", $month_end)
  183. ->pluck('id')->toArray();
  184. $monthly_dd_order_key_list = MonthlyDdOrder::Clear($user, $data)->where('del_time', 0)->where("month", ">=", $month_start)->where("month", "<", $month_end)
  185. ->select('id', DB::raw("FROM_UNIXTIME(month, '%Y-%m') as month_str"))
  186. ->pluck("month_str", 'id')->toArray();
  187. $month_device_salary = MonthlyDdOrderDetails::wherein('main_id', $monthly_dd_order_ids)
  188. ->select("device_id", "depreciation_amount", "main_id")
  189. ->get()->toArray();
  190. return collect($month_device_salary)->mapWithKeys(function ($val) use ($monthly_dd_order_key_list) {
  191. $month = $monthly_dd_order_key_list[$val['main_id']] ?? null;
  192. // 如果没有找到月份,返回空数组,mapWithKeys 会自动忽略它
  193. if (!$month) {
  194. return [];
  195. }
  196. return [$val['device_id'] . '_' . $month => (int)round($val['depreciation_amount'] * 100)];
  197. })->toArray();
  198. }
  199. /**
  200. * 基于key用于统计信息的总数量
  201. * @param $list
  202. * @param $key
  203. * @return array
  204. */
  205. public function calculateCount($list, $key)
  206. {
  207. //统计人员相关的合计信息,用于补全计算差额
  208. $collect = collect($list);
  209. return $collect->groupBy(function ($item) use ($key) {
  210. return collect($key)->map(function ($k) use ($item) {
  211. return $item[$k] ?? '';
  212. })->implode('_');
  213. })->map(function ($group) {
  214. return $group->count();
  215. })->toArray();
  216. }
  217. /**
  218. * 基于用于统计信息的汇总信息(时)
  219. * @param $list
  220. * @param $key
  221. * @param $sum
  222. * @return array
  223. */
  224. public function calculateSumForHour($list, $key, $sum)
  225. {
  226. //统计人员相关的合计信息,用于补全计算差额
  227. $collect = collect($list);
  228. return $collect->groupBy(function ($item) use ($key) {
  229. // 动态拼接分组 Key:例如 "101_2023-10-01"
  230. return collect($key)->map(fn($k) => $item[$k] ?? '')->implode('_');
  231. })->map(function ($group) use ($sum) {
  232. // 1. 动态对指定字段求和,算出小时并保留两位小数
  233. $hours = round($group->sum($sum) / 60, 2);
  234. // 2. 乘以 100 并转为整数,存储为分位(解决浮点数精度问题)
  235. return (int)round($hours * 100);
  236. })->toArray();
  237. }
  238. /**
  239. * 基于用于统计信息的汇总信息
  240. * @param $list
  241. * @param $key
  242. * @param $sum
  243. * @return array
  244. */
  245. public function calculateSum($list, $key, $sum)
  246. {
  247. //统计人员相关的合计信息,用于补全计算差额
  248. $collect = collect($list);
  249. return $collect->groupBy(function ($item) use ($key) {
  250. // 动态拼接分组 Key:例如 "101_2023-10-01"
  251. return collect($key)->map(fn($k) => $item[$k] ?? '')->implode('_');
  252. })->map(function ($group) use ($sum) {
  253. return $group->sum($sum);
  254. })->toArray();
  255. }
  256. /**
  257. * 计算key的比例和统计分种维度的总计和平均工资
  258. * @param $month_employee_list
  259. * @return array
  260. */
  261. public function calculateRatioForMonth($month_employee_list,$employee_monthly_total_min,$salary_map,$key1,$key2){
  262. $item_month_list = [];
  263. $all_salary = [];
  264. $all_key_salary = [];
  265. foreach ($month_employee_list as $item) {
  266. $key = collect($key1)->map(fn($k) => $item[$k] ?? '')->implode('_');
  267. $item_key = collect($key2)->map(fn($k) => $item[$k] ?? '')->implode('_');
  268. if (!isset($item_month_list[$item_key])) {
  269. $item_month_list[$item_key] = [
  270. "month" => $item['order_month'],
  271. "total_work" => $item['total_work'],
  272. "work_minutes" => 0,
  273. "ratio" => 0,
  274. "total_min" => 0,
  275. "total_salary" => 0,
  276. "item_id" => $item['item_id'],
  277. "employee_id" => $item['employee_id'],
  278. "allocated_salary" => 0,
  279. ];
  280. }
  281. $total_min = $employee_monthly_total_min[$key] ?? 0;
  282. $total_salary = $salary_map[$key] ?? 0;
  283. if(!isset($all_key_salary[$key])) {
  284. if(!isset($all_salary[$item['order_month']])) $all_salary[$item['order_month']] = 0;
  285. $all_salary[$item['order_month']] += $total_salary;
  286. $all_key_salary[$key] = 1;
  287. }
  288. $item_month_list[$item_key]['total_salary'] = $total_salary;
  289. $item_month_list[$item_key]['total_hour'] = round($total_min/60,2);
  290. $item_month_list[$item_key]['total_min'] += $total_salary;
  291. // B. 计算工资分摊:(项目工时 / 月总工时) * 月工资
  292. if ($total_min > 0) {
  293. $ratio = round($item['total_work'] / $total_min,2);
  294. $allocated_salary = round($ratio * $total_salary);
  295. } else {
  296. $allocated_salary = 0;
  297. $ratio = 0;
  298. }
  299. $item_month_list[$item_key]['ratio'] += round($ratio,2);
  300. $item_month_list[$item_key]['allocated_salary'] += $allocated_salary;
  301. $item_month_list[$item_key]['work_minutes'] += $item['total_work'];
  302. }
  303. return [$item_month_list,$all_salary];
  304. }
  305. /**
  306. * 计算设备的的比例和统计分种维度的总计和平均工资
  307. * 并且返回总计金额和总计工时
  308. * @param $month_employee_list
  309. * @return array
  310. */
  311. public function calculateDeviceRatioForMonth($month_device_list,$device_monthly_total_min,$salary_map,$key1,$key2){
  312. $item_month_list = [];
  313. $device_total_depreciation = [];
  314. foreach ($month_device_list as $item) {
  315. $key = collect($key1)->map(fn($k) => $item[$k] ?? '')->implode('_');
  316. $item_key = collect($key2)->map(fn($k) => $item[$k] ?? '')->implode('_');
  317. $total_depreciation = $salary_map[$key] ?? 0;
  318. $total_min = $device_monthly_total_min[$key] ?? 0;
  319. if (!isset($item_month_list[$item_key])) {
  320. $item_month_list[$item_key] = [
  321. "month" => $item['order_month'],
  322. "allocated_depreciation" => 0,
  323. "work_minutes" => 0,
  324. "total_min" => $total_min,
  325. "item_id" => $item['item_id'],
  326. "device_id" => $item['device_id'],
  327. "total_depreciation" => $total_depreciation,
  328. ];
  329. $device_total_depreciation['total_hours'][$key] = round($total_min/60,2)*100;
  330. $device_total_depreciation['total_depreciation'][$key] = $total_depreciation*100;
  331. }
  332. // B. 计算工资分摊:(项目工时 / 月总工时) * 月工资
  333. if ($total_min > 0) {
  334. $ratio = round($item['total_work'] / $total_min, 3);
  335. $allocated_salary = round($ratio * $total_depreciation, 2);
  336. } else {
  337. $ratio = 0;
  338. $allocated_salary = 0;
  339. }
  340. $item_month_list[$item_key]['allocated_depreciation'] += $allocated_salary;
  341. $item_month_list[$item_key]['work_minutes'] += $item['total_work'];
  342. $item_month_list[$item_key]['ratio'] = $ratio;
  343. }
  344. return [$item_month_list,$device_total_depreciation];
  345. }
  346. /**
  347. * 闭包中调用计算余数方法
  348. * @param $key
  349. * @param $item
  350. * @param $count
  351. * @param $sum
  352. * @param $word_keys
  353. * @return void
  354. */
  355. public function calculateClosure($key, &$item, &$count, &$sum, $word_keys)
  356. {
  357. // 数据格式
  358. // $word_keys = [
  359. // "employee_work_count" =>
  360. // [
  361. // "key" => "total_work",
  362. // "value" => "total_work_hours",
  363. // "type" => "hour",
  364. // ]
  365. // ];
  366. foreach ($word_keys as $k => $v) {
  367. if($v['type'] == 'ratio'&&!isset($sum[$k][$key])) $sum[$k][$key] = 100;
  368. if( !isset($sum[$k][$key])) $sum[$k][$key] = 0;
  369. if (--$count[$k][$key] > 0) {
  370. if($v['type'] == "hour") $current_hours = round($item[$v['key']] / 60, 2);
  371. elseif($v['type'] == "money") $current_hours = round($item[$v['key']] / 100, 2);
  372. else $current_hours = $item[$v['key']];
  373. if($v['type'] == "100b") {
  374. $sum[$k][$key] -= $current_hours ;
  375. $item[$v['value']] = $current_hours/100;
  376. }
  377. else{
  378. $sum[$k][$key] -= $current_hours * 100;
  379. $item[$v['value']] = $current_hours;
  380. }
  381. } else {
  382. if($v['type'] == "day") $item[$v['value']] = $sum[$k][$key];
  383. else $item[$v['value']] = round($sum[$k][$key] / 100, 2);
  384. }
  385. }
  386. }
  387. }