cqp 3 هفته پیش
والد
کامیت
f110a626cf
3فایلهای تغییر یافته به همراه184 افزوده شده و 10 حذف شده
  1. 1 1
      app/Model/Item.php
  2. 59 9
      app/Service/ItemService.php
  3. 124 0
      app/Service/ProgressCalculatorService.php

+ 1 - 1
app/Model/Item.php

@@ -14,7 +14,7 @@ class Item extends DataScopeBaseModel
     const table_column = "item_employee";
     const table_id_column = "item_id";
 
-    public static $field = ['title','id','code','start_time','end_time','mark','crt_id','crt_time','state','budget','charge_id','item_attribute','field','is_review_required','review_id','priority_id', 'approval_state'];
+    public static $field = ['title','id','code','start_time','end_time','mark','crt_id','crt_time','state','budget','charge_id','item_attribute','field','is_review_required','review_id','priority_id', 'approval_state','progress'];
     public static $report_field_1 = ['title','id','code','start_time','end_time','mark','budget','field'];
 
     const TYPE_MINUS_ONE = -1;

+ 59 - 9
app/Service/ItemService.php

@@ -39,8 +39,8 @@ class ItemService extends Service
             'item_node_mission' => ItemNodeMission::class,
         ];
         $field = [
-            'item' => ['id','code', 'title', 'start_time', 'end_time', 'charge_id', 'state', 'crt_id'],
-            'item_node' => ['id','code', 'title', 'start_time', 'end_time', 'charge_id', 'state', 'crt_id'],
+            'item' => ['id','code', 'title', 'start_time', 'end_time', 'charge_id', 'state', 'crt_id','progress'],
+            'item_node' => ['id','code', 'title', 'start_time', 'end_time', 'charge_id', 'state', 'crt_id','progress'],
             'item_node_mission' => ['id','code', 'title', 'start_time', 'end_time', 'charge_id', 'state', 'progress', 'crt_id'],
         ];
         if (empty($data['type']) && ! isset($type[$data['type']])) return [false , '类型错误'];
@@ -81,7 +81,6 @@ class ItemService extends Service
             $data[$key]['charge_name'] = $emp[$value['charge_id']] ?? '';
             $data[$key]['state_title'] = ItemNodeMission::State_Type[$value['state']] ?? "";
             $data[$key]['type'] = $erg['type'];
-            if(! isset($value['progress'])) $data[$key]['progress'] = rand(1,100); // todo
         }
 
         return $data;
@@ -95,8 +94,8 @@ class ItemService extends Service
             'item_node_mission' => ItemNodeMission::class,
         ];
         $field = [
-            'item' => ['id','code', 'title', 'start_time', 'end_time', 'charge_id', 'state', 'crt_id'],
-            'item_node' => ['id','code', 'title', 'start_time', 'end_time', 'charge_id', 'state', 'crt_id'],
+            'item' => ['id','code', 'title', 'start_time', 'end_time', 'charge_id', 'state', 'crt_id','progress'],
+            'item_node' => ['id','code', 'title', 'start_time', 'end_time', 'charge_id', 'state', 'crt_id','progress'],
             'item_node_mission' => ['id','code', 'title', 'start_time', 'end_time', 'charge_id', 'state', 'progress', 'crt_id'],
         ];
         $search = [
@@ -331,6 +330,9 @@ class ItemService extends Service
             $model->state = Item::TYPE_THREE;
             $model->save();
 
+            //更新进度
+            ProgressCalculatorService::calculate(ProgressCalculatorService::TYPE_ITEM, $model->id);
+
             DB::commit();
         }catch (\Exception $exception){
             DB::rollBack();
@@ -402,6 +404,9 @@ class ItemService extends Service
                 return [false, $msg];
             }
 
+            //更新进度
+            ProgressCalculatorService::calculate(ProgressCalculatorService::TYPE_ITEM, $model->id);
+
             DB::commit();
         }catch (\Exception $exception){
             DB::rollBack();
@@ -708,7 +713,7 @@ class ItemService extends Service
             3 => [
                 'model' => ItemNodeMission::class,
                 'name'  => '任务',
-                'relations' => []
+                'relations' => ['model' => ItemNodeMission::class, 'key' => 'parent_item_node_mission_id', 'msg' => '该任务下已存在子任务,请先删除子任务'],
             ],
         ];
 
