cqp пре 3 месеци
родитељ
комит
f554cb232d

+ 13 - 0
app/Http/Controllers/Api/RuleSetController.php

@@ -86,4 +86,17 @@ class RuleSetController extends BaseController
             return $this->json_return(201,$data);
         }
     }
+
+    public function isSetMonthCalendar(Request $request)
+    {
+        $service = new RuleSetService();
+        $user = $request->userData;
+        list($status,$data) = $service->isSetMonthCalendar($request->all(),$user);
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+    }
 }

+ 52 - 1
app/Service/DeviceService.php

@@ -2,6 +2,7 @@
 
 namespace App\Service;
 
+use App\Model\CalendarDetails;
 use App\Model\Employee;
 use App\Model\Device;
 use Illuminate\Support\Facades\DB;
@@ -166,18 +167,68 @@ class DeviceService extends Service
         return [true, ''];
     }
 
-    public function fillData($data){
+    public function fillData($data, $ergs, $user){
         if(empty($data['data'])) return $data;
 
         $emp = (new EmployeeService())->getEmployeeMap(array_unique(array_column($data['data'],'crt_id')));
+
+        $map = [];
+        if(isset($ergs['search_for_month_work'])) {
+            $device_ids = array_column($data['data'], 'id');
+            list($status, $map) = $this->getDevicesMonthStats($device_ids, $ergs['search_for_month_work'], $user);
+        }
         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]['in_time'] = $value['in_time'] ? date('Y-m-d',$value['in_time']) : '';
             $data['data'][$key]['crt_name'] = $emp[$value['crt_id']] ?? '';
             $data['data'][$key]['is_use_title'] = Device::Use[$value['is_use']] ?? '';
             $data['data'][$key]['type_title'] = Device::$type[$value['type']] ?? '';
+            if(isset($ergs['search_for_month_work'])) $data['data'][$key]['month_dw'] = $map[$value['id']] ?? [];
         }
 
         return $data;
     }
+
+    public function getDevicesMonthStats($device_ids, $month, $user)
+    {
+        $topDepartId = $user['top_depart_id'];
+        if(is_numeric($month)){
+            $monthStart = $month;
+        }else{
+            $monthStart = $this->changeDateToDate($month);
+            $monthStr = date("Y-m", $monthStart);
+        }
+        $endTime = strtotime("+1 month", $monthStart) - 1;
+
+        // 1. 获取当月标准工作日天数
+        $standardWorkDays = DB::table('calendar_details')
+            ->where('top_depart_id', $topDepartId)
+            ->where('del_time', 0)
+            ->where('time', '>=', $monthStart)
+            ->where('time', '<=', $endTime)
+            ->where('is_work', CalendarDetails::TYPE_ONE)
+            ->count();
+
+        if ($standardWorkDays <= 0) return [false, '工作日信息未设置'];
+
+        // 2. 获取公司通用工时设置 (设备统一使用这个)
+        $commonWorkMin = DB::table('work_range_details')
+            ->where('top_depart_id', $topDepartId)
+            ->where('del_time', 0)
+            ->sum('total_work_min');
+
+        if ($commonWorkMin <= 0) return [false, '公司工作时段未设置'];
+
+        // 3. 计算结果
+        $finalWorkMin = $standardWorkDays * $commonWorkMin;
+        $result = [];
+        foreach ($device_ids as $deviceId) {
+            $result[$deviceId] = [
+                'attendance_days' => (float)$standardWorkDays, // 设备没有请假加班,出勤天数即标准天数
+                'final_work_hour' => round($finalWorkMin / 60, 2), // 标准总工时转小时
+            ];
+        }
+
+        return [true, $result];
+    }
 }

+ 92 - 13
app/Service/DeviceWorkService.php

@@ -80,8 +80,6 @@ class DeviceWorkService extends Service
                     'rd_total_days' => $value['rd_total_days'],
                     'total_hours' => $value['total_hours'],
                     'rd_total_hours' => $value['rd_total_hours'],
-                    'start_time' => $value['start_time'],
-                    'end_time' => $value['end_time'],
                     'crt_time' => $time,
                     'top_depart_id' => $value['top_depart_id'],
                 ];
@@ -93,7 +91,7 @@ class DeviceWorkService extends Service
     private function getDetail($id){
         $data = MonthlyDwOrderDetails::where('del_time',0)
             ->where('main_id', $id)
-            ->select('device_id', 'total_days', 'rd_total_days', 'total_hours', 'rd_total_hours', 'start_time', 'end_time')
+            ->select('device_id', 'total_days', 'rd_total_days', 'total_hours', 'rd_total_hours')
             ->get()->toArray();
 
         $id = array_column($data,'device_id');
@@ -105,8 +103,6 @@ class DeviceWorkService extends Service
             $merge = [];
             $merge['device_title'] = $tmp['title'];
             $merge['device_code'] = $tmp['code'];
-            $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);
         }
 
@@ -191,7 +187,97 @@ class DeviceWorkService extends Service
         return [true, $list];
     }
 
