cqp 3 месяцев назад
Родитель
Сommit
b7a52c77f4

+ 0 - 67
app/Http/Controllers/Api/ItemController.php

@@ -73,71 +73,4 @@ class ItemController extends BaseController
             return $this->json_return(201,$data);
         }
     }
-
-    public function calendarEdit(Request $request)
-    {
-        $service = new ItemService();
-        $user = $request->userData;
-        list($status,$data) = $service->calendarEdit($request->all(),$user);
-
-        if($status){
-            return $this->json_return(200,'',$data);
-        }else{
-            return $this->json_return(201,$data);
-        }
-    }
-
-    public function calendarAdd(Request $request)
-    {
-        $service = new ItemService();
-        $user = $request->userData;
-        list($status,$data) = $service->calendarAdd($request->all(),$user);
-
-        if($status){
-            return $this->json_return(200,'',$data);
-        }else{
-            return $this->json_return(201,$data);
-        }
-
-    }
-
-    public function calendarDel(Request $request)
-    {
-        $service = new ItemService();
-        $user = $request->userData;
-        list($status,$data) = $service->calendarDel($request->all());
-
-        if($status){
-            return $this->json_return(200,'',$data);
-        }else{
-            return $this->json_return(201,$data);
-        }
-
-    }
-
-    public function calendarList(Request $request)
-    {
-        $service = new ItemService();
-        $user = $request->userData;
-        list($status,$data) = $service->calendarList($request->all(),$user);
-
-        if($status){
-            return $this->json_return(200,'',$data);
-        }else{
-            return $this->json_return(201,$data);
-        }
-    }
-
-    public function calendarDetail(Request $request)
-    {
-        $service = new ItemService();
-        $user = $request->userData;
-        list($status,$data) = $service->calendarDetail($request->all(),$user);
-
-        if($status){
-            return $this->json_return(200,'',$data);
-        }else{
-            return $this->json_return(201,$data);
-        }
-    }
 }

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

@@ -0,0 +1,76 @@
+<?php
+
+namespace App\Http\Controllers\Api;
+
+use App\Service\PersonWorkService;
+use Illuminate\Http\Request;
+
+class PersonWorkController extends BaseController
+{
+    public function monthlyPwOrderEdit(Request $request)
+    {
+        $service = new PersonWorkService();
+        $user = $request->userData;
+        list($status,$data) = $service->monthlyPwOrderEdit($request->all(),$user);
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+    }
+
+    public function monthlyPwOrderAdd(Request $request)
+    {
+        $service = new PersonWorkService();
+        $user = $request->userData;
+        list($status,$data) = $service->monthlyPwOrderAdd($request->all(),$user);
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+
+    }
+
+    public function monthlyPwOrderDel(Request $request)
+    {
+        $service = new PersonWorkService();
+        $user = $request->userData;
+        list($status,$data) = $service->monthlyPwOrderDel($request->all());
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+
+    }
+
+    public function monthlyPwOrderList(Request $request)
+    {
+        $service = new PersonWorkService();
+        $user = $request->userData;
+        list($status,$data) = $service->monthlyPwOrderList($request->all(),$user);
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+    }
+
+    public function monthlyPwOrderDetail(Request $request)
+    {
+        $service = new PersonWorkService();
+        $user = $request->userData;
+        list($status,$data) = $service->monthlyPwOrderDetail($request->all(),$user);
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+    }
+}

+ 16 - 0
app/Model/MonthlyPwOrder.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace App\Model;
+
+class MonthlyPwOrder extends DataScopeBaseModel
+{
+    protected $table = "monthly_pw_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_pw_order";
+}

+ 12 - 0
app/Model/MonthlyPwOrderDetails.php

@@ -0,0 +1,12 @@
+<?php
+
+namespace App\Model;
+
+class MonthlyPwOrderDetails extends DataScopeBaseModel
+{
+    protected $guarded = [];
+    protected $table = "monthly_pw_order_details"; //指定表
+    const CREATED_AT = 'crt_time';
+    const UPDATED_AT = 'upd_time';
+    protected $dateFormat = 'U';
+}

+ 23 - 0
app/Service/ExportFileService.php

@@ -132,6 +132,29 @@ class ExportFileService extends Service
         return [true, $this->saveExportData($return,$header)];
     }
 
