diff --git a/app/admin/controller/ApprovalFlowsController.php b/app/admin/controller/ApprovalFlowsController.php index 8f8ae24..c2babff 100644 --- a/app/admin/controller/ApprovalFlowsController.php +++ b/app/admin/controller/ApprovalFlowsController.php @@ -22,14 +22,14 @@ class ApprovalFlowsController extends BaseController * @param array $data * @return array */ - protected function indexFormat($data) - { - if (isset($data['created_at'])) - $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); - if (isset($data['updated_at'])) - $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); - return $data; - } + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } public function desc() diff --git a/app/admin/controller/ApprovalInstancesController.php b/app/admin/controller/ApprovalInstancesController.php new file mode 100644 index 0000000..483ec40 --- /dev/null +++ b/app/admin/controller/ApprovalInstancesController.php @@ -0,0 +1,33 @@ +service = new ApprovalInstancesService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/admin/controller/ApprovalRecordsController.php b/app/admin/controller/ApprovalRecordsController.php new file mode 100644 index 0000000..46712b3 --- /dev/null +++ b/app/admin/controller/ApprovalRecordsController.php @@ -0,0 +1,75 @@ +service = new ApprovalRecordsService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['records']) && count($data['records']) > 0) { + foreach ($data['records'] as $key => &$record) { + $result = [ + 'id' => $record['id'], + 'flows_name' => $record['approvalInstance']['flows']['name'], + + 'instance_id' => $record['instance_id'], + + 'biz_id' => $record['approvalInstance']['biz_id'], + 'biz_desc' => '备货单【'.$record['approvalInstance']['bizInfo']['order_no'].'】', + 'biz_type' => $record['approvalInstance']['flow_type'], + + 'initiator_name' => $record['approvalInstance']['initiator']['name'], + 'initiator_avatar' => $record['approvalInstance']['initiator']['avatar'], + + 'approver_name' => $record['approver']['name'], + 'approver_avatar' => $record['approver']['avatar'], + + 'step_index' => $record['step_index'], + 'action' => $record['action'], + 'status' => SysDictItem::where('item_value', $record['status'])->where('status', 1)->value('item_label'), + 'comment' => $record['comment'], + 'approved_at' => $record['approved_at'], + + 'finished_at' => $record['approvalInstance']['finished_at'], + 'deadline_at' => $record['approvalInstance']['deadline_at'], + + 'created_at' => $record['created_at'] + ]; + $record = $result; + } + } + return $data; + } + + + public function approve(Request $request) + { + try { + $res = $this->service->serviceApprove($request); + return $this->success([], $res['msg']); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } +} \ No newline at end of file diff --git a/app/admin/logic/ApprovalInstancesLogic.php b/app/admin/logic/ApprovalInstancesLogic.php new file mode 100644 index 0000000..bc8ce88 --- /dev/null +++ b/app/admin/logic/ApprovalInstancesLogic.php @@ -0,0 +1,17 @@ + 'array', // 自动转换 JSON 字段为数组 + 'finished_at' => 'datetime', + 'deadline_at' => 'datetime', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + /** + * 一个审批实例可以有多个审批记录 + */ + public function approvalRecords(): HasMany + { + return $this->hasMany(ApprovalRecords::class, 'instance_id'); + } + + /** + * 获取当前步骤的所有审批记录(pending 状态) + */ + public function currentStepRecords() + { + return $this->approvalRecords() + ->where('step_index', $this->current_step) + ->where('status', 'pending'); + } + + public function flows() + { + return $this->belongsTo(ApprovalFlows::class, 'flow_type', 'type'); + } + + public function initiator() + { + return $this->belongsTo(User::class, 'initiator_id', 'id'); + } + + + public function bizInfo() + { + return $this->hasOne(StockOrders::class, 'id', 'biz_id'); + } +} \ No newline at end of file diff --git a/app/admin/model/ApprovalRecords.php b/app/admin/model/ApprovalRecords.php new file mode 100644 index 0000000..a970ce4 --- /dev/null +++ b/app/admin/model/ApprovalRecords.php @@ -0,0 +1,84 @@ + 'datetime', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + /** + * 一条审批记录属于一个审批实例 + */ + public function approvalInstance(): BelongsTo + { + return $this->belongsTo(ApprovalInstances::class, 'instance_id'); + } + + /** + * 关联审批人 + */ + public function approver() + { + return $this->belongsTo(User::class, 'approver_id'); + } +} \ No newline at end of file diff --git a/app/admin/service/ApprovalInstancesService.php b/app/admin/service/ApprovalInstancesService.php new file mode 100644 index 0000000..db9b0cb --- /dev/null +++ b/app/admin/service/ApprovalInstancesService.php @@ -0,0 +1,31 @@ +validator; + } +} \ No newline at end of file diff --git a/app/admin/service/ApprovalRecordsService.php b/app/admin/service/ApprovalRecordsService.php new file mode 100644 index 0000000..0bc6325 --- /dev/null +++ b/app/admin/service/ApprovalRecordsService.php @@ -0,0 +1,75 @@ +validator; + } + + + protected function buildQueryConfig(Request $request, string $scene = 'default'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'index': + $config['order']['id'] = 'desc'; + + $config['with'] = [ + 'approvalInstance', + 'approver', + 'approvalInstance.flows', + 'approvalInstance.initiator', + 'approvalInstance.bizInfo' + ]; + break; + case 'show': + $config['where']['id'] = $p['id']; + break; + case 'store': + break; + case 'update': + $config['where']['id'] = $p['id']; + break; + case 'destroy': + $config['where']['id'] = $p['id']; + break; + } + + return $config; + } + + + public function serviceApprove(Request $request) + { + $param = $request->all(); + $appro = new \app\common\utils\approval\service\ApprovalFlowService(); + return $appro->approve($param['instance_id'], $request->user->uid, $param['action'], $param['comment']); + } +} \ No newline at end of file diff --git a/app/admin/service/SysDictService.php b/app/admin/service/SysDictService.php index 2d29b21..aa1d2bf 100644 --- a/app/admin/service/SysDictService.php +++ b/app/admin/service/SysDictService.php @@ -5,6 +5,7 @@ namespace app\admin\service; use app\admin\validate\SysDictValidate; use app\common\traits\crud\CrudServiceTrait; use app\common\model\SysDict; +use app\common\model\SysDictCategory; use support\Request; class SysDictService @@ -133,4 +134,90 @@ class SysDictService return $data; } + + + + /* + + 示例用法: + + // 1) 获取分类为“车辆类型”的所有字典 + $list = $dictService->getDictsByCategoryName('车辆类型'); + // => [['dict_code' => 'SUV', 'dict_name' => '越野车'], …] + + // 2) 获取某分类下某个 code 的名称 + $name = $dictService->getDictName('车辆类型', 'SUV'); + // => "越野车" + + // 3) 获取某分类下拉选项 + $options = $dictService->getDictOptions('车辆类型'); + // => ['SUV' => '越野车', 'SEDAN' => '轿车', …] + + // 4) 通过 code 直接拿名称(不管分类) + $name = $dictService->getNameByCode('SUV'); + // => "越野车" + + */ + + + /** + * 根据分类名获取字典列表 + */ + public function getDictsByCategoryName(string $categoryName): array + { + $category = SysDictCategory::where('category_name', $categoryName) + ->where('status', 1) + ->first(); + + if (!$category) { + return []; + } + + return $category->dicts()->get(['dict_code', 'dict_name'])->toArray(); + } + + /** + * 根据分类名 + 字典code 获取字典项名称 + */ + public function getDictName(string $categoryName, string $dictCode): ?string + { + $dict = SysDict::whereHas('category', function ($q) use ($categoryName) { + $q->where('category_name', $categoryName)->where('status', 1); + }) + ->where('dict_code', $dictCode) + ->where('status', 1) + ->whereNull('deleted_at') + ->first(); + + return $dict?->dict_name; + } + + /** + * 根据分类名获取下拉键值对 + * 返回格式:['code1' => '名称1', 'code2' => '名称2'] + */ + public function getDictOptions(string $categoryName): array + { + $dicts = $this->getDictsByCategoryName($categoryName); + + $options = []; + foreach ($dicts as $item) { + $options[$item['dict_code']] = $item['dict_name']; + } + + return $options; + } + + /** + * 根据字典 code 获取对应的名称(不依赖分类) + */ + public function getNameByCode(string $dictCode): ?string + { + $dict = SysDict::where('dict_code', $dictCode) + ->where('status', 1) + ->whereNull('deleted_at') + ->first(); + + return $dict?->dict_name; + } } \ No newline at end of file diff --git a/app/admin/validate/ApprovalInstancesValidate.php b/app/admin/validate/ApprovalInstancesValidate.php new file mode 100644 index 0000000..e6641be --- /dev/null +++ b/app/admin/validate/ApprovalInstancesValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/admin/validate/ApprovalRecordsValidate.php b/app/admin/validate/ApprovalRecordsValidate.php new file mode 100644 index 0000000..796768c --- /dev/null +++ b/app/admin/validate/ApprovalRecordsValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/model/StockOrders.php b/app/common/model/StockOrders.php index a52c791..815783e 100644 --- a/app/common/model/StockOrders.php +++ b/app/common/model/StockOrders.php @@ -89,4 +89,10 @@ class StockOrders extends Model { return $this->hasOne(VehMoneyValue::class, 'object_id', 'id')->where('object_scene', 'stock_orders'); } + + + public function salesperson() + { + return $this->hasOne(User::class, 'id', 'salesperson_id'); + } } \ No newline at end of file diff --git a/app/common/model/SysDict.php b/app/common/model/SysDict.php index ad7cf5c..02534df 100644 --- a/app/common/model/SysDict.php +++ b/app/common/model/SysDict.php @@ -56,4 +56,10 @@ class SysDict extends Model { return $this->hasMany(SysDictItem::class, 'dict_id', 'id'); } + + + public function category() + { + return $this->belongsTo(SysDictCategory::class, 'category_id', 'id'); + } } \ No newline at end of file diff --git a/app/common/model/SysDictCategory.php b/app/common/model/SysDictCategory.php index fef0219..cbca49a 100644 --- a/app/common/model/SysDictCategory.php +++ b/app/common/model/SysDictCategory.php @@ -46,4 +46,13 @@ class SysDictCategory extends Model * 不允许自动填充模型字段 */ protected $guarded = []; + + + public function dicts() + { + return $this->hasMany(SysDict::class, 'category_id', 'id') + ->where('status', 1) + ->whereNull('deleted_at') + ->orderBy('id', 'asc'); + } } \ No newline at end of file diff --git a/app/common/service/StockOrdersService.php b/app/common/service/StockOrdersService.php index 402937c..fadd6cc 100644 --- a/app/common/service/StockOrdersService.php +++ b/app/common/service/StockOrdersService.php @@ -73,6 +73,7 @@ class StockOrdersService $config['with'][] = 'items.moneyValues'; $config['with'][] = 'accounts'; $config['with'][] = 'moneyValues'; + $config['with'][] = 'salesperson'; break; case 'store': break; @@ -219,6 +220,10 @@ class StockOrdersService flg('备货单【'.$order->order_no.'】创建终版成功,但更新原单失败'); } } + // 启动审批 + $appro = new \app\common\utils\approval\service\ApprovalFlowService(); + $result = $appro->start('stock_order_flow', 'stock_orders', $res->id, $res->toArray(), $res->salesperson_id); + var_dump($result); return $res; } @@ -242,7 +247,7 @@ class StockOrdersService $fileName = ($pdfData['order_no'] ?? date('YmdHis')) . '.pdf'; // 本地保存 - $filePath = \app\common\utils\pdf\service\PdfStorage::saveBinary( + \app\common\utils\pdf\service\PdfStorage::saveBinary( $pdfBin, $dir, $fileName @@ -354,6 +359,9 @@ class StockOrdersService $veh = [ 'order_no' => $item->order_no, 'salesperson_snapshot' => $item->salesperson_snapshot, + // 'sign_img' => $item->salesperson->sign_img, + 'sign_img_url' => (!empty($item->salesperson->sign_img))?(new \app\common\service\OssService)->generatePrivateUrl($item->salesperson->sign_img):'', + 'created_at' => date('Y-m-d', strtotime($item->created_at)), 'pi_no' => $item->pi_no, 'biz_date' => date('Y-m-d', $item->biz_date), @@ -381,7 +389,7 @@ class StockOrdersService 'acc_index' => $accIndex, 'cif_total_amount' => $cifTotalAmount ]; - + return $veh; } } \ No newline at end of file diff --git a/app/common/utils/approval/service/ApprovalFlowService.php b/app/common/utils/approval/service/ApprovalFlowService.php index a44e1c4..0e32dfb 100644 --- a/app/common/utils/approval/service/ApprovalFlowService.php +++ b/app/common/utils/approval/service/ApprovalFlowService.php @@ -1,436 +1,232 @@ where('type', $flowType) - ->where('is_active', 1) - ->first(); + return Db::transaction(function () use ($flowType, $bizType, $bizId, $formData, $initiatorId) { + $flow = Db::table('approval_flows')->where('type', $flowType)->where('is_active', 1)->first(); + if (!$flow) throw new BusinessException("审批流程未找到或已停用: {$flowType}"); - if (!$flow) { - throw new BusinessException("审批流程未找到或已停用: {$flowType}"); - } + $steps = json_decode($flow->steps, true); + if (!is_array($steps) || empty($steps)) throw new BusinessException("流程步骤配置错误"); - $steps = json_decode($flow->steps, true); - if (!is_array($steps) || empty($steps)) { - throw new BusinessException("流程步骤配置错误"); - } + // 找首个有效步骤索引(考虑条件) + $firstIndex = $this->getNextStepIndex($steps, $formData, -1); + if ($firstIndex === null) throw new BusinessException("未找到可执行的审批步骤"); + $firstStep = $steps[$firstIndex]; - // 2. 查找第一个有效步骤(考虑 condition) - $firstStep = $this->getNextValidStep($steps, $formData); - if (!$firstStep) { - throw new BusinessException("未找到可执行的审批步骤"); - } + $now = date('Y-m-d H:i:s'); + $deadlineAt = null; + if (($flow->timeout_hours ?? 0) > 0) { + $deadlineAt = date('Y-m-d H:i:s', time() + $flow->timeout_hours * 3600); + } - // 3. 计算截止时间 - $deadlineAt = null; - if ($flow->timeout_hours > 0) { - $deadlineAt = date('Y-m-d H:i:s', time() + $flow->timeout_hours * 3600); - } - - // 4. 创建审批实例 - $instanceId = Db::table('approval_instances')->insertGetId([ - 'flow_type' => $flowType, - 'biz_type' => $bizType, - 'biz_id' => $bizId, - 'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE), - 'initiator_id' => $initiatorId, - 'status' => 'processing', // processing 表示正在进行中 - 'current_step' => 0, - 'deadline_at' => $deadlineAt, - 'created_at' => date('Y-m-d H:i:s'), - 'updated_at' => date('Y-m-d H:i:s'), - ]); - - // 5. 创建审批记录(待办) - $approverIds = $this->resolveApprovers($firstStep, (object)[ - 'initiator_id' => $initiatorId, - 'form_data' => json_encode($formData) - ]); - - foreach ($approverIds as $approverId) { - Db::table('approval_records')->insert([ - 'instance_id' => $instanceId, - 'approver_id' => $approverId, - 'step_index' => 0, - 'action' => '', // 未操作时为空 - 'status' => 'pending', - 'approved_at' => date('Y-m-d H:i:s', 0), // 未审批时设置为初始时间 - 'created_at' => date('Y-m-d H:i:s'), - 'updated_at' => date('Y-m-d H:i:s'), + $instanceId = Db::table('approval_instances')->insertGetId([ + 'flow_type' => $flowType, + 'biz_type' => $bizType, + 'biz_id' => $bizId, + 'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'initiator_id' => $initiatorId, + 'status' => 'processing', + 'current_step' => $firstIndex, + 'deadline_at' => $deadlineAt, + 'created_at' => $now, + 'updated_at' => $now, ]); - } - // 6. 通知审批人 - $instance = (object)[ - 'id' => $instanceId, - 'flow_type' => $flowType, - 'form_data' => json_encode($formData), - 'initiator_id' => $initiatorId - ]; - $this->sendNotification($approverIds, $instance, 'pending', false); + $instance = (object)[ + 'id' => $instanceId, + 'flow_type' => $flowType, + 'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'initiator_id' => $initiatorId, + 'current_step' => $firstIndex, + 'status' => 'processing', + ]; - // 7. 通知抄送人(当前步骤) - $this->notifyCCs($firstStep, $instance, 'pending'); + // 解析首步审批人(支持 role_id 或 roles.name) + $approverIds = $this->resolveApprovers($firstStep, $instance, true); + foreach ($approverIds as $uid) { + $this->insertApprovalRecord($instanceId, $firstIndex, $uid); + } - return ['instance_id' => $instanceId]; + $this->sendNotification($approverIds, $instance, 'pending', false); + $this->notifyCCs($firstStep, $instance, 'pending'); + + return ['instance_id' => $instanceId]; + }); } - /** - * 执行审批 - */ + /** 执行审批(幂等友好) */ public function approve(int $instanceId, int $approverId, string $action, string $comment = '') { - // 验证操作是否有效 - if (!in_array($action, ['approve', 'reject'])) { + if (!in_array($action, ['approve','reject'], true)) { throw new BusinessException("无效的审批操作"); } - $instance = Db::table('approval_instances')->where('id', $instanceId)->first(); - if (!$instance) { - throw new BusinessException("审批实例未找到"); - } + return Db::transaction(function () use ($instanceId, $approverId, $action, $comment) { + $instance = Db::table('approval_instances')->where('id', $instanceId)->lockForUpdate()->first(); + if (!$instance) throw new BusinessException("审批实例未找到"); + if ($instance->status !== 'processing') { + // return ['status' => 'noop', 'reason' => 'instance_not_processing', 'instance_status' => $instance->status]; + // throw new BusinessException("实例未处理"); + return ['status' => 'noop', 'msg' => '实例未处理']; + } - // 获取流程定义 - $flow = Db::table('approval_flows') - ->where('type', $instance->flow_type) - ->first(); - - if (!$flow) { - throw new BusinessException("审批流程定义未找到"); - } + $flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first(); + if (!$flow) throw new BusinessException("审批流程定义未找到"); - $steps = json_decode($flow->steps, true); - $currentStepIndex = $instance->current_step; - $currentStep = $steps[$currentStepIndex] ?? null; + $steps = json_decode($flow->steps, true); + $idx = (int)$instance->current_step; + $step = $steps[$idx] ?? null; + if (!$step) throw new BusinessException("当前步骤配置错误"); - if (!$currentStep) { - throw new BusinessException("当前步骤配置错误"); - } + // 找到该用户在本步的 pending 记录(加锁) + $pendingRecord = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('approver_id', $approverId) + ->where('step_index', $idx) + ->where('status', 'pending') + ->lockForUpdate() + ->first(); - // 检查用户是否有权限审批 - if (!$this->canUserApproveStep($currentStep, $approverId, $instance)) { - throw new BusinessException("您无权审批此步骤"); - } + if (!$pendingRecord) { + // 已处理过? + $anyRecord = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('approver_id', $approverId) + ->where('step_index', $idx) + ->first(); + if ($anyRecord && $anyRecord->status !== 'pending') { + // return ['status' => 'noop', 'reason' => 'already_handled', 'record_status' => $anyRecord->status]; + // throw new BusinessException("已处理"); + return ['status' => 'noop', 'msg' => '已处理']; + } + // 步骤已推进 + if ((int)$instance->current_step !== $idx) { + // return ['status' => 'noop', 'reason' => 'not_current_step', 'current_step' => (int)$instance->current_step]; + // throw new BusinessException("非当前步骤"); + return ['status' => 'noop', 'msg' => '非当前步骤']; + } + // return ['status' => 'noop', 'reason' => 'no_pending_record']; + return ['status' => 'noop', 'msg' => '无待处理记录']; + } - // 检查用户是否已经审批过 - $existingRecord = Db::table('approval_records') - ->where('instance_id', $instanceId) - ->where('approver_id', $approverId) - ->where('step_index', $currentStepIndex) - ->first(); - - if ($existingRecord && $existingRecord->status !== 'pending') { - throw new BusinessException("您已经审批过此步骤"); - } + $now = date('Y-m-d H:i:s'); - // 更新审批记录 - Db::table('approval_records') - ->where('instance_id', $instanceId) - ->where('approver_id', $approverId) - ->where('step_index', $currentStepIndex) - ->where('status', 'pending') - ->update([ - 'action' => $action, - 'status' => $action, // 状态与操作一致 - 'comment' => $comment, - 'approved_at' => date('Y-m-d H:i:s'), - 'updated_at' => date('Y-m-d H:i:s'), + // 1) 更新当前人的审批记录 + Db::table('approval_records')->where('id', $pendingRecord->id)->update([ + 'action' => $action, + 'status' => ($action === 'approve') ? 'approved' : 'rejected', + 'comment' => $comment, + 'approved_at' => $now, + 'updated_at' => $now, ]); - // 检查当前步骤是否完成(根据 type: any, all) - $stepCompleted = $this->isStepCompleted($instanceId, $currentStepIndex, $currentStep['type']); + // 2) 聚合本步结论(any / all) + $typeRaw = strtolower($step['type'] ?? 'all'); + $type = in_array($typeRaw, ['any','all']) ? $typeRaw : 'all'; + $outcome = $this->getStepOutcome($instanceId, $idx, $type); // 'pending' | 'approve' | 'reject' - if (!$stepCompleted) { - // 步骤未完成,不进入下一步 - return ['status' => 'step_pending']; - } + if ($outcome === 'pending') { + // return ['status' => 'step_pending']; + return ['status' => 'step_pending', 'msg' => '步骤待定']; + } - // 步骤已完成,进入下一步或结束 - $nextStepIndex = $this->getNextStepIndex($steps, json_decode($instance->form_data, true), $currentStepIndex); + // 3) 有结论:关闭本步其余 pending + $this->closeOtherPending($instanceId, $idx, [$approverId]); - if ($nextStepIndex === null) { - // 流程结束 + // 4) 拒绝 => 结束 + if ($outcome === 'reject') { + Db::table('approval_instances')->where('id', $instanceId)->update([ + 'status' => 'rejected', + 'finished_at' => $now, + 'updated_at' => $now, + ]); + $this->sendNotification([$instance->initiator_id], $instance, 'reject', false); + // return ['status' => 'completed', 'result' => 'reject']; + return ['status' => 'completed', 'msg' => '已拒绝']; + } + + // 5) 通过 => 进入下一步或结束 + $formData = json_decode($instance->form_data, true) ?: []; + $nextIndex = $this->getNextStepIndex($steps, $formData, $idx); + + if ($nextIndex === null) { + Db::table('approval_instances')->where('id', $instanceId)->update([ + 'status' => 'approved', + 'finished_at' => $now, + 'updated_at' => $now, + ]); + $this->sendNotification([$instance->initiator_id], $instance, 'approve', false); + // return ['status' => 'completed', 'result' => 'approve']; + return ['status' => 'completed', 'msg' => '已同意']; + } + + // 乐观推进 current_step + $affected = Db::table('approval_instances') + ->where('id', $instanceId) + ->where('current_step', $idx) + ->where('status', 'processing') + ->update(['current_step' => $nextIndex, 'updated_at' => $now]); + if (!$affected) { + // return ['status' => 'noop', 'reason' => 'concurrent_progressed']; + return ['status' => 'noop', 'msg' => '并发进展']; + } + + // 生成下一步待办 + 通知 + $nextStep = $steps[$nextIndex]; + $nextApprovers = $this->resolveApprovers($nextStep, $instance, true); + foreach ($nextApprovers as $uid) { + $this->insertApprovalRecord($instanceId, $nextIndex, $uid); + } + $this->sendNotification($nextApprovers, $instance, 'pending', false); + $this->notifyCCs($nextStep, $instance, 'pending'); + + // return ['status' => 'next_step', 'step_name' => $nextStep['name'] ?? ('Step '.$nextIndex)]; + return ['status' => 'next_step', 'msg' => '进行下一步:'.$nextStep['name'] ?? ('Step '.$nextIndex)]; + }); + } + + /** 撤回(仅发起人在无已处理记录前可撤回) */ + public function revoke(int $instanceId, int $initiatorId, string $reason = '') + { + return Db::transaction(function () use ($instanceId, $initiatorId, $reason) { + $instance = Db::table('approval_instances')->where('id', $instanceId)->lockForUpdate()->first(); + if (!$instance) throw new BusinessException('审批实例未找到'); + if ((int)$instance->initiator_id !== (int)$initiatorId) throw new BusinessException('无权撤回'); + if ($instance->status !== 'processing') throw new BusinessException('流程已结束或不可撤回'); + + $handled = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->whereIn('status', ['approved','rejected','timeout','skipped']) + ->count() > 0; + if ($handled) throw new BusinessException('当前进度不可撤回'); + + $now = date('Y-m-d H:i:s'); Db::table('approval_instances')->where('id', $instanceId)->update([ - 'status' => $action, // 最终状态与最后一次操作一致 - 'finished_at' => date('Y-m-d H:i:s'), - 'updated_at' => date('Y-m-d H:i:s'), + 'status' => 'revoked', + 'finished_at' => $now, + 'updated_at' => $now, ]); - // 通知发起人结果 - $this->sendNotification([$instance->initiator_id], $instance, $action, false); + Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('status', 'pending') + ->update(['status' => 'skipped', 'approved_at' => $now, 'updated_at' => $now]); - return ['status' => 'completed', 'result' => $action]; - } - - // 进入下一步 - Db::table('approval_instances')->where('id', $instanceId)->update([ - 'current_step' => $nextStepIndex, - 'updated_at' => date('Y-m-d H:i:s'), - ]); - - // 获取下一步信息 - $nextStep = $steps[$nextStepIndex]; - - // 创建下一步的审批记录 - $nextApprovers = $this->resolveApprovers($nextStep, $instance); - foreach ($nextApprovers as $approverId) { - Db::table('approval_records')->insert([ - 'instance_id' => $instanceId, - 'approver_id' => $approverId, - 'step_index' => $nextStepIndex, - 'action' => '', - 'status' => 'pending', - 'approved_at' => date('Y-m-d H:i:s', 0), - 'created_at' => date('Y-m-d H:i:s'), - 'updated_at' => date('Y-m-d H:i:s'), - ]); - } - - // 通知下一步审批人 - $this->sendNotification($nextApprovers, $instance, 'pending', false); - - // 通知下一步抄送人 - $this->notifyCCs($nextStep, $instance, 'pending'); - - return ['status' => 'next_step', 'step_name' => $nextStep['name']]; + $this->sendNotification([$initiatorId], $instance, 'revoked', false); + return ['status' => 'completed', 'result' => 'revoked']; + }); } - /** - * 解析审批人 - */ - private function resolveApprovers(array $step, object $instance): array - { - $resolved = []; - $formData = json_decode($instance->form_data ?? '{}', true); - - foreach ($step['approvers'] as $approverConfig) { - $approverIds = []; - - switch ($approverConfig['type']) { - case 'role': - $approverIds = $this->getUserIdsByRole($approverConfig['value']); - break; - case 'user': - $approverIds = [(int)$approverConfig['value']]; - break; - case 'initiator_leader': - $leaderId = $this->getLeaderIdByUserId($instance->initiator_id); - if ($leaderId) $approverIds = [$leaderId]; - break; - case 'field_user': - if (isset($formData[$approverConfig['value']])) { - $approverIds = [(int)$formData[$approverConfig['value']]]; - } - break; - case 'dept_head': - $approverIds = $this->getDeptHeadId($approverConfig['value']); - if ($approverIds) $approverIds = [$approverIds]; - break; - default: - throw new BusinessException("不支持的审批人类型: {$approverConfig['type']}"); - } - - $resolved = array_merge($resolved, $approverIds); - } - - return array_unique(array_filter($resolved)); - } - - /** - * 通知抄送人 - */ - private function notifyCCs(array $step, object $instance, string $action = 'pending') - { - $ccConfig = $step['cc'] ?? []; - $ccUserIds = []; - $formData = json_decode($instance->form_data ?? '{}', true); - - foreach ($ccConfig as $cc) { - switch ($cc['type']) { - case 'role': - $ccUserIds = array_merge($ccUserIds, $this->getUserIdsByRole($cc['value'])); - break; - case 'user': - $ccUserIds[] = (int)$cc['value']; - break; - case 'initiator_leader': - $leaderId = $this->getLeaderIdByUserId($instance->initiator_id); - if ($leaderId) $ccUserIds[] = $leaderId; - break; - case 'field_user': - if (isset($formData[$cc['value']])) { - $ccUserIds[] = (int)$formData[$cc['value']]; - } - break; - case 'dept_head': - $deptHeadId = $this->getDeptHeadId($cc['value']); - if ($deptHeadId) $ccUserIds[] = $deptHeadId; - break; - default: - break; - } - } - - $ccUserIds = array_unique(array_filter($ccUserIds)); - - if (!empty($ccUserIds)) { - // 记录抄送信息到数据库 - foreach ($ccUserIds as $userId) { - // 检查是否已存在抄送记录 - $exists = Db::table('approval_ccs') - ->where('instance_id', $instance->id) - ->where('user_id', $userId) - ->exists(); - - if (!$exists) { - Db::table('approval_ccs')->insert([ - 'instance_id' => $instance->id, - 'user_id' => $userId, - 'notified_at' => date('Y-m-d H:i:s'), - 'created_at' => date('Y-m-d H:i:s'), - 'updated_at' => date('Y-m-d H:i:s'), - ]); - } - } - - // 发送通知 - $this->sendNotification($ccUserIds, $instance, $action, true); - } - } - - /** - * 评估条件 - */ - private function evaluateCondition(?string $condition, array $formData): bool - { - if (!$condition) return true; - - $pattern = '/^(\w+)\s*(==|!=|>|<|>=|<=)\s*(.+)$/'; - if (!preg_match($pattern, $condition, $matches)) { - throw new BusinessException("条件格式错误: {$condition}"); - } - - $field = $matches[1]; - $op = $matches[2]; - $value = trim($matches[3], "\"'"); - - $actualValue = $formData[$field] ?? null; - - if (is_numeric($value) && is_numeric($actualValue)) { - $value = (float)$value; - $actualValue = (float)$actualValue; - } - - switch ($op) { - case '==': return $actualValue == $value; - case '!=': return $actualValue != $value; - case '>': return $actualValue > $value; - case '<': return $actualValue < $value; - case '>=': return $actualValue >= $value; - case '<=': return $actualValue <= $value; - default: throw new BusinessException("不支持的操作符: {$op}"); - } - } - - /** - * 获取下一个有效步骤索引(考虑 condition) - */ - private function getNextStepIndex(array $steps, array $formData, int $currentStepIndex): ?int - { - for ($i = $currentStepIndex + 1; $i < count($steps); $i++) { - $step = $steps[$i]; - if (!isset($step['condition']) || $this->evaluateCondition($step['condition'], $formData)) { - return $i; - } - } - return null; - } - - /** - * 检查用户是否有权限审批 - */ - private function canUserApproveStep(array $step, int $userId, object $instance): bool - { - $approvers = $this->resolveApprovers($step, $instance); - return in_array($userId, $approvers); - } - - /** - * 检查当前步骤是否完成 - */ - private function isStepCompleted(int $instanceId, int $stepIndex, string $type): bool - { - // 获取当前步骤的所有审批记录 - $records = Db::table('approval_records') - ->where('instance_id', $instanceId) - ->where('step_index', $stepIndex) - ->get(); - - if ($records->isEmpty()) { - return false; - } - - switch ($type) { - case 'any': - // 任意一人审批即可(approve或reject都算完成) - foreach ($records as $record) { - if ($record->status !== 'pending') { - return true; - } - } - return false; - - case 'all': - // 所有人都必须审批 - foreach ($records as $record) { - if ($record->status === 'pending') { - return false; - } - } - return true; - - default: - return false; - } - } - - /** - * 发送通知(你可以替换为你的通知系统) - */ - private function sendNotification(array $userIds, object $instance, string $action, bool $isCC) - { - $actionText = [ - 'pending' => '待审批', - 'approve' => '已通过', - 'reject' => '已拒绝' - ][$action] ?? $action; - - $prefix = $isCC ? '[抄送]' : ''; - $message = "{$prefix}您有一条审批 {$actionText}:流程ID {$instance->id},类型 {$instance->flow_type}"; - - // TODO: 调用你的消息系统,如邮件、站内信、企业微信等 - // 示例:MessageService::send($userIds, $message); - echo "通知用户 " . implode(',', $userIds) . ":$message\n"; - } - - /** - * 获取用户待审批列表 - * @param int $userId 用户ID - * @return array - */ + /** 我的待办 */ public function getPendingApprovals(int $userId): array { return Db::table('approval_records as ar') @@ -439,30 +235,23 @@ class ApprovalFlowService ->where('ar.status', 'pending') ->where('ai.status', 'processing') ->select('ai.*') + ->distinct() ->get() ->toArray(); } - /** - * 获取审批历史记录 - * @param int $instanceId - * @return array - */ + /** 审批历史 */ public function getApprovalHistory(int $instanceId): array { return Db::table('approval_records') ->where('instance_id', $instanceId) ->orderBy('step_index') - ->orderBy('approved_at') + ->orderByRaw('COALESCE(approved_at, created_at) asc') ->get() ->toArray(); } - /** - * 获取审批流程图数据 (简化版) - * @param int $instanceId - * @return array - */ + /** 流程图数据(把 skipped/timeout 视作已完结的中性状态) */ public function getFlowDiagramData(int $instanceId): array { $instance = Db::table('approval_instances')->find($instanceId); @@ -471,96 +260,496 @@ class ApprovalFlowService $flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first(); if (!$flow) return []; - $steps = json_decode($flow->steps, true); + $steps = json_decode($flow->steps, true) ?: []; $records = Db::table('approval_records') ->where('instance_id', $instanceId) - ->orderBy('step_index') - ->orderBy('approved_at') + ->orderBy('step_index')->orderByRaw('COALESCE(approved_at, created_at) asc') ->get() ->groupBy('step_index'); - $diagramData = []; - foreach ($steps as $index => $step) { - $status = 'pending'; - $actions = $records->get($index, collect())->toArray(); + $formData = json_decode($instance->form_data, true) ?: []; + $data = []; + foreach ($steps as $i => $step) { + $actions = ($records->get($i) ?? collect())->toArray(); if (!empty($actions)) { - $hasReject = collect($actions)->contains('action', 'reject'); - if ($hasReject) { - $status = 'rejected'; - } else { - $status = 'approved'; + $cntApproved = 0; $cntRejected = 0; $cntPending = 0; + foreach ($actions as $a) { + if ($a->status === 'approved') $cntApproved++; + elseif ($a->status === 'rejected') $cntRejected++; + elseif ($a->status === 'pending') $cntPending++; + // skipped/timeout 不阻断完结显示 + } + if ($cntRejected > 0) $status = 'rejected'; + elseif ($cntPending > 0) $status = 'processing'; + elseif ($cntApproved > 0) $status = 'approved'; + else $status = 'approved'; // 全是 skipped/timeout 也视为已过 + } else { + if ($i < (int)$instance->current_step) { + if (isset($step['condition']) && !$this->evaluateCondition($step['condition'], $formData)) { + $status = 'skipped'; + } else { + $status = 'approved'; + } + } elseif ($i === (int)$instance->current_step && $instance->status === 'processing') { + $status = 'processing'; + } else { + $status = 'pending'; } - } elseif ($index < $instance->current_step) { - $status = 'approved'; - } elseif ($index == $instance->current_step) { - $status = 'processing'; } - $diagramData[] = [ - 'index' => $index, - 'name' => $step['name'], - 'type' => $step['type'], - 'status' => $status, - 'actions' => $actions + $data[] = [ + 'index' => $i, + 'name' => $step['name'] ?? ('Step '.$i), + 'type' => $step['type'] ?? 'all', + 'status' => $status, + 'actions' => $actions, ]; } - return $diagramData; + return $data; } - /** - * 处理超时的审批实例 (定时任务调用) - */ + /** 定时任务:处理超时(实例级) */ public function handleTimeouts() { $now = date('Y-m-d H:i:s'); - $expiredInstances = Db::table('approval_instances') + $expired = Db::table('approval_instances') ->where('status', 'processing') + ->whereNotNull('deadline_at') ->where('deadline_at', '<=', $now) ->get(); - foreach ($expiredInstances as $instance) { - $flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first(); - if ($flow && $flow->timeout_action) { - $updateData = [ + foreach ($expired as $instance) { + Db::transaction(function () use ($instance, $now) { + $flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first(); + if (!$flow) return; + + $final = ($flow->timeout_action === 'auto_approve') ? 'approved' : 'rejected'; + $finalAction = ($flow->timeout_action === 'auto_approve') ? 'approve' : 'reject'; + + Db::table('approval_instances')->where('id', $instance->id)->update([ + 'status' => $final, 'finished_at' => $now, - 'updated_at' => $now - ]; - if ($flow->timeout_action === 'auto_reject') { - $updateData['status'] = 'reject'; - } elseif ($flow->timeout_action === 'auto_approve') { - $updateData['status'] = 'approve'; - } - - Db::table('approval_instances')->where('id', $instance->id)->update($updateData); - $this->notifyInitiator($instance, $flow->timeout_action); - } + 'updated_at' => $now, + ]); + + Db::table('approval_records') + ->where('instance_id', $instance->id) + ->where('step_index', $instance->current_step) + ->where('status', 'pending') + ->update(['status' => 'timeout', 'approved_at' => $now, 'updated_at' => $now]); + + $this->notifyInitiator($instance, $finalAction); + }); } } - // --- 辅助方法 --- + // ---------- 内部工具 ---------- - private function getUserIdsByRole(string $roleCode): array + /** 解析审批人(role/user/initiator_leader/field_user/dept_head) */ + private function resolveApprovers(array $step, object $instance, bool $avoidSelf = true): array { - return Db::table('user_roles as ur') - ->join('roles as r', 'ur.role_id', '=', 'r.id') - ->where('r.code', $roleCode) - ->pluck('ur.user_id') - ->toArray(); + $resolved = []; + $formData = json_decode($instance->form_data ?? '{}', true) ?: []; + + $confs = $step['approvers'] ?? []; + foreach ($confs as $cfg) { + $typ = $cfg['type'] ?? ''; + $val = $cfg['value'] ?? null; + $ids = []; + + switch ($typ) { + case 'role': + $sys = isset($cfg['system_id']) ? (int)$cfg['system_id'] : null; + $includeGlobal = array_key_exists('include_global', $cfg) ? (bool)$cfg['include_global'] : true; + $ids = $this->getUserIdsByRole($val, $sys, true, $includeGlobal); + break; + case 'user': + $ids = [(int)$val]; + break; + case 'initiator_leader': + $leaderId = $this->getLeaderIdByUserId((int)$instance->initiator_id); + if ($leaderId) $ids = [(int)$leaderId]; + break; + case 'field_user': + if (isset($formData[$val])) $ids = [(int)$formData[$val]]; + break; + case 'dept_head': + $head = $this->getDeptHeadId((int)$val); + if ($head) $ids = [(int)$head]; + break; + default: + throw new BusinessException("不支持的审批人类型: {$typ}"); + } + $resolved = array_merge($resolved, $ids); + } + + $resolved = array_unique(array_filter(array_map('intval', $resolved))); + if ($avoidSelf) { + $resolved = array_values(array_filter($resolved, fn($id) => $id !== (int)$instance->initiator_id)); + } + if (empty($resolved)) { + throw new BusinessException("步骤未解析到任何审批人"); + } + return $resolved; + } + + /** 通知抄送人 */ + private function notifyCCs(array $step, object $instance, string $action = 'pending'): void + { + $ccConfig = $step['cc'] ?? []; + if (empty($ccConfig)) return; + + $formData = json_decode($instance->form_data ?? '{}', true) ?: []; + $uids = []; + + foreach ($ccConfig as $cc) { + $typ = $cc['type'] ?? ''; + $val = $cc['value'] ?? null; + + switch ($typ) { + case 'role': + $uids = array_merge($uids, $this->getUserIdsByRole($val, null, true, true)); + break; + case 'user': $uids[] = (int)$val; break; + case 'initiator_leader': + $leaderId = $this->getLeaderIdByUserId((int)$instance->initiator_id); + if ($leaderId) $uids[] = (int)$leaderId; + break; + case 'field_user': + if (isset($formData[$val])) $uids[] = (int)$formData[$val]; + break; + case 'dept_head': + $head = $this->getDeptHeadId((int)$val); + if ($head) $uids[] = (int)$head; + break; + default: break; + } + } + + $uids = array_unique(array_filter(array_map('intval', $uids))); + if (empty($uids)) return; + + $now = date('Y-m-d H:i:s'); + foreach ($uids as $uid) { + $exists = Db::table('approval_ccs') + ->where('instance_id', $instance->id) + ->where('user_id', $uid) + ->count() > 0; + if (!$exists) { + Db::table('approval_ccs')->insert([ + 'instance_id' => $instance->id, + 'user_id' => $uid, + 'notified_at' => $now, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + + $this->sendNotification($uids, $instance, $action, true); + } + + /** 条件求值(简单表达式) */ + private function evaluateCondition(?string $condition, array $formData): bool + { + if (!$condition) return true; + $pattern = '/^(\w+)\s*(==|!=|>|<|>=|<=)\s*(.+)$/'; + if (!preg_match($pattern, $condition, $m)) { + throw new BusinessException("条件格式错误: {$condition}"); + } + $field = $m[1]; $op = $m[2]; $value = trim($m[3], "\"'"); + $actual = $formData[$field] ?? null; + if (is_numeric($value) && is_numeric($actual)) { $value=(float)$value; $actual=(float)$actual; } + return match ($op) { + '==' => $actual == $value, + '!=' => $actual != $value, + '>' => $actual > $value, + '<' => $actual < $value, + '>=' => $actual >= $value, + '<=' => $actual <= $value, + default => throw new BusinessException("不支持的操作符: {$op}"), + }; + } + + /** 获取下一个有效步骤索引(考虑 condition) */ + private function getNextStepIndex(array $steps, array $formData, int $currentStepIndex): ?int + { + for ($i = $currentStepIndex + 1; $i < count($steps); $i++) { + $s = $steps[$i]; + if (!isset($s['condition']) || $this->evaluateCondition($s['condition'], $formData)) { + return $i; + } + } + return null; + } + + /** + * 聚合本步骤结论(按类型) + * 返回:'pending' | 'approve' | 'reject' + * + * any:有 reject => reject;否则有 approve => approve;否则 pending + * all:有 reject => reject;有 pending => pending;否则(全 approved 或 approved+skipped/timeout)=> approve + */ + private function getStepOutcome(int $instanceId, int $stepIndex, string $type): string + { + $records = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('step_index', $stepIndex) + ->get(); + + $pending = 0; $approved = 0; $rejected = 0; + foreach ($records as $r) { + if ($r->status === 'pending') { $pending++; } + elseif ($r->status === 'approved'){ $approved++; } + elseif ($r->status === 'rejected'){ $rejected++; } + // skipped/timeout 不影响聚合结论 + } + + $type = strtolower($type); + if ($type === 'any') { + if ($rejected > 0) return 'reject'; + if ($approved > 0) return 'approve'; + return 'pending'; + } + // all + if ($rejected > 0) return 'reject'; + if ($pending > 0) return 'pending'; + return $approved > 0 ? 'approve' : 'pending'; + } + + /** 关闭本步其余待办为 skipped */ + private function closeOtherPending(int $instanceId, int $stepIndex, array $keepApproverIds = []): void + { + $now = date('Y-m-d H:i:s'); + $q = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('step_index', $stepIndex) + ->where('status', 'pending'); + + if (!empty($keepApproverIds)) $q->whereNotIn('approver_id', $keepApproverIds); + + $q->update(['status'=>'skipped','approved_at'=>$now,'updated_at'=>$now]); + } + + /** 创建待办(去重) */ + private function insertApprovalRecord(int $instanceId, int $stepIndex, int $approverId): void + { + $now = date('Y-m-d H:i:s'); + $exists = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('step_index', $stepIndex) + ->where('approver_id', $approverId) + ->count() > 0; + if (!$exists) { + Db::table('approval_records')->insert([ + 'instance_id' => $instanceId, + 'approver_id' => $approverId, + 'step_index' => $stepIndex, + 'action' => null, + 'status' => 'pending', + 'comment' => null, + 'approved_at' => null, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + + /** 发送通知(接你自己的消息系统即可) */ + private function sendNotification(array $userIds, object $instance, string $action, bool $isCC): void + { + $map = ['pending'=>'待审批','approve'=>'已通过','reject'=>'已拒绝','revoked'=>'已撤回']; + $prefix = $isCC ? '[抄送]' : ''; + $text = $map[$action] ?? $action; + $msg = "{$prefix}您有一条审批 {$text}:流程ID {$instance->id},类型 {$instance->flow_type}"; + // MessageService::send($userIds, $msg); + + $pushData = [ + 'sender_id' => 0, + 'sender_type' => 'system', + 'title' => $msg, + 'content' => $msg, + 'scene' => 1, + 'recipient' => $userIds, + 'push_method' => 1 + ]; + $msgId = (new MessageNotificationsService())->pushNotice($pushData); + var_dump($msgId); + } + + private function notifyInitiator(object $instance, string $result): void + { + $this->sendNotification([(int)$instance->initiator_id], $instance, $result, false); + } + + // ---------- 数据访问(roles / user_roles 结构) ---------- + + /** + * 从角色取用户ID + * @param mixed $roleIdOrName 数字 => role_id;字符串 => roles.name + * @param int|null $systemId 限定 roles.system_id;null 不限定 + * @param bool $onlyActive 仅取 status=1 角色,默认 true + * @param bool $includeGlobal 是否包含全局角色(system_id=0),默认 true + */ + private function getUserIdsByRole($roleIdOrName, ?int $systemId = null, bool $onlyActive = true, bool $includeGlobal = true): array + { + if (is_int($roleIdOrName) || (is_string($roleIdOrName) && ctype_digit($roleIdOrName))) { + $roleId = (int)$roleIdOrName; + $needJoin = $onlyActive || !is_null($systemId) || $includeGlobal === false; + + if (!$needJoin) { + $rows = Db::table('user_roles')->where('role_id', $roleId)->select('user_id')->get(); + } else { + $q = Db::table('user_roles as ur') + ->join('roles as r', 'ur.role_id', '=', 'r.id') + ->where('ur.role_id', $roleId) + ->select('ur.user_id'); + if ($onlyActive) $q->where('r.status', 1); + if (!is_null($systemId)) { + $includeGlobal ? $q->whereIn('r.system_id', [0, $systemId]) : $q->where('r.system_id', $systemId); + } + $rows = $q->get(); + } + } else { + $name = (string)$roleIdOrName; + $q = Db::table('user_roles as ur') + ->join('roles as r', 'ur.role_id', '=', 'r.id') + ->where('r.name', $name) + ->select('ur.user_id'); + if ($onlyActive) $q->where('r.status', 1); + if (!is_null($systemId)) { + $includeGlobal ? $q->whereIn('r.system_id', [0, $systemId]) : $q->where('r.system_id', $systemId); + } + $rows = $q->get(); + } + + $ids = []; + foreach ($rows as $row) $ids[] = (int)$row->user_id; + return array_values(array_unique($ids)); } private function getLeaderIdByUserId(int $userId): ?int { - return Db::table('user')->where('id', $userId)->value('report_to'); + $id = Db::table('user')->where('id', $userId)->value('report_to'); + return $id ? (int)$id : null; } private function getDeptHeadId(int $deptId): ?int { - return Db::table('departments')->where('id', $deptId)->value('head_user_id'); + $id = Db::table('departments')->where('id', $deptId)->value('head_user_id'); + return $id ? (int)$id : null; } - private function notifyInitiator(object $instance, string $result) + + /** + * 查询某个审批实例的审批记录(附带步骤名/类型,可选带出审批人姓名) + * + * @param int $instanceId + * @param string|null $userNameColumn 可选:如 'name' 或 'nickname'。传 null 则不 join 用户表 + * @return array{ + * instance: array, + * steps: array, + * records: array, + * summary: array{approved:int,rejected:int,pending:int,skipped:int,timeout:int,total:int} + * } + */ + public function getInstanceRecords(int $instanceId, ?string $userNameColumn = null): array { - echo "通知发起人 {$instance->initiator_id} 审批结果: {$result}\n"; + // 1) 实例 + 流程步骤 + $instance = Db::table('approval_instances')->where('id', $instanceId)->first(); + if (!$instance) { + throw new BusinessException('审批实例未找到'); + } + $flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first(); + if (!$flow) { + throw new BusinessException('审批流程定义未找到'); + } + $steps = json_decode($flow->steps, true) ?: []; + + // 2) 记录查询(可选带出审批人姓名) + if ($userNameColumn) { + // 注意:确保 user 表存在该列 + $records = Db::table('approval_records as ar') + ->leftJoin('user as u', 'ar.approver_id', '=', 'u.id') + ->where('ar.instance_id', $instanceId) + ->orderBy('ar.step_index') + ->orderByRaw('COALESCE(ar.approved_at, ar.created_at) asc') + ->select( + 'ar.*', + Db::raw("u.`{$userNameColumn}` as approver_name") + ) + ->get() + ->toArray(); + } else { + $records = Db::table('approval_records as ar') + ->where('ar.instance_id', $instanceId) + ->orderBy('ar.step_index') + ->orderByRaw('COALESCE(ar.approved_at, ar.created_at) asc') + ->select('ar.*') + ->get() + ->toArray(); + } + + // 3) 附加步骤名/类型;统计汇总 + $summary = ['approved'=>0,'rejected'=>0,'pending'=>0,'skipped'=>0,'timeout'=>0,'total'=>0]; + foreach ($records as &$r) { + $idx = (int)$r->step_index; + $meta = $steps[$idx] ?? []; + $r->step_name = $meta['name'] ?? ('Step '.$idx); + $r->step_type = $meta['type'] ?? 'all'; + + if (isset($summary[$r->status])) { + $summary[$r->status]++; + } + $summary['total']++; + } + unset($r); + + return [ + 'instance' => [ + 'id' => (int)$instance->id, + 'flow_type' => $instance->flow_type, + 'biz_type' => $instance->biz_type, + 'biz_id' => $instance->biz_id, + 'status' => $instance->status, + 'current_step' => (int)$instance->current_step, + 'created_at' => $instance->created_at, + 'updated_at' => $instance->updated_at, + 'finished_at' => $instance->finished_at, + ], + 'steps' => array_map(function($s){ + return [ + 'name' => $s['name'] ?? '', + 'type' => $s['type'] ?? 'all', + 'condition' => $s['condition'] ?? null, + ]; + }, $steps), + 'records' => array_map(function($r){ + // 转成数组(Illuminate 返回的是 stdClass) + return [ + 'id' => (int)$r->id, + 'instance_id' => (int)$r->instance_id, + 'approver_id' => (int)$r->approver_id, + 'step_index' => (int)$r->step_index, + 'action' => $r->action, + 'status' => $r->status, + 'comment' => $r->comment, + 'approved_at' => $r->approved_at, + 'created_at' => $r->created_at, + 'updated_at' => $r->updated_at, + 'approver_name'=> property_exists($r, 'approver_name') ? $r->approver_name : null, + 'step_name' => $r->step_name, + 'step_type' => $r->step_type, + ]; + }, $records), + 'summary' => $summary, + ]; } } \ No newline at end of file diff --git a/app/common/utils/approval/service/ApprovalFlowService_bak.php b/app/common/utils/approval/service/ApprovalFlowService_bak.php index 37d45c9..f988a48 100644 --- a/app/common/utils/approval/service/ApprovalFlowService_bak.php +++ b/app/common/utils/approval/service/ApprovalFlowService_bak.php @@ -1,195 +1,285 @@ true, 'instance_id' => ...] 或抛出异常 + * 启动审批流程 */ public function start(string $flowType, string $bizType, string $bizId, array $formData, int $initiatorId): array { - // 1. 获取流程定义 - $flow = Db::table('approval_flows') - ->where('type', $flowType) - ->where('is_active', 1) - ->first(); - - if (!$flow) { - throw new BusinessException("审批流程 '{$flowType}' 不存在或已停用"); - } + return Db::transaction(function () use ($flowType, $bizType, $bizId, $formData, $initiatorId) { + // 1) 流程定义 + $flow = Db::table('approval_flows') + ->where('type', $flowType) + ->where('is_active', 1) + ->first(); - // 2. 创建审批实例 - $instanceId = Db::table('approval_instances')->insertGetId([ - 'flow_type' => $flowType, - 'biz_id' => $bizId, - 'biz_type' => $bizType, - 'form_data' => json_encode($formData), - 'initiator_id' => $initiatorId, - 'status' => 'processing', - 'current_step' => 0, - 'deadline_at' => $flow->timeout_hours > 0 ? - (new DateTime())->modify("+{$flow->timeout_hours} hours")->format('Y-m-d H:i:s') : null, - 'created_at' => date('Y-m-d H:i:s'), - 'updated_at' => date('Y-m-d H:i:s'), - ]); - - // 3. 通知下一个审批人 - $this->notifyNextApprovers($instanceId); - - return ['success' => true, 'instance_id' => $instanceId]; - } - - /** - * 执行审批操作 - * @param int $instanceId 审批实例ID - * @param int $approverId 审批人ID - * @param string $action 操作 (approve, reject, revoke) - * @param string $comment 审批意见 - * @return array 操作结果 - */ - public function approve(int $instanceId, int $approverId, string $action, string $comment = ''): array - { - // 1. 获取实例和流程 - $instance = Db::table('approval_instances')->find($instanceId); - if (!$instance) { - throw new BusinessException("审批实例不存在"); - } - if ($instance->status !== 'processing') { - throw new BusinessException("审批实例状态不正确"); - } - - $flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first(); - if (!$flow) { - throw new BusinessException("关联的审批流程不存在"); - } - - $steps = json_decode($flow->steps, true); - $currentStepIndex = $instance->current_step; - - if ($currentStepIndex >= count($steps)) { - throw new BusinessException("审批流程已结束"); - } - - $currentStep = $steps[$currentStepIndex]; - - // 2. 验证权限和条件 - if (!$this->canUserApproveStep($currentStep, $approverId, $instance)) { - throw new BusinessException("您无权审批此步骤"); - } - - // 3. 记录审批行为 - Db::table('approval_records')->insert([ - 'instance_id' => $instanceId, - 'approver_id' => $approverId, - 'step_index' => $currentStepIndex, - 'action' => $action, - 'comment' => $comment, - 'approved_at' => date('Y-m-d H:i:s'), - 'created_at' => date('Y-m-d H:i:s'), - 'updated_at' => date('Y-m-d H:i:s'), - ]); - - // 4. 处理审批结果 - $updateData = ['updated_at' => date('Y-m-d H:i:s')]; - if ($action === 'reject') { - $updateData['status'] = 'rejected'; - $updateData['finished_at'] = date('Y-m-d H:i:s'); - Db::table('approval_instances')->where('id', $instanceId)->update($updateData); - $this->notifyInitiator($instance, 'rejected'); - return ['result' => 'rejected']; - } - - if ($action === 'revoke') { - if ($instance->initiator_id !== $approverId || $instance->status !== 'processing') { - throw new BusinessException("无权撤回或状态不允许"); + if (!$flow) { + throw new BusinessException("审批流程未找到或已停用: {$flowType}"); } - $updateData['status'] = 'revoked'; - $updateData['finished_at'] = date('Y-m-d H:i:s'); - Db::table('approval_instances')->where('id', $instanceId)->update($updateData); - $this->notifyAllApprovers($instance, 'revoked'); - return ['result' => 'revoked']; - } - // 5. 处理 approve 逻辑 - $nextStepIndex = $this->calculateNextStepIndex($instanceId, $currentStepIndex, $currentStep); - $updateData['current_step'] = $nextStepIndex; + $steps = json_decode($flow->steps, true); + if (!is_array($steps) || empty($steps)) { + throw new BusinessException("流程步骤配置错误"); + } - if ($nextStepIndex >= count($steps)) { - $updateData['status'] = 'approved'; - $updateData['finished_at'] = date('Y-m-d H:i:s'); - Db::table('approval_instances')->where('id', $instanceId)->update($updateData); - $this->notifyInitiator($instance, 'approved'); - $this->notifyCc($instanceId); - } else { - Db::table('approval_instances')->where('id', $instanceId)->update($updateData); - $this->notifyNextApprovers($instanceId); - } + // 2) 寻找首个有效步骤索引(考虑条件) + $firstIndex = $this->getNextStepIndex($steps, $formData, -1); + if ($firstIndex === null) { + throw new BusinessException("未找到可执行的审批步骤"); + } + $firstStep = $steps[$firstIndex]; - return ['result' => 'approved', 'next_step' => $nextStepIndex]; + // 3) 计算实例截止时间(实例级,可扩展到步骤级) + $deadlineAt = null; + if (($flow->timeout_hours ?? 0) > 0) { + $deadlineAt = date('Y-m-d H:i:s', time() + $flow->timeout_hours * 3600); + } + + // 4) 创建审批实例 + $now = date('Y-m-d H:i:s'); + $instanceId = Db::table('approval_instances')->insertGetId([ + 'flow_type' => $flowType, + 'biz_type' => $bizType, + 'biz_id' => $bizId, + 'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'initiator_id' => $initiatorId, + 'status' => 'processing', + 'current_step' => $firstIndex, + 'deadline_at' => $deadlineAt, + 'created_at' => $now, + 'updated_at' => $now, + ]); + + // 5) 创建首步待办 + $instance = (object)[ + 'id' => $instanceId, + 'flow_type' => $flowType, + 'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'initiator_id' => $initiatorId, + 'current_step' => $firstIndex, + 'status' => 'processing', + ]; + $approverIds = $this->resolveApprovers($firstStep, $instance, true); + + foreach ($approverIds as $uid) { + // 唯一键会防重复 + $this->insertApprovalRecord($instanceId, $firstIndex, $uid); + } + + // 6) 通知审批人 & 抄送 + $this->sendNotification($approverIds, $instance, 'pending', false); + $this->notifyCCs($firstStep, $instance, 'pending'); + + return ['instance_id' => $instanceId]; + }); } /** - * 获取用户待审批列表 - * @param int $userId 用户ID - * @return array + * 执行审批 + */ + public function approve(int $instanceId, int $approverId, string $action, string $comment = '') + { + if (!in_array($action, ['approve', 'reject'], true)) { + throw new BusinessException("无效的审批操作"); + } + + return Db::transaction(function () use ($instanceId, $approverId, $action, $comment) { + // 锁实例,保证并发安全 + $instance = Db::table('approval_instances')->where('id', $instanceId)->lockForUpdate()->first(); + if (!$instance) { + throw new BusinessException("审批实例未找到"); + } + if ($instance->status !== 'processing') { + throw new BusinessException("流程已结束或不可审批"); + } + + // 读取流程与当前步 + $flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first(); + if (!$flow) throw new BusinessException("审批流程定义未找到"); + + $steps = json_decode($flow->steps, true); + $idx = (int)$instance->current_step; + $step = $steps[$idx] ?? null; + if (!$step) throw new BusinessException("当前步骤配置错误"); + + // 锁当前步所有记录,避免同时推进 + $pendingRecord = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('approver_id', $approverId) + ->where('step_index', $idx) + ->where('status', 'pending') + ->lockForUpdate() + ->first(); + if (!$pendingRecord) { + throw new BusinessException("未找到待审批记录"); + } + + $now = date('Y-m-d H:i:s'); + + // 1) 更新当前人的审批记录 + Db::table('approval_records')->where('id', $pendingRecord->id)->update([ + 'action' => $action, + 'status' => ($action === 'approve') ? 'approved' : 'rejected', + 'comment' => $comment, + 'approved_at' => $now, + 'updated_at' => $now, + ]); + + // 2) 聚合判断本步骤结果 + $outcome = $this->getStepOutcome($instanceId, $idx); // 'pending'|'approve'|'reject' + $type = $step['type'] ?? 'all'; + + $stepDone = false; $finalReject = false; $finalApprove = false; + if ($type === 'any') { + if ($outcome === 'reject') { $stepDone = true; $finalReject = true; } + elseif ($outcome === 'approve') { $stepDone = true; $finalApprove = true; } + } else { // all + if ($outcome === 'reject') { $stepDone = true; $finalReject = true; } + elseif ($outcome === 'approve') { $stepDone = true; $finalApprove = true; } + } + + if (!$stepDone) { + return ['status' => 'step_pending']; + } + + // 3) 本步已有结论:关闭其余 pending 待办为 skipped + $this->closeOtherPending($instanceId, $idx, [$approverId]); + + // 4) 步骤结论为拒绝 -> 结束 + if ($finalReject) { + Db::table('approval_instances')->where('id', $instanceId)->update([ + 'status' => 'rejected', + 'finished_at' => $now, + 'updated_at' => $now, + ]); + $this->sendNotification([$instance->initiator_id], $instance, 'reject', false); + return ['status' => 'completed', 'result' => 'reject']; + } + + // 5) 进入下一步(基于表单快照与条件继续寻找) + $formData = json_decode($instance->form_data, true) ?: []; + $nextIndex = $this->getNextStepIndex($steps, $formData, $idx); + if ($nextIndex === null) { + // 流程结束(全通过) + Db::table('approval_instances')->where('id', $instanceId)->update([ + 'status' => 'approved', + 'finished_at' => $now, + 'updated_at' => $now, + ]); + $this->sendNotification([$instance->initiator_id], $instance, 'approve', false); + return ['status' => 'completed', 'result' => 'approve']; + } + + // 6) 推进实例 current_step(乐观条件) + $affected = Db::table('approval_instances') + ->where('id', $instanceId) + ->where('current_step', $idx) + ->where('status', 'processing') + ->update([ + 'current_step' => $nextIndex, + 'updated_at' => $now, + ]); + if (!$affected) { + throw new BusinessException('流程已被他人推进,请刷新后重试'); + } + + // 7) 生成下一步待办 + 通知 + $nextStep = $steps[$nextIndex]; + $nextApprovers = $this->resolveApprovers($nextStep, $instance, true); + foreach ($nextApprovers as $uid) { + $this->insertApprovalRecord($instanceId, $nextIndex, $uid); + } + $this->sendNotification($nextApprovers, $instance, 'pending', false); + $this->notifyCCs($nextStep, $instance, 'pending'); + + return ['status' => 'next_step', 'step_name' => $nextStep['name'] ?? ('Step '.$nextIndex)]; + }); + } + + /** + * 撤回流程(可选) + * 仅限发起人在第一条“已处理记录”出现前撤回 + */ + public function revoke(int $instanceId, int $initiatorId, string $reason = '') + { + return Db::transaction(function () use ($instanceId, $initiatorId, $reason) { + $instance = Db::table('approval_instances')->where('id', $instanceId)->lockForUpdate()->first(); + if (!$instance) throw new BusinessException('审批实例未找到'); + if ((int)$instance->initiator_id !== (int)$initiatorId) throw new BusinessException('无权撤回'); + + if ($instance->status !== 'processing') throw new BusinessException('流程已结束或不可撤回'); + + // 已有处理记录则不允许撤回 + $handled = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->whereIn('status', ['approved','rejected','timeout','skipped']) + ->count() > 0; + if ($handled) throw new BusinessException('当前进度不可撤回'); + + $now = date('Y-m-d H:i:s'); + Db::table('approval_instances')->where('id', $instanceId)->update([ + 'status' => 'revoked', + 'finished_at' => $now, + 'updated_at' => $now, + ]); + + // 关闭所有 pending 待办 + Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('status', 'pending') + ->update(['status' => 'skipped', 'approved_at' => $now, 'updated_at' => $now]); + + $this->sendNotification([$initiatorId], $instance, 'revoked', false); + return ['status' => 'completed', 'result' => 'revoked']; + }); + } + + /** + * 我的待办 */ public function getPendingApprovals(int $userId): array { - // 简化查询:找到用户需要审批的实例ID - // 更复杂的逻辑(如并行会签)需要更精细的查询 - $instanceIds = Db::table('approval_records') - ->where('approver_id', $userId) - ->pluck('instance_id') - ->unique() - ->toArray(); - - if (empty($instanceIds)) { - return []; - } - - return Db::table('approval_instances') - ->whereIn('id', $instanceIds) - ->where('status', 'processing') + return Db::table('approval_records as ar') + ->join('approval_instances as ai', 'ar.instance_id', '=', 'ai.id') + ->where('ar.approver_id', $userId) + ->where('ar.status', 'pending') + ->where('ai.status', 'processing') + ->select('ai.*') + ->distinct() ->get() ->toArray(); } /** - * 获取审批历史记录 - * @param int $instanceId - * @return array + * 审批历史 */ public function getApprovalHistory(int $instanceId): array { return Db::table('approval_records') ->where('instance_id', $instanceId) ->orderBy('step_index') - ->orderBy('approved_at') + ->orderByRaw('COALESCE(approved_at, created_at) asc') ->get() ->toArray(); } /** - * 获取审批流程图数据 (简化版) - * @param int $instanceId - * @return array + * 流程图数据(识别 skipped) */ public function getFlowDiagramData(int $instanceId): array { @@ -199,233 +289,463 @@ class ApprovalFlowService_bak $flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first(); if (!$flow) return []; - $steps = json_decode($flow->steps, true); + $steps = json_decode($flow->steps, true) ?: []; $records = Db::table('approval_records') ->where('instance_id', $instanceId) - ->orderBy('step_index') - ->orderBy('approved_at') + ->orderBy('step_index')->orderByRaw('COALESCE(approved_at, created_at) asc') ->get() ->groupBy('step_index'); - $diagramData = []; - foreach ($steps as $index => $step) { - $status = 'pending'; - $actions = $records->get($index, collect())->toArray(); + $formData = json_decode($instance->form_data, true) ?: []; + $data = []; + foreach ($steps as $i => $step) { + $actions = ($records->get($i) ?? collect())->toArray(); + $status = 'pending'; if (!empty($actions)) { - $hasReject = collect($actions)->contains('action', 'reject'); - if ($hasReject) { - $status = 'rejected'; - } else { - if ($step['type'] === 'parallel') { - // 简化处理 - $status = 'approved'; + $hasReject = false; $allApproved = true; + foreach ($actions as $a) { + if ($a->status === 'rejected') $hasReject = true; + if ($a->status !== 'approved') $allApproved = false; + } + if ($hasReject) $status = 'rejected'; + elseif ($allApproved) $status = 'approved'; + else $status = 'processing'; + } else { + // 无记录:可能被跳过、未到达、已过 + if ($i < (int)$instance->current_step) { + // 如果条件不满足,认为 skipped,否则视为 approved(历史兼容) + if (isset($step['condition']) && !$this->evaluateCondition($step['condition'], $formData)) { + $status = 'skipped'; } else { $status = 'approved'; } + } elseif ($i === (int)$instance->current_step && $instance->status === 'processing') { + $status = 'processing'; + } else { + // 还未到达 + $status = 'pending'; } - } elseif ($index < $instance->current_step) { - $status = 'approved'; - } elseif ($index == $instance->current_step) { - $status = 'processing'; } - $diagramData[] = [ - 'index' => $index, - 'name' => $step['name'], - 'type' => $step['type'], - 'status' => $status, - 'actions' => $actions + $data[] = [ + 'index' => $i, + 'name' => $step['name'] ?? ('Step '.$i), + 'type' => $step['type'] ?? 'all', + 'status' => $status, + 'actions' => $actions, ]; } - return $diagramData; + return $data; } /** - * 处理超时的审批实例 (定时任务调用) + * 定时任务:处理超时实例(实例级) */ public function handleTimeouts() { $now = date('Y-m-d H:i:s'); - $expiredInstances = Db::table('approval_instances') + $expired = Db::table('approval_instances') ->where('status', 'processing') + ->whereNotNull('deadline_at') ->where('deadline_at', '<=', $now) ->get(); - foreach ($expiredInstances as $instance) { - $flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first(); - if ($flow && $flow->timeout_action) { - $updateData = [ + foreach ($expired as $instance) { + Db::transaction(function () use ($instance, $now) { + $flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first(); + if (!$flow) return; + + $final = ($flow->timeout_action === 'auto_approve') ? 'approved' : 'rejected'; + $finalAction = ($flow->timeout_action === 'auto_approve') ? 'approve' : 'reject'; + + // 更新实例 + Db::table('approval_instances')->where('id', $instance->id)->update([ + 'status' => ($final === 'approved') ? 'approved' : 'rejected', 'finished_at' => $now, - 'updated_at' => $now - ]; - if ($flow->timeout_action === 'auto_reject') { - $updateData['status'] = 'rejected'; - Db::table('approval_instances')->where('id', $instance->id)->update($updateData); - $this->notifyInitiator($instance, 'rejected_timeout'); - } elseif ($flow->timeout_action === 'auto_approve') { - $updateData['status'] = 'approved'; - Db::table('approval_instances')->where('id', $instance->id)->update($updateData); - $this->notifyInitiator($instance, 'approved_timeout'); - } - } + 'updated_at' => $now, + ]); + + // 关闭当前步所有 pending 记录为 timeout + Db::table('approval_records') + ->where('instance_id', $instance->id) + ->where('step_index', $instance->current_step) + ->where('status', 'pending') + ->update(['status' => 'timeout', 'approved_at' => $now, 'updated_at' => $now]); + + $this->notifyInitiator($instance, $finalAction); + }); } } - // --- 辅助方法 --- + // === 内部工具 === - private function canUserApproveStep(array $step, int $userId, object $instance): bool - { - $approvers = $this->resolveApprovers($step, $instance); - return in_array($userId, $approvers); - } - - // private function resolveApprovers(array $step, object $instance): array - // { - // $resolved = []; - // foreach ($step['approvers'] as $approver) { - // if ($approver['type'] === 'role') { - // // 示例:根据角色查找用户ID,你需要替换为实际逻辑 - // if ($approver['value'] === 'admin') { - // $resolved[] = 1; // 假设 admin 角色的用户ID是1 - // } elseif ($approver['value'] === 'manager') { - // $resolved[] = 2; $resolved[] = 3; // 假设有多个manager - // } - // } elseif ($approver['type'] === 'user') { - // $resolved[] = (int)$approver['value']; - // } elseif ($approver['type'] === 'initiator_leader') { - // // 示例:获取发起人的领导 - // $resolved[] = 4; // 假设领导ID是4 - // } - // } - // return array_unique($resolved); - // } - - private function resolveApprovers(array $step, object $instance): array + /** + * 解析审批人 + * $avoidSelf:是否过滤发起人本人 + */ + private function resolveApprovers(array $step, object $instance, bool $avoidSelf = true): array { $resolved = []; + $formData = json_decode($instance->form_data ?? '{}', true) ?: []; - foreach ($step['approvers'] as $approverConfig) { - $approverIds = []; + $confs = $step['approvers'] ?? []; + foreach ($confs as $cfg) { + $typ = $cfg['type'] ?? ''; + $val = $cfg['value'] ?? null; + $ids = []; - switch ($approverConfig['type']) { - // 1. 按角色查找 + switch ($typ) { case 'role': - $roleCode = $approverConfig['value']; - // 所有拥有该角色的用户ID - $approverIds = $this->getUserIdsByRole($roleCode); - break; + // $ids = $this->getUserIdsByRole($val); - // 2. 按具体用户ID + // 支持: + // - {"type":"role","value":1} // 按 role_id + // - {"type":"role","value":"销售经理"} // 按 roles.name + // - {"type":"role","value":"销售经理","system_id": 2} // 限定系统;默认也包含全局(system_id=0) + $val = $cfg['value'] ?? null; + $sys = isset($cfg['system_id']) ? (int)$cfg['system_id'] : null; + // include_global 默认 true(把 system_id=0 的“全局角色”也算上) + $includeGlobal = array_key_exists('include_global', $cfg) ? (bool)$cfg['include_global'] : true; + $ids = $this->getUserIdsByRole($val, $sys, true, $includeGlobal); + + break; case 'user': - $approverIds = [(int)$approverConfig['value']]; + $ids = [(int)$val]; break; - - // 3. 按发起人的直属领导 (业务一般是组长) case 'initiator_leader': - // 用户表中 report_to 字段 - $approverIds = $this->getLeaderIdByUserId($instance->initiator_id); - if ($approverIds) { - $approverIds = [$approverIds]; - } else { - $approverIds = []; // 没有领导,跳过 - } + $leaderId = $this->getLeaderIdByUserId((int)$instance->initiator_id); + if ($leaderId) $ids = [(int)$leaderId]; break; - - // 4. 按业务字段关联的用户 (高级功能) case 'field_user': - $fieldName = $approverConfig['value']; // 如 'project_manager_id' - $formData = json_decode($instance->form_data, true); - if (isset($formData[$fieldName])) { - $approverIds = [(int)$formData[$fieldName]]; - } else { - $approverIds = []; - } + if (isset($formData[$val])) $ids = [(int)$formData[$val]]; break; - - // 5. 按组织架构层级 (如发起人所在部门的负责人) case 'dept_head': - $deptId = $approverConfig['value']; // 也可以从 instance 或 formData 中获取 - $approverIds = $this->getDeptHeadId($deptId); - if ($approverIds) { - $approverIds = [$approverIds]; - } else { - $approverIds = []; - } + $head = $this->getDeptHeadId((int)$val); + if ($head) $ids = [(int)$head]; break; - default: - throw new BusinessException("不支持的审批人类型: {$approverConfig['type']}"); + throw new BusinessException("不支持的审批人类型: {$typ}"); } - - $resolved = array_merge($resolved, $approverIds); + $resolved = array_merge($resolved, $ids); } - return array_unique($resolved); + $resolved = array_unique(array_filter(array_map('intval', $resolved))); + if ($avoidSelf) { + $resolved = array_values(array_filter($resolved, fn($id) => $id !== (int)$instance->initiator_id)); + } + if (empty($resolved)) { + throw new BusinessException("步骤未解析到任何审批人"); + } + return $resolved; } - // 角色层 - private function getUserIdsByRole(string $roleCode): array + /** + * 通知抄送人 + */ + private function notifyCCs(array $step, object $instance, string $action = 'pending'): void { - // user_roles 表和 roles 表 - return Db::table('user_roles as ur') - ->join('roles as r', 'ur.role_id', '=', 'r.id') - ->where('r.code', $roleCode) // 假设 roles 表有 code 字段 - ->pluck('ur.user_id') - ->toArray(); + $ccConfig = $step['cc'] ?? []; + if (empty($ccConfig)) return; + + $formData = json_decode($instance->form_data ?? '{}', true) ?: []; + $uids = []; + + foreach ($ccConfig as $cc) { + $typ = $cc['type'] ?? ''; + $val = $cc['value'] ?? null; + + switch ($typ) { + case 'role': + $uids = array_merge($uids, $this->getUserIdsByRole($val)); + break; + case 'user': + $uids[] = (int)$val; + break; + case 'initiator_leader': + $leaderId = $this->getLeaderIdByUserId((int)$instance->initiator_id); + if ($leaderId) $uids[] = (int)$leaderId; + break; + case 'field_user': + if (isset($formData[$val])) $uids[] = (int)$formData[$val]; + break; + case 'dept_head': + $head = $this->getDeptHeadId((int)$val); + if ($head) $uids[] = (int)$head; + break; + default: + // ignore + break; + } + } + + $uids = array_unique(array_filter(array_map('intval', $uids))); + if (empty($uids)) return; + + $now = date('Y-m-d H:i:s'); + foreach ($uids as $uid) { + // 唯一键防重;这里仍做一次 exists 判断 + $exists = Db::table('approval_ccs') + ->where('instance_id', $instance->id) + ->where('user_id', $uid) + ->count() > 0; + if (!$exists) { + Db::table('approval_ccs')->insert([ + 'instance_id' => $instance->id, + 'user_id' => $uid, + 'notified_at' => $now, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + + $this->sendNotification($uids, $instance, $action, true); } - // 领导层 + /** + * 条件求值:支持 {field} {op} {value} + * op: ==, !=, >, <, >=, <= + */ + private function evaluateCondition(?string $condition, array $formData): bool + { + if (!$condition) return true; + + $pattern = '/^(\w+)\s*(==|!=|>|<|>=|<=)\s*(.+)$/'; + if (!preg_match($pattern, $condition, $m)) { + throw new BusinessException("条件格式错误: {$condition}"); + } + + $field = $m[1]; + $op = $m[2]; + $value = trim($m[3], "\"'"); + + $actual = $formData[$field] ?? null; + + // 数值比较 + if (is_numeric($value) && is_numeric($actual)) { + $value = (float)$value; + $actual = (float)$actual; + } + + switch ($op) { + case '==': return $actual == $value; + case '!=': return $actual != $value; + case '>': return $actual > $value; + case '<': return $actual < $value; + case '>=': return $actual >= $value; + case '<=': return $actual <= $value; + } + throw new BusinessException("不支持的操作符: {$op}"); + } + + /** + * 获取下一个有效步骤索引(考虑 condition) + * @param int $currentStepIndex 从该索引之后开始找;如果传 -1 则从开头找“首个有效” + */ + private function getNextStepIndex(array $steps, array $formData, int $currentStepIndex): ?int + { + for ($i = $currentStepIndex + 1; $i < count($steps); $i++) { + $s = $steps[$i]; + if (!isset($s['condition']) || $this->evaluateCondition($s['condition'], $formData)) { + return $i; + } + } + return null; + } + + /** + * 聚合本步骤结论 + * 返回:'pending' | 'approve' | 'reject' + */ + private function getStepOutcome(int $instanceId, int $stepIndex): string + { + $records = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('step_index', $stepIndex) + ->get(); + + $hasPending = false; $hasReject = false; $allApproved = true; + + foreach ($records as $r) { + if ($r->status === 'pending') { $hasPending = true; $allApproved = false; } + elseif ($r->status === 'rejected') { $hasReject = true; $allApproved = false; } + elseif ($r->status !== 'approved') { $allApproved = false; } + } + + if ($hasPending) return 'pending'; + if ($hasReject) return 'reject'; + return $allApproved ? 'approve' : 'reject'; + } + + /** + * 关闭本步其余待办(标记为 skipped) + */ + private function closeOtherPending(int $instanceId, int $stepIndex, array $keepApproverIds = []): void + { + $now = date('Y-m-d H:i:s'); + $q = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('step_index', $stepIndex) + ->where('status', 'pending'); + + if (!empty($keepApproverIds)) { + $q->whereNotIn('approver_id', $keepApproverIds); + } + + $q->update([ + 'status' => 'skipped', + 'approved_at' => $now, + 'updated_at' => $now, + ]); + } + + private function insertApprovalRecord(int $instanceId, int $stepIndex, int $approverId): void + { + $now = date('Y-m-d H:i:s'); + + // 防重复:唯一键兜底,这里先查一次 + $exists = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('step_index', $stepIndex) + ->where('approver_id', $approverId) + ->count() > 0; + + if (!$exists) { + Db::table('approval_records')->insert([ + 'instance_id' => $instanceId, + 'approver_id' => $approverId, + 'step_index' => $stepIndex, + 'action' => null, // 待办阶段无动作 + 'status' => 'pending', // 明确 pending + 'comment' => null, + 'approved_at' => null, // 未审批为 NULL + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + + private function sendNotification(array $userIds, object $instance, string $action, bool $isCC): void + { + $map = [ + 'pending' => '待审批', + 'approve' => '已通过', + 'reject' => '已拒绝', + 'revoked' => '已撤回', + ]; + $prefix = $isCC ? '[抄送]' : ''; + $text = $map[$action] ?? $action; + $msg = "{$prefix}您有一条审批 {$text}:流程ID {$instance->id},类型 {$instance->flow_type}"; + // TODO: 接入你的消息系统(站内信/企微/飞书…) + // MessageService::send($userIds, $msg); + // echo "通知 ".implode(',', $userIds).":{$msg}\n"; + } + + private function notifyInitiator(object $instance, string $result): void + { + $this->sendNotification([(int)$instance->initiator_id], $instance, $result, false); + } + + // === 数据访问 === + + // private function getUserIdsByRole(string $roleCode): array + // { + // // 兼容 pluck 行为(不同版本可能返回 array 或 Collection) + // $rows = Db::table('user_roles as ur') + // ->join('roles as r', 'ur.role_id', '=', 'r.id') + // ->where('r.code', $roleCode) + // ->select('ur.user_id') + // ->get(); + + // $ids = []; + // foreach ($rows as $row) { + // $ids[] = (int)$row->user_id; + // } + // return array_values(array_unique($ids)); + // } + + /** + * 从角色取用户ID列表 + * + * @param mixed $roleIdOrName 数字 => role_id,字符串 => roles.name 精确匹配 + * @param int|null $systemId 可选:限定 roles.system_id;默认不传=不过滤 + * @param bool $onlyActive 是否仅取 status=1 的角色(默认 true) + * @param bool $includeGlobal 是否包含全局角色(system_id=0),默认 true + * @return int[] + */ + private function getUserIdsByRole($roleIdOrName, ?int $systemId = null, bool $onlyActive = true, bool $includeGlobal = true): array + { + // 查询构造 + // 情况A:纯数字 => 当作 role_id,优先直接走 user_roles(role_id) 索引 + if (is_int($roleIdOrName) || (is_string($roleIdOrName) && ctype_digit($roleIdOrName))) { + $roleId = (int)$roleIdOrName; + + // 如果不需要校验 roles 的 system/status,就不 join,性能更好 + $needJoin = $onlyActive || !is_null($systemId) || $includeGlobal === false; + + if (!$needJoin) { + $rows = Db::table('user_roles') + ->where('role_id', $roleId) + ->select('user_id') + ->get(); + } else { + $q = Db::table('user_roles as ur') + ->join('roles as r', 'ur.role_id', '=', 'r.id') + ->where('ur.role_id', $roleId) + ->select('ur.user_id'); + + if ($onlyActive) { + $q->where('r.status', 1); + } + if (!is_null($systemId)) { + if ($includeGlobal) { + $q->whereIn('r.system_id', [0, $systemId]); + } else { + $q->where('r.system_id', $systemId); + } + } + $rows = $q->get(); + } + } + // 情况B:字符串 => 当作 roles.name + else { + $name = (string)$roleIdOrName; + $q = Db::table('user_roles as ur') + ->join('roles as r', 'ur.role_id', '=', 'r.id') + ->where('r.name', $name) + ->select('ur.user_id'); + + if ($onlyActive) { + $q->where('r.status', 1); + } + if (!is_null($systemId)) { + if ($includeGlobal) { + $q->whereIn('r.system_id', [0, $systemId]); + } else { + $q->where('r.system_id', $systemId); + } + } + $rows = $q->get(); + } + + $ids = []; + foreach ($rows as $row) { + $ids[] = (int)$row->user_id; + } + return array_values(array_unique($ids)); + } + + private function getLeaderIdByUserId(int $userId): ?int { - // users 表 report_to 字段 - return Db::table('user')->where('id', $userId)->value('report_to'); + $id = Db::table('user')->where('id', $userId)->value('report_to'); + return $id ? (int)$id : null; } - // 组织架构层 private function getDeptHeadId(int $deptId): ?int { - // departments 表 head_user_id 字段(部门逻辑暂时不用,根据后续业务实际情况开发) - return Db::table('departments')->where('id', $deptId)->value('head_user_id'); - } - - private function calculateNextStepIndex(int $instanceId, int $currentStepIndex, array $currentStep): int - { - // 对于 sequential 和 any,单次 approve 即进入下一步 - if ($currentStep['type'] === 'sequential' || $currentStep['type'] === 'any') { - return $currentStepIndex + 1; - } elseif ($currentStep['type'] === 'parallel') { - // 对于并行会签,需要检查是否所有审批人都已 approve - // 这里简化处理,实际需要精确计算 - // 假设只要有一次 approve 就算完成(不准确,仅为示例) - // 正确做法是查询所有 approvers 是否都 approve 了 - return $currentStepIndex + 1; - } - return $currentStepIndex + 1; - } - - private function notifyNextApprovers(int $instanceId) - { - // 实现你的通知逻辑,如发送站内信、邮件、企业微信等 - // 可以查询 instance_id 对应的当前步骤审批人 - echo "通知下一个审批人 (Instance ID: {$instanceId})\n"; - } - - private function notifyInitiator(object $instance, string $result) - { - // 通知发起人审批结果 - echo "通知发起人 {$instance->initiator_id} 审批结果: {$result}\n"; - } - - private function notifyAllApprovers(object $instance, string $status) - { - // 通知所有相关审批人(如撤回时) - echo "通知所有审批人,实例 {$instance->id} 状态变为: {$status}\n"; - } - - private function notifyCc(int $instanceId) - { - // 通知抄送人 - echo "通知抄送人 (Instance ID: {$instanceId})\n"; + $id = Db::table('departments')->where('id', $deptId)->value('head_user_id'); + return $id ? (int)$id : null; } } \ No newline at end of file diff --git a/app/common/utils/approval/service/ApprovalFlowService_bak2.php b/app/common/utils/approval/service/ApprovalFlowService_bak2.php deleted file mode 100644 index 9b6babd..0000000 --- a/app/common/utils/approval/service/ApprovalFlowService_bak2.php +++ /dev/null @@ -1,620 +0,0 @@ -where('type', $flowType) - ->where('is_active', 1) - ->first(); - - if (!$flow) { - throw new BusinessException("审批流程未找到或已停用: {$flowType}"); - } - - $steps = json_decode($flow->steps, true); - if (!is_array($steps) || empty($steps)) { - throw new BusinessException("流程步骤配置错误"); - } - - // 2. 查找第一个有效步骤(考虑 condition) - $firstStep = $this->getNextValidStep($steps, $formData); - if (!$firstStep) { - throw new BusinessException("未找到可执行的审批步骤"); - } - - // 3. 创建审批实例 - $instanceId = Db::table('approval_instances')->insertGetId([ - 'flow_type' => $flowType, - 'biz_type' => $bizType, - 'biz_id' => $bizId, - 'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE), - 'initiator_id' => $initiatorId, - 'status' => 'processing', - 'current_step' => 0, - 'created_at' => date('Y-m-d H:i:s'), - 'updated_at' => date('Y-m-d H:i:s'), - ]); - - // 4. 创建审批记录(待办) - $approverIds = $this->resolveApprovers($firstStep, (object)['initiator_id' => $initiatorId]); - foreach ($approverIds as $approverId) { - Db::table('approval_records')->insert([ - 'instance_id' => $instanceId, - 'step_name' => $firstStep['name'], - 'approver_id' => $approverId, - 'status' => 'pending', - 'created_at' => date('Y-m-d H:i:s'), - ]); - } - - // 5. 通知审批人 - $this->sendNotification($approverIds, (object)['id' => $instanceId, 'flow_type' => $flowType], 'pending', false); - - // 6. 通知抄送人(当前步骤) - $this->notifyCCs($firstStep, (object)['id' => $instanceId, 'flow_type' => $flowType, 'form_data' => json_encode($formData)], 'pending'); - - return ['instance_id' => $instanceId]; - } - - /** - * 执行审批 - */ - public function approve(int $instanceId, int $approverId, string $action, string $comment = '') - { - $instance = Db::table('approval_instances')->where('id', $instanceId)->first(); - if (!$instance) { - throw new BusinessException("审批实例未找到"); - } - - $steps = json_decode($instance->steps, true) ?: json_decode(Db::table('approval_flows')->where('type', $instance->flow_type)->value('steps'), true); - $currentStep = $steps[$instance->current_step] ?? null; - - if (!$currentStep) { - throw new BusinessException("当前步骤配置错误"); - } - - // 检查用户是否有权限审批 - if (!$this->canUserApproveStep($currentStep, $approverId, $instance)) { - throw new BusinessException("您无权审批此步骤"); - } - - // 更新审批记录 - Db::table('approval_records') - ->where('instance_id', $instanceId) - ->where('approver_id', $approverId) - ->where('status', 'pending') - ->update([ - 'status' => $action, - 'comment' => $comment, - 'approved_at' => date('Y-m-d H:i:s'), - 'updated_at' => date('Y-m-d H:i:s'), - ]); - - // 检查当前步骤是否完成(根据 type: any, all, sequential) - $stepCompleted = $this->isStepCompleted($instanceId, $instance->current_step, $currentStep['type']); - - if (!$stepCompleted) { - // 步骤未完成,不进入下一步 - return ['status' => 'step_pending']; - } - - // 步骤已完成,进入下一步或结束 - $nextStep = $this->getNextValidStep($steps, json_decode($instance->form_data, true), $instance->current_step + 1); - - if ($nextStep === null) { - // 流程结束 - Db::table('approval_instances')->where('id', $instanceId)->update([ - 'status' => $action === 'approve' ? 'approved' : 'rejected', - 'updated_at' => date('Y-m-d H:i:s'), - ]); - - // 通知发起人结果 - $this->sendNotification([$instance->initiator_id], $instance, $action, false); - - return ['status' => 'completed', 'result' => $action]; - } - - // 进入下一步 - $nextStepIndex = array_search($nextStep, $steps); - Db::table('approval_instances')->where('id', $instanceId)->update([ - 'current_step' => $nextStepIndex, - 'updated_at' => date('Y-m-d H:i:s'), - ]); - - // 创建下一步的审批记录 - $nextApprovers = $this->resolveApprovers($nextStep, $instance); - foreach ($nextApprovers as $approverId) { - Db::table('approval_records')->insert([ - 'instance_id' => $instanceId, - 'step_name' => $nextStep['name'], - 'approver_id' => $approverId, - 'status' => 'pending', - 'created_at' => date('Y-m-d H:i:s'), - ]); - } - - // 通知下一步审批人 - $this->sendNotification($nextApprovers, $instance, 'pending', false); - - // 通知下一步抄送人 - $this->notifyCCs($nextStep, $instance, 'pending'); - - return ['status' => 'next_step', 'step_name' => $nextStep['name']]; - } - - /** - * 解析审批人 - */ - private function resolveApprovers(array $step, object $instance): array - { - $resolved = []; - $formData = json_decode($instance->form_data ?? '{}', true); - - foreach ($step['approvers'] as $approverConfig) { - $approverIds = []; - - switch ($approverConfig['type']) { - case 'role': - $approverIds = $this->getUserIdsByRole($approverConfig['value']); - break; - case 'user': - $approverIds = [(int)$approverConfig['value']]; - break; - case 'initiator_leader': - $leaderId = $this->getLeaderIdByUserId($instance->initiator_id); - if ($leaderId) $approverIds = [$leaderId]; - break; - case 'field_user': - if (isset($formData[$approverConfig['value']])) { - $approverIds = [(int)$formData[$approverConfig['value']]]; - } - break; - case 'dept_head': - $approverIds = $this->getDeptHeadId($approverConfig['value']); - if ($approverIds) $approverIds = [$approverIds]; - break; - default: - throw new BusinessException("不支持的审批人类型: {$approverConfig['type']}"); - } - - $resolved = array_merge($resolved, $approverIds); - } - - return array_unique(array_filter($resolved)); - } - - /** - * 通知抄送人 - */ - private function notifyCCs(array $step, object $instance, string $action = 'pending') - { - $ccConfig = $step['cc'] ?? []; - $ccUserIds = []; - $formData = json_decode($instance->form_data ?? '{}', true); - - foreach ($ccConfig as $cc) { - switch ($cc['type']) { - case 'role': - $ccUserIds = array_merge($ccUserIds, $this->getUserIdsByRole($cc['value'])); - break; - case 'user': - $ccUserIds[] = (int)$cc['value']; - break; - case 'initiator_leader': - $leaderId = $this->getLeaderIdByUserId($instance->initiator_id); - if ($leaderId) $ccUserIds[] = $leaderId; - break; - case 'field_user': - if (isset($formData[$cc['value']])) { - $ccUserIds[] = (int)$formData[$cc['value']]; - } - break; - case 'dept_head': - $deptHeadId = $this->getDeptHeadId($cc['value']); - if ($deptHeadId) $ccUserIds[] = $deptHeadId; - break; - default: - break; - } - } - - $ccUserIds = array_unique(array_filter($ccUserIds)); - - if (!empty($ccUserIds)) { - $this->sendNotification($ccUserIds, $instance, $action, true); - } - } - - /** - * 评估条件 - */ - private function evaluateCondition(?string $condition, array $formData): bool - { - if (!$condition) return true; - - $pattern = '/^(\w+)\s*(==|!=|>|<|>=|<=)\s*(.+)$/'; - if (!preg_match($pattern, $condition, $matches)) { - throw new BusinessException("条件格式错误: {$condition}"); - } - - $field = $matches[1]; - $op = $matches[2]; - $value = trim($matches[3], "\"'"); - - $actualValue = $formData[$field] ?? null; - - if (is_numeric($value) && is_numeric($actualValue)) { - $value = (float)$value; - $actualValue = (float)$actualValue; - } - - switch ($op) { - case '==': return $actualValue == $value; - case '!=': return $actualValue != $value; - case '>': return $actualValue > $value; - case '<': $actualValue < $value; - case '>=': return $actualValue >= $value; - case '<=': return $actualValue <= $value; - default: throw new BusinessException("不支持的操作符: {$op}"); - } - } - - /** - * 获取下一个有效步骤(考虑 condition) - */ - private function getNextValidStep(array $steps, array $formData, int $startIndex = 0): ?array - { - for ($i = $startIndex; $i < count($steps); $i++) { - $step = $steps[$i]; - if (!isset($step['condition']) || $this->evaluateCondition($step['condition'], $formData)) { - return $step; - } - } - return null; - } - - /** - * 检查用户是否有权限审批 - */ - private function canUserApproveStep(array $step, int $userId, object $instance): bool - { - $approvers = $this->resolveApprovers($step, $instance); - return in_array($userId, $approvers); - } - - /** - * 检查当前步骤是否完成 - */ - private function isStepCompleted(int $instanceId, int $stepIndex, string $type): bool - { - $pendingCount = Db::table('approval_records') - ->where('instance_id', $instanceId) - ->where('status', 'pending') - ->count(); - - if ($type === 'any') { - return $pendingCount === 0; // 任一通过即可 - } - - if ($type === 'all' || $type === 'sequential') { - return $pendingCount === 0; // 所有人都必须审批 - } - - return false; - } - - /** - * 发送通知(你可以替换为你的通知系统) - */ - private function sendNotification(array $userIds, object $instance, string $action, bool $isCC) - { - $actionText = [ - 'pending' => '待审批', - 'approve' => '已通过', - 'reject' => '已拒绝' - ][$action] ?? $action; - - $prefix = $isCC ? '[抄送]' : ''; - $message = "{$prefix}您有一条审批 {$actionText}:流程ID {$instance->id},类型 {$instance->flow_type}"; - - // TODO: 调用你的消息系统,如邮件、站内信、企业微信等 - // 示例:MessageService::send($userIds, $message); - echo "通知用户 " . implode(',', $userIds) . ":$message\n"; - } - - /** - * 获取用户待审批列表 - * @param int $userId 用户ID - * @return array - */ - public function getPendingApprovals(int $userId): array - { - // 简化查询:找到用户需要审批的实例ID - // 更复杂的逻辑(如并行会签)需要更精细的查询 - $instanceIds = Db::table('approval_records') - ->where('approver_id', $userId) - ->pluck('instance_id') - ->unique() - ->toArray(); - - if (empty($instanceIds)) { - return []; - } - - return Db::table('approval_instances') - ->whereIn('id', $instanceIds) - ->where('status', 'processing') - ->get() - ->toArray(); - } - - /** - * 获取审批历史记录 - * @param int $instanceId - * @return array - */ - public function getApprovalHistory(int $instanceId): array - { - return Db::table('approval_records') - ->where('instance_id', $instanceId) - ->orderBy('step_index') - ->orderBy('approved_at') - ->get() - ->toArray(); - } - - /** - * 获取审批流程图数据 (简化版) - * @param int $instanceId - * @return array - */ - public function getFlowDiagramData(int $instanceId): array - { - $instance = Db::table('approval_instances')->find($instanceId); - if (!$instance) return []; - - $flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first(); - if (!$flow) return []; - - $steps = json_decode($flow->steps, true); - $records = Db::table('approval_records') - ->where('instance_id', $instanceId) - ->orderBy('step_index') - ->orderBy('approved_at') - ->get() - ->groupBy('step_index'); - - $diagramData = []; - foreach ($steps as $index => $step) { - $status = 'pending'; - $actions = $records->get($index, collect())->toArray(); - - if (!empty($actions)) { - $hasReject = collect($actions)->contains('action', 'reject'); - if ($hasReject) { - $status = 'rejected'; - } else { - if ($step['type'] === 'parallel') { - // 简化处理 - $status = 'approved'; - } else { - $status = 'approved'; - } - } - } elseif ($index < $instance->current_step) { - $status = 'approved'; - } elseif ($index == $instance->current_step) { - $status = 'processing'; - } - - $diagramData[] = [ - 'index' => $index, - 'name' => $step['name'], - 'type' => $step['type'], - 'status' => $status, - 'actions' => $actions - ]; - } - return $diagramData; - } - - /** - * 处理超时的审批实例 (定时任务调用) - */ - public function handleTimeouts() - { - $now = date('Y-m-d H:i:s'); - $expiredInstances = Db::table('approval_instances') - ->where('status', 'processing') - ->where('deadline_at', '<=', $now) - ->get(); - - foreach ($expiredInstances as $instance) { - $flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first(); - if ($flow && $flow->timeout_action) { - $updateData = [ - 'finished_at' => $now, - 'updated_at' => $now - ]; - if ($flow->timeout_action === 'auto_reject') { - $updateData['status'] = 'rejected'; - Db::table('approval_instances')->where('id', $instance->id)->update($updateData); - $this->notifyInitiator($instance, 'rejected_timeout'); - } elseif ($flow->timeout_action === 'auto_approve') { - $updateData['status'] = 'approved'; - Db::table('approval_instances')->where('id', $instance->id)->update($updateData); - $this->notifyInitiator($instance, 'approved_timeout'); - } - } - } - } - - // --- 辅助方法 --- - - // private function canUserApproveStep(array $step, int $userId, object $instance): bool - // { - // $approvers = $this->resolveApprovers($step, $instance); - // return in_array($userId, $approvers); - // } - - // private function resolveApprovers(array $step, object $instance): array - // { - // $resolved = []; - // foreach ($step['approvers'] as $approver) { - // if ($approver['type'] === 'role') { - // // 示例:根据角色查找用户ID,你需要替换为实际逻辑 - // if ($approver['value'] === 'admin') { - // $resolved[] = 1; // 假设 admin 角色的用户ID是1 - // } elseif ($approver['value'] === 'manager') { - // $resolved[] = 2; $resolved[] = 3; // 假设有多个manager - // } - // } elseif ($approver['type'] === 'user') { - // $resolved[] = (int)$approver['value']; - // } elseif ($approver['type'] === 'initiator_leader') { - // // 示例:获取发起人的领导 - // $resolved[] = 4; // 假设领导ID是4 - // } - // } - // return array_unique($resolved); - // } - - // private function resolveApprovers(array $step, object $instance): array - // { - // $resolved = []; - - // foreach ($step['approvers'] as $approverConfig) { - // $approverIds = []; - - // switch ($approverConfig['type']) { - // // 1. 按角色查找 - // case 'role': - // $roleCode = $approverConfig['value']; - // // 所有拥有该角色的用户ID - // $approverIds = $this->getUserIdsByRole($roleCode); - // break; - - // // 2. 按具体用户ID - // case 'user': - // $approverIds = [(int)$approverConfig['value']]; - // break; - - // // 3. 按发起人的直属领导 (业务一般是组长) - // case 'initiator_leader': - // // 用户表中 report_to 字段 - // $approverIds = $this->getLeaderIdByUserId($instance->initiator_id); - // if ($approverIds) { - // $approverIds = [$approverIds]; - // } else { - // $approverIds = []; // 没有领导,跳过 - // } - // break; - - // // 4. 按业务字段关联的用户 (高级功能) - // case 'field_user': - // $fieldName = $approverConfig['value']; // 如 'project_manager_id' - // $formData = json_decode($instance->form_data, true); - // if (isset($formData[$fieldName])) { - // $approverIds = [(int)$formData[$fieldName]]; - // } else { - // $approverIds = []; - // } - // break; - - // // 5. 按组织架构层级 (如发起人所在部门的负责人) - // case 'dept_head': - // $deptId = $approverConfig['value']; // 也可以从 instance 或 formData 中获取 - // $approverIds = $this->getDeptHeadId($deptId); - // if ($approverIds) { - // $approverIds = [$approverIds]; - // } else { - // $approverIds = []; - // } - // break; - - // default: - // throw new BusinessException("不支持的审批人类型: {$approverConfig['type']}"); - // } - - // $resolved = array_merge($resolved, $approverIds); - // } - - // return array_unique($resolved); - // } - - // 角色层 - private function getUserIdsByRole(string $roleCode): array - { - // user_roles 表和 roles 表 - return Db::table('user_roles as ur') - ->join('roles as r', 'ur.role_id', '=', 'r.id') - ->where('r.code', $roleCode) // 假设 roles 表有 code 字段 - ->pluck('ur.user_id') - ->toArray(); - } - - // 领导层 - private function getLeaderIdByUserId(int $userId): ?int - { - // users 表 report_to 字段 - return Db::table('user')->where('id', $userId)->value('report_to'); - } - - // 组织架构层 - private function getDeptHeadId(int $deptId): ?int - { - // departments 表 head_user_id 字段(部门逻辑暂时不用,根据后续业务实际情况开发) - return Db::table('departments')->where('id', $deptId)->value('head_user_id'); - } - - private function calculateNextStepIndex(int $instanceId, int $currentStepIndex, array $currentStep): int - { - // 对于 sequential 和 any,单次 approve 即进入下一步 - if ($currentStep['type'] === 'sequential' || $currentStep['type'] === 'any') { - return $currentStepIndex + 1; - } elseif ($currentStep['type'] === 'parallel') { - // 对于并行会签,需要检查是否所有审批人都已 approve - // 这里简化处理,实际需要精确计算 - // 假设只要有一次 approve 就算完成(不准确,仅为示例) - // 正确做法是查询所有 approvers 是否都 approve 了 - return $currentStepIndex + 1; - } - return $currentStepIndex + 1; - } - - private function notifyNextApprovers(int $instanceId) - { - // 实现你的通知逻辑,如发送站内信、邮件、企业微信等 - // 可以查询 instance_id 对应的当前步骤审批人 - echo "通知下一个审批人 (Instance ID: {$instanceId})\n"; - } - - private function notifyInitiator(object $instance, string $result) - { - // 通知发起人审批结果 - echo "通知发起人 {$instance->initiator_id} 审批结果: {$result}\n"; - } - - private function notifyAllApprovers(object $instance, string $status) - { - // 通知所有相关审批人(如撤回时) - echo "通知所有审批人,实例 {$instance->id} 状态变为: {$status}\n"; - } - - private function notifyCc(int $instanceId) - { - // 通知抄送人 - echo "通知抄送人 (Instance ID: {$instanceId})\n"; - } -} \ No newline at end of file diff --git a/app/common/utils/pdf/template/stockOrder.stub b/app/common/utils/pdf/template/stockOrder.stub index df20d14..6b93ecc 100644 --- a/app/common/utils/pdf/template/stockOrder.stub +++ b/app/common/utils/pdf/template/stockOrder.stub @@ -244,10 +244,10 @@
下单人: - +
-
下单时间:2025-09-08
+
下单时间:
diff --git a/app/controller/IndexController.php b/app/controller/IndexController.php index 8039205..f7d924d 100644 --- a/app/controller/IndexController.php +++ b/app/controller/IndexController.php @@ -2,20 +2,10 @@ namespace app\controller; -use app\common\model\InquiryLog; -use app\common\model\User; -use app\common\model\UserRoles; -use app\common\model\UserSystems; -use app\common\service\MessageNotificationsService; -use app\common\traits\DataPermission; -use app\common\utils\email\MailSend; use app\common\utils\scaffold\Generate; use support\Request; use support\Db; use support\Cache; -use app\inquiry\service\InquiryLogService; -use Illuminate\Support\Facades\Mail; -use support\Redis; class IndexController { @@ -79,25 +69,25 @@ class IndexController $options = [ 'controller' => [ 'module' => 'admin', - 'name' => 'approval_flows' + 'name' => 'approval_instances' ], 'model' => [ 'module' => 'admin', - 'name' => 'approval_flows' + 'name' => 'approval_instances' ], 'service' => [ 'module' => 'admin', - 'name' => 'approval_flows' + 'name' => 'approval_instances' ], 'logic' => [ 'module' => 'admin', - 'name' => 'approval_flows' + 'name' => 'approval_instances' ], 'validate' => [ 'module' => 'admin', - 'name' => 'approval_flows' + 'name' => 'approval_instances' ], - 'table' => 'approval_flows' + 'table' => 'approval_instances' ]; $result = (new Generate($options))->doGenerate(); @@ -116,9 +106,9 @@ class IndexController $initiatorId = 74; $appro = new \app\common\utils\approval\service\ApprovalFlowService(); - // $sp = $appro->start($flowType, $bizType, $bizId, $formData, $initiatorId); + $sp = $appro->start($flowType, $bizType, $bizId, $formData, $initiatorId); - $sp = $appro->approve(1, 1, 'approve'); + // $sp = $appro->approve(9, 1, 'reject'); var_dump($sp); return json([ diff --git a/config/route.php b/config/route.php index da3d540..8f693e3 100644 --- a/config/route.php +++ b/config/route.php @@ -298,6 +298,15 @@ Route::group('/admin', function () { Route::any('/update', [app\admin\controller\ApprovalFlowsController::class, 'update']); Route::any('/destroy', [app\admin\controller\ApprovalFlowsController::class, 'destroy']); }); + + // 审批记录 + Route::group('/approval/records', function () { + Route::any('/list', [app\admin\controller\ApprovalRecordsController::class, 'index']); + Route::any('/show', [app\admin\controller\ApprovalRecordsController::class, 'show']); + Route::any('/store', [app\admin\controller\ApprovalRecordsController::class, 'store']); + Route::any('/action', [app\admin\controller\ApprovalRecordsController::class, 'approve']); + Route::any('/destroy', [app\admin\controller\ApprovalRecordsController::class, 'destroy']); + }); }); diff --git a/public/pdf/BH20250908014.pdf b/public/pdf/BH20250908014.pdf new file mode 100644 index 0000000..9b4b9bd Binary files /dev/null and b/public/pdf/BH20250908014.pdf differ