-    public function monthlyDwOrderRule(&$data, $user, $is_add = true){
+    public function monthlyDwOrderRule(&$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, '设备月度工时单明细不能为空'];
+
+        // --- 1. 批量获取设备档案信息 (用于展示 [编码]名称) ---
+        $deviceIds = array_column($data['details'], 'device_id');
+        $deviceMap = DB::table('device')
+            ->whereIn('id', $deviceIds)
+            ->where('top_depart_id', $data['top_depart_id'])
+            ->get(['id', 'code', 'title'])
+            ->mapWithKeys(fn($item) => [$item->id => "[{$item->code}]{$item->title}"])
+            ->toArray();
+
+        // --- 2. 获取设备考勤基准 ---
+        list($status, $deviceStats) = (new DeviceService())->getDevicesMonthStats($deviceIds, $data['month'], $user);
+        if (!$status) return [false, $deviceStats];
+
+        // 字段中文映射,用于报错
+        $fieldNames = [
+            'total_days'    => '出勤总天数',
+            'rd_total_days' => '研发出勤天数',
+            'total_hours'   => '出勤总工时',
+            'rd_total_hours'=> '研发总工时'
+        ];
+
+        // --- 3. 循环校验明细 ---
+        foreach ($data['details'] as $key => $value) {
+            $line = $key + 1; // 行号
+            if (empty($value['device_id'])) return [false, "第{$line}行:设备ID不能为空"];
+
+            $deviceId = $value['device_id'];
+            $deviceDisplayName = $deviceMap[$deviceId] ?? "";
+            if(empty($deviceDisplayName)) return [false, "第{$line}行:设备不存在或已被删除"];
+
+            // 基础数字格式检查
+            foreach ($fieldNames as $field => $cnName) {
+                $precision = 2;
+                $res = $this->checkNumber($value[$field], $precision, 'non-negative');
+                if (!$res['valid']) {
+                    return [false, "第{$line}行:设备{$deviceDisplayName}的{$cnName}填写不规范({$res['error']})"];
+                }
+            }
+
+            // --- 4. 业务逻辑校验 ---
+            $sysData = $deviceStats[$deviceId] ?? null;
+            if ($sysData) {
+                // A. 内部逻辑:研发不能大于总额
+                if ($value['rd_total_days'] > $value['total_days']) {
+                    return [false, "第{$line}行:设备{$deviceDisplayName}的研发出勤天数不能大于出勤总天数"];
+                }
+                if ($value['rd_total_hours'] > $value['total_hours']) {
+                    return [false, "第{$line}行:设备{$deviceDisplayName}的研发总工时不能大于出勤总工时"];
+                }
+
+                // B. 外部逻辑:不能超过系统根据日历算出的上限
+                if ($value['total_days'] != $sysData['attendance_days']) {
+                    return [false, "第{$line}行:设备{$deviceDisplayName}的出勤总天数({$value['total_days']})不等于当月标准天数({$sysData['attendance_days']})"];
+                }
+                if ($value['total_hours'] != $sysData['final_work_hour']) {
+                    return [false, "第{$line}行:设备{$deviceDisplayName}的出勤总工时({$value['total_hours']})不等于当月标准工时({$sysData['final_work_hour']})"];
+                }
+            }
+
+            $data['details'][$key]['top_depart_id'] = $data['top_depart_id'];
+        }
+
+        // --- 5. 查重与唯一性校验 ---
+        list($status, $msg) = $this->checkArrayRepeat($data['details'], 'device_id', '设备');
+        if (!$status) return [false, $msg];
+
+        $query = MonthlyDwOrder::where('top_depart_id', $data['top_depart_id'])
+            ->where('month', $data['month'])
+            ->where('del_time', 0);
+
+        if (!$is_add) {
+            if (empty($data['id'])) return [false, 'ID不能为空'];
+            $query->where('id', '<>', $data['id']);
+        }
+
+        if ($query->exists()) {
+            return [false, date("Y-m", $data['month']) . '已存在设备月度研发工时单'];
+        }
+
+        return [true, ''];
+    }
+
+    public function monthlyDwOrderRule1(&$data, $user, $is_add = true){
         if(empty($data['month'])) return [false, '月份不能为空'];
         $data['month'] = $this->changeDateToDate($data['month']);
 
@@ -207,11 +293,6 @@ class DeviceWorkService extends Service
             if(! $res['valid']) return [false,'出勤总工时:' . $res['error']];
             $res = $this->checkNumber($value['rd_total_hours'],2,'non-negative');
             if(! $res['valid']) return [false,'研发总工时:' . $res['error']];
-
-            if(empty($value['start_time'])) return [false, '开始时间不能为空'];
-            $data['details'][$key]['start_time'] = $this->changeDateToDate($value['start_time']);
-            if(empty($value['end_time'])) return [false, '结束时间不能为空'];
-            $data['details'][$key]['end_time'] = $this->changeDateToDate($value['end_time']);
             $data['details'][$key]['top_depart_id'] = $data['top_depart_id'];
         }
         list($status, $msg) = $this->checkArrayRepeat($data['details'],'device_id','设备');
