cqp преди 3 месеца
родител
ревизия
3071837330

+ 76 - 0
app/Http/Controllers/Api/PersonSalaryController.php

@@ -0,0 +1,76 @@
+<?php
+
+namespace App\Http\Controllers\Api;
+
+use App\Service\PersonSalaryService;
+use Illuminate\Http\Request;
+
+class PersonSalaryController extends BaseController
+{
+    public function monthlyPsOrderEdit(Request $request)
+    {
+        $service = new PersonSalaryService();
+        $user = $request->userData;
+        list($status,$data) = $service->monthlyPsOrderEdit($request->all(),$user);
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+    }
+
+    public function monthlyPsOrderAdd(Request $request)
+    {
+        $service = new PersonSalaryService();
+        $user = $request->userData;
+        list($status,$data) = $service->monthlyPsOrderAdd($request->all(),$user);
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+
+    }
+
+    public function monthlyPsOrderDel(Request $request)
+    {
+        $service = new PersonSalaryService();
+        $user = $request->userData;
+        list($status,$data) = $service->monthlyPsOrderDel($request->all());
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+
+    }
+
+    public function monthlyPsOrderList(Request $request)
+    {
+        $service = new PersonSalaryService();
+        $user = $request->userData;
+        list($status,$data) = $service->monthlyPsOrderList($request->all(),$user);
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+    }
+
+    public function monthlyPsOrderDetail(Request $request)
+    {
+        $service = new PersonSalaryService();
+        $user = $request->userData;
+        list($status,$data) = $service->monthlyPsOrderDetail($request->all(),$user);
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+    }
+}

+ 17 - 0
app/Model/MonthlyPsOrder.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace App\Model;
+
+class MonthlyPsOrder extends DataScopeBaseModel
+{
+    //人员月度工资单
+    protected $table = "monthly_ps_order"; //指定表
+    const CREATED_AT = 'crt_time';
+    const UPDATED_AT = 'upd_time';
+    protected $dateFormat = 'U';
+    const employee_column = "crt_id";
+
+    public static $field = ['id','code','crt_id','crt_time','month'];
+
+    const Order_type = "monthly_ps_order";
+}

+ 13 - 0
app/Model/MonthlyPsOrderDetails.php

@@ -0,0 +1,13 @@
+<?php
+
+namespace App\Model;
+
+class MonthlyPsOrderDetails extends DataScopeBaseModel
+{
+    //人员月度工资详细
+    protected $guarded = [];
+    protected $table = "monthly_ps_order_details"; //指定表
+    const CREATED_AT = 'crt_time';
+    const UPDATED_AT = 'upd_time';
+    protected $dateFormat = 'U';
+}

+ 1 - 1
app/Service/DeviceWorkService.php

@@ -148,7 +148,7 @@ class DeviceWorkService extends Service
         $customer = MonthlyDwOrder::where('del_time',0)
         $customer = MonthlyDwOrder::where('del_time',0)
             ->where('id',$data['id'])
             ->where('id',$data['id'])
             ->first();
             ->first();
-        if(empty($customer)) return [false,'项目不存在或已被删除'];
+        if(empty($customer)) return [false,'设备月度工时单不存在或已被删除'];
         $customer = $customer->toArray();
         $customer = $customer->toArray();
         $customer['crt_name'] = Employee::where('id',$customer['crt_id'])->value('title');
         $customer['crt_name'] = Employee::where('id',$customer['crt_id'])->value('title');
         $customer['crt_time'] = $customer['crt_time'] ? date("Y-m-d H:i:s",$customer['crt_time']): '';
         $customer['crt_time'] = $customer['crt_time'] ? date("Y-m-d H:i:s",$customer['crt_time']): '';

+ 18 - 0
app/Service/ExportFileService.php

@@ -191,6 +191,24 @@ class ExportFileService extends Service
         return [true, $this->saveExportData($return,$header)];
         return [true, $this->saveExportData($return,$header)];
     }
     }
 
 
+    public function monthPsOrder($ergs,$user){
+        // 导出数据
+        $return = [];
+        $header_default = $user['e_header_default'];
+        $column = array_column($header_default,'export');
+        $header = array_column($header_default,'value');
+
+        $service = new PersonSalaryService();
+        $model = $service->monthlyPsOrderCommon($ergs, $user);
+
+        $model->chunk(500,function ($data) use(&$return, $service, $column){
+            // 直接处理这一批主表数据,将其与详情合并平铺
+            $service->fillDataForExport($data->toArray(), $column, $return);
+        });
+
+        return [true, $this->saveExportData($return,$header)];
+    }
+
     public function saveExportData($data, $headers, $type = 'default',$file_name = ''){
     public function saveExportData($data, $headers, $type = 'default',$file_name = ''){
         if(empty($file_name)) $file_name = self::$filename . "_". date("Y-m-d") . "_". rand(1000,9999);
         if(empty($file_name)) $file_name = self::$filename . "_". date("Y-m-d") . "_". rand(1000,9999);
         $filename =  $file_name . '.' . 'xlsx';
         $filename =  $file_name . '.' . 'xlsx';

+ 289 - 11
app/Service/ImportService.php

@@ -9,6 +9,7 @@ use App\Model\Employee;
 use App\Model\Fee;
 use App\Model\Fee;
 use App\Model\Item;
 use App\Model\Item;
 use App\Model\ItemDetails;
 use App\Model\ItemDetails;
+use App\Model\MonthlyPsOrder;
 use App\Model\MonthlyPwOrder;
 use App\Model\MonthlyPwOrder;
 use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\DB;
 use Maatwebsite\Excel\Facades\Excel;
 use Maatwebsite\Excel\Facades\Excel;
@@ -24,6 +25,7 @@ class ImportService extends Service
         'fee', // 费用
         'fee', // 费用
         'monthPwOrder', // 人员月度研发工时单
         'monthPwOrder', // 人员月度研发工时单
         'monthDwOrder', // 设备月度研发工时单
         'monthDwOrder', // 设备月度研发工时单
+        'monthPsOrder', // 人员月度工资单
     ];
     ];
 
 
     public function getTableTitleXls($data,$user){
     public function getTableTitleXls($data,$user){
@@ -110,6 +112,17 @@ class ImportService extends Service
         return [true, [$config_array, $filename]];
         return [true, [$config_array, $filename]];
     }
     }
 
 