@@ -727,6 +732,10 @@ class ItemService extends Service
             return [false, "{$cfg['name']}处于审核中"];
         }
 
+        if ($target->state == $cfg['model']::TYPE_THREE) {
+            return [false, "{$cfg['name']}已完成"];
+        }
+
         // 5. 关联依赖校验
         foreach ($cfg['relations'] as $rel) {
             $exists = $rel['model']::where('del_time', 0)
@@ -830,7 +839,6 @@ class ItemService extends Service
             $node_tmp = $tag[$value['node_id']] ?? [];
             $list[$key]['node_title'] = $node_tmp['title'] ?? '';
             $list[$key]['node_code'] = $node_tmp['code'] ?? '';
-            $list[$key]['progress'] = rand(1, 100); //todo
         }
 
         return $list;
@@ -997,6 +1005,8 @@ class ItemService extends Service
                 ->where('del_time',0)
                 ->first();
             if(empty($item)) return [false, '项目不存在或已被删除'];
+            if($item->state == ItemNodeMission::TYPE_THREE) return [false, '项目已完结,操作失败'];
+            if($item->approval_state == ItemNodeMission::TYPE_MINUS_ONE) return [false, '项目审核中,操作失败'];
 
             $bool = Item::where('code',$data['code'])
                 ->where('top_depart_id', $data['top_depart_id'])
@@ -1170,6 +1180,9 @@ class ItemService extends Service
             $model->state = ItemNode::TYPE_THREE;
             $model->save();
 
+            //更新进度
+            ProgressCalculatorService::calculate(ProgressCalculatorService::TYPE_NODE, $model->id);
+
             DB::commit();
         }catch (\Exception $exception){
             DB::rollBack();
@@ -1239,6 +1252,9 @@ class ItemService extends Service
                 return [false, $msg];
             }
 
+            //更新进度
+            ProgressCalculatorService::calculate(ProgressCalculatorService::TYPE_NODE, $model->id);
+
             DB::commit();
         }catch (\Exception $exception){
             DB::rollBack();
@@ -1597,7 +1613,6 @@ class ItemService extends Service
             $node_tmp = $tag[$value['mission_id']] ?? [];
             $list[$key]['mission_title'] = $node_tmp['title'] ?? '';
             $list[$key]['mission_code'] = $node_tmp['code'] ?? '';
-            $list[$key]['progress'] = rand(1, 100); //todo
         }
 
         return $list;
@@ -1693,7 +1708,11 @@ class ItemService extends Service
         $item = Item::where('id', $data['item_id'])->where('del_time',0)->first();
         if(empty($item)) return [false, '项目不存在或已被删除'];
         $item = $item->toArray();
+        if($item['state'] == Item::TYPE_THREE) return [false, '项目已完结,操作失败'];
         if(empty($data['title'])) return [false, '节点名称不能为空'];
+        if(! isset($data['node_weight'])) return [false, '节点权重不存在'];
+        list($status, $res) = $this->checkWeight($data['node_weight'], '节点权重');
+        if(! $status) return [false, $res];
         if(! empty($data['start_time'])) $data['start_time'] = $this->changeDateToDate($data['start_time']);
         if(! empty($data['end_time'])) $data['end_time'] = $this->changeDateToDate($data['end_time'],true);
         if(! empty($data['start_time']) && ! empty($data['end_time']) && $data['start_time'] > $data['end_time']) return [false, '开始时间不能大于结束时间'];
@@ -1746,6 +1765,7 @@ class ItemService extends Service
                 ->where('del_time',0)
                 ->first();
             if(empty($item)) return [false, '项目节点不存在或已被删除'];
+            if($item->approval_state == ItemNode::TYPE_MINUS_ONE) return [false, '节点审核中,操作失败'];
 
             $bool = ItemNode::where('title', $data['title'])
                 ->where('item_id',$data['item_id'])