@@ -307,8 +388,6 @@ class DeviceWorkService extends Service
                 'rd_total_days'  => $item->rd_total_days,
                 'total_hours'    => $item->total_hours,
                 'rd_total_hours' => $item->rd_total_hours,
-                'start_time'     => $item->start_time ? date("Y-m-d", $item->start_time) : '',
-                'end_time'       => $item->end_time ? date("Y-m-d", $item->end_time) : '',
             ];
         }
         return $res; // 返回 [main_id => [detail_row, detail_row]]

+ 109 - 2
app/Service/EmployeeService.php

@@ -2,6 +2,7 @@
 
 namespace App\Service;
 
+use App\Model\CalendarDetails;
 use App\Model\Depart;
 use App\Model\Employee;
 use App\Model\EmployeeDepartPermission;
@@ -278,12 +279,12 @@ class EmployeeService extends Service
     public function employeeList($data,$user){
         $model = $this->employeeCommon($data, $user);
         $list = $this->limit($model,'',$data);
-        $list = $this->organizationEmployeeData($list);
+        $list = $this->organizationEmployeeData($list, $data, $user);
 
         return [true, $list];
     }
 
-    public function organizationEmployeeData($data)
+    public function organizationEmployeeData($data, $ergs, $user)
     {
         if (empty($data['data'])) return $data;
 
@@ -291,6 +292,8 @@ class EmployeeService extends Service
         $employee_ids = array_column($data['data'], 'id');
         list($status, $extraMap) = $this->getEmployee($employee_ids);
 
+        $map = [];
+        if(isset($ergs['search_for_month_work'])) list($status, $map) = $this->getEmployeesMonthStats($employee_ids, $ergs['search_for_month_work'], $user);
         foreach ($data['data'] as &$item) {
             $id = $item['id'];
             $extra = $extraMap[$id] ?? null;
@@ -306,11 +309,115 @@ class EmployeeService extends Service
             $item['sex_title'] = Employee::SEX_TYPE[$item['sex']] ?? "";
             $item['education_title'] = Employee::Education[$item['education']] ?? "";
             $item['crt_time']       = !empty($item['crt_time']) ? date("Y-m-d", $item['crt_time']) : "";
+            if (isset($ergs['search_for_month_work'])) {
+                $item['month_pw'] = []; // 默认空数组
+                if ($status) {
+                    $item['month_pw'] = $map[$item['id']] ?? [];
+                }
+            }
         }
 
         return $data;
     }
 