+    public function fee($ergs,$user){
+        // 导出数据
+        $return = [];
+        $header_default = $user['e_header_default'];
+        $column = array_column($header_default,'export');
+        $header = array_column($header_default,'value');
+
+        $service = new FeeService();
+        $model = $service->feeCommon($ergs, $user);
+
+        $model->chunk(500,function ($data) use(&$return, $service, $column, $user){
+            $data = $data->toArray();
+            $list['data'] = $data;
+
+            //订单数据
+            $list = $service->fillFeeList($list, $user, true);
+            //返回数据
+            $this->fillData($list['data'], $column, $return);
+        });
+
+        return [true, $this->saveExportData($return,$header)];
+    }
+
     public function one($ergs, $user){
         $service = new EmployeeService();
         $model = $service->employeeCommon($ergs, $user);

+ 10 - 4
app/Service/FeeService.php

@@ -93,12 +93,18 @@ class FeeService extends Service
         return [true,['data' => $list,'tree' => $list_tree]];
     }
 
-    public function fillFeeList($list,$user){
+    public function fillFeeList($list,$user, $is_export = false){
         if(empty($list)) return $list;
 
-//        foreach ($list as $key => $value){
-//
-//        }
+        if($is_export){
+            $map = Fee::where('del_time',0)
+                ->whereIn('id', array_column($list['data'], 'parent_id'))
+                ->pluck('code','id')
+                ->toArray();
+            foreach ($list['data'] as $key => $value){
+                $list['data'][$key]['parent_code'] = $map[$value['parent_id']] ?? "";
+            }
+        }
 
         return $list;
     }

+ 240 - 1
app/Service/ImportService.php

@@ -6,11 +6,13 @@ use App\Exports\TableHeadExport;
 use App\Import\ImportAll;
 use App\Model\Device;
 use App\Model\Employee;
+use App\Model\Fee;
 use App\Model\Item;
 use App\Model\ItemDetails;
 use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Log;
 use Maatwebsite\Excel\Facades\Excel;
+use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions\F;
 use PhpOffice\PhpSpreadsheet\IOFactory;
 use PhpOffice\PhpSpreadsheet\Shared\Date;
 
@@ -19,6 +21,7 @@ class ImportService extends Service
     public static $type = [
         'device', // 设备
         'item', // 项目
+        'fee', // 费用
     ];
 
     public function getTableTitleXls($data,$user){
@@ -71,6 +74,17 @@ class ImportService extends Service
         return [true, [$config_array, $filename]];
     }
 
+    private function fee($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){
 //        //不超时
@@ -491,7 +505,231 @@ class ImportService extends Service
         return [$manMap, $devMap];
     }
 
+    // 费用 ----------------------------------
+    public function feeImport($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. 详细校验
+        list($error, $update_map, $parent_code_map) = $this->feeCheck($array, $user, $table_config);
+        if (!empty($error)) return [0, $error];
+
+        $time = time();
+        $insert = [];
+        $update = [];
+        $all_codes = [];
+
+        // --- 修正点 1: 必须确保索引提取准确 ---
+        $keys = array_column($table_config, 'key');
+        $codeIdx = array_search('code', $keys);
+        // ------------------------------------
+
+        foreach ($array as $key => $value) {
+            $cCode = trim($value[$codeIdx] ?? '');
+            if($cCode === '') continue;
+
+            $all_codes[] = $cCode;
+            $main_tmp = [];
+            foreach ($value as $k => $val){
+                if(!empty($table_config[$k]['is_main'])){
+                    if ($table_config[$k]['key'] !== 'parent_id') {
+                        $main_tmp[$table_config[$k]['key']] = $val;
+                    }
+                }
+            }
+
+            if (isset($update_map[$key])) {
+                $update[] = array_merge($main_tmp, ['id' => $update_map[$key]]);
+            } else {
+                $main_tmp['top_depart_id'] = $user['top_depart_id'];
+                $main_tmp['crt_time'] = $time;
+                $main_tmp['parent_id'] = 0;
+                $insert[] = $main_tmp;
+            }
+        }
+
+        DB::beginTransaction();
+        try {
+            if (!empty($insert)) {
+                foreach (array_chunk($insert, 500) as $chunk) {
+                    Fee::insert($chunk);
+                }
+            }
+
+            if (!empty($update)) {
+                foreach ($update as $item) {
+                    $uId = $item['id']; unset($item['id']);
+                    Fee::where('id', $uId)->update($item);
+                }
+            }
+
+            // --- 修正点 2: 核心回填逻辑 ---
+            $newCodeToIdMap = Fee::where('del_time', 0)
+                ->where('top_depart_id', $user['top_depart_id'])
+                ->whereIn('code', array_unique($all_codes))
+                ->pluck('id', 'code')
+                ->toArray();
+
+            foreach ($parent_code_map as $rowIndex => $pCode) {
+                // 这里必须通过 $rowIndex 从原始 $array 中精准获取当前行的编码
+                $currentCode = isset($array[$rowIndex][$codeIdx]) ? trim($array[$rowIndex][$codeIdx]) : '';
+                $pCode = trim($pCode);
+
+                if ($currentCode === '' || $pCode === '') continue;
+
+                $currentId = $newCodeToIdMap[$currentCode] ?? null;
+                $parentId = $newCodeToIdMap[$pCode] ?? null;
+
+                // 严谨判断:只有当前记录存在,且上级记录也存在,且两者不是同一个 ID 时才更新
+                if ($currentId && $parentId && $currentId != $parentId) {
+                    Fee::where('id', $currentId)->update(['parent_id' => $parentId]);
+                }
+            }
+
+            DB::commit();
+        } catch (\Exception $e) {
+            DB::rollBack();
+            return [false, "失败:" . $e->getMessage() . " (行号:" . $e->getLine() . ")"];
+        }
+        return [true, ''];
+    }
+
+    private function feeCheck(&$array, $user, $table_config)
+    {
+        $keys = array_column($table_config, 'key');
+        $codeIdx = array_search('code', $keys);
+        $parentIdx = array_search('parent_id', $keys);
+
+        // 1. 获取基础数据
+        list($dbFeeMap, $excelCodesMap) = $this->getFeeList($array, $user, $codeIdx);
+
+        $errors = [];
+        $update = [];
+        $parent_code_map = [];
+
+        // 2. 建立 Excel 内部父子关系映射(用于环路追溯)
+        $currentExcelMap = [];
+        foreach ($array as $row) {
+            $c = trim($row[$codeIdx] ?? '');
+            $p = trim($row[$parentIdx] ?? '');
+            if ($c !== '') $currentExcelMap[$c] = $p;
+        }
+
+        // 3. 逐行校验
+        foreach ($array as $rowIndex => $value) {
+            $valCode = trim($value[$codeIdx] ?? '');
+            $valParentCode = trim($value[$parentIdx] ?? '');
+
+            if ($valCode === '') continue;
+
+            // 更新状态记录
+            if (isset($dbFeeMap[$valCode])) {
+                $update[$rowIndex] = $dbFeeMap[$valCode]['id'];
+            }
+
+            if ($valParentCode !== '') {
+                // --- A. 存在性校验 ---
+                // 这里会检查 006 是否在数据库,或者是否在本次 Excel 的其他行中
+                if (!isset($dbFeeMap[$valParentCode]) && !isset($excelCodesMap[$valParentCode])) {
+                    $errors[] = "第{$rowIndex}行:上级编码[{$valParentCode}]在系统和文件中均不存在";
+                    continue;
+                }
+
+                // --- B. 自引用校验 ---
+                if ($valCode === $valParentCode) {
+                    $errors[] = "第{$rowIndex}行:上级编码不能是自身";
+                    continue;
+                }
+
+                // --- C. 环路追溯 ---
+                $visited = [];
+                $res = $this->findLoopInAncestors($valParentCode, $valCode, $currentExcelMap, $dbFeeMap, $visited);
+                if ($res !== false) {
+                    if ($res['type'] === 'LOOP_SELF') {
+                        $errors[] = "第{$rowIndex}行:编码[{$valCode}]与上级[{$valParentCode}]存在循环引用";
+                    } else {
+                        $errors[] = "第{$rowIndex}行:上级[{$valParentCode}]的溯源链条已成环";
+                    }
+                    continue;
+                }
+
+                // 记录有效的父级关系
+                $parent_code_map[$rowIndex] = $valParentCode;
+            }
+        }
+
+        $error_string = !empty($errors) ? implode('|', $errors) : "";
+        return [$error_string, $update, $parent_code_map];
+    }
+
+    private function getFeeList($array, $user, $index)
+    {
+        // 关键:一定要把 Excel 里所有的 code 这一列全部拿出来,并去除空值
+        $codesInExcel = [];
+        foreach ($array as $row) {
+            $c = trim($row[$index] ?? '');
+            if ($c !== '') {
+                $codesInExcel[$c] = true;
+            }
+        }
+
+        $allFees = Fee::where('del_time', 0)
+            ->where('top_depart_id', $user['top_depart_id'])
+            ->select('id', 'code', 'parent_id')
+            ->get();
+
+        $dbFeeMap = [];
+        foreach ($allFees as $fee) {
+            $dbFeeMap[$fee->code] = [
+                'id' => $fee->id,
+                'code' => $fee->code,
+                'parent_id' => $fee->parent_id
+            ];
+        }
+
+        // 返回数据库映射和 Excel 编码映射
+        return [$dbFeeMap, $codesInExcel];
+    }
+
+    private function findLoopInAncestors($startParentCode, $targetCode, $excelMap, $dbFeeMap, &$visited)
+    {
+        $current = $startParentCode;
+        while ($current !== '' && $current !== 0) {
+            if ($current === $targetCode) {
+                return ['type' => 'LOOP_SELF', 'code' => $current];
+            }
+            if (isset($visited[$current])) {
+                return ['type' => 'EXISTING_LOOP', 'code' => $current];
+            }
+            $visited[$current] = true;
+
+            if (isset($excelMap[$current]) && $excelMap[$current] !== '') {
+                $current = $excelMap[$current];
+            } elseif (isset($dbFeeMap[$current])) {
+                $parentId = $dbFeeMap[$current]['parent_id'];
+                $parentCode = '';
+                foreach ($dbFeeMap as $item) {
+                    if ($item['id'] == $parentId) {
+                        $parentCode = $item['code'];
+                        break;
+                    }
+                }
+                $current = $parentCode;
+            } else {
+                break;
+            }
+        }
+        return false;
+    }
 
     //公共校验 -----------------------------------------
     private function checkCommon($array, $table_config) {
@@ -550,7 +788,8 @@ class ImportService extends Service
     //模板校验 -----------------------------------------
     private function compareTableAndReturn($upload, $param){
         if(empty($upload)) return [false, '表头不能为空'];
-        $config_array = $this->getTableConfig($param['type']);
+        $config = $this->getTableConfig($param['type']);
+        $config_array = $config['array'];
         if(empty($config_array)) return [false, '导入配置表头文件不存在'];
 
         foreach ($config_array as $key => $value){

+ 307 - 0
app/Service/PersonWorkService.php

@@ -0,0 +1,307 @@
+<?php
+
+namespace App\Service;
+
+use App\Model\Device;
+use App\Model\Employee;
+use App\Model\Item;
+use App\Model\ItemDetails;
+use App\Model\MonthlyPwOrder;
+use App\Model\MonthlyPwOrderDetails;
+use Illuminate\Support\Facades\DB;
+
+class PersonWorkService extends Service
+{
+    public function monthlyPwOrderEdit($data,$user){
+        list($status,$msg) = $this->monthlyPwOrderRule($data, $user, false);
+        if(!$status) return [$status,$msg];
+
+        try {
+            DB::beginTransaction();
+
+            $model = Item::where('id',$data['id'])->first();
+            $model->month = $data['month'] ?? 0;
+            $model->save();
+
+            $time = time();
+            MonthlyPwOrderDetails::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 monthlyPwOrderAdd($data,$user){
+        list($status,$msg) = $this->monthlyPwOrderRule($data, $user);
+        if(!$status) return [$status,$msg];
+
+        try {
+            DB::beginTransaction();
+
+            $model = new MonthlyPwOrder();
+            $model->code = $this->generateBillNo([
+                'top_depart_id' => $user['top_depart_id'],
+                'type' => MonthlyPwOrder::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'],
+                    'total_days' => $value['total_days'],
+                    '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'],
+                ];
+            }
+            if(! empty($unit)) MonthlyPwOrderDetails::insert($unit);
+        }
+    }
+
+    private function getDetail($id){
+        $data = MonthlyPwOrderDetails::where('del_time',0)
+            ->where('main_id', $id)
+            ->select('employee_id', 'total_days', 'rd_total_days', 'total_hours', 'rd_total_hours', 'start_time', 'end_time')
+            ->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');
+
+        $unit = [];
+        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' => $unit,
+        ];
+
+        foreach ($detail as $key => $value) {
+            if (empty($value)) {
+                $detail[$key] = (object)[]; // 转成 stdClass 对象
+            }
+        }
+
+        return $detail;
+    }
+
+
+    public function monthlyPwOrderDel($data){
+        if($this->isEmpty($data,'id')) return [false,'请选择数据!'];
+
+        try {
+            DB::beginTransaction();
+            $time = time();
+
+            MonthlyPwOrder::where('del_time',0)
+                ->whereIn('id',$data['id'])
+                ->update(['del_time' => $time]);
+
+            MonthlyPwOrderDetails::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 monthlyPwOrderDetail($data, $user){
+        if($this->isEmpty($data,'id')) return [false,'请选择数据!'];
+        $customer = MonthlyPwOrder::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 monthlyPwOrderCommon($data,$user, $field = []){
+        if(empty($field)) $field = MonthlyPwOrder::$field;
+
+        $model = MonthlyPwOrder::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 monthlyPwOrderList($data,$user){
+        $model = $this->monthlyPwOrderCommon($data, $user);
+        $list = $this->limit($model,'',$data);
+        $list = $this->fillData($list);
+
+        return [true, $list];
+    }
+
+    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, '人员月度工时单明细不能为空'];
+        foreach ($data['details'] as $key => $value){
+            if(empty($value['employee_id'])) return [false, '人员不能为空'];
+            $res = $this->checkNumber($data['total_days'],0,'non-negative');
+            if(! $res['valid']) return [false,'出勤总天数:' . $res['error']];
+            $res = $this->checkNumber($data['rd_total_days'],0,'non-negative');
+            if(! $res['valid']) return [false,'研发出勤总天数:' . $res['error']];
+            $res = $this->checkNumber($data['total_hours'],2,'non-negative');
+            if(! $res['valid']) return [false,'出勤总工时:' . $res['error']];
+            $res = $this->checkNumber($data['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($data['start_time']);
+            if(empty($value['end_time'])) return [false, '结束时间不能为空'];
+            $data['details'][$key]['end_time'] = $this->changeDateToDate($data['end_time']);
+            $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 = MonthlyPwOrder::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 = Item::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, $is_export = false){
+        if(empty($data['data'])) return $data;
+
+        $emp = (new EmployeeService())->getEmployeeMap(array_unique(array_column($data['data'],'crt_id')));
+        $map = [];
+        if($is_export) $map = $this->getDetailsMap(array_column($data['data'],'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']] ?? '';
+            $tmp = $map[$value['id']] ?? [];
+            $data['data'][$key]['details'] = $tmp['man'] ?? "";
+        }
+
+        return $data;
+    }
+
+    public function getDetailsMap($item_id){
+        // 1. 获取明细数据,注意一定要选出 item_id
+        $data = ItemDetails::where('del_time', 0)
+            ->whereIn('item_id', $item_id)
+            ->select('item_id', 'data_id', 'type')
+            ->get();
+
+        // 2. 收集 ID 并按类型分类(去重以减少查询压力)
+        $manIds = $data->where('type', ItemDetails::type_one)->pluck('data_id')->unique()->toArray();
+        $devIds = $data->where('type', ItemDetails::type_two)->pluck('data_id')->unique()->toArray();
+
+        // 3. 一次性查出对应的映射 (假设人员表字段是 number, 设备表是 code)
+        // 注意:pluck('显示内容', 'ID')
+        $manMap = Employee::whereIn('id', $manIds)->pluck('number', 'id')->toArray();
+        $devMap = Device::whereIn('id', $devIds)->pluck('code', 'id')->toArray();
+
+        // 4. 按 item_id 分组处理成字符串
+        $result = [];
+        foreach ($item_id as $id) {
+            // 初始化每个 item_id 的默认结构
+            $result[$id] = [
+                'man' => '',
+                'device' => ''
+            ];
+        }
+
+        // 5. 遍历明细填充数据
+        $grouped = $data->groupBy('item_id');
+        foreach ($grouped as $itemId => $items) {
+            $mans = [];
+            $devices = [];
+
+            foreach ($items as $item) {
+                if ($item->type == ItemDetails::type_one) {
+                    if (isset($manMap[$item->data_id])) {
+                        $mans[] = $manMap[$item->data_id];
+                    }
+                } else {
+                    if (isset($devMap[$item->data_id])) {
+                        $devices[] = $devMap[$item->data_id];
+                    }
+                }
+            }
+
+            $result[$itemId] = [
+                'man' => implode(',', $mans),
+                'device' => implode(',', $devices)
+            ];
+        }
+
+        return $result;
+    }
+}

+ 36 - 0
app/Service/Service.php

@@ -3,6 +3,7 @@
 namespace App\Service;
 
 
+use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Redis;
 
 /**
@@ -446,4 +447,39 @@ class Service
 
         return [true, ''];
     }
+
+    /**
+     * 生成单据编号
+     * @param int $top_depart_id 公司ID
+     * @param string $type 单据类型标识 (如 'FEE')
+     * @param string $prefix 编号前缀 (如 'FE')
+     * @param int $length 流水号长度
+     * @return string
+     */
+    public function generateBillNo($need = [], $length = 5)
+    {
+        $top_depart_id = $need['top_depart_id'];
+        $type = $need['type'];
+        $prefix = $need['prefix'] ?? '';
+        $period = $need['period']; // 202603
+
+        // 1. 原子更新:存在则自增,不存在则初始化
+        // 这样处理不需要 select for update,性能最高且并发安全
+        DB::statement("
+        INSERT INTO sys_bill_sequences (top_depart_id, bill_type, period, current_value) 
+        VALUES (?, ?, ?, 1) 
+        ON DUPLICATE KEY UPDATE current_value = current_value + 1
+    ", [$top_depart_id, $type, $period]);
+
+        // 2. 获取本次生成的流水号
+        $sequence = DB::table('sys_bill_sequences')
+            ->where('top_depart_id', $top_depart_id)
+            ->where('bill_type', $type)
+            ->where('period', $period)
+            ->value('current_value');
+
+        // 3. 拼接输出:前缀 + 年月 + 补齐位数的流水号
+        // 结果示例:FE26030001
+        return $prefix . $period . str_pad($sequence, $length, '0', STR_PAD_LEFT);
+    }
 }

+ 39 - 0
config/excel/fee.php

@@ -0,0 +1,39 @@
+<?php
+return [
+    "name" => "费用档案",
+    "array" => [
+        [
+            'key' =>'code',
+            'export' =>'code',
+            'value' => '编码',
+            'required' => true,
+            'is_main' => true,
+            'default' => "",
+            'unique' => true,
+            'enums' => [],
+            'comments' => '必填'
+        ],
+        [
+            'key' =>'title',
+            'export' =>'title',
+            'value' => '名称',
+            'required' => true,
+            'is_main' => true,
+            'default' => "",
+            'unique' => false,
+            'enums' => [],
+            'comments' => '必填'
+        ],
+        [
+            'key' =>'parent_id',
+            'export' =>'parent_code',
+            'value' => '上级费用',
+            'required' => false,
+            'is_main' => true,
+            'default' => "",
+            'unique' => false,
+            'enums' => [],
+            'comments' => '选填(填写编码)'
+        ],
+    ]
+];

+ 7 - 0
routes/api.php

@@ -88,5 +88,12 @@ Route::group(['middleware'=> ['checkLogin']],function ($route){
     $route->any('feeEdit', 'Api\FeeController@feeEdit')->name('fee.edit');
     $route->any('feeDel', 'Api\FeeController@feeDel')->name('fee.del');
     $route->any('feeList', 'Api\FeeController@feeList');
+
+    //人员月度研发工时单
+    $route->any('monthlyPwOrderList', 'Api\PersonWorkController@monthlyPwOrderList');
+    $route->any('monthlyPwOrderEdit', 'Api\PersonWorkController@monthlyPwOrderEdit');
+    $route->any('monthlyPwOrderAdd', 'Api\PersonWorkController@monthlyPwOrderAdd');
+    $route->any('monthlyPwOrderDel', 'Api\PersonWorkController@monthlyPwOrderDel');
+    $route->any('monthlyPwOrderDetail', 'Api\PersonWorkController@monthlyPwOrderDetail');
 });