+    private function monthPsOrder($data,$user){
+        $config = $this->getTableConfig($data['type']);
+        if(empty($config)) return [false, ['导入配置表头文件不存在','']];
+
+        $config_array = $config['array'] ?? [];
+        //生成下载文件
+        $filename =  $config['name'] . "导入模板_" . time() . '.' . 'xlsx';
+
+        return [true, [$config_array, $filename]];
+    }
+
     //导入入口
     //导入入口
     public function importAll($data,$user){
     public function importAll($data,$user){
 //        //不超时
 //        //不超时
@@ -246,6 +259,8 @@ class ImportService extends Service
         $map_type = array_flip(Device::$type);
         $map_type = array_flip(Device::$type);
         $map_type_2 = array_flip(Device::Use);
         $map_type_2 = array_flip(Device::Use);
         foreach ($array as $rowIndex => $value) {
         foreach ($array as $rowIndex => $value) {
+            $displayLine = $rowIndex + 1;
+
             $valCode = $value[$codeIdx] ?? '';
             $valCode = $value[$codeIdx] ?? '';
 
 
             // 记录更新 ID
             // 记录更新 ID
@@ -254,12 +269,12 @@ class ImportService extends Service
             }
             }
 
 
             if(empty($map_type[$value[$typeIdx]])){
             if(empty($map_type[$value[$typeIdx]])){
-                $errors[] = "第{$rowIndex}行固定资产类型错误";
+                $errors[] = "第{$displayLine}行固定资产类型错误";
             }else{
             }else{
                 $array[$rowIndex][$typeIdx] = $map_type[$value[$typeIdx]];
                 $array[$rowIndex][$typeIdx] = $map_type[$value[$typeIdx]];
             }
             }
             if(empty($map_type_2[$value[$type2Idx]])){
             if(empty($map_type_2[$value[$type2Idx]])){
-                $errors[] = "第{$rowIndex}行是否启用错误";
+                $errors[] = "第{$displayLine}行是否启用错误";
             }else{
             }else{
                 $array[$rowIndex][$type2Idx] = $map_type_2[$value[$type2Idx]];
                 $array[$rowIndex][$type2Idx] = $map_type_2[$value[$type2Idx]];
             }
             }
@@ -268,7 +283,7 @@ class ImportService extends Service
             if($dateIdx !== false && !empty($value[$dateIdx])){
             if($dateIdx !== false && !empty($value[$dateIdx])){
                 list($status, $msg) = $this->convertExcelCellToDate($value[$dateIdx]);
                 list($status, $msg) = $this->convertExcelCellToDate($value[$dateIdx]);
                 if(!$status) {
                 if(!$status) {
-                    $errors[] = "第{$rowIndex}行日期格式错误";
+                    $errors[] = "第{$displayLine}行日期格式错误";
                 } else {
                 } else {
                     $array[$rowIndex][$dateIdx] = $msg;
                     $array[$rowIndex][$dateIdx] = $msg;
                 }
                 }
@@ -443,6 +458,8 @@ class ImportService extends Service
         $state_type_map = array_flip(Item::State_Type);
         $state_type_map = array_flip(Item::State_Type);
 
 
         foreach ($array as $rowIndex => $rowValue) {
         foreach ($array as $rowIndex => $rowValue) {
+            $displayLine = $rowIndex + 1;
+
             $valCode = $rowValue[$codeIdx] ?? '';
             $valCode = $rowValue[$codeIdx] ?? '';
 
 
             // 1. 判定更新还是新增
             // 1. 判定更新还是新增
@@ -453,7 +470,7 @@ class ImportService extends Service
             // 2. 状态校验
             // 2. 状态校验
             $state_text = $rowValue[$stateIdx] ?? '';
             $state_text = $rowValue[$stateIdx] ?? '';
             if (!isset($state_type_map[$state_text])) {
             if (!isset($state_type_map[$state_text])) {
-                $errors[] = "第{$rowIndex}行:状态[{$state_text}]无效";
+                $errors[] = "第{$displayLine}行:状态[{$state_text}]无效";
             } else {
             } else {
                 $array[$rowIndex][$stateIdx] = $state_type_map[$state_text];
                 $array[$rowIndex][$stateIdx] = $state_type_map[$state_text];
             }
             }
@@ -462,7 +479,7 @@ class ImportService extends Service
             foreach ([$dateIdx, $date2Idx] as $dIdx) {
             foreach ([$dateIdx, $date2Idx] as $dIdx) {
                 if ($dIdx !== false && !empty($rowValue[$dIdx])) {
                 if ($dIdx !== false && !empty($rowValue[$dIdx])) {
                     list($s, $m) = $this->convertExcelCellToDate($rowValue[$dIdx]);
                     list($s, $m) = $this->convertExcelCellToDate($rowValue[$dIdx]);
-                    if (!$s) $errors[] = "第{$rowIndex}行:日期格式非法";
+                    if (!$s) $errors[] = "第{$displayLine}行:日期格式非法";
                     else $array[$rowIndex][$dIdx] = $m;
                     else $array[$rowIndex][$dIdx] = $m;
                 }
                 }
             }
             }
@@ -472,7 +489,7 @@ class ImportService extends Service
                 foreach (explode(',', $rowValue[$manIdx]) as $mNum) {
                 foreach (explode(',', $rowValue[$manIdx]) as $mNum) {
                     $mNum = trim($mNum);
                     $mNum = trim($mNum);
                     if (!isset($man_map[$mNum])) {
                     if (!isset($man_map[$mNum])) {
-                        $errors[] = "第{$rowIndex}行:人员编码[{$mNum}]不存在";
+                        $errors[] = "第{$displayLine}行:人员工号[{$mNum}]不存在";
                     } else {
                     } else {
                         $detail_storage[$rowIndex][] = [
                         $detail_storage[$rowIndex][] = [
                             'type' => ItemDetails::type_one,
                             'type' => ItemDetails::type_one,
@@ -487,7 +504,7 @@ class ImportService extends Service
                 foreach (explode(',', $rowValue[$deviceIdx]) as $dCode) {
                 foreach (explode(',', $rowValue[$deviceIdx]) as $dCode) {
                     $dCode = trim($dCode);
                     $dCode = trim($dCode);
                     if (!isset($device_map[$dCode])) {
                     if (!isset($device_map[$dCode])) {
-                        $errors[] = "第{$rowIndex}行:资产编码[{$dCode}]不存在";
+                        $errors[] = "第{$displayLine}行:设备的资产编码[{$dCode}]不存在";
                     } else {
                     } else {
                         $detail_storage[$rowIndex][] = [
                         $detail_storage[$rowIndex][] = [
                             'type' => ItemDetails::type_two,
                             'type' => ItemDetails::type_two,
@@ -651,6 +668,8 @@ class ImportService extends Service
 
 
         // 3. 逐行校验
         // 3. 逐行校验
         foreach ($array as $rowIndex => $value) {
         foreach ($array as $rowIndex => $value) {
+            $displayLine = $rowIndex + 1;
+
             $valCode = trim($value[$codeIdx] ?? '');
             $valCode = trim($value[$codeIdx] ?? '');
             $valParentCode = trim($value[$parentIdx] ?? '');
             $valParentCode = trim($value[$parentIdx] ?? '');
 
 
@@ -665,13 +684,13 @@ class ImportService extends Service
                 // --- A. 存在性校验 ---
                 // --- A. 存在性校验 ---
                 // 这里会检查 006 是否在数据库,或者是否在本次 Excel 的其他行中
                 // 这里会检查 006 是否在数据库,或者是否在本次 Excel 的其他行中
                 if (!isset($dbFeeMap[$valParentCode]) && !isset($excelCodesMap[$valParentCode])) {
                 if (!isset($dbFeeMap[$valParentCode]) && !isset($excelCodesMap[$valParentCode])) {
-                    $errors[] = "第{$rowIndex}行:上级编码[{$valParentCode}]在系统和文件中均不存在";
+                    $errors[] = "第{$displayLine}行:上级编码[{$valParentCode}]在系统和文件中均不存在";
                     continue;
                     continue;
                 }
                 }
 
 
                 // --- B. 自引用校验 ---
                 // --- B. 自引用校验 ---
                 if ($valCode === $valParentCode) {
                 if ($valCode === $valParentCode) {
-                    $errors[] = "第{$rowIndex}行:上级编码不能是自身";
+                    $errors[] = "第{$displayLine}行:上级编码不能是自身";
                     continue;
                     continue;
                 }
                 }
 
 
@@ -680,9 +699,9 @@ class ImportService extends Service
                 $res = $this->findLoopInAncestors($valParentCode, $valCode, $currentExcelMap, $dbFeeMap, $visited);
                 $res = $this->findLoopInAncestors($valParentCode, $valCode, $currentExcelMap, $dbFeeMap, $visited);
                 if ($res !== false) {
                 if ($res !== false) {
                     if ($res['type'] === 'LOOP_SELF') {
                     if ($res['type'] === 'LOOP_SELF') {
-                        $errors[] = "第{$rowIndex}行:编码[{$valCode}]与上级[{$valParentCode}]存在循环引用";
+                        $errors[] = "第{$displayLine}行:编码[{$valCode}]与上级[{$valParentCode}]存在循环引用";
                     } else {
                     } else {
-                        $errors[] = "第{$rowIndex}行:上级[{$valParentCode}]的溯源链条已成环";
+                        $errors[] = "第{$displayLine}行:上级[{$valParentCode}]的溯源链条已成环";
                     }
                     }
                     continue;
                     continue;
                 }
                 }
@@ -878,6 +897,11 @@ class ImportService extends Service
         $startIdx = array_search('start_time', $keys);
         $startIdx = array_search('start_time', $keys);
         $endIdx = array_search('end_time', $keys);
         $endIdx = array_search('end_time', $keys);
 
 
+        $numIdx = array_search('total_days', $keys);
+        $num2Idx = array_search('rd_total_days', $keys);
+        $num3Idx = array_search('total_hours', $keys);
+        $num4Idx = array_search('rd_total_hours', $keys);
+
         // 1. 预加载基础数据
         // 1. 预加载基础数据
         $allEmpNumbers = array_filter(array_unique(array_column($array, $empIdx)));
         $allEmpNumbers = array_filter(array_unique(array_column($array, $empIdx)));
         $dbEmps = Employee::where('del_time', 0)
         $dbEmps = Employee::where('del_time', 0)
@@ -911,6 +935,11 @@ class ImportService extends Service
             $valStartRaw = $row[$startIdx] ?? '';
             $valStartRaw = $row[$startIdx] ?? '';
             $valEndRaw = $row[$endIdx] ?? '';
             $valEndRaw = $row[$endIdx] ?? '';
 
 
+            $valNumRaw = $row[$numIdx] ?? 0;
+            $valNum2Raw = $row[$num2Idx] ?? 0;
+            $valNum3Raw = $row[$num3Idx] ?? 0;
+            $valNum4Raw = $row[$num4Idx] ?? 0;
+
             // --- A. 日期转换校验 ---
             // --- A. 日期转换校验 ---
             // 转换月份
             // 转换月份
             list($mStatus, $valMonthTs) = $this->convertExcelCellToDate($valMonthRaw);
             list($mStatus, $valMonthTs) = $this->convertExcelCellToDate($valMonthRaw);
@@ -978,6 +1007,16 @@ class ImportService extends Service
                 }
                 }
             }
             }
 
 
+            // --- F. 数字校验
+            $res = $this->checkNumber($valNumRaw,0,'non-negative');
+            if(! $res['valid']) $errors[] = "第{$displayLine}行出勤总天数:" . $res['error'];
+            $res = $this->checkNumber($valNum2Raw,0,'non-negative');
+            if(! $res['valid']) $errors[] = "第{$displayLine}行研发出勤总天数:" . $res['error'];
+            $res = $this->checkNumber($valNum3Raw,2,'non-negative');
+            if(! $res['valid']) $errors[] = "第{$displayLine}行出勤总工时:" . $res['error'];
+            $res = $this->checkNumber($valNum4Raw,2,'non-negative');
+            if(! $res['valid']) $errors[] = "第{$displayLine}行研发总工时:" . $res['error'];
+
             // 将转换后的时间戳写回原数组,方便后续写入数据库时直接使用
             // 将转换后的时间戳写回原数组,方便后续写入数据库时直接使用
             $array[$rowIndex][$monthIdx] = $valMonthTs;
             $array[$rowIndex][$monthIdx] = $valMonthTs;
             $array[$rowIndex][$startIdx] = $startTime;
             $array[$rowIndex][$startIdx] = $startTime;
@@ -1110,6 +1149,11 @@ class ImportService extends Service
         $startIdx = array_search('start_time', $keys);
         $startIdx = array_search('start_time', $keys);
         $endIdx = array_search('end_time', $keys);
         $endIdx = array_search('end_time', $keys);
 
 
+        $numIdx = array_search('total_days', $keys);
+        $num2Idx = array_search('rd_total_days', $keys);
+        $num3Idx = array_search('total_hours', $keys);
+        $num4Idx = array_search('rd_total_hours', $keys);
+
         // 1. 预加载基础数据
         // 1. 预加载基础数据
         $allEmpNumbers = array_filter(array_unique(array_column($array, $devIdx)));
         $allEmpNumbers = array_filter(array_unique(array_column($array, $devIdx)));
         $dbDevs = Device::where('del_time', 0)
         $dbDevs = Device::where('del_time', 0)
@@ -1143,6 +1187,11 @@ class ImportService extends Service
             $valStartRaw = $row[$startIdx] ?? '';
             $valStartRaw = $row[$startIdx] ?? '';
             $valEndRaw = $row[$endIdx] ?? '';
             $valEndRaw = $row[$endIdx] ?? '';
 
 
+            $valNumRaw = $row[$numIdx] ?? 0;
+            $valNum2Raw = $row[$num2Idx] ?? 0;
+            $valNum3Raw = $row[$num3Idx] ?? 0;
+            $valNum4Raw = $row[$num4Idx] ?? 0;
+
             // --- A. 日期转换校验 ---
             // --- A. 日期转换校验 ---
             // 转换月份
             // 转换月份
             list($mStatus, $valMonthTs) = $this->convertExcelCellToDate($valMonthRaw);
             list($mStatus, $valMonthTs) = $this->convertExcelCellToDate($valMonthRaw);
@@ -1210,6 +1259,16 @@ class ImportService extends Service
                 }
                 }
             }
             }
 
 
+            // --- F. 数字校验
+            $res = $this->checkNumber($valNumRaw,0,'non-negative');
+            if(! $res['valid']) $errors[] = "第{$displayLine}行出勤总天数:" . $res['error'];
+            $res = $this->checkNumber($valNum2Raw,0,'non-negative');
+            if(! $res['valid']) $errors[] = "第{$displayLine}行研发出勤总天数:" . $res['error'];
+            $res = $this->checkNumber($valNum3Raw,2,'non-negative');
+            if(! $res['valid']) $errors[] = "第{$displayLine}行出勤总工时:" . $res['error'];
+            $res = $this->checkNumber($valNum4Raw,2,'non-negative');
+            if(! $res['valid']) $errors[] = "第{$displayLine}行研发总工时:" . $res['error'];
+
             // 将转换后的时间戳写回原数组,方便后续写入数据库时直接使用
             // 将转换后的时间戳写回原数组,方便后续写入数据库时直接使用
             $array[$rowIndex][$monthIdx] = $valMonthTs;
             $array[$rowIndex][$monthIdx] = $valMonthTs;
             $array[$rowIndex][$startIdx] = $startTime;
             $array[$rowIndex][$startIdx] = $startTime;
@@ -1220,6 +1279,225 @@ class ImportService extends Service
         return [$error_string, $update_map, $dbDevs];
         return [$error_string, $update_map, $dbDevs];
     }
     }
 
 
+    // 人员月度工资单
+    public function monthPsOrderImport($array, $user, $other_param)
+    {
+        $upload = $array[0];
+        list($status, $msg) = $this->compareTableAndReturn($upload, $other_param);
+        if (!$status) return [false, $msg];
+        $table_config = $msg;
+
+        unset($array[0]);
+        if (empty($array)) return [false, '导入数据不能为空'];
+
+        list($array, $error) = $this->checkCommon($array, $table_config);
+        if (!empty($error)) return [0, $error];
+
+        // 2. 详细校验 (这里会返回 update_map 和 dbEmps 映射)
+        list($error, $update_map, $dbEmps) = $this->monthPsOrderCheck($array, $user, $table_config);
+        if (!empty($error)) return [0, $error];
+
+        $keys = array_column($table_config, 'key');
+        $codeIdx = array_search('code', $keys);
+        $monthIdx = array_search('month', $keys);
+        $empIdx = array_search('employee_id', $keys);
+
+        // --- 步骤 1: 数据聚合分组 ---
+        // 将 Excel 数据按“单据”维度归类
+        $groups = [];
+        foreach ($array as $rowIndex => $row) {
+            $cCode = trim($row[$codeIdx] ?? '');
+            $cMonthTs = $row[$monthIdx]; // 校验阶段已转为时间戳
+
+            // 聚合 Key:有编码用编码,没编码用月份标记
+            $aggKey = $cCode ?: "MONTH_" . $cMonthTs;
+
+            if (!isset($groups[$aggKey])) {
+                $groups[$aggKey] = [
+                    'main_id'  => $update_map[$rowIndex] ?? 0, // 如果存在则是编辑
+                    'code'     => $cCode,
+                    'month'    => $cMonthTs,
+                    'details'  => []
+                ];
+            }
+
+            // 准备详情行数据
+            $detailTmp = [];
+            foreach ($table_config as $k => $conf) {
+                if (!$conf['is_main']) {
+                    $fieldKey = $conf['key'];
+                    $fieldVal = $row[$k];
+                    // 如果是人员,转换为 ID
+                    if ($fieldKey == 'employee_id') {
+                        $fieldVal = $dbEmps[$fieldVal] ?? 0;
+                    }
+                    $detailTmp[$fieldKey] = $fieldVal;
+                }
+            }
+            $groups[$aggKey]['details'][] = $detailTmp;
+        }
+
+        // --- 步骤 2: 开启事务写入 ---
+        DB::beginTransaction();
+        try {
+            $time = time();
+            foreach ($groups as $aggKey => $group) {
+                $mainId = $group['main_id'];
+
+                if ($mainId > 0) {
+                    // 删除旧详情
+                    DB::table('monthly_ps_order_details')->where('del_time',0)
+                        ->where('main_id', $mainId)
+                        ->update(['del_time' => $time]);
+                } else {
+                    // B. 新增逻辑
+                    $newCode = $this->generateBillNo([
+                        'top_depart_id' => $user['top_depart_id'],
+                        'type'          => MonthlyPsOrder::Order_type,
+                        'period'        => date("Ym", $group['month'])
+                    ]);
+
+                    $mainId = DB::table('monthly_ps_order')->insertGetId([
+                        'code'          => $newCode,
+                        'month'         => $group['month'],
+                        'top_depart_id' => $user['top_depart_id'],
+                        'crt_id'        => $user['id'],
+                        'crt_time'      => $time,
+                        'upd_time'      => $time,
+                        'del_time'      => 0
+                    ]);
+                }
+
+                // C. 批量插入详情
+                $insertDetails = [];
+                foreach ($group['details'] as $detail) {
+                    $detail['main_id']      = $mainId;
+                    $detail['top_depart_id']= $user['top_depart_id'];
+                    $detail['crt_time']     = $time;
+                    $detail['del_time']     = 0;
+                    $insertDetails[] = $detail;
+                }
+
+                if (!empty($insertDetails)) {
+                    DB::table('monthly_ps_order_details')->insert($insertDetails);
+                }
+            }
+
+            DB::commit();
+        } catch (\Exception $e) {
+            DB::rollBack();
+            return [false, "失败:" . $e->getMessage() . " (行号:" . $e->getLine() . ")"];
+        }
+
+        return [true, ''];
+    }
+
+    private function monthPsOrderCheck(&$array, $user, $table_config)
+    {
+        $keys = array_column($table_config, 'key');
+        $codeIdx = array_search('code', $keys);
+        $monthIdx = array_search('month', $keys);
+        $empIdx = array_search('employee_id', $keys);
+
+        $numIdx = array_search('salary', $keys);
+        $num2Idx = array_search('social_insurance', $keys);
+        $num3Idx = array_search('public_housing_fund', $keys);
+
+        // 1. 预加载基础数据
+        $allEmpNumbers = array_filter(array_unique(array_column($array, $empIdx)));
+        $dbEmps = Employee::where('del_time', 0)
+            ->whereIn('number', $allEmpNumbers)
+            ->where('top_depart_id', $user['top_depart_id'])
+            ->pluck('id', 'number')->toArray();
+
+        $allCodes = array_filter(array_unique(array_column($array, $codeIdx)));
+        $dbOrders = DB::table('monthly_ps_order')
+            ->whereIn('code', $allCodes)
+            ->where('top_depart_id', $user['top_depart_id'])
+            ->get()->keyBy('code');
+
+        $existingMonths = DB::table('monthly_ps_order')
+            ->where('top_depart_id', $user['top_depart_id'])
+            ->pluck('month')
+            ->toArray();
+        $existingMonthsMap = array_fill_keys($existingMonths, true);
+
+        $excelAggregator = [];
+        $errors = [];
+        $update_map = [];
+
+        foreach ($array as $rowIndex => $row) {
+            // 用户看到的行号(假设数据从第2行开始)
+            $displayLine = $rowIndex + 1;
+
+            $valCode = trim($row[$codeIdx] ?? '');
+            $valMonthRaw = trim($row[$monthIdx] ?? '');
+            $valEmp = trim($row[$empIdx] ?? '');
+
+            $valNumRaw = $row[$numIdx] ?? 0;
+            $valNum2Raw = $row[$num2Idx] ?? 0;
+            $valNum3Raw = $row[$num3Idx] ?? 0;
+
+            // --- A. 日期转换校验 ---
+            // 转换月份
+            list($mStatus, $valMonthTs) = $this->convertExcelCellToDate($valMonthRaw);
+            if (!$mStatus) {
+                $errors[] = "第{$displayLine}行:月份格式错误({$valMonthRaw})";
+                continue;
+            }
+            // 强制格式化为当月1号的时间戳,确保聚合逻辑一致
+            $valMonthTs = strtotime(date('Y-m-01', $valMonthTs));
+
+            $aggKey = $valCode ?: "NEW_ORDER_" . $valMonthTs;
+
+            // --- B. 校验单据与月份的一致性 ---
+            if ($valCode && isset($dbOrders[$valCode])) {
+                $dbOrder = $dbOrders[$valCode];
+                if ($dbOrder->month != $valMonthTs) {
+                    $errors[] = "第{$displayLine}行:单据编码[{$valCode}]对应月份为[" . date('Y-m', $dbOrder->month) . "],与导入月份不符";
+                }
+                $update_map[$rowIndex] = $dbOrder->id;
+            }
+
+            // --- C. 校验人员存在性与单据内唯一性 ---
+            if (!isset($dbEmps[$valEmp])) {
+                $errors[] = "第{$displayLine}行:人员工号[{$valEmp}]不存在";
+            } else {
+                if (isset($excelAggregator[$aggKey]['emps'][$valEmp])) {
+                    $errors[] = "第{$displayLine}行:人员[{$valEmp}]在同一单据中重复";
+                }
+                $excelAggregator[$aggKey]['emps'][$valEmp] = true;
+
+                if (isset($excelAggregator[$aggKey]['month_ts']) && $excelAggregator[$aggKey]['month_ts'] !== $valMonthTs) {
+                    $errors[] = "第{$displayLine}行:同组数据的月份不统一";
+                }
+                $excelAggregator[$aggKey]['month_ts'] = $valMonthTs;
+            }
+
+            // --- E. 月份唯一单据校验 (新增) ---
+            if (!$valCode) {
+                if (isset($existingMonthsMap[$valMonthTs])) {
+                    $errors[] = "第{$displayLine}行:月份[ " . date('Y-m', $valMonthTs) . " ]已存在单据,请填编码编辑";
+                }
+            }
+
+            // --- F. 数字校验
+            $res = $this->checkNumber($valNumRaw,0,'non-negative');
+            if(! $res['valid']) $errors[] = "第{$displayLine}行工资总额:" . $res['error'];
+            $res = $this->checkNumber($valNum2Raw,0,'non-negative');
+            if(! $res['valid']) $errors[] = "第{$displayLine}行社保:" . $res['error'];
+            $res = $this->checkNumber($valNum3Raw,2,'non-negative');
+            if(! $res['valid']) $errors[] = "第{$displayLine}行公积金:" . $res['error'];
+
+            // 将转换后的时间戳写回原数组,方便后续写入数据库时直接使用
+            $array[$rowIndex][$monthIdx] = $valMonthTs;
+        }
+
+        $error_string = !empty($errors) ? implode('|', $errors) : "";
+        return [$error_string, $update_map, $dbEmps];
+    }
+
+
     //公共校验 -----------------------------------------
     //公共校验 -----------------------------------------
     private function checkCommon($array, $table_config) {
     private function checkCommon($array, $table_config) {
         $error = [];
         $error = [];

+ 297 - 0
app/Service/PersonSalaryService.php

@@ -0,0 +1,297 @@
+<?php
+
+namespace App\Service;
+
+use App\Model\Employee;
+use App\Model\MonthlyPsOrder;
+use App\Model\MonthlyPsOrderDetails;
+use Illuminate\Support\Facades\DB;
+
+class PersonSalaryService extends Service
+{
+    public function MonthlyPsOrderEdit($data,$user){
+        list($status,$msg) = $this->MonthlyPsOrderRule($data, $user, false);
+        if(!$status) return [$status,$msg];
+
+        try {
+            DB::beginTransaction();
+
+            $model = MonthlyPsOrder::where('id',$data['id'])->first();
+//            $model->month = $data['month'] ?? 0;
+//            $model->save();
+
+            $time = time();
+            MonthlyPsOrderDetails::where('del_time',0)
+                ->where('main_id', $model->id)
+                ->update(['del_time' => $time]);
+            $this->saveDetail($model->id, $time, $data);
+
+            DB::commit();
+        }catch (\Exception $exception){
+            DB::rollBack();
+            return [false,$exception->getMessage()];
+        }
+
+        return [true, ''];
+    }
+
+    public function MonthlyPsOrderAdd($data,$user){
+        list($status,$msg) = $this->MonthlyPsOrderRule($data, $user);
+        if(!$status) return [$status,$msg];
+
+        try {
+            DB::beginTransaction();
+
+            $model = new MonthlyPsOrder();
+            $model->code = $this->generateBillNo([
+                'top_depart_id' => $user['top_depart_id'],
+                'type' => MonthlyPsOrder::Order_type,
+                'period' => date("Ym", $data['month'])
+            ]);
+            $model->month = $data['month'] ?? 0;
+            $model->crt_id = $user['id'];
+            $model->top_depart_id = $data['top_depart_id'];
+            $model->save();
+
+            $this->saveDetail($model->id, time(), $data);
+
+            DB::commit();
+        }catch (\Exception $exception){
+            DB::rollBack();
+            return [false,$exception->getMessage()];
+        }
+
+        return [true, ''];
+    }
+
+    private function saveDetail($id, $time, $data){
+        if(! empty($data['details'])){
+            $unit = [];
+            foreach ($data['details'] as $value){
+                $unit[] = [
+                    'main_id' => $id,
+                    'employee_id' => $value['employee_id'],
+                    'salary' => $value['salary'],
+                    'social_insurance' => $value['social_insurance'],
+                    'public_housing_fund' => $value['public_housing_fund'],
+                    'crt_time' => $time,
+                    'top_depart_id' => $value['top_depart_id'],
+                ];
+            }
+            if(! empty($unit)) MonthlyPsOrderDetails::insert($unit);
+        }
+    }
+
+    private function getDetail($id){
+        $data = MonthlyPsOrderDetails::where('del_time',0)
+            ->where('main_id', $id)
+            ->select('employee_id', 'salary', 'social_insurance', 'public_housing_fund')
+            ->get()->toArray();
+
+        $id = array_column($data,'employee_id');
+        $map = Employee::whereIn('id', $id)->select('title','id','number')->get()->toArray();
+        $map = array_column($map,null,'id');
+
+        foreach ($data as $key => $value){
+            $tmp = $map[$value['employee_id']] ?? [];
+            $merge = [];
+            $merge['employee_title'] = $tmp['title'];
+            $merge['employee_number'] = $tmp['number'];
+            $merge['start_time'] = date("Y-m-d", $value['start_time']);
+            $merge['end_time'] = date("Y-m-d", $value['end_time']);
+            $data[$key] = array_merge($value, $merge);
+        }
+
+        $detail = [
+            'details' => $data,
+        ];
+
+        foreach ($detail as $key => $value) {
+            if (empty($value)) {
+                $detail[$key] = (object)[]; // 转成 stdClass 对象
+            }
+        }
+
+        return $detail;
+    }
+
+    public function MonthlyPsOrderDel($data){
+        if($this->isEmpty($data,'id')) return [false,'请选择数据!'];
+
+        try {
+            DB::beginTransaction();
+            $time = time();
+
+            MonthlyPsOrder::where('del_time',0)
+                ->whereIn('id',$data['id'])
+                ->update(['del_time' => $time]);
+
+            MonthlyPsOrderDetails::where('del_time',0)
+                ->whereIn('main_id', $data['id'])
+                ->update(['del_time' => $time]);
+
+            DB::commit();
+        }catch (\Exception $exception){
+            DB::rollBack();
+            return [false,$exception->getMessage()];
+        }
+
+        return [true, ''];
+    }
+
+    public function MonthlyPsOrderDetail($data, $user){
+        if($this->isEmpty($data,'id')) return [false,'请选择数据!'];
+        $customer = MonthlyPsOrder::where('del_time',0)
+            ->where('id',$data['id'])
+            ->first();
+        if(empty($customer)) return [false,'人员月度工资单不存在或已被删除'];
+        $customer = $customer->toArray();
+        $customer['crt_name'] = Employee::where('id',$customer['crt_id'])->value('title');
+        $customer['crt_time'] = $customer['crt_time'] ? date("Y-m-d H:i:s",$customer['crt_time']): '';
+        $customer['month'] = $customer['month'] ? date("Y-m",$customer['month']): '';
+
+        $details = $this->getDetail($data['id']);
+        $customer = array_merge($customer, $details);
+
+        return [true, $customer];
+    }
+
+    public function MonthlyPsOrderCommon($data,$user, $field = []){
+        if(empty($field)) $field = MonthlyPsOrder::$field;
+
+        $model = MonthlyPsOrder::Clear($user,$data);
+        $model = $model->where('del_time',0)
+            ->select($field)
+            ->orderby('id', 'desc');
+
+        if(! empty($data['code'])) $model->where('code', 'LIKE', '%'.$data['code'].'%');
+        if(! empty($data['id'])) $model->whereIn('id', $data['id']);
+        if(! empty($data['crt_time'][0]) && ! empty($data['crt_time'][1])) {
+            $return = $this->changeDateToTimeStampAboutRange($data['crt_time']);
+            $model->where('crt_time','>=',$return[0]);
+            $model->where('crt_time','<=',$return[1]);
+        }
+
+        return $model;
+    }
+
+    public function MonthlyPsOrderList($data,$user){
+        $model = $this->MonthlyPsOrderCommon($data, $user);
+        $list = $this->limit($model,'',$data);
+        $list = $this->fillData($list);
+
+        return [true, $list];
+    }
+
+    public function MonthlyPsOrderRule(&$data, $user, $is_add = true){
+        if(empty($data['month'])) return [false, '月份不能为空'];
+        $data['month'] = $this->changeDateToDate($data['month']);
+
+        $data['top_depart_id'] = $user['top_depart_id'];
+        if(empty($data['details'])) return [false, '人员月度工时单明细不能为空'];
+        foreach ($data['details'] as $key => $value){
+            if(empty($value['employee_id'])) return [false, '人员不能为空'];
+            $res = $this->checkNumber($value['salary'],2,'non-negative');
+            if(! $res['valid']) return [false,'工资:' . $res['error']];
+            $res = $this->checkNumber($value['social_insurance'],2,'non-negative');
+            if(! $res['valid']) return [false,'社保:' . $res['error']];
+            $res = $this->checkNumber($value['public_housing_fund'],2,'non-negative');
+            if(! $res['valid']) return [false,'公积金:' . $res['error']];
+            $data['details'][$key]['top_depart_id'] = $data['top_depart_id'];
+        }
+        list($status, $msg) = $this->checkArrayRepeat($data['details'],'employee_id','人员');
+        if(! $status) return [false, $msg];
+
+        if($is_add){
+            $bool = MonthlyPsOrder::where('code',$data['code'])
+                ->where('top_depart_id', $data['top_depart_id'])
+                ->where('del_time',0)
+                ->exists();
+        }else{
+            if(empty($data['id'])) return [false,'ID不能为空'];
+            $bool = MonthlyPsOrder::where('code',$data['code'])
+                ->where('top_depart_id', $data['top_depart_id'])
+                ->where('id','<>',$data['id'])
+                ->where('del_time',0)
+                ->exists();
+        }
+        if($bool) return [false, date("Y-m", $data['month']) . '已存在人员月度工资单'];
+
+        return [true, ''];
+    }
+
+    public function fillData($data){
+        if(empty($data['data'])) return $data;
+
+        $emp = (new EmployeeService())->getEmployeeMap(array_unique(array_column($data['data'],'crt_id')));
+        foreach ($data['data'] as $key => $value){
+            $data['data'][$key]['crt_time'] = $value['crt_time'] ? date('Y-m-d H:i:s',$value['crt_time']) : '';
+            $data['data'][$key]['month'] = $value['month'] ? date('Y-m',$value['month']) : '';
+            $data['data'][$key]['crt_name'] = $emp[$value['crt_id']] ?? '';
+        }
+
+        return $data;
+    }
+
+    public function fillDataForExport($data, $column, &$return)
+    {
+        if(empty($data)) return;
+
+        $mainIds = array_column($data, 'id');
+        // 获取详情映射 [main_id => [details...]]
+        $detailsMap = $this->getDetailsMap($mainIds);
+
+        // 默认空行模板
+        $defaultRow = array_fill_keys($column, '');
+
+        foreach ($data as $main) {
+            $mainId = $main['id'];
+            $details = $detailsMap[$mainId] ?? [];
+
+            // 提取主表信息
+            $mainInfo = [
+                'code'  => $main['code'],
+                'month' => $main['month'] ? date('Y-m', $main['month']) : '',
+            ];
+
+            if (empty($details)) {
+                // 如果没有详情,至少导出一行主表信息(可选)
+                $return[] = array_merge($defaultRow, $mainInfo);
+            } else {
+                // 核心:遍历详情,每一行详情都合并主表信息
+                foreach ($details as $sub) {
+                    // 合并主表字段 + 详情字段
+                    $fullRow = array_merge($mainInfo, $sub);
+                    // 过滤掉不在导出列里的字段,并补充缺失列
+                    $return[] = array_merge($defaultRow, array_intersect_key($fullRow, $defaultRow));
+                }
+            }
+        }
+    }
+
+    public function getDetailsMap($main_ids)
+    {
+        // 获取详情
+        $details = MonthlyPsOrderDetails::where('del_time', 0)
+            ->whereIn('main_id', $main_ids)
+            ->get();
+
+        // 获取人员信息
+        $empIds = $details->pluck('employee_id')->unique();
+        $empMap = Employee::whereIn('id', $empIds)->get()->keyBy('id');
+
+        $res = [];
+        foreach ($details as $item) {
+            $tmpEmp = $empMap[$item->employee_id] ?? null;
+
+            // 组装每一行详情需要展示的字段
+            $res[$item->main_id][] = [
+                'employee_number' => $tmpEmp ? $tmpEmp->number : '',
+                'salary'     => $item->salary,
+                'social_insurance'  => $item->social_insurance,
+                'public_housing_fund'    => $item->public_housing_fund,
+            ];
+        }
+        return $res; // 返回 [main_id => [detail_row, detail_row]]
+    }
+}

+ 1 - 1
app/Service/PersonWorkService.php

@@ -147,7 +147,7 @@ class PersonWorkService extends Service
         $customer = MonthlyPwOrder::where('del_time',0)
         $customer = MonthlyPwOrder::where('del_time',0)
             ->where('id',$data['id'])
             ->where('id',$data['id'])
             ->first();
             ->first();
-        if(empty($customer)) return [false,'项目不存在或已被删除'];
+        if(empty($customer)) return [false,'人员月度工时单不存在或已被删除'];
         $customer = $customer->toArray();
         $customer = $customer->toArray();
         $customer['crt_name'] = Employee::where('id',$customer['crt_id'])->value('title');
         $customer['crt_name'] = Employee::where('id',$customer['crt_id'])->value('title');
         $customer['crt_time'] = $customer['crt_time'] ? date("Y-m-d H:i:s",$customer['crt_time']): '';
         $customer['crt_time'] = $customer['crt_time'] ? date("Y-m-d H:i:s",$customer['crt_time']): '';

+ 72 - 0
config/excel/monthPsOrder.php

@@ -0,0 +1,72 @@
+<?php
+return [
+    "name" => "人员月度工资单",
+    "array" => [
+        [
+            'key' =>'code',
+            'export' =>'code',
+            'value' => '单据编码',
+            'required' => false,
+            'is_main' => true,
+            'default' => "",
+            'unique' => false,
+            'enums' => [],
+            'comments' => '(如果填写则是编辑)'
+        ],
+        [
+            'key' =>'month',
+            'export' =>'month',
+            'value' => '月份',
+            'required' => true,
+            'is_main' => true,
+            'default' => "",
+            'unique' => false,
+            'enums' => [],
+            'comments' => '必填(2026-02)'
+        ],
+        [
+            'key' =>'employee_id',
+            'export' =>'employee_number',
+            'value' => '人员',
+            'required' => true,
+            'is_main' => false,
+            'default' => "",
+            'unique' => false,
+            'enums' => [],
+            'comments' => '必填(填写人员工号)'
+        ],
+        [
+            'key' =>'salary',
+            'export' =>'salary',
+            'value' => '工资总额',
+            'required' => true,
+            'is_main' => false,
+            'default' => 0,
+            'unique' => false,
+            'enums' => [],
+            'comments' => '必填'
+        ],
+        [
+            'key' =>'social_insurance',
+            'export' =>'social_insurance',
+            'value' => '社保',
+            'required' => true,
+            'is_main' => false,
+            'default' => 0,
+            'unique' => false,
+            'enums' => [],
+            'comments' => '必填'
+        ],
+        [
+            'key' =>'public_housing_fund',
+            'export' =>'public_housing_fund',
+            'value' => '公积金',
+            'required' => true,
+            'is_main' => false,
+            'default' => 0,
+            'unique' => false,
+            'enums' => [],
+            'comments' => '必填'
+        ],
+    ]
+];

+ 7 - 0
routes/api.php

@@ -102,5 +102,12 @@ Route::group(['middleware'=> ['checkLogin']],function ($route){
     $route->any('monthlyDwOrderAdd', 'Api\DeviceWorkController@monthlyDwOrderAdd');
     $route->any('monthlyDwOrderAdd', 'Api\DeviceWorkController@monthlyDwOrderAdd');
     $route->any('monthlyDwOrderDel', 'Api\DeviceWorkController@monthlyDwOrderDel');
     $route->any('monthlyDwOrderDel', 'Api\DeviceWorkController@monthlyDwOrderDel');
     $route->any('monthlyDwOrderDetail', 'Api\DeviceWorkController@monthlyDwOrderDetail');
     $route->any('monthlyDwOrderDetail', 'Api\DeviceWorkController@monthlyDwOrderDetail');
+
+    //人员月度工资单
+    $route->any('monthlyPsOrderList', 'Api\PersonSalaryController@monthlyPsOrderList');
+    $route->any('monthlyPsOrderEdit', 'Api\PersonSalaryController@monthlyPsOrderEdit');
+    $route->any('monthlyPsOrderAdd', 'Api\PersonSalaryController@monthlyPsOrderAdd');
+    $route->any('monthlyPsOrderDel', 'Api\PersonSalaryController@monthlyPsOrderDel');
+    $route->any('monthlyPsOrderDetail', 'Api\PersonSalaryController@monthlyPsOrderDetail');
 });
 });