浏览代码

小高薪

cqp 3 月之前
父节点
当前提交
ba83384cf4

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

@@ -0,0 +1,89 @@
+<?php
+
+namespace App\Http\Controllers\Api;
+
+use App\Service\RuleSetService;
+use Illuminate\Http\Request;
+
+class RuleSetController extends BaseController
+{
+    public function ruleSetEdit(Request $request)
+    {
+        $service = new RuleSetService();
+        $user = $request->userData;
+        list($status,$data) = $service->ruleSetEdit($request->all(),$user);
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+    }
+
+    public function ruleSetAdd(Request $request)
+    {
+        $service = new RuleSetService();
+        $user = $request->userData;
+        list($status,$data) = $service->ruleSetAdd($request->all(),$user);
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+
+    }
+
+    public function ruleSetDel(Request $request)
+    {
+        $service = new RuleSetService();
+        $user = $request->userData;
+        list($status,$data) = $service->ruleSetDel($request->all());
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+
+    }
+
+    public function ruleSetList(Request $request)
+    {
+        $service = new RuleSetService();
+        $user = $request->userData;
+        list($status,$data) = $service->ruleSetList($request->all(),$user);
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+    }
+
+    public function ruleSetDetail(Request $request)
+    {
+        $service = new RuleSetService();
+        $user = $request->userData;
+        list($status,$data) = $service->ruleSetDetail($request->all(),$user);
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+    }
+
+    public function ruleSetCreate(Request $request)
+    {
+        $service = new RuleSetService();
+        $user = $request->userData;
+        list($status,$data) = $service->ruleSetCreate($request->all(),$user);
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+    }
+}

+ 16 - 0
app/Model/RuleSet.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace App\Model;
+
+class RuleSet extends DataScopeBaseModel
+{
+    protected $table = "rule_set"; //指定表
+    const CREATED_AT = 'crt_time';
+    const UPDATED_AT = 'upd_time';
+    protected $dateFormat = 'U';
+    const employee_column = "crt_id";
+
+    public static $field = ['id','code','month','crt_id','crt_time'];
+
+    const Order_type = "rule_set";
+}

+ 19 - 0
app/Model/RuleSetDetails.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace App\Model;
+
+class RuleSetDetails extends DataScopeBaseModel
+{
+    protected $guarded = [];
+    protected $table = "rule_set_details"; //指定表
+    const CREATED_AT = 'crt_time';
+    const UPDATED_AT = 'upd_time';
+    protected $dateFormat = 'U';
+
+    const type_one = 1;
+    const type_two = 2;
+    public static $type_name = [
+        self::type_one => '人',
+        self::type_two => '设备',
+    ];
+}

+ 18 - 0
app/Service/ExportFileService.php

@@ -250,6 +250,24 @@ class ExportFileService extends Service
         return [true, $this->saveExportData($return,$header)];
     }
 