@@ -1793,7 +1813,6 @@ class ItemService extends Service
             $data['data'][$key]['node_title'] = $node_tmp['title'] ?? '';
             $data['data'][$key]['node_code'] = $node_tmp['code'] ?? '';
             $data['data'][$key]['item_title'] = $item[$value['item_id']] ?? '';
-            $data['data'][$key]['progress'] = rand(1,100); // todo
         }
 
         return $data;
@@ -1826,6 +1845,9 @@ class ItemService extends Service
             $model->state = ItemNodeMission::TYPE_THREE;
             $model->save();
 
+            //更新进度
+            ProgressCalculatorService::calculate(ProgressCalculatorService::TYPE_MISSION, $model->id);
+
             DB::commit();
         }catch (\Exception $exception){
             DB::rollBack();
@@ -1895,6 +1917,9 @@ class ItemService extends Service
                 return [false, $msg];
             }
 
+            //更新进度
+            ProgressCalculatorService::calculate(ProgressCalculatorService::TYPE_MISSION, $model->id);
+
             DB::commit();
         }catch (\Exception $exception){
             DB::rollBack();
@@ -2446,12 +2471,16 @@ class ItemService extends Service
         $item = ItemNode::where('id', $data['item_node_id'])->where('del_time',0)->first();
         if(empty($item)) return [false, '项目节点不存在或已被删除'];
         $item = $item->toArray();
+        if($item['state'] == ItemNode::TYPE_THREE) return [false, '节点已完结,操作失败'];
         $data['item_id'] = $item['item_id'];
         if(! empty($data['parent_item_node_mission_id'])) {
             $bool = ItemNodeMission::where('id', $data['parent_item_node_mission_id'])->where('del_time',0)->exists();
             if(! $bool) return [false, '父任务不存在或已被删除'];
         }
         if(empty($data['title'])) return [false, '任务名称不能为空'];
+        if(! isset($data['mission_weight'])) return [false, '任务权重不存在'];
+        list($status, $res) = $this->checkWeight($data['mission_weight'], '任务权重');
+        if(! $status) return [false, $res];
         if(! empty($data['start_time'])) $data['start_time'] = $this->changeDateToDate($data['start_time']);
         if(! empty($data['end_time'])) $data['end_time'] = $this->changeDateToDate($data['end_time'],true);
         if(! empty($data['start_time']) && ! empty($data['end_time']) && $data['start_time'] > $data['end_time']) return [false, '开始时间不能大于结束时间'];
@@ -2501,6 +2530,7 @@ class ItemService extends Service
                 ->where('del_time',0)
                 ->first();
             if(empty($item)) return [false, '项目节点下任务不存在或已被删除'];
+            if($item->approval_state == ItemNodeMission::TYPE_MINUS_ONE) return [false, '任务审核中,操作失败'];
 
             $bool = ItemNodeMission::where('title', $data['title'])
                 ->where('item_node_id',$data['item_node_id'])
@@ -2622,6 +2652,26 @@ class ItemService extends Service
             ->toArray();
     }
 
