BIService.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  1. <?php
  2. namespace App\Service;
  3. use App\Model\DailyDwOrder;
  4. use App\Model\DailyDwOrderDetails;
  5. use App\Model\DailyPwOrder;
  6. use App\Model\DailyPwOrderDetails;
  7. use App\Model\ExpenseClaims;
  8. use App\Model\ExpenseClaimsDetails;
  9. use App\Model\Fee;
  10. use App\Model\Item;
  11. use App\Model\MonthlyDdOrder;
  12. use App\Model\MonthlyDdOrderDetails;
  13. use App\Model\MonthlyPsOrder;
  14. use App\Model\MonthlyPsOrderDetails;
  15. use App\Model\PLeaveOverOrder;
  16. use App\Model\PLeaveOverOrderDetails;
  17. use Illuminate\Support\Facades\DB;
  18. class BIService extends Service
  19. {
  20. private function commonRule($data)
  21. {
  22. if (isset($data['time']) && !empty($data['time'])) {
  23. $start = $this->changeDateToDate($data['time'][0]);
  24. $end = $this->changeDateToDate($data['time'][1], true);
  25. $data['month_start'] = date("Y-m-d", $start);
  26. $data['month_end'] = date("Y-m-d", $end);
  27. }
  28. if (!isset($data['month_start'])) $month_start = date('Y-01-01', strtotime('-1 year'));
  29. else $month_start = date('Y-m-01', strtotime($data['month_start']));
  30. if (!isset($data['month_end'])) $month_end = date('Y-01-01', strtotime('+1 year', strtotime($month_start)));
  31. else {
  32. $start_year = date('Y', strtotime($month_start));
  33. $end_year = date('Y', strtotime($data['month_end']));
  34. if ($start_year != $end_year) return [false, "查询不得跨年!", ""];
  35. $month_end = date('Y-m-01', strtotime($data['month_end'] . ' +1 month'));
  36. }
  37. return [true, strtotime($month_start), strtotime($month_end)];
  38. }
  39. public function homePageData($data, $user)
  40. {
  41. // mark 存在需求冲突 如果补录的项目 比如 2015年 那么一开始展示的年份就是2015 和2026年6月10号需求冲突
  42. // 先注释原逻辑 换新
  43. // $model = Item::TopClear($user,$data);
  44. // $start_time = $model->where('del_time',0)
  45. // ->orderby('id', 'desc')->value("start_time");
  46. if(isset($data['year'])){
  47. $start_time = strtotime($data['year']. '-01-01');
  48. }else{
  49. $start_time = time();
  50. }
  51. $year = date('Y-01-01', $start_time);
  52. $data['month_start'] = $year;
  53. list($status, $month_start, $month_end) = $this->commonRule($data);
  54. if (!$status) return [false, $month_start];
  55. //当年项目总数
  56. $model = Item::TopClear($user,$data);
  57. $intersectCount = $model->where('start_time', '<=', $month_end) // 项目开始时间 <= 区间结束时间
  58. ->where('end_time', '>=', $month_start) // 项目结束时间 >= 区间开始时间
  59. ->count();
  60. //人员费用
  61. list($total_man, $salary_map) = $this->getEmployeeSalary($month_start, $month_end, $data, $user);
  62. //折旧费用
  63. list($total_zj, $zj_map) = $this->getDeviceSalary($month_start, $month_end, $data, $user);
  64. //费用报销
  65. list($total_other, $other_map, $return_fee) = $this->getFeeItemSalary($month_start, $month_end, $data, $user);
  66. //研发费用总额
  67. $total = bcadd(bcadd($total_man, $total_zj,3), $total_other,3);
  68. //研发费用结构
  69. $rd = [
  70. 0 => [
  71. 'title' => '人工费用',
  72. 'rate' => $total == 0 ? 0 : bcmul(bcdiv($total_man,$total,4),100,2),
  73. 'rate_unit' => '%',
  74. 'total' => $total_man,
  75. 'total_unit' => '¥',
  76. ],
  77. 1 => [
  78. 'title' => '折旧费用',
  79. 'rate' => $total == 0 ? 0 :bcmul(bcdiv($total_zj,$total,4),100,2),
  80. 'rate_unit' => '%',
  81. 'total' => $total_zj,
  82. 'total_unit' => '¥',
  83. ],
  84. 2 => [
  85. 'title' => '费用报销',
  86. 'rate' => $total == 0 ? 0 :bcmul(bcdiv($total_other,$total,4),100,2),
  87. 'rate_unit' => '%',
  88. 'total' => $total_other,
  89. 'total_unit' => '¥',
  90. ],
  91. ];
  92. //研发费用趋势
  93. $rd_trend = $this->makeTrend($month_start, $salary_map, $zj_map, $other_map);
  94. //研发费用报销占比
  95. $rd_rate = $return_fee;
  96. //加班 请假 总时长
  97. list($over_time, $leave_time) = $this->overLeave($month_start, $month_end, $data, $user);
  98. // 项目人员工时汇总
  99. $man_work = $this->manWork($month_start, $month_end, $data, $user);
  100. //加计扣除金额
  101. $statisticService = new StatisticService();
  102. $attendance = $statisticService->employeeAttendanceMonthStatistic(["year"=>$year, "s" => "/api/employeeAttendanceMonthStatistic"],$user);
  103. $discount = 0;
  104. foreach ($attendance[1]['list'] as $v){
  105. $discount += $v['jj_total_amount']*100;
  106. }
  107. return [true, [
  108. 'rd_total' => $total,
  109. 'rd_unit' => '¥',
  110. 'rd_title' => '研发费用总额', //-----
  111. 'discount_total' => round($discount/100,2),
  112. 'discount_unit' => '¥',
  113. 'discount_title' => '加计扣除金额', //------
  114. 'over_time' => $over_time, // 加班
  115. 'leave_time' => $leave_time, // 请假
  116. 'rd' => $rd, //研发费用结构
  117. 'rd_trend' => $rd_trend, //研发费用趋势
  118. 'rd_rate' => $rd_rate, // 研发费用报销占比
  119. 'man_work' => $man_work, //项目人员工时汇总
  120. 'year' => date('Y',strtotime($year)),
  121. 'item_num' => $intersectCount,
  122. 'start_year' => date('Y',time())
  123. ]];
  124. }
  125. private function getEmployeeSalary($month_start, $month_end, $data, $user)
  126. {
  127. $monthly_ps_order = MonthlyPsOrder::Clear($user, $data)
  128. ->where('del_time', 0)
  129. ->where("month", ">=", $month_start)
  130. ->where("month", "<", $month_end)
  131. ->pluck('month','id')
  132. ->toArray();
  133. $monthly_ps_order_ids = array_keys($monthly_ps_order);
  134. $month_employee_salary = MonthlyPsOrderDetails::whereIn('main_id', $monthly_ps_order_ids)
  135. ->select("base_salary","performance_salary","other","bonus", "main_id")
  136. ->get()
  137. ->toArray();
  138. $total = 0;
  139. $salary_map = [];
  140. foreach ($month_employee_salary as $value) {
  141. $currentAmount = bcadd($value['base_salary'], $value['performance_salary'],3);
  142. $currentAmount = bcadd($currentAmount, $value['other'],3);
  143. $currentAmount = bcadd($currentAmount, $value['bonus'],3);
  144. if(isset($monthly_ps_order[$value['main_id']])){
  145. $month = $monthly_ps_order[$value['main_id']];
  146. if(isset($salary_map[$month])){
  147. $salary_map[$month] = bcadd($salary_map[$month], $currentAmount,3);
  148. }else{
  149. $salary_map[$month] = $currentAmount;
  150. }
  151. }
  152. $total = bcadd($total, $currentAmount,3);
  153. }
  154. return [$total, $salary_map];
  155. }
  156. private function getDeviceSalary($month_start, $month_end, $data, $user)
  157. {
  158. $monthly_dd_order= MonthlyDdOrder::Clear($user, $data)
  159. ->where('del_time', 0)
  160. ->where("month", ">=", $month_start)
  161. ->where("month", "<", $month_end)
  162. ->pluck('month','id')
  163. ->toArray();
  164. $monthly_dd_order_ids = array_keys($monthly_dd_order);
  165. $month_device_salary = MonthlyDdOrderDetails::whereIn('main_id', $monthly_dd_order_ids)
  166. ->select("depreciation_amount as amount", "main_id")
  167. ->get()
  168. ->toArray();
  169. $total = 0;
  170. $amount_map = [];
  171. foreach ($month_device_salary as $value) {
  172. $currentAmount = (string)$value['amount'];
  173. if(isset($monthly_dd_order[$value['main_id']])){
  174. $month = $monthly_dd_order[$value['main_id']];
  175. if(isset($amount_map[$month])){
  176. $amount_map[$month] = bcadd($amount_map[$month], $currentAmount,3);
  177. }else{
  178. $amount_map[$month] = $currentAmount;
  179. }
  180. }
  181. $total = bcadd($total, $currentAmount,3);
  182. }
  183. return [$total, $amount_map];
  184. }
  185. private function getFeeItemSalary($month_start, $month_end, $data, $user)
  186. {
  187. $e_order = ExpenseClaims::Clear($user, $data)
  188. ->where('del_time', 0)
  189. ->where("month", ">=", $month_start)
  190. ->where("month", "<", $month_end)
  191. ->pluck('month','id')
  192. ->toArray();
  193. $e_order_ids = array_keys($e_order);
  194. $e_order_detail = ExpenseClaimsDetails::whereIn('expense_claims_id', $e_order_ids)
  195. ->select("amount", "expense_claims_id as main_id", "fee_id")
  196. ->get()
  197. ->toArray();
  198. $fee = Fee::TopClear($user, $data);
  199. $fee_map = $fee->whereIn('id', array_unique(array_column($e_order_detail,'fee_id')))->pluck('title','id')->toArray();
  200. $total = 0;
  201. $amount_map = [];
  202. $fee_amount_map = [];
  203. foreach ($e_order_detail as $value) {
  204. $currentAmount = (string)$value['amount'];
  205. if(isset($e_order[$value['main_id']])){
  206. $month = $e_order[$value['main_id']];
  207. if(isset($amount_map[$month])){
  208. $amount_map[$month] = bcadd($amount_map[$month], $currentAmount,3);
  209. }else{
  210. $amount_map[$month] = $currentAmount;
  211. }
  212. }
  213. $fee_tmp = $fee_map[$value['fee_id']] ?? "";
  214. if(! empty($fee_tmp)){
  215. if(isset($fee_amount_map[$fee_tmp])){
  216. $fee_amount_map[$fee_tmp] = bcadd($fee_amount_map[$fee_tmp], $currentAmount,3);
  217. }else{
  218. $fee_amount_map[$fee_tmp] = $currentAmount;
  219. }
  220. }
  221. $total = bcadd($total, $currentAmount,3);
  222. }
  223. $return_fee = [];
  224. foreach ($fee_amount_map as $key => $value){
  225. $amount = (string)$value;
  226. $return_fee[] = [
  227. "title" => $key,
  228. "total" => $amount,
  229. "total_unit" => "元",
  230. "rate" => bcmul(bcdiv($amount,$total,4),100,2),
  231. "rate_unit" => "%"
  232. ];
  233. }unset($fee_amount_map);
  234. return [$total, $amount_map, $return_fee];
  235. }
  236. private function makeTrend($month_start, $salary_map, $zj_map, $other_map)
  237. {
  238. $trend = [];
  239. for ($i = 0; $i < 12; $i++) {
  240. // 计算每个月第一天的时间戳作为 Key
  241. $timestamp = strtotime("+$i month", $month_start);
  242. $monthName = ($i + 1) . '月';
  243. $trend[$timestamp] = [
  244. 'month' => $monthName,
  245. // 'salary' => "0.000",
  246. // 'zj' => "0.000",
  247. // 'other' => "0.000",
  248. 'total' => "0.000",
  249. ];
  250. }
  251. // 2. 将传入的 Map 数据填充进去
  252. foreach ($trend as $timestamp => &$item) {
  253. // 使用 ?? "0" 容错,并强制转为 string 保证 bcadd 精度
  254. $s = $salary_map[$timestamp] ?? "0";
  255. $z = $zj_map[$timestamp] ?? "0";
  256. $o = $other_map[$timestamp] ?? "0";
  257. // $item['salary'] = bcadd($s, "0", 3);
  258. // $item['zj'] = bcadd($z, "0", 3);
  259. // $item['other'] = bcadd($o, "0", 3);
  260. // 计算该月总计
  261. $item['total'] = bcadd(bcadd($s, $z, 3), $o, 3);
  262. }
  263. return array_values($trend);
  264. }
  265. private function overLeave($month_start, $month_end, $data, $user)
  266. {
  267. $id = PLeaveOverOrder::Clear($user, $data)
  268. ->where('del_time', 0)
  269. ->where("order_time", ">=", $month_start)
  270. ->where("order_time", "<", $month_end)
  271. ->pluck('id')
  272. ->toArray();
  273. $p = PLeaveOverOrderDetails::whereIn('main_id', $id)
  274. ->select("type", "total_min")
  275. ->get()
  276. ->toArray();
  277. $total = $total_1 = 0;
  278. foreach ($p as $value) {
  279. $total_min = (string)$value['total_min'];
  280. if($value['type'] == PLeaveOverOrderDetails::TYPE_ONE){
  281. $total = bcadd($total, $total_min,2);
  282. }else{
  283. $total_1 = bcadd($total_1, $total_min,2);
  284. }
  285. }
  286. // 最终返回或输出前转换
  287. $total_hours = bcdiv($total, 60, 2);
  288. $total_1_hours = bcdiv($total_1, 60, 2);
  289. return [$total_1_hours, $total_hours];
  290. }
  291. private function manWork($month_start, $month_end, $data, $user)
  292. {
  293. $daily_pw = DailyPwOrder::Clear($user, $data)
  294. ->where('del_time', 0)
  295. ->where("order_time", ">=", $month_start)
  296. ->where("order_time", "<", $month_end)
  297. ->select('id','item_id')
  298. ->get()->toArray();
  299. $item_map = Item::whereIn('id',array_unique(array_column($daily_pw,'item_id')))
  300. ->pluck('title','id')
  301. ->toArray();
  302. $p = DailyPwOrderDetails::whereIn('main_id', array_column($daily_pw,'id'))
  303. ->select("item_id", "total_work_min")
  304. ->get()
  305. ->toArray();
  306. $total = 0;
  307. $map = [];
  308. foreach ($p as $value) {
  309. $total_min = (string)$value['total_work_min'];
  310. if(isset($map[$value['item_id']])){
  311. $map[$value['item_id']] = bcadd($map[$value['item_id']], $total_min,2);
  312. }else{
  313. $map[$value['item_id']] = $total_min;
  314. }
  315. $total = bcadd($total, $total_min,2);
  316. }
  317. $return = [];
  318. foreach ($map as $key => $value){
  319. $rate = bcmul(bcdiv($value, $total,4),100,2);
  320. $total_hours = bcdiv($value, 60, 2);
  321. $return[] = [
  322. 'title' => $item_map[$key] ?? "",
  323. 'total_work_hour' => $total_hours,
  324. 'total_work_hour_unit' => "小时",
  325. "rate" => $rate,
  326. "rate_unit" => "%",
  327. ];
  328. }
  329. return $return;
  330. }
  331. public function cockpit($data, $user)
  332. {
  333. // 1. 声明时区对象
  334. $utcZone = new \DateTimeZone('UTC');
  335. $localZone = new \DateTimeZone('Asia/Shanghai'); // 你的数据库/服务器本地时区(北京时间)
  336. if (!empty($data['time'][0]) && !empty($data['time'][1])) {
  337. // 【A. 满足 employeeSalarySummary 的 UTC 干净时间】
  338. // 提取前端 ISO 串中的纯日期,强制转为 UTC 视角的开始和结束
  339. $rawStartUtc = new \DateTime($data['time'][0], $utcZone);
  340. $rawEndUtc = new \DateTime($data['time'][1], $utcZone);
  341. $startDateTimeUtc = new \DateTime($rawStartUtc->format('Y-m-d') . ' 00:00:00', $utcZone);
  342. $endDateTimeUtc = new \DateTime($rawEndUtc->format('Y-m-d') . ' 23:59:59', $utcZone);
  343. // 【B. 满足 getManWork 的本地 00点和23点 时间戳】
  344. $startTimestampLocal = (new \DateTime($rawStartUtc->format('Y-m-d') . ' 00:00:00', $localZone))->getTimestamp();
  345. $endTimestampLocal = (new \DateTime($rawEndUtc->format('Y-m-d') . ' 23:59:59', $localZone))->getTimestamp();
  346. } else {
  347. // 【没传参时的默认兜底逻辑】
  348. // UTC 视角的当月起止
  349. $startDateTimeUtc = new \DateTime('first day of this month 00:00:00', $utcZone);
  350. $endDateTimeUtc = new \DateTime('last day of this month 23:59:59', $utcZone);
  351. // 本地视角的当月起止时间戳
  352. $startTimestampLocal = (new \DateTime('first day of this month 00:00:00', $localZone))->getTimestamp();
  353. $endTimestampLocal = (new \DateTime('last day of this month 23:59:59', $localZone))->getTimestamp();
  354. }
  355. $data['start_time'] = $startTimestampLocal;
  356. $data['end_time'] = $endTimestampLocal;
  357. $array = $this->getManWork($data, $user);
  358. // 当月设备工时信息
  359. $array2 = $this->getDeviceWork($data, $user);
  360. $array = array_merge_recursive($array, $array2);
  361. $array3 = $this->getDeviceOldWork($data, $user);
  362. $array = array_merge_recursive($array, $array3);
  363. $array3 = $this->getFeeOrder($data, $user);
  364. $array = array_merge_recursive($array, $array3);
  365. $passTime = [
  366. $startDateTimeUtc->format('Y-m-d'),
  367. $endDateTimeUtc->format('Y-m-d')
  368. ];
  369. list($status, $return) = (new StatisticService())->employeeSalarySummary(['time' => $passTime], $user);
  370. if(! $status) return [false, $return];
  371. $total = array_sum(array_column($return,'money'));
  372. $array['rd_total_money'] = $total;
  373. return [true, $array];
  374. }
  375. private function getManWork($data, $user){
  376. $model = DailyPwOrder::Clear($user, $data);
  377. $id = $model->where('del_time', 0)
  378. ->where('order_time', '>=', $data['start_time'])
  379. ->where('order_time', '<=', $data['end_time'])
  380. ->pluck('id')
  381. ->toArray();
  382. $details = DailyPwOrderDetails::where('del_time', 0)
  383. ->whereIn('main_id', $id) // 💡 避坑提示:原代码如果是数组 $id,这里建议用 whereIn
  384. ->select('employee_id', 'total_work_min')
  385. ->get()->toArray();
  386. // 1. 计算当月总工时(将二维数组中所有 'total_work_min' 字段求和)
  387. $totalWorkMin = array_sum(array_column($details, 'total_work_min'));
  388. // 2. 如果你需要把分钟数转换为“小时”,可以除以 60(保留两位小数,根据业务需要决定是否转换)
  389. $totalWorkHours = round($totalWorkMin / 60, 2);
  390. // 3. 计算参与人员总数(先提取所有员工ID,再通过 array_unique 去重,最后 count 计算数量)
  391. $employeeIds = array_column($details, 'employee_id');
  392. $uniqueEmployeeIds = array_unique($employeeIds);
  393. $totalEmployees = count($uniqueEmployeeIds);
  394. return [
  395. 'man_total_work_hours' => $totalWorkHours, // 当月总工时
  396. 'man_total_employees' => $totalEmployees, // 参与人员总数
  397. ];
  398. }
  399. private function getDeviceWork($data, $user){
  400. $model = DailyDwOrder::Clear($user, $data);
  401. $id = $model->where('del_time', 0)
  402. ->where('order_time', '>=', $data['start_time'])
  403. ->where('order_time', '<=', $data['end_time'])
  404. ->pluck('id')
  405. ->toArray();
  406. $details = DailyDwOrderDetails::where('del_time', 0)
  407. ->whereIn('main_id', $id) // 💡 避坑提示:原代码如果是数组 $id,这里建议用 whereIn
  408. ->select('device_id', 'total_work_min')
  409. ->get()->toArray();
  410. // 1. 计算当月总工时(将二维数组中所有 'total_work_min' 字段求和)
  411. $totalWorkMin = array_sum(array_column($details, 'total_work_min'));
  412. // 2. 如果你需要把分钟数转换为“小时”,可以除以 60(保留两位小数,根据业务需要决定是否转换)
  413. $totalWorkHours = round($totalWorkMin / 60, 2);
  414. $employeeIds = array_column($details, 'device_id');
  415. $uniqueEmployeeIds = array_unique($employeeIds);
  416. $totalEmployees = count($uniqueEmployeeIds);
  417. return [
  418. 'device_total_work_hours' => $totalWorkHours, // 当月总工时
  419. 'device_total_num' => $totalEmployees, // 参与人员总数
  420. ];
  421. }
  422. private function getDeviceOldWork($data, $user){
  423. $model = MonthlyDdOrder::Clear($user, $data);
  424. $id = $model->where('del_time', 0)
  425. ->where('month', '>=', $data['start_time'])
  426. ->where('month', '<=', $data['end_time'])
  427. ->pluck('id')
  428. ->toArray();
  429. $details = MonthlyDdOrderDetails::where('del_time', 0)
  430. ->whereIn('main_id', $id)
  431. ->select('device_id', 'depreciation_amount')
  432. ->get()->toArray();
  433. $totalWorkMin = array_sum(array_column($details, 'depreciation_amount'));
  434. return [
  435. 'device_total_money' => $totalWorkMin,
  436. ];
  437. }
  438. private function getFeeOrder($data, $user){
  439. $model = ExpenseClaims::Clear($user, $data);
  440. $id = $model->where('del_time', 0)
  441. ->where('month', '>=', $data['start_time'])
  442. ->where('month', '<=', $data['end_time'])
  443. ->pluck('id')
  444. ->toArray();
  445. $details = ExpenseClaimsDetails::where('del_time', 0)
  446. ->whereIn('expense_claims_id', $id)
  447. ->select('item_id', 'amount')
  448. ->get()->toArray();
  449. $totalWorkMin = array_sum(array_column($details, 'amount'));
  450. $employeeIds = array_column($details, 'item_id');
  451. $uniqueEmployeeIds = array_unique($employeeIds);
  452. $totalEmployees = count($uniqueEmployeeIds);
  453. return [
  454. 'fee_total_money' => $totalWorkMin,
  455. 'fee_total_num' => $totalEmployees,
  456. ];
  457. }
  458. public function homePageItemData($data, $user){
  459. // 项目相关查询
  460. $item = $this->ItemAboutSearch($data, $user);
  461. }
  462. private function ItemAboutSearch($data, $user){
  463. $model = Item::TopClear($user,$data);
  464. $model = $model->where('del_time',0)
  465. ->select('title','start_time','end_time','state','charge_id','')
  466. ->orderby('id', 'desc');
  467. }
  468. }