+    public function ruleSet($ergs,$user){
+        // 导出数据
+        $return = [];
+        $header_default = $user['e_header_default'];
+        $column = array_column($header_default,'export');
+        $header = array_column($header_default,'value');
+
+        $service = new RuleSetService();
+        $model = $service->ruleSetCommon($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 = ''){
         if(empty($file_name)) $file_name = self::$filename . "_". date("Y-m-d") . "_". rand(1000,9999);
         $filename =  $file_name . '.' . 'xlsx';

+ 234 - 92
app/Service/ImportService.php

@@ -14,6 +14,8 @@ use App\Model\ItemDetails;
 use App\Model\MonthlyDdOrder;
 use App\Model\MonthlyPsOrder;
 use App\Model\MonthlyPwOrder;
+use App\Model\RuleSet;
+use App\Model\RuleSetDetails;
 use Illuminate\Support\Facades\DB;
 use Maatwebsite\Excel\Facades\Excel;
 use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions\F;
@@ -32,15 +34,16 @@ class ImportService extends Service
         'monthDwOrder', // 设备月度研发工时单
         'monthPsOrder', // 人员月度工资单
         'monthDdOrder', // 设备月度折旧单
+        'ruleSet', // 规则配置单
     ];
 
     public function getTableTitleXls($data,$user){
         if(empty($data['type'])) return [false,'缺少类型'];
 
         //获取配置文件
-        $fuc = $data['type'];
-        if (! method_exists(self::class, $fuc)) return [false, "导入文件方法获取不存在,请联系开发"];
-        list($status,$return) = $this->$fuc($data,$user);
+//        $fuc = $data['type'];
+//        if (! method_exists(self::class, $fuc)) return [false, "导入文件方法获取不存在,请联系开发"];
+        list($status,$return) = $this->title($data,$user);
         list($msg,$filename) = $return;
         if(! $status) return [false, $msg];
 
@@ -62,95 +65,7 @@ class ImportService extends Service
         return config($config) ?? [];
     }
 
-    private function depart($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]];
-    }
-
-    private function employee($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]];
-    }
-
-    private function device($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]];
-    }
-
-    private function item($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]];
-    }
-
-    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]];
-    }
-
-    private function monthPwOrder($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]];
-    }
-
-    private function monthDwOrder($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]];
-    }
-
-    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]];
-    }
-
-    private function monthDdOrder($data,$user){
+    private function title($data,$user){
         $config = $this->getTableConfig($data['type']);
         if(empty($config)) return [false, ['导入配置表头文件不存在','']];
 
@@ -2253,9 +2168,236 @@ class ImportService extends Service
         return [$error_string, $update_map, $dbDevs];
     }
 
+    // 规则配置单
+    public function ruleSetImport($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, $dbDevs) = $this->ruleSetCheck($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);
+        $rateIdx = array_search('rate', $keys);
+
+        $groups = [];
+        foreach ($array as $rowIndex => $row) {
+            $mCode = trim($row[$codeIdx] ?? '');
+            $month = $row[$monthIdx];
+            $aggKey = $mCode ?: "NEW_" . $month;
+
+            if (!isset($groups[$aggKey])) {
+                $groups[$aggKey] = [
+                    'id' => $update_map[$rowIndex] ?? 0,
+                    'month' => $month,
+                    'details' => []
+                ];
+            }
+            $groups[$aggKey]['details'][] = [
+                'item_id' => $row['parsed_item_id'],
+                'data_id' => $row['parsed_data_id'],
+                'type'    => $row['parsed_type'],
+                'rate'    => $row[$rateIdx],
+            ];
+        }
+
+        $time = time();
+        DB::beginTransaction();
+        try {
+            foreach ($groups as $group) {
+                $mainId = $group['id'];
+                if ($mainId) {
+                    DB::table('rule_set_details')
+                        ->where('del_time',0)
+                        ->where('main_id', $mainId)->update(['del_time' => $time]);
+                } else {
+                    $newCode = $this->generateBillNo([
+                        'top_depart_id' => $user['top_depart_id'],
+                        'type' => RuleSet::Order_type,
+                        'period' => date("Ym", $group['month'])
+                    ]);
+                    $mainId = DB::table('rule_set')->insertGetId([
+                        'code' => $newCode,
+                        'month' => $group['month'],
+                        'top_depart_id' => $user['top_depart_id'],
+                        'crt_id' => $user['id'],
+                        'crt_time' => $time
+                    ]);
+                }
+
+                foreach ($group['details'] as &$d) {
+                    $d['main_id'] = $mainId;
+                    $d['top_depart_id'] = $user['top_depart_id'];
+                    $d['crt_time'] = $time;
+                }
+                DB::table('rule_set_details')->insert($group['details']);
+            }
+            DB::commit();
+        } catch (\Exception $e) {
+            DB::rollBack();
+            return [false, "失败: " . $e->getMessage()];
+        }
+
+        return [true, ''];
+    }
+
+    private function ruleSetCheck(&$array, $user, $table_config)
+    {
+        $keys = array_column($table_config, 'key');
+
+        // 1. 索引提取
+        $mCodeIdx = array_search('code', $keys); // 主表单号
+        $monthIdx = array_search('month', $keys); // 月份
+        $typeIdx = array_search('type', $keys);  // 明细类型
+        $dCodeIdx = array_search('code_2', $keys); // 明细编码 (工号或资产编号)
+        $itemIdIdx = array_search('item_id', $keys); // 项目编码
+        $rateIdx = array_search('rate', $keys); // 比例
+
+        // 预处理扫描变量
+        $uniqueMonths = [];
+        $empCodes = [];
+        $devCodes = [];
+        $itemCodes = [];
+        $type_map = array_flip(RuleSetDetails::$type_name);
+
+        // --- 步骤 1: 预处理扫描 ---
+        foreach ($array as $rowIndex => $row) {
+            // 月份预转
+            list($mStatus, $valMonthTs) = $this->convertExcelCellToDate($row[$monthIdx] ?? '');
+            if ($mStatus) {
+                $valMonthTs = strtotime(date('Y-m-01', $valMonthTs));
+                $array[$rowIndex][$monthIdx] = $valMonthTs;
+                $uniqueMonths[] = $valMonthTs;
+            }
+
+            $type = $type_map[$row[$typeIdx]] ?? 0;
+            $dCode = trim($row[$dCodeIdx] ?? '');
+            $iCode = trim($row[$itemIdIdx] ?? '');
+
+            if ($type == RuleSetDetails::type_one && $dCode !== '') $empCodes[] = $dCode;
+            if ($type == RuleSetDetails::type_two && $dCode !== '') $devCodes[] = $dCode;
+            if ($iCode !== '') $itemCodes[] = $iCode;
+        }
+
+        // --- 步骤 2: 批量预加载数据库数据 ---
+        $dbEmps = Employee::where('del_time', 0)->where('top_depart_id', $user['top_depart_id'])
+            ->whereIn('number', array_unique($empCodes))->pluck('id', 'number')->toArray();
+        $dbDevs = Device::where('del_time', 0)->where('top_depart_id', $user['top_depart_id'])
+            ->whereIn('code', array_unique($devCodes))->pluck('id', 'code')->toArray();
+        $dbItems = Item::where('del_time', 0)->where('top_depart_id', $user['top_depart_id'])
+            ->whereIn('code', array_unique($itemCodes))->pluck('id', 'code')->toArray();
+
+        // 加载已有单据
+        $allMCodes = array_filter(array_unique(array_column($array, $mCodeIdx)));
+        $dbOrders = DB::table('rule_set')->where('top_depart_id', $user['top_depart_id'])
+            ->whereIn('code', $allMCodes)->get()->keyBy('code');
+
+        $existingMonthsMap = DB::table('rule_set')->where('top_depart_id', $user['top_depart_id'])
+            ->whereIn('month', array_unique($uniqueMonths))->pluck('id', 'month')->toArray();
+
+        $errors = [];
+        $update_map = [];
+        $aggregator = [];   // 用于校验分摊比例之和
+        $uniqueCheck = [];  // 用于校验同一个单据+项目内 人/设备唯一性
 
+        // --- 步骤 3: 详细校验循环 ---
+        foreach ($array as $rowIndex => $row) {
+            $displayLine = $rowIndex + 1;
+            $valMCode = trim($row[$mCodeIdx] ?? '');
+            $valMonthTs = $row[$monthIdx];
+            $typeTitle = trim($row[$typeIdx] ?? '');
+            $type = $type_map[$typeTitle] ?? 0;
+            $valDCode = trim($row[$dCodeIdx] ?? '');
+            $valICode = trim($row[$itemIdIdx] ?? '');
+            $valRate = $row[$rateIdx];
+
+            if (!is_numeric($valMonthTs)) {
+                $errors[] = "第{$displayLine}行:月份格式非法";
+                continue;
+            }
+
+            // A. 编辑/新增逻辑校验
+            if ($valMCode !== '') {
+                if (!isset($dbOrders[$valMCode])) {
+                    $errors[] = "第{$displayLine}行:系统不存在单号[{$valMCode}]";
+                } else {
+                    if ($dbOrders[$valMCode]->month != $valMonthTs) {
+                        $errors[] = "第{$displayLine}行:单号{$valMCode}编辑时不允许修改月份";
+                    }
+                    $update_map[$rowIndex] = $dbOrders[$valMCode]->id;
+                }
+            } else {
+                if (isset($existingMonthsMap[$valMonthTs])) {
+                    $errors[] = "第{$displayLine}行:月份[" . date('Y-m', $valMonthTs) . "]已存在配置单,请填写单号进行编辑";
+                }
+            }
+
+            // B. 人员/设备/项目存在性校验
+            $dataId = 0;
+            if ($type == RuleSetDetails::type_one) {
+                $dataId = $dbEmps[$valDCode] ?? 0;
+                if (!$dataId) $errors[] = "第{$displayLine}行:工号[{$valDCode}]不存在";
+            } elseif ($type == RuleSetDetails::type_two) {
+                $dataId = $dbDevs[$valDCode] ?? 0;
+                if (!$dataId) $errors[] = "第{$displayLine}行:资产编码[{$valDCode}]不存在";
+            } else {
+                $errors[] = "第{$displayLine}行:明细类型错误";
+            }
+
+            $itemId = $dbItems[$valICode] ?? 0;
+            if (!$itemId) $errors[] = "第{$displayLine}行:项目编码[{$valICode}]不存在";
+
+            // C. 比例校验
+            $res = $this->checkNumber($valRate, 2, 'positive');
+            if (!$res['valid']) {
+                $errors[] = "第{$displayLine}行:比例{$res['error']}";
+            }
+
+            // --- D. 修正后的聚合校验 ---
+            if ($dataId && $itemId) {
+                // 当前业务聚合的 Key(以单号或月份区分单据)
+                $billKey = $valMCode ?: "MONTH_" . $valMonthTs;
+
+                // D1. 唯一性:同一个单据里,同一个项目,同一个[人/设备]只能出现一次
+                $rowUniqueKey = $billKey . '_' . $itemId . '_' . $type . '_' . $dataId;
+                if (isset($uniqueCheck[$rowUniqueKey])) {
+                    $errors[] = "第{$displayLine}行:同一规则配置单,项目[{$valICode}]下人或设备不能重复录入";
+                }
+                $uniqueCheck[$rowUniqueKey] = true;
+
+                // D2. 比例和:同一个单据里,同一个人/设备在所有项目里的比例之和必须为100%
+                $totalRateKey = $billKey . '_' . $type . '_' . $dataId;
+                $aggregator[$totalRateKey]['total_rate'] = ($aggregator[$totalRateKey]['total_rate'] ?? 0) + $valRate;
+                $aggregator[$totalRateKey]['title'] = $valDCode;
+                $aggregator[$totalRateKey]['type_name'] = $typeTitle;
+                $aggregator[$totalRateKey]['month_str'] = date('Y-m', $valMonthTs); // 记录月份供报错使用
+            }
+
+            // 存入处理后的结果
+            $array[$rowIndex]['parsed_data_id'] = $dataId;
+            $array[$rowIndex]['parsed_item_id'] = $itemId;
+            $array[$rowIndex]['parsed_type'] = $type;
+        }
+
+        // --- 步骤 4: 比例汇总校验 ---
+        foreach ($aggregator as $key => $info) {
+            if (abs($info['total_rate'] - 100) > 0.0001) {
+                $errors[] = "汇总错误:月份[{$info['month_str']}]下,{$info['type_name']}[{$info['title']}]的所有项目分摊比例总和为 {$info['total_rate']}%,不等于 100%";            }
+        }
+
+        $error_string = !empty($errors) ? implode('|', $errors) : "";
+        return [$error_string, $update_map, $dbItems];
+    }
 
     //公共校验 -----------------------------------------
     private function checkCommon($array, $table_config) {

+ 1 - 1
app/Service/ItemService.php

@@ -255,7 +255,7 @@ class ItemService extends Service
         if(empty($data['device_list'])) return [false, '设备不能为空'];
         foreach ($data['device_list'] as $key => $value){
             if(empty($value['type'])) return [false, '类型不能为空'];
-            if(empty($value['data_id'])) return [false, '数据ID不能为空'];
+            if(empty($value['data_id'])) return [false, '设备ID不能为空'];
             $data['device_list'][$key]['top_depart_id'] = $data['top_depart_id'];
         }
         list($status, $msg) = $this->checkArrayRepeat($data['device_list'],'data_id','设备');

+ 511 - 0
app/Service/RuleSetService.php

@@ -0,0 +1,511 @@
+<?php
+
+namespace App\Service;
+
+use App\Model\Device;
+use App\Model\Employee;
+use App\Model\Item;
+use App\Model\RuleSet;
+use App\Model\RuleSetDetails;
+use Illuminate\Support\Facades\DB;
+
+class RuleSetService extends Service
+{
+    public function ruleSetEdit($data,$user){
+        list($status,$msg) = $this->ruleSetRule($data, $user, false);
+        if(!$status) return [$status,$msg];
+
+        try {
+            DB::beginTransaction();
+
+            $model = RuleSet::where('id',$data['id'])->first();
+//            $model->code = $data['code'] ?? '';
+//            $model->month = $data['month'] ?? 0;
+//            $model->save();
+
+            $time = time();
+            RuleSetDetails::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 ruleSetAdd($data,$user){
+        list($status,$msg) = $this->ruleSetRule($data, $user);
+        if(!$status) return [$status,$msg];
+
+        try {
+            DB::beginTransaction();
+
+            $model = new RuleSet();
+            $model->code = $data['code'] ?? '';
+            $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['man_list'])){
+            $unit = [];
+            foreach ($data['man_list'] as $value){
+                $unit[] = [
+                    'main_id' => $id,
+                    'item_id' => $value['item_id'],
+                    'type' => $value['type'],
+                    'data_id' => $value['data_id'],
+                    'rate' => $value['rate'],
+                    'crt_time' => $time,
+                    'top_depart_id' => $value['top_depart_id'],
+                ];
+            }
+            if(! empty($unit)) RuleSetDetails::insert($unit);
+        }
+
+        if(! empty($data['device_list'])){
+            $receipt = [];
+            foreach ($data['device_list'] as $value){
+                $receipt[] = [
+                    'main_id' => $id,
+                    'item_id' => $value['item_id'],
+                    'type' => $value['type'],
+                    'data_id' => $value['data_id'],
+                    'rate' => $value['rate'],
+                    'crt_time' => $time,
+                    'top_depart_id' => $value['top_depart_id'],
+                ];
+            }
+            if(! empty($receipt)) RuleSetDetails::insert($receipt);
+        }
+    }
+
+    private function getDetail($id){
+        $data = RuleSetDetails::where('del_time',0)
+            ->where('main_id', $id)
+            ->get()->toArray();
+
+        $id = $id2 = $item_id = [];
+        foreach ($data as $value){
+            if($value['type'] == RuleSetDetails::type_one) {
+                $id[] = $value['data_id'];
+            }else{
+                $id2[] = $value['data_id'];
+            }
+            if(! in_array($value['item_id'], $item_id)) $item_id[] = $value['item_id'];
+        }
+        $map = Employee::whereIn('id', $id)->select('title','id','number')->get()->toArray();
+        $map = array_column($map,null,'id');
+        $map2 = Device::whereIn('id', $id2)->select('code','id','title')->get()->toArray();
+        $map2 = array_column($map2,null,'id');
+        $map3 = Item::whereIn('id', $item_id)->select('title','id','code')->get()->toArray();
+        $map3 = array_column($map3,null,'id');
+
+        $unit = $receipt = [];
+        foreach ($data as $value){
+            $item = $map3[$value['item_id']] ?? [];
+            if($value['type'] == RuleSetDetails::type_one) {
+                $tmp = $map[$value['data_id']] ?? [];
+                $unit[] = [
+                    'type' => $value['type'],
+                    'rate' => $value['rate'],
+                    'data_id' => $value['data_id'],
+                    'data_title' => $tmp['title'],
+                    'data_code' => $tmp['number'],
+                    'item_id' => $value['item_id'],
+                    'item_title' => $item['title'] ?? "",
+                    'item_code' => $item['code'] ?? "",
+                ];
+            }else{
+                $tmp = $map2[$value['data_id']] ?? [];
+                $receipt[] = [
+                    'type' => $value['type'],
+                    'rate' => $value['rate'],
+                    'data_id' => $value['data_id'],
+                    'data_title' => $tmp['title'] ?? "",
+                    'data_code' => $tmp['code'] ?? "",
+                    'item_id' => $value['item_id'],
+                    'item_title' => $item['title'] ?? "",
+                    'item_code' => $item['code'] ?? "",
+                ];
+            }
+        }
+
+        $detail = [
+            'man_list' => $unit,
+            'device_list' => $receipt,
+        ];
+
+        foreach ($detail as $key => $value) {
+            if (empty($value)) {
+                $detail[$key] = (object)[]; // 转成 stdClass 对象
+            }
+        }
+
+        return $detail;
+    }
+
+    public function ruleSetDel($data){
+        if($this->isEmpty($data,'id')) return [false,'请选择数据!'];
+
+        try {
+            DB::beginTransaction();
+            $time = time();
+
+            RuleSet::where('del_time',0)
+                ->whereIn('id',$data['id'])
+                ->update(['del_time' => $time]);
+
+            RuleSetDetails::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 ruleSetDetail($data, $user){
+        if($this->isEmpty($data,'id')) return [false,'请选择数据!'];
+        $customer = RuleSet::where('del_time',0)
+            ->where('id',$data['id'])
+            ->first();
+        if(empty($customer)) return [false,'规则配置单不存在或已被删除'];
+        $customer = $customer->toArray();
+        $customer['month']  = ! empty($customer['month']) ? date("Y-m", $customer['month']) : "";
+        $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']): '';
+
+        $details = $this->getDetail($data['id']);
+        $customer = array_merge($customer, $details);
+
+        return [true, $customer];
+    }
+
+    public function ruleSetCommon($data,$user, $field = []){
+        if(empty($field)) $field = RuleSet::$field;
+
+        $model = RuleSet::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 ruleSetList($data,$user){
+        $model = $this->ruleSetCommon($data, $user);
+        $list = $this->limit($model,'',$data);
+        $list = $this->fillData($list);
+
+        return [true, $list];
+    }
+
+    public function ruleSetRule(&$data, $user, $is_add = true){
+        $data['top_depart_id'] = $user['top_depart_id'];
+        if(empty($data['month'])) return [false, '月份不能为空'];
+        $data['month'] = $this->changeDateToDate($data['month']);
+
+        // --- 1. 人员列表校验 ---
+        if(empty($data['man_list'])) return [false, '人员不能为空'];
+        $manRates = []; // 用于累加每个人员的比例
+        foreach ($data['man_list'] as $key => $value){
+            if(empty($value['type'])) return [false, '类型不能为空'];
+            if(empty($value['data_id'])) return [false, '人员不能为空'];
+            if(empty($value['item_id'])) return [false, '项目不能为空'];
+
+            $res = $this->checkNumber($value['rate'], 2, 'non-negative');
+            if(! $res['valid']) return [false, '人员工时分摊比例:' . $res['error']];
+
+            $data['man_list'][$key]['top_depart_id'] = $data['top_depart_id'];
+
+            // 累加比例 (以 data_id 为 key)
+            $manId = $value['data_id'];
+            $manRates[$manId] = ($manRates[$manId] ?? 0) + $value['rate'];
+        }
+
+        // 校验每个人员的比例之和是否为 100
+        foreach ($manRates as $mId => $total) {
+            if (abs($total - 100) > 0.0001) {
+                return [false, "ID为[{$mId}]的人员分摊比例合计为{$total}%,必须等于100%"];
+            }
+        }
+
+        // --- 2. 设备列表校验 ---
+        if(empty($data['device_list'])) return [false, '设备不能为空'];
+        $deviceRates = []; // 用于累加每个设备的比例
+        foreach ($data['device_list'] as $key => $value){
+            if(empty($value['type'])) return [false, '类型不能为空'];
+            if(empty($value['data_id'])) return [false, '设备ID不能为空'];
+            if(empty($value['item_id'])) return [false, '项目不能为空'];
+
+            $res = $this->checkNumber($value['rate'], 2, 'non-negative');
+            if(! $res['valid']) return [false, '设备工时分摊比例:' . $res['error']];
+
+            $data['device_list'][$key]['top_depart_id'] = $data['top_depart_id'];
+
+            // 累加比例
+            $devId = $value['data_id'];
+            $deviceRates[$devId] = ($deviceRates[$devId] ?? 0) + $value['rate'];
+        }
+
+        // 校验每个设备的比例之和是否为 100
+        foreach ($deviceRates as $dId => $total) {
+            if (abs($total - 100) > 0.0001) {
+                return [false, "ID为[{$dId}]的设备分摊比例合计为{$total}%,必须等于100%"];
+            }
+        }
+
+        // --- 3. 重复单据校验 ---
+        if($is_add){
+            $bool = RuleSet::where('month',$data['month'])
+                ->where('top_depart_id', $data['top_depart_id'])
+                ->where('del_time',0)
+                ->exists();
+        }else{
+            if(empty($data['id'])) return [false,'ID不能为空'];
+            $bool = RuleSet::where('month',$data['month'])
+                ->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');
+        // 1. 获取详情映射 [main_id => [ [code_2=>..., title=>..., item_code=>...], ... ]]
+        $detailsMap = $this->getDetailsMap($mainIds);
+
+        foreach ($data as $main) {
+            $mainId = $main['id'];
+            $details = $detailsMap[$mainId] ?? [];
+
+            // 2. 提取并格式化主表共有信息
+            $mainInfo = [
+                'code'  => $main['code'],
+                'month' => !empty($main['month']) ? date('Y-m', $main['month']) : '',
+            ];
+
+            if (empty($details)) {
+                // 如果单据没有明细,则只导出一行主表信息(确保数据完整)
+                $tempRow = [];
+                foreach ($column as $col) {
+                    $tempRow[] = $mainInfo[$col] ?? '';
+                }
+                $return[] = $tempRow;
+            } else {
+                // 3. 核心平铺逻辑:每一行详情都带上主表单号
+                foreach ($details as $sub) {
+                    // 合并主表数据和详情数据
+                    $fullRowData = array_merge($mainInfo, $sub);
+
+                    $tempRow = [];
+                    // 严格按照导出 Excel 的列顺序填充数据
+                    foreach ($column as $col) {
+                        $tempRow[] = $fullRowData[$col] ?? '';
+                    }
+                    $return[] = $tempRow;
+                }
+            }
+        }
+    }
+
+    public function getDetailsMap($mainIds)
+    {
+        $details = RuleSetDetails::where('del_time', 0)
+            ->whereIn('main_id', $mainIds)
+            ->get();
+
+        if ($details->isEmpty()) return [];
+
+        // 1. 提取所有需要关联的 ID
+        $empIds = $details->where('type', RuleSetDetails::type_one)->pluck('data_id')->unique();
+        $devIds = $details->where('type', RuleSetDetails::type_two)->pluck('data_id')->unique();
+        $itemIds = $details->pluck('item_id')->unique();
+
+        // 2. 批量获取档案 Map
+        $empMap = Employee::whereIn('id', $empIds)->get()->keyBy('id');
+        $devMap = Device::whereIn('id', $devIds)->get()->keyBy('id');
+        $itemMap = Item::whereIn('id', $itemIds)->get()->keyBy('id');
+
+        $typeNames = RuleSetDetails::$type_name;
+
+        $res = [];
+        foreach ($details as $item) {
+            $detailRow = [];
+
+            // 类型名称(明细类型列)
+            $detailRow['type_title'] = $typeNames[$item->type] ?? '';
+
+            // 处理人员或设备的编码与名称(影子列)
+            if ($item->type == RuleSetDetails::type_one) {
+                $emp = $empMap[$item->data_id] ?? null;
+                $detailRow['code_2'] = $emp ? $emp->number : ''; // 明细编码
+                $detailRow['title']  = $emp ? $emp->title : '';   // 明细名称
+            } else {
+                $dev = $devMap[$item->data_id] ?? null;
+                $detailRow['code_2'] = $dev ? $dev->code : '';   // 明细编码
+                $detailRow['title']  = $dev ? $dev->title : '';   // 明细名称
+            }
+
+            // 处理项目编码与名称
+            $project = $itemMap[$item->item_id] ?? null;
+            $detailRow['item_code']  = $project ? $project->code : '';
+            $detailRow['item_title'] = $project ? $project->title : '';
+
+            // 比例
+            $detailRow['rate'] = $item->rate;
+
+            // 归档到对应的单据下
+            $res[$item->main_id][] = $detailRow;
+        }
+
+        return $res;
+    }
+
+    public function ruleSetCreate($data, $user){
+        $data['top_depart_id'] = $user['top_depart_id'];
+        if(empty($data['month'])) return [false, '月份不能为空'];
+
+        // 1. 获取当月 1 号 00:00:00 的时间戳
+        $monthStart = $this->changeDateToDate($data['month']);
+        $data['month'] = $monthStart;
+
+        // 2. 计算当月结束时间(下个月 1 号减 1 秒)
+        $monthEnd = strtotime('+1 month', $monthStart) - 1;
+
+        // 3. 查找在该月份有效期内的项目 (项目开始 <= 月末 且 项目结束 >= 月初)
+        $itemIds = DB::table('item')
+            ->where('top_depart_id', $data['top_depart_id'])
+            ->where('del_time', 0)
+            ->where('start_time', '<=', $monthEnd)
+            ->where('end_time', '>=', $monthStart)
+            ->pluck('id')
+            ->toArray();
+
+        if (empty($itemIds)) return [false, "该月份下无项目信息"];
+
+        // 4. 获取这些项目下绑定的所有人员和设备
+        $details = DB::table('item_details')
+            ->whereIn('item_id', $itemIds)
+            ->where('top_depart_id', $data['top_depart_id'])
+            ->where('del_time', 0)
+            ->get();
+
+        $manMap = [];    // [人员ID => [项目ID1, 项目ID2...]]
+        $deviceMap = []; // [设备ID => [项目ID1, 项目ID2...]]
+
+        foreach ($details as $row) {
+            if ($row->type == RuleSetDetails::type_one) {
+                $manMap[$row->data_id][] = $row->item_id;
+            } elseif ($row->type == RuleSetDetails::type_two) {
+                $deviceMap[$row->data_id][] = $row->item_id;
+            }
+        }
+
+        // 5. 生成随机比例分配 (复用之前定义的分配函数)
+        $man_list = $this->distributeRates($manMap, RuleSetDetails::type_one);
+        $device_list = $this->distributeRates($deviceMap, RuleSetDetails::type_two);
+
+        return [true, [
+            'month' => date('Y-m', $monthStart),
+            'man_list' => $man_list,
+            'device_list' => $device_list
+        ]];
+    }
+
+    /**
+     * 核心算法:按对象随机分配整数比例,确保总和 100
+     */
+    private function distributeRates($map, $type)
+    {
+        $result = [];
+        foreach ($map as $dataId => $items) {
+            $count = count($items);
+            $rates = [];
+
+            if ($count === 1) {
+                $rates = [100];
+            } else {
+                $sum = 0;
+                $temp = [];
+                for ($i = 0; $i < $count; $i++) {
+                    $rand = rand(10, 100);
+                    $temp[] = $rand;
+                    $sum += $rand;
+                }
+
+                $currentTotal = 0;
+                foreach ($temp as $i => $weight) {
+                    if ($i === $count - 1) {
+                        $rates[] = 100 - $currentTotal;
+                    } else {
+                        $val = (int)round(($weight / $sum) * 100);
+                        $val = $val <= 0 ? 1 : $val; // 确保不为 0
+                        $rates[] = $val;
+                        $currentTotal += $val;
+                    }
+                }
+            }
+
+            foreach ($items as $index => $itemId) {
+                $result[] = [
+                    'type' => $type,
+                    'data_id' => $dataId,
+                    'item_id' => $itemId,
+                    'rate' => $rates[$index]
+                ];
+            }
+        }
+        return $result;
+    }
+}

+ 94 - 0
config/excel/ruleSet.php

@@ -0,0 +1,94 @@
+<?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' =>'type',
+            'export' =>'type_title',
+            'value' => '明细类型',
+            'required' => true,
+            'is_main' => false,
+            'default' => 0,
+            'unique' => false,
+            'enums' => array_values(\App\Model\RuleSetDetails::$type_name),
+            'comments' => '必填'
+        ],
+        [
+            'key' =>'code_2',
+            'export' =>'code_2',
+            'value' => '编码',
+            'required' => true,
+            'is_main' => false,
+            'default' => "",
+            'unique' => false,
+            'enums' => [],
+            'comments' => '必填(人员的工号或设备的资产编码)'
+        ],
+        [
+            'key' =>'title',
+            'export' =>'title',
+            'value' => '名称',
+            'required' => false,
+            'is_main' => false,
+            'default' => "",
+            'unique' => false,
+            'enums' => [],
+            'comments' => '选填(便于操作人查看编码对应的名称)'
+        ],
+        [
+            'key' =>'item_id',
+            'export' =>'item_code',
+            'value' => '项目编码',
+            'required' => true,
+            'is_main' => false,
+            'unique' => false,
+            'enums' => [],
+            'default' => "",
+            'comments' => "必填"
+        ],
+        [
+            'key' =>'item_title',
+            'export' =>'item_title',
+            'value' => '项目名称',
+            'required' => false,
+            'is_main' => false,
+            'unique' => false,
+            'enums' => [],
+            'default' => "",
+            'comments' => "选填(便于操作人查看编码对应的名称)"
+        ],
+        [
+            'key' =>'rate',
+            'export' =>'rate',
+            'value' => '工时分摊比例(%)',
+            'required' => true,
+            'is_main' => false,
+            'unique' => false,
+            'enums' => [],
+            'default' => "",
+            'comments' => "必填 (数字最多支持两位小数)"
+        ],
+    ]
+];

+ 8 - 0
routes/api.php

@@ -116,5 +116,13 @@ Route::group(['middleware'=> ['checkLogin']],function ($route){
     $route->any('monthlyDdOrderAdd', 'Api\DeviceDepreciationController@monthlyDdOrderAdd');
     $route->any('monthlyDdOrderDel', 'Api\DeviceDepreciationController@monthlyDdOrderDel');
     $route->any('monthlyDdOrderDetail', 'Api\DeviceDepreciationController@monthlyDdOrderDetail');
+
+    //规则配置
+    $route->any('ruleSetList', 'Api\RuleSetController@ruleSetList');
+    $route->any('ruleSetEdit', 'Api\RuleSetController@ruleSetEdit');
+    $route->any('ruleSetAdd', 'Api\RuleSetController@ruleSetAdd');
+    $route->any('ruleSetDel', 'Api\RuleSetController@ruleSetDel');
+    $route->any('ruleSetDetail', 'Api\RuleSetController@ruleSetDetail');
+    $route->any('ruleSetCreate', 'Api\RuleSetController@ruleSetCreate');
 });