BIService.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. <?php
  2. namespace App\Service;
  3. use App\Model\DailyPwOrder;
  4. use App\Model\DailyPwOrderDetails;
  5. use App\Model\Depart;
  6. use App\Model\Employee;
  7. use App\Model\EmployeeDepartPermission;
  8. use App\Model\ExpenseClaims;
  9. use App\Model\ExpenseClaimsDetails;
  10. use App\Model\Fee;
  11. use App\Model\Item;
  12. use App\Model\MonthlyDdOrder;
  13. use App\Model\MonthlyDdOrderDetails;
  14. use App\Model\MonthlyPsOrder;
  15. use App\Model\MonthlyPsOrderDetails;
  16. use App\Model\PLeaveOverOrder;
  17. use App\Model\PLeaveOverOrderDetails;
  18. use App\Model\WorkRangeDetails;
  19. use Illuminate\Support\Facades\DB;
  20. use Illuminate\Support\Facades\Hash;
  21. class BIService extends Service
  22. {
  23. private function commonRule($data)
  24. {
  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. if (!isset($data['month_start'])) $month_start = date('Y-01-01', strtotime('-1 year'));
  32. else $month_start = date('Y-m-01', strtotime($data['month_start']));
  33. if (!isset($data['month_end'])) $month_end = date('Y-01-01', strtotime('+1 year', strtotime($month_start)));
  34. else {
  35. $start_year = date('Y', strtotime($month_start));
  36. $end_year = date('Y', strtotime($data['month_end']));
  37. if ($start_year != $end_year) return [false, "查询不得跨年!", ""];
  38. $month_end = date('Y-m-01', strtotime($data['month_end'] . ' +1 month'));
  39. }
  40. return [true, strtotime($month_start), strtotime($month_end)];
  41. }
  42. public function homePageData($data, $user)
  43. {
  44. list($status, $month_start, $month_end) = $this->commonRule($data);
  45. if (!$status) return [false, $month_start];
  46. //人员费用
  47. list($total_man, $salary_map) = $this->getEmployeeSalary($month_start, $month_end, $data, $user);
  48. //折旧费用
  49. list($total_zj, $zj_map) = $this->getDeviceSalary($month_start, $month_end, $data, $user);
  50. //费用报销
  51. list($total_other, $other_map, $return_fee) = $this->getFeeItemSalary($month_start, $month_end, $data, $user);
  52. //研发费用总额
  53. $total = bcadd(bcadd($total_man, $total_zj,3), $total_other,3);
  54. //研发费用结构
  55. $rd = [
  56. 0 => [
  57. 'title' => '人工费用',
  58. 'rate' => $total == 0 ? 0 : bcmul(bcdiv($total_man,$total,4),100,2),
  59. 'rate_unit' => '%',
  60. 'total' => $total_man,
  61. 'total_unit' => '¥',
  62. ],
  63. 1 => [
  64. 'title' => '折旧费用',
  65. 'rate' => $total == 0 ? 0 :bcmul(bcdiv($total_zj,$total,4),100,2),
  66. 'rate_unit' => '%',
  67. 'total' => $total_zj,
  68. 'total_unit' => '¥',
  69. ],
  70. 2 => [
  71. 'title' => '费用报销',
  72. 'rate' => $total == 0 ? 0 :bcmul(bcdiv($total_other,$total,4),100,2),
  73. 'rate_unit' => '%',
  74. 'total' => $total_other,
  75. 'total_unit' => '¥',
  76. ],
  77. ];
  78. //研发费用趋势
  79. $rd_trend = $this->makeTrend($month_start, $salary_map, $zj_map, $other_map);
  80. //研发费用报销占比
  81. $rd_rate = $return_fee;
  82. //加班 请假 总时长
  83. list($over_time, $leave_time) = $this->overLeave($month_start, $month_end, $data, $user);
  84. // 项目人员工时汇总
  85. $man_work = $this->manWork($month_start, $month_end, $data, $user);
  86. //加计扣除金额
  87. $discount = 0; //todo
  88. return [true, [
  89. 'rd_total' => $total,
  90. 'rd_unit' => '¥',
  91. 'rd_title' => '研发费用总额', //-----
  92. 'discount_total' => $discount,
  93. 'discount_unit' => '¥',
  94. 'discount_title' => '加计扣除金额', //------
  95. 'over_time' => $over_time, // 加班
  96. 'leave_time' => $leave_time, // 请假
  97. 'rd' => $rd, //研发费用结构
  98. 'rd_trend' => $rd_trend, //研发费用趋势
  99. 'rd_rate' => $rd_rate, // 研发费用报销占比
  100. 'man_work' => $man_work, //项目人员工时汇总
  101. ]];
  102. }
  103. private function getEmployeeSalary($month_start, $month_end, $data, $user)
  104. {
  105. $monthly_ps_order = MonthlyPsOrder::Clear($user, $data)
  106. ->where('del_time', 0)
  107. ->where("month", ">=", $month_start)
  108. ->where("month", "<", $month_end)
  109. ->pluck('month','id')
  110. ->toArray();
  111. $monthly_ps_order_ids = array_keys($monthly_ps_order);
  112. $month_employee_salary = MonthlyPsOrderDetails::whereIn('main_id', $monthly_ps_order_ids)
  113. ->select("base_salary","performance_salary","other","bonus", "main_id")
  114. ->get()
  115. ->toArray();
  116. $total = 0;
  117. $salary_map = [];
  118. foreach ($month_employee_salary as $value) {
  119. $currentAmount = bcadd($value['base_salary'], $value['performance_salary'],3);
  120. $currentAmount = bcadd($currentAmount, $value['other'],3);
  121. $currentAmount = bcadd($currentAmount, $value['bonus'],3);
  122. if(isset($monthly_ps_order[$value['main_id']])){
  123. $month = $monthly_ps_order[$value['main_id']];
  124. if(isset($salary_map[$month])){
  125. $salary_map[$month] = bcadd($salary_map[$month], $currentAmount,3);
  126. }else{
  127. $salary_map[$month] = $currentAmount;
  128. }
  129. }
  130. $total = bcadd($total, $currentAmount,3);
  131. }
  132. return [$total, $salary_map];
  133. }
  134. private function getDeviceSalary($month_start, $month_end, $data, $user)
  135. {
  136. $monthly_dd_order= MonthlyDdOrder::Clear($user, $data)
  137. ->where('del_time', 0)
  138. ->where("month", ">=", $month_start)
  139. ->where("month", "<", $month_end)
  140. ->pluck('month','id')
  141. ->toArray();
  142. $monthly_dd_order_ids = array_keys($monthly_dd_order);
  143. $month_device_salary = MonthlyDdOrderDetails::whereIn('main_id', $monthly_dd_order_ids)
  144. ->select("depreciation_amount as amount", "main_id")
  145. ->get()
  146. ->toArray();
  147. $total = 0;
  148. $amount_map = [];
  149. foreach ($month_device_salary as $value) {
  150. $currentAmount = (string)$value['amount'];
  151. if(isset($monthly_dd_order[$value['main_id']])){
  152. $month = $monthly_dd_order[$value['main_id']];
  153. if(isset($amount_map[$month])){
  154. $amount_map[$month] = bcadd($amount_map[$month], $currentAmount,3);
  155. }else{
  156. $amount_map[$month] = $currentAmount;
  157. }
  158. }
  159. $total = bcadd($total, $currentAmount,3);
  160. }
  161. return [$total, $amount_map];
  162. }
  163. private function getFeeItemSalary($month_start, $month_end, $data, $user)
  164. {
  165. $e_order = ExpenseClaims::Clear($user, $data)
  166. ->where('del_time', 0)
  167. ->where("month", ">=", $month_start)
  168. ->where("month", "<", $month_end)
  169. ->pluck('month','id')
  170. ->toArray();
  171. $e_order_ids = array_keys($e_order);
  172. $e_order_detail = ExpenseClaimsDetails::whereIn('expense_claims_id', $e_order_ids)
  173. ->select("amount", "expense_claims_id as main_id", "fee_id")
  174. ->get()
  175. ->toArray();
  176. $fee = Fee::TopClear($user, $data);
  177. $fee_map = $fee->whereIn('id', array_unique(array_column($e_order_detail,'fee_id')))->pluck('title','id')->toArray();
  178. $total = 0;
  179. $amount_map = [];
  180. $fee_amount_map = [];
  181. foreach ($e_order_detail as $value) {
  182. $currentAmount = (string)$value['amount'];
  183. if(isset($e_order[$value['main_id']])){
  184. $month = $e_order[$value['main_id']];
  185. if(isset($amount_map[$month])){
  186. $amount_map[$month] = bcadd($amount_map[$month], $currentAmount,3);
  187. }else{
  188. $amount_map[$month] = $currentAmount;
  189. }
  190. }
  191. $fee_tmp = $fee_map[$value['fee_id']] ?? "";
  192. if(! empty($fee_tmp)){
  193. if(isset($fee_amount_map[$fee_tmp])){
  194. $fee_amount_map[$fee_tmp] = bcadd($fee_amount_map[$fee_tmp], $currentAmount,3);
  195. }else{
  196. $fee_amount_map[$fee_tmp] = $currentAmount;
  197. }
  198. }
  199. $total = bcadd($total, $currentAmount,3);
  200. }
  201. $return_fee = [];
  202. foreach ($fee_amount_map as $key => $value){
  203. $amount = (string)$value;
  204. $return_fee[] = [
  205. "title" => $key,
  206. "total" => $amount,
  207. "total_unit" => "元",
  208. "rate" => bcmul(bcdiv($amount,$total,4),100,2),
  209. "rate_unit" => "%"
  210. ];
  211. }unset($fee_amount_map);
  212. return [$total, $amount_map, $return_fee];
  213. }
  214. private function makeTrend($month_start, $salary_map, $zj_map, $other_map)
  215. {
  216. $trend = [];
  217. for ($i = 0; $i < 12; $i++) {
  218. // 计算每个月第一天的时间戳作为 Key
  219. $timestamp = strtotime("+$i month", $month_start);
  220. $monthName = ($i + 1) . '月';
  221. $trend[$timestamp] = [
  222. 'month' => $monthName,
  223. // 'salary' => "0.000",
  224. // 'zj' => "0.000",
  225. // 'other' => "0.000",
  226. 'total' => "0.000",
  227. ];
  228. }
  229. // 2. 将传入的 Map 数据填充进去
  230. foreach ($trend as $timestamp => &$item) {
  231. // 使用 ?? "0" 容错,并强制转为 string 保证 bcadd 精度
  232. $s = $salary_map[$timestamp] ?? "0";
  233. $z = $zj_map[$timestamp] ?? "0";
  234. $o = $other_map[$timestamp] ?? "0";
  235. // $item['salary'] = bcadd($s, "0", 3);
  236. // $item['zj'] = bcadd($z, "0", 3);
  237. // $item['other'] = bcadd($o, "0", 3);
  238. // 计算该月总计
  239. $item['total'] = bcadd(bcadd($s, $z, 3), $o, 3);
  240. }
  241. return array_values($trend);
  242. }
  243. private function overLeave($month_start, $month_end, $data, $user)
  244. {
  245. $id = PLeaveOverOrder::Clear($user, $data)
  246. ->where('del_time', 0)
  247. ->where("order_time", ">=", $month_start)
  248. ->where("order_time", "<", $month_end)
  249. ->pluck('id')
  250. ->toArray();
  251. $p = PLeaveOverOrderDetails::whereIn('main_id', $id)
  252. ->select("type", "total_min")
  253. ->get()
  254. ->toArray();
  255. $total = $total_1 = 0;
  256. foreach ($p as $value) {
  257. $total_min = (string)$value['total_min'];
  258. if($value['type'] == PLeaveOverOrderDetails::TYPE_ONE){
  259. $total = bcadd($total, $total_min,2);
  260. }else{
  261. $total_1 = bcadd($total_1, $total_min,2);
  262. }
  263. }
  264. // 最终返回或输出前转换
  265. $total_hours = bcdiv($total, 60, 2);
  266. $total_1_hours = bcdiv($total_1, 60, 2);
  267. return [$total_1_hours, $total_hours];
  268. }
  269. private function manWork($month_start, $month_end, $data, $user)
  270. {
  271. $daily_pw = DailyPwOrder::Clear($user, $data)
  272. ->where('del_time', 0)
  273. ->where("order_time", ">=", $month_start)
  274. ->where("order_time", "<", $month_end)
  275. ->select('id','item_id')
  276. ->get()->toArray();
  277. $item_map = Item::whereIn('id',array_unique(array_column($daily_pw,'item_id')))
  278. ->pluck('title','id')
  279. ->toArray();
  280. $p = DailyPwOrderDetails::whereIn('main_id', array_column($daily_pw,'id'))
  281. ->select("item_id", "total_work_min")
  282. ->get()
  283. ->toArray();
  284. $total = 0;
  285. $map = [];
  286. foreach ($p as $value) {
  287. $total_min = (string)$value['total_work_min'];
  288. if(isset($map[$value['item_id']])){
  289. $map[$value['item_id']] = bcadd($map[$value['item_id']], $total_min,2);
  290. }else{
  291. $map[$value['item_id']] = $total_min;
  292. }
  293. $total = bcadd($total, $total_min,2);
  294. }
  295. $return = [];
  296. foreach ($map as $key => $value){
  297. $rate = bcmul(bcdiv($value, $total,4),100,2);
  298. $total_hours = bcdiv($value, 60, 2);
  299. $return[] = [
  300. 'title' => $item_map[$key] ?? "",
  301. 'total_work_hour' => $total_hours,
  302. 'total_work_hour_unit' => "小时",
  303. "rate" => $rate,
  304. "rate_unit" => "%",
  305. ];
  306. }
  307. return $return;
  308. }
  309. public function initializationCompany($data, $user){
  310. if(empty($data['title'])) return [false, '公司名称不能为空'];
  311. if(empty($data['code'])) return [false, '公司代码不能为空'];
  312. // ^[A-Za-z]+$ 表示从头到尾只能是英文字母(不区分大小写)
  313. if (!preg_match('/^[A-Za-z]+$/', $data['code']) || mb_strlen($data['code']) < 4) return [false, "公司代码必须全为英文且长度需大于等于 4 位"];
  314. $exists = Depart::where('parent_id', 0)
  315. ->where('del_time', 0)
  316. ->where(function($query) use ($data) {
  317. $query->where('title', $data['title'])
  318. ->orWhere('code', $data['code']);
  319. })
  320. ->exists();
  321. if($exists) return [false, '公司名称或公司代码已存在,新增失败'];
  322. $account = $data['code'] . "_" . 'admin';
  323. $exists = Employee::where('del_time', 0)
  324. ->where('account', $account)
  325. ->exists();
  326. if($exists) return [false, '创建账号已存在,新增失败'];
  327. try {
  328. DB::beginTransaction();
  329. //创建公司
  330. $model = new Depart();
  331. $model->parent_id = 0;
  332. $model->title = $data['title'];
  333. $model->code = $data['code'];
  334. $model->top_depart_id = 0;
  335. $model->save();
  336. //公司ID
  337. $top_depart_id = $model->id;
  338. $password = $this->generateAt8CharPassword();
  339. //创建账号
  340. $model_2 = new Employee();
  341. $model_2->title = $data['title'];
  342. $model_2->code = $data['code'];
  343. $model_2->account = $account;
  344. $model_2->password = Hash::make($password);
  345. $model_2->is_admin = Employee::IS_ADMIN_TWO;
  346. $model_2->top_depart_id = $top_depart_id;
  347. $model_2->state = Employee::TYPE_ONE;
  348. $model_2->save();
  349. $employee_id = $model_2->id;
  350. //关联人员的部门初始信息
  351. $model_3 = new EmployeeDepartPermission();
  352. $model_3->employee_id = $employee_id;
  353. $model_3->depart_id = 0;
  354. $model_3->top_depart_id = $top_depart_id;
  355. $model_3->save();
  356. //公司上班时段
  357. $work_range[] = [
  358. 'top_depart_id' => $top_depart_id,
  359. 'start_time_hour' => 9,
  360. 'start_time_min' => 0,
  361. 'end_time_hour' => 12,
  362. 'end_time_min' => 0,
  363. 'total_work_min' => 180,
  364. ];
  365. $work_range[] = [
  366. 'top_depart_id' => $top_depart_id,
  367. 'start_time_hour' => 13,
  368. 'start_time_min' => 0,
  369. 'end_time_hour' => 18,
  370. 'end_time_min' => 0,
  371. 'total_work_min' => 300,
  372. ];
  373. WorkRangeDetails::insert($work_range);
  374. DB::commit();
  375. }catch (\Throwable $exception){
  376. DB::rollBack();
  377. return [false, $exception->getMessage()];
  378. }
  379. return [true, ['account' => $account, 'password' => $password]];
  380. }
  381. function generateAt8CharPassword() {
  382. $lettersNumbers = 'abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789';
  383. $password = '@'; // 1. 先把固定的 @ 放进去
  384. // 2. 随机抽取 7 位英文或数字
  385. for ($i = 0; $i < 7; $i++) {
  386. $password .= $lettersNumbers[random_int(0, strlen($lettersNumbers) - 1)];
  387. }
  388. // 3. 打乱顺序,让 @ 的位置不固定
  389. return str_shuffle($password);
  390. }
  391. }