+    public function getEmployeesMonthStats($employee_ids, $month, $user)
+    {
+        $topDepartId = $user['top_depart_id'];
+
+        // 1. 月份初始化
+        if(is_numeric($month)){
+            $monthStart = $month;
+        }else{
+            $monthStart = $this->changeDateToDate($month);
+            $monthStr = date("Y-m", $monthStart);
+        }
+
+        $endTime = strtotime("+1 month", $monthStart) - 1;
+
+        // 2. 获取当月标准工作日天数 (基数)
+        $standardWorkDays = DB::table('calendar_details')
+            ->where('top_depart_id', $topDepartId)
+            ->where('del_time', 0)
+            ->where('time', '>=', $monthStart)
+            ->where('time', '<=', $endTime)
+            ->where('is_work', CalendarDetails::TYPE_ONE)
+            ->count();
+
+        if ($standardWorkDays <= 0) return [false, '工作日信息未设置'];
+
+        // 3. 批量获取个性化工时设置
+        $empWorkRanges = DB::table('employee_work_range')
+            ->whereIn('employee_id', $employee_ids)
+            ->where('top_depart_id', $topDepartId)
+            ->select('employee_id', DB::raw('SUM(total_work_min) as total_min'))
+            ->groupBy('employee_id')
+            ->pluck('total_min', 'employee_id')
+            ->toArray();
+
+        // 4. 获取公司通用工时设置
+        $commonWorkMin = DB::table('work_range_details')
+            ->where('top_depart_id', $topDepartId)
+            ->where('del_time', 0)
+            ->sum('total_work_min');
+        if(empty($commonWorkMin)) return [false, '公司工作时段未设置'];
+
+        // 5. 批量获取请假与加班汇总
+        $actualStats = DB::table('p_leave_over_order_details as d')
+            ->join('p_leave_over_order as m', 'd.main_id', '=', 'm.id')
+            ->whereIn('d.employee_id', $employee_ids)
+            ->where('m.order_time', '>=', $monthStart)
+            ->where('m.order_time', '<=', $endTime)
+            ->where('m.del_time', 0)
+            ->where('d.del_time', 0)
+            ->select(
+                'd.employee_id',
+                DB::raw("SUM(CASE WHEN m.type = 1 THEN d.total_min ELSE 0 END) as leave_min"),
+                DB::raw("SUM(CASE WHEN m.type = 2 THEN d.total_min ELSE 0 END) as overtime_min")
+            )
+            ->groupBy('d.employee_id')
+            ->get()
+            ->keyBy('employee_id');
+
+        // 6. 核心逻辑汇总
+        $result = [];
+        foreach ($employee_ids as $empId) {
+            // A. 确定该人员每日标准时长 (基数)
+            $dailyStandardMin = isset($empWorkRanges[$empId]) ? (float)$empWorkRanges[$empId] : (float)$commonWorkMin;
+
+            // 如果每日标准时长没设置,跳过该人防止除以0错误
+            if ($dailyStandardMin <= 0) continue;
+
+            // B. 获取加班和请假数据
+            $empActual = $actualStats->get($empId);
+            $leaveMin = $empActual ? (float)$empActual->leave_min : 0;
+            $overtimeMin = $empActual ? (float)$empActual->overtime_min : 0;
+
+            // 净增减工时(分)
+            $netDiffMin = $overtimeMin - $leaveMin;
+
+            // C. 计算出勤总工时 (标准总分钟 + 净增减)
+            $standardMonthMin = $standardWorkDays * $dailyStandardMin;
+            $finalWorkMin = $standardMonthMin + $netDiffMin;
+
+            // D. 计算出勤总天数 (标准天数 + 净增减折算天数)
+            // 计算公式:出勤天数 = (标准总分钟 + 加班分钟 - 请假分钟) / 每日标准分钟
+            $finalAttendanceDays = round($finalWorkMin / $dailyStandardMin, 2);
+
+            $result[$empId] = [
+//                'standard_days'      => $standardWorkDays,     // 标准工作天数
+                'attendance_days'    => $finalAttendanceDays,  // 出勤总天数 * (结算后)
+//                'standard_month_min' => $standardMonthMin,     // 标准总工时 (分钟)
+//                'final_work_min'     => $finalWorkMin,         // 出勤总工时 * (结算后分钟)
+                'final_work_hour'    => round($finalWorkMin / 60, 2), // 出勤总工时 * (结算后小时)
+//                'leave_min'          => $leaveMin,
+//                'overtime_min'       => $overtimeMin,
+//                'daily_standard_min' => $dailyStandardMin
+            ];
+        }
+
+        return [true, $result];
+    }
+
     public function getEmployee(array $employee_ids)
     {
         if (empty($employee_ids)) return [false, []];

+ 2 - 2
app/Service/ExportFileService.php

@@ -138,12 +138,12 @@ class ExportFileService extends Service
         $service = new DeviceService();
         $model = $service->deviceCommon($ergs, $user);
 
-        $model->chunk(500,function ($data) use(&$return, $service, $column){
+        $model->chunk(500,function ($data) use(&$return, $service, $column,$ergs, $user){
             $data = $data->toArray();
             $list['data'] = $data;
 
             //订单数据
-            $list = $service->fillData($list);
+            $list = $service->fillData($list, $ergs, $user);
             //返回数据
             $this->fillData($list['data'], $column, $return);
         });

+ 279 - 19
app/Service/ImportService.php

@@ -1385,6 +1385,140 @@ class ImportService extends Service
     }
 
     private function monthPwOrderCheck(&$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('total_days', $keys);
+        $num2Idx = array_search('rd_total_days', $keys);
+        $num3Idx = array_search('total_hours', $keys);
+        $num4Idx = array_search('rd_total_hours', $keys);
+
+        $topDepartId = $user['top_depart_id'];
+        $errors = [];
+
+        // --- 1. 预处理月份与工号 ---
+        $allEmpNumbers = [];
+        $allMonthsTs = [];
+        foreach ($array as $rowIndex => $row) {
+            $valMonthRaw = trim($row[$monthIdx] ?? '');
+            list($mStatus, $valMonthTs) = $this->convertExcelCellToDate($valMonthRaw);
+            if (!$mStatus) {
+                $errors[] = "第" . ($rowIndex + 1) . "行:月份格式错误";
+                continue;
+            }
+            $valMonthTs = strtotime(date('Y-m-01', $valMonthTs));
+            $array[$rowIndex][$monthIdx] = $valMonthTs;
+            $allMonthsTs[] = $valMonthTs;
+            if (!empty($row[$empIdx])) $allEmpNumbers[] = trim($row[$empIdx]);
+        }
+        $allMonthsTs = array_unique($allMonthsTs);
+        $allEmpNumbers = array_unique($allEmpNumbers);
+
+        // --- 2. 批量预加载 ---
+        // A. 人员档案
+        $empModels = Employee::where('del_time', 0)->whereIn('number', $allEmpNumbers)
+            ->where('top_depart_id', $topDepartId)->get(['id', 'number', 'title'])->keyBy('number');
+        $dbEmps = $empModels->pluck('id', 'number')->toArray();
+
+        // B. 系统考勤基准 (按月获取)
+        $systemStatsMap = [];
+        $service = new EmployeeService();
+        foreach ($allMonthsTs as $ts) {
+            list($sStatus, $stats) = $service->getEmployeesMonthStats(array_values($dbEmps), $ts, $user);
+            if (!$sStatus) {
+                $errors[] = "月份[" . date('Y-m', $ts) . "]:" . $stats;
+                continue;
+            }
+            $systemStatsMap[$ts] = $stats;
+        }
+        if (!empty($errors)) return [implode('|', $errors), [], []];
+
+        // C. 核心修正:单据查重(建立 月份 -> 单号 的映射,避免循环内查询)
+        $allCodes = array_filter(array_unique(array_column($array, $codeIdx)));
+
+        // 1) 所有的 Excel 涉及月份在数据库中已有的单据映射
+        $existingMonthsMap = DB::table('monthly_pw_order')
+            ->where('top_depart_id', $topDepartId)
+            ->where('del_time', 0)
+            ->whereIn('month', $allMonthsTs)
+            ->pluck('code', 'month') // 注意:这里直接查出 month => code
+            ->toArray();
+
+        // 2) 用户填写的单据号对应的数据库记录
+        $dbOrdersByCode = DB::table('monthly_pw_order')
+            ->whereIn('code', $allCodes)
+            ->where('top_depart_id', $topDepartId)
+            ->where('del_time', 0)
+            ->get()
+            ->keyBy('code');
+
+        // --- 3. 业务逻辑全量检查 ---
+        $excelAggregator = [];
+        $update_map = [];
+
+        foreach ($array as $rowIndex => $row) {
+            $line = $rowIndex + 1;
+            $valCode = trim($row[$codeIdx] ?? '');
+            $valMonthTs = $row[$monthIdx];
+            $valEmpNum = trim($row[$empIdx] ?? '');
+            $monthStr = date('Y-m', $valMonthTs);
+
+            if (!isset($empModels[$valEmpNum])) {
+                $errors[] = "第{$line}行:工号[{$valEmpNum}]不存在";
+                continue;
+            }
+            $empId = $empModels[$valEmpNum]->id;
+            $empTitle = "[{$valEmpNum}]" . $empModels[$valEmpNum]->title;
+
+            // --- 核心修正:单据单号提示逻辑 ---
+            if ($valCode !== '') {
+                if (isset($dbOrdersByCode[$valCode])) {
+                    if ($dbOrdersByCode[$valCode]->month != $valMonthTs) {
+                        $errors[] = "第{$line}行:单据[{$valCode}]月份应为" . date('Y-m', $dbOrdersByCode[$valCode]->month) . ",请核实";
+                    }
+                    $update_map[$rowIndex] = $dbOrdersByCode[$valCode]->id;
+                } else {
+                    $errors[] = "第{$line}行:填写的单据编号[{$valCode}]在系统中不存在";
+                }
+            } else {
+                // 如果没填单号,但该月已存在单据,从预加载的 existingMonthsMap 中获取正确单号
+                if (isset($existingMonthsMap[$valMonthTs])) {
+                    $correctCode = $existingMonthsMap[$valMonthTs];
+                    $errors[] = "第{$line}行:月份[{$monthStr}]已存在研发单,单号为:[{$correctCode}],请填写该单号进行编辑";
+                }
+            }
+
+            // --- 考勤上限校验 ---
+            $sysEmpData = $systemStatsMap[$valMonthTs][$empId] ?? null;
+            if ($sysEmpData) {
+                $inTotalDays = (float)($row[$numIdx] ?? 0);
+                $inRdDays    = (float)($row[$num2Idx] ?? 0);
+                $inTotalHours= (float)($row[$num3Idx] ?? 0);
+                $inRdHours   = (float)($row[$num4Idx] ?? 0);
+
+                // 使用误差范围比较,避免浮点数精度问题
+                if (abs($inTotalDays - $sysEmpData['attendance_days']) > 0.01) {
+                    $errors[] = "第{$line}行:{$empTitle}出勤总天数({$inTotalDays})与核算({$sysEmpData['attendance_days']})不符";
+                }
+                if (abs($inTotalHours - $sysEmpData['final_work_hour']) > 0.01) {
+                    $errors[] = "第{$line}行:{$empTitle}出勤总工时({$inTotalHours})与核算({$sysEmpData['final_work_hour']})不符";
+                }
+                if ($inRdDays > $inTotalDays + 0.01) $errors[] = "第{$line}行:{$empTitle}研发天数超过总天数";
+                if ($inRdHours > $inTotalHours + 0.01) $errors[] = "第{$line}行:{$empTitle}研发工时超过总工时";
+            }
+
+            if (isset($excelAggregator[$valCode ?: $valMonthTs]['emps'][$valEmpNum])) {
+                $errors[] = "第{$line}行:人员在同单据内重复";
+            }
+            $excelAggregator[$valCode ?: $valMonthTs]['emps'][$valEmpNum] = true;
+        }
+
+        return [!empty($errors) ? implode('|', $errors) : "", $update_map, $dbEmps];
+    }
+
+    private function monthPwOrderCheck1(&$array, $user, $table_config)
     {
         $keys = array_column($table_config, 'key');
         $codeIdx = array_search('code', $keys);
@@ -1634,8 +1768,151 @@ class ImportService extends Service
         $codeIdx = array_search('code', $keys);
         $monthIdx = array_search('month', $keys);
         $devIdx = array_search('device_id', $keys);
-        $startIdx = array_search('start_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);
+
+        $topDepartId = $user['top_depart_id'];
+        $errors = [];
+
+        // --- 1. 提取月份和资产编码 ---
+        $allDevCodes = [];
+        $allMonthsTs = [];
+        foreach ($array as $rowIndex => $row) {
+            list($mStatus, $valMonthTs) = $this->convertExcelCellToDate($row[$monthIdx] ?? '');
+            if (!$mStatus) {
+                $errors[] = "第" . ($rowIndex + 1) . "行:月份格式错误";
+                continue;
+            }
+            $valMonthTs = strtotime(date('Y-m-01', $valMonthTs));
+            $array[$rowIndex][$monthIdx] = $valMonthTs;
+            $allMonthsTs[] = $valMonthTs;
+            if (!empty($row[$devIdx])) $allDevCodes[] = trim($row[$devIdx]);
+        }
+        $allMonthsTs = array_unique($allMonthsTs);
+
+        // --- 2. 预加载档案与基准 ---
+        $devModels = Device::where('del_time', 0)
+            ->whereIn('code', $allDevCodes)
+            ->where('top_depart_id', $topDepartId)
+            ->get(['id', 'code', 'title'])
+            ->keyBy('code');
+        $dbDevs = $devModels->pluck('id', 'code')->toArray();
+
+        $systemStatsMap = [];
+        $allDeviceIds = array_values($dbDevs);
+        $service = new DeviceService();
+        foreach ($allMonthsTs as $ts) {
+            list($sStatus, $stats) = $service->getDevicesMonthStats($allDeviceIds, $ts, $user);
+            if (!$sStatus) {
+                $errors[] = "月份[" . date('Y-m', $ts) . "]解析失败:{$stats}";
+                $systemStatsMap[$ts] = null;
+            } else {
+                $systemStatsMap[$ts] = $stats;
+            }
+        }
+        // 如果基准获取有误(如日历未设),直接返回错误,不执行后续逻辑
+        if (!empty($errors)) return [implode('|', $errors), [], []];
+
+        // --- 3. 核心修正:单据查重与单号映射 (避免循环内查询) ---
+        $allCodes = array_filter(array_unique(array_column($array, $codeIdx)));
+
+        // 获取该租户相关月份已存在的单据映射 (Month => Code)
+        $existingMonthsMap = DB::table('monthly_dw_order')
+            ->where('top_depart_id', $topDepartId)
+            ->where('del_time', 0)
+            ->whereIn('month', $allMonthsTs)
+            ->pluck('code', 'month')
+            ->toArray();
+
+        // 获取 Excel 中填写的单号对应的数据库记录 (Code => Object)
+        $dbOrdersByCode = DB::table('monthly_dw_order')
+            ->whereIn('code', $allCodes)
+            ->where('top_depart_id', $topDepartId)
+            ->where('del_time', 0)
+            ->get()
+            ->keyBy('code');
+
+        $excelAggregator = [];
+        $update_map = [];
+
+        // --- 4. 循环校验 ---
+        foreach ($array as $rowIndex => $row) {
+            $line = $rowIndex + 1;
+            $valMonthTs = $row[$monthIdx];
+            $monthStr = date('Y-m', $valMonthTs);
+            $valDevCode = trim($row[$devIdx] ?? '');
+            $valCode = trim($row[$codeIdx] ?? '');
+
+            // A. 设备档案检查
+            if (!isset($devModels[$valDevCode])) {
+                $errors[] = "第{$line}行:设备编码[{$valDevCode}]不存在";
+                continue;
+            }
+            $deviceId = $devModels[$valDevCode]->id;
+            $devTitle = "[{$valDevCode}]" . $devModels[$valDevCode]->title;
+
+            // B. 单据号存在性与意图校验
+            if ($valCode !== '') {
+                // 意图:编辑已存在的单据
+                if (isset($dbOrdersByCode[$valCode])) {
+                    $dbOrder = $dbOrdersByCode[$valCode];
+                    if ($dbOrder->month != $valMonthTs) {
+                        $errors[] = "第{$line}行:单据[{$valCode}]所属月份为" . date('Y-m', $dbOrder->month) . ",与当前月份不符";
+                    }
+                    $update_map[$rowIndex] = $dbOrder->id;
+                } else {
+                    // 填了单号但库里没有
+                    $errors[] = "第{$line}行:单据编号[{$valCode}]无效或不存在";
+                }
+            } else {
+                // 意图:新增。校验是否该月已有单据
+                if (isset($existingMonthsMap[$valMonthTs])) {
+                    $correctCode = $existingMonthsMap[$valMonthTs];
+                    $errors[] = "第{$line}行:月份[{$monthStr}]已存在研发单,单号为:[{$correctCode}],请填写该单号进行更新";
+                }
+            }
+
+            // C. 考勤基准对比
+            $sysData = $systemStatsMap[$valMonthTs][$deviceId] ?? null;
+            if ($sysData) {
+                $inTotalDays = (float)($row[$numIdx] ?? 0);
+                $inRdDays    = (float)($row[$num2Idx] ?? 0);
+                $inTotalHours= (float)($row[$num3Idx] ?? 0);
+                $inRdHours   = (float)($row[$num4Idx] ?? 0);
+
+                // 1. 内部逻辑:研发 <= 总额
+                if ($inRdDays > $inTotalDays + 0.01) $errors[] = "第{$line}行:{$devTitle}研发天数大于总天数";
+                if ($inRdHours > $inTotalHours + 0.01) $errors[] = "第{$line}行:{$devTitle}研发工时大于总工时";
+
+                // 2. 外部逻辑:总额 == 系统核算
+                if (abs($inTotalDays - $sysData['attendance_days']) > 0.01) {
+                    $errors[] = "第{$line}行:{$devTitle}总天数({$inTotalDays})不等于系统核算({$sysData['attendance_days']})";
+                }
+                if (abs($inTotalHours - $sysData['final_work_hour']) > 0.01) {
+                    $errors[] = "第{$line}行:{$devTitle}总工时({$inTotalHours})不等于系统核算({$sysData['final_work_hour']})";
+                }
+            }
+
+            // D. 组内查重 (单号或月份作为聚合维度)
+            $aggKey = $valCode ?: "NEW_" . $valMonthTs;
+            if (isset($excelAggregator[$aggKey]['devs'][$valDevCode])) {
+                $errors[] = "第{$line}行:资产编码[{$valDevCode}]在同单据中重复";
+            }
+            $excelAggregator[$aggKey]['devs'][$valDevCode] = true;
+        }
+
+        $error_string = !empty($errors) ? implode('|', $errors) : "";
+        return [$error_string, $update_map, $dbDevs];
+    }
+
+    private function monthDwOrderCheck1(&$array, $user, $table_config)
+    {
+        $keys = array_column($table_config, 'key');
+        $codeIdx = array_search('code', $keys);
+        $monthIdx = array_search('month', $keys);
+        $devIdx = array_search('device_id', $keys);
 
         $numIdx = array_search('total_days', $keys);
         $num2Idx = array_search('rd_total_days', $keys);
@@ -1742,23 +2019,6 @@ class ImportService extends Service
                 $excelAggregator[$aggKey]['devs'][$valDev] = true;
             }
 
-            // F. 日期范围校验
-            list($sS, $startTime) = $this->convertExcelCellToDate($row[$startIdx] ?? '');
-            list($eS, $endTime)   = $this->convertExcelCellToDate($row[$endIdx] ?? '');
-            if (!$sS || !$eS) {
-                $errors[] = "第{$displayLine}行:明细日期格式错误";
-            } else {
-                if ($startTime > $endTime) {
-                    $errors[] = "第{$displayLine}行:开始日期不能大于结束日期";
-                }
-                $monthEndTs = strtotime("+1 month", $valMonthTs) - 1;
-                if ($startTime < $valMonthTs || $endTime > $monthEndTs) {
-                    $errors[] = "第{$displayLine}行:明细日期超出[" . date('Y-m', $valMonthTs) . "]范围";
-                }
-                $array[$rowIndex][$startIdx] = $startTime;
-                $array[$rowIndex][$endIdx]   = $endTime;
-            }
-
             // G. 数字格式校验 (checkNumber 方法)
             foreach ([$numIdx => '天数', $num2Idx => '研发天数', $num3Idx => '工时', $num4Idx => '研发工时'] as $idx => $label) {
                 $res = $this->checkNumber($row[$idx] ?? 0, 2, 'non-negative');

+ 72 - 1
app/Service/PersonWorkService.php

@@ -187,7 +187,78 @@ class PersonWorkService extends Service
         return [true, $list];
     }
 
-    public function monthlyPwOrderRule(&$data, $user, $is_add = true){
+    public function monthlyPwOrderRule(&$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, '人员月度工时单明细不能为空'];
+
+        //获取系统计算的考勤基准数据 ---
+        $empIds = array_column($data['details'], 'employee_id');
+        list($status, $systemStats) = (new EmployeeService())->getEmployeesMonthStats($empIds, $data['month'], $user);
+        if (!$status) return [false, $systemStats]; // 如果日历未设置,直接拦截
+
+        foreach ($data['details'] as $key => $value) {
+            if (empty($value['employee_id'])) return [false, '人员不能为空'];
+            $empId = $value['employee_id'];
+
+            // 基础数字格式检查
+            foreach (['total_days', 'rd_total_days', 'total_hours', 'rd_total_hours'] as $field) {
+                $precision = 2;
+                $res = $this->checkNumber($value[$field], $precision, 'non-negative');
+                if (!$res['valid']) return [false, $value['employee_title'] . "的" . $field . ":" . $res['error']];
+            }
+
+            // --- 业务逻辑校验:出勤天数与工时合法性 ---
+            $sysData = $systemStats[$empId] ?? null;
+            if ($sysData) {
+                // 1. 研发天数不能大于出勤总天数
+                if ($value['rd_total_days'] > $value['total_days']) {
+                    return [false, "第" . ($key + 1) . "行:研发出勤天数不能大于出勤总天数"];
+                }
+
+                // 2. 研发工时不能大于出勤总工时
+                if ($value['rd_total_hours'] > $value['total_hours']) {
+                    return [false, "第" . ($key + 1) . "行:研发总工时不能大于出勤总工时"];
+                }
+
+                // 4. 校验出勤总天数是否超过了系统计算的上限
+                if ($value['total_days'] != $sysData['attendance_days']) {
+                    return [false, "人员[{$empId}]填写的出勤总天数({$value['total_days']})不等于系统核算的天数({$sysData['attendance_days']})"];
+                }
+
+                //校验出勤总工时是否超过了系统计算的上限
+                if ($value['total_hours'] != $sysData['final_work_hour']) {
+                    return [false, "人员[{$empId}]填写的出勤总工时({$value['total_hours']})不等于系统核算的工时({$sysData['final_work_hour']})"];
+                }
+            }
+
+            $data['details'][$key]['top_depart_id'] = $data['top_depart_id'];
+        }
+
+        // --- 查重与唯一性校验 ---
+        list($status, $msg) = $this->checkArrayRepeat($data['details'], 'employee_id', '人员');
+        if (!$status) return [false, $msg];
+
+        $query = MonthlyPwOrder::where('top_depart_id', $data['top_depart_id'])
+            ->where('month', $data['month'])
+            ->where('del_time', 0);
+
+        if (!$is_add) {
+            if (empty($data['id'])) return [false, 'ID不能为空'];
+            $query->where('id', '<>', $data['id']);
+        }
+
+        if ($query->exists()) {
+            return [false, date("Y-m", $data['month']) . '已存在人员月度研发工时单'];
+        }
+
+        return [true, ''];
+    }
+
+    public function monthlyPwOrderRule1(&$data, $user, $is_add = true){
         if(empty($data['month'])) return [false, '月份不能为空'];
         $data['month'] = $this->changeDateToDate($data['month']);
 

+ 24 - 0
app/Service/RuleSetService.php

@@ -2,6 +2,7 @@
 
 namespace App\Service;
 
+use App\Model\CalendarDetails;
 use App\Model\Device;
 use App\Model\Employee;
 use App\Model\Item;
@@ -428,6 +429,29 @@ class RuleSetService extends Service
         return $res;
     }
 
+    public function isSetMonthCalendar($data, $user){
+        $data['top_depart_id'] = $user['top_depart_id'];
+        if(empty($data['month'])) return [false, '月份不能为空'];
+
+        // 1. 月份初始化
+        $monthStart = $this->changeDateToDate($data['month']);
+        $monthStr = date("Y-m", $monthStart);
+        $endTime = strtotime("+1 month", $monthStart) - 1;
+
+        // 2. 获取当月标准工作日天数 (基数)
+        $standardWorkDays = DB::table('calendar_details')
+            ->where('top_depart_id', $data['top_depart_id'])
+            ->where('del_time', 0)
+            ->where('time', '>=', $monthStart)
+            ->where('time', '<=', $endTime)
+            ->where('is_work', CalendarDetails::TYPE_ONE)
+            ->count();
+
+        if ($standardWorkDays <= 0) return [false, $monthStr . '还未设置工作日'];
+
+        return [true, ''];
+    }
+
     public function ruleSetCreate($data, $user){
         $data['top_depart_id'] = $user['top_depart_id'];
         if(empty($data['month'])) return [false, '月份不能为空'];

+ 1 - 23
config/excel/monthDwOrder.php

@@ -89,28 +89,6 @@ return [
             'unique' => false,
             'enums' => [],
             'comments' => '必填'
-        ],
-        [
-            'key' =>'start_time',
-            'export' =>'start_time',
-            'value' => '开始日期',
-            'required' => true,
-            'is_main' => false,
-            'default' => "",
-            'unique' => false,
-            'enums' => [],
-            'comments' => '必填'
-        ],
-        [
-            'key' =>'end_time',
-            'export' =>'end_time',
-            'value' => '结束日期',
-            'required' => true,
-            'is_main' => false,
-            'unique' => false,
-            'enums' => [],
-            'default' => "",
-            'comments' => ""
-        ],
+        ]
     ]
 ];

+ 4 - 0
routes/api.php

@@ -156,5 +156,9 @@ Route::group(['middleware'=> ['checkLogin']],function ($route){
     $route->any('pLeaveOverAdd', 'Api\PLeaveOverController@pLeaveOverAdd');
     $route->any('pLeaveOverDel', 'Api\PLeaveOverController@pLeaveOverDel');
     $route->any('pLeaveOverDetail', 'Api\PLeaveOverController@pLeaveOverDetail');
+
+    //获取默认月考勤数据 人和设备
+    $route->any('isSetMonthCalendar', 'Api\RuleSetController@isSetMonthCalendar');
+
 });