+    /**
+     * 通用权重校验
+     * @param int|float $weight 权重值
+     * @param string $fieldTitle 字段名称提示(如:节点权重、任务权重)
+     * @return array [bool, string]
+     */
+    public function checkWeight($weight, $fieldTitle = '权重') {
+        // 1. 判断是否为空或非数字
+        if (!is_numeric($weight)) {
+            return [false, $fieldTitle . '必须是数字'];
+        }
+
+        // 2. 校验范围 0 - 100
+        if ($weight < 0 || $weight > 100) {
+            return [false, $fieldTitle . '范围必须在 0 到 100 之间'];
+        }
+
+        return [true, ''];
+    }
+
     /**
      * 触发生成审批流
      * @param $data 草稿数据

+ 124 - 0
app/Service/ProgressCalculatorService.php

@@ -0,0 +1,124 @@
+<?php
+
+namespace App\Service;
+
+
+use App\Model\Item;
+use App\Model\ItemNode;
+use App\Model\ItemNodeMission;
+
+class ProgressCalculatorService extends Service
+{
+    // 定义常量,方便调用
+    const TYPE_MISSION = 'mission';
+    const TYPE_NODE    = 'node';
+    const TYPE_ITEM    = 'item';
+
+    /**
+     * 统一进度计算入口
+     * @param string $type 发生变更的层级类型: mission, node, item
+     * @param int $id 对应层级的主键 ID
+     */
+    public static function calculate($type, $id) {
+        if (empty($id)) return;
+
+        switch ($type) {
+            case self::TYPE_MISSION:
+                // 1. 如果是任务变了,找到未删除的任务,进而拿到它所属的节点 ID
+                $mission = ItemNodeMission::where('id', $id)
+                    ->where('del_time', 0)
+                    ->first();
+                if ($mission) {
+                    self::handleNodeProgress($mission->item_node_id);
+                }
+                break;
+
+            case self::TYPE_NODE:
+                // 2. 如果是节点变了,直接计算该节点
+                self::handleNodeProgress($id);
+                break;
+
+            case self::TYPE_ITEM:
+                // 3. 如果是项目层级触发,直接计算项目
+                self::handleItemProgress($id);
+                break;
+        }
+    }
+
+    /**
+     * 核心逻辑:计算节点进度并自动向上触发项目计算(私有,不对外)
+     */
+    private static function handleNodeProgress($nodeId) {
+        // 严格限制:只有未删除的节点才参与计算
+        $node = ItemNode::where('id', $nodeId)
+            ->where('del_time', 0)
+            ->first();
+        if (!$node) return;
+
+        // 查找该节点下所有未删除的任务
+        $missions = ItemNodeMission::where('item_node_id', $nodeId)
+            ->where('del_time', 0)
+            ->get();
+
+        if ($missions->isEmpty()) {
+            // 无任务,进度由自身状态决定
+            $node->progress = ($node->state == ItemNodeMission::TYPE_THREE) ? 100.00 : 0.00;
+        } else {
+            $totalWeight = $missions->sum('mission_weight');
+            if ($totalWeight <= 0) {
+                // 如果建了任务但权重全是 0,按任务个数平均分
+                $completedCount = $missions->where('state', 3)->count();
+                $node->progress = round(($completedCount / $missions->count()) * 100, 2);
+            } else {
+                // 已完成(state = 3)的任务权重和
+                $completedWeight = $missions->where('state', 3)->sum('mission_weight');
+                $node->progress = round(($completedWeight / $totalWeight) * 100, 2);
+            }
+        }
+        $node->save();
+
+        // 【自动冒泡】节点算完,自动去算它所属的项目
+        self::handleItemProgress($node->item_id);
+    }
+
+    /**
+     * 核心逻辑:计算项目进度(私有,不对外)
+     */
+    private static function handleItemProgress($itemId) {
+        // 严格限制:只有未删除的项目才参与计算
+        $item = Item::where('id', $itemId)
+            ->where('del_time', 0)
+            ->first();
+        if (!$item) return;
+
+        // 查找该项目下所有未删除的节点
+        $nodes = ItemNode::where('item_id', $itemId)
+            ->where('del_time', 0)
+            ->get();
+
+        if ($nodes->isEmpty()) {
+            // 无节点,进度由自身状态决定
+            $item->progress = ($item->state == ItemNode::TYPE_THREE) ? 100.00 : 0.00;;
+            $item->save();
+            return;
+        }
+
+        $totalWeight = $nodes->sum('node_weight');
+        if ($totalWeight <= 0) {
+            // 如果有节点但权重全写了 0,按节点个数平均分配
+            $currentSum = 0;
+            foreach ($nodes as $node) {
+                $currentSum += ($node->progress / 100);
+            }
+            $item->progress = round(($currentSum / $nodes->count()) * 100, 2);
+        } else {
+            // 采用复合算法:SUM(节点权重 * 节点自身的 progress) / 总权重
+            $currentSum = 0;
+            foreach ($nodes as $node) {
+                $currentSum += $node->node_weight * ($node->progress / 100);
+            }
+            $item->progress = round(($currentSum / $totalWeight) * 100, 2);
+        }
+        $item->save();
+    }
+}