751 lines
29 KiB
PHP
751 lines
29 KiB
PHP
<?php
|
||
namespace app\common\utils\approval\service;
|
||
|
||
use support\exception\BusinessException;
|
||
use support\Db;
|
||
|
||
/**
|
||
* 审批流程服务(Webman + Illuminate DB)
|
||
* - 支持条件步骤、会签(any/all)、并发安全(事务+锁)、抄送、超时处理
|
||
* - 统一状态:
|
||
* 实例 status: processing | approved | rejected | revoked | timed_out
|
||
* 记录 status: pending | approved | rejected | skipped | timeout
|
||
* 记录 action: approve | reject | null
|
||
*/
|
||
class ApprovalFlowService_bak
|
||
{
|
||
// === 对外方法 ===
|
||
|
||
/**
|
||
* 启动审批流程
|
||
*/
|
||
public function start(string $flowType, string $bizType, string $bizId, array $formData, int $initiatorId): array
|
||
{
|
||
return Db::transaction(function () use ($flowType, $bizType, $bizId, $formData, $initiatorId) {
|
||
// 1) 流程定义
|
||
$flow = Db::table('approval_flows')
|
||
->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) 寻找首个有效步骤索引(考虑条件)
|
||
$firstIndex = $this->getNextStepIndex($steps, $formData, -1);
|
||
if ($firstIndex === null) {
|
||
throw new BusinessException("未找到可执行的审批步骤");
|
||
}
|
||
$firstStep = $steps[$firstIndex];
|
||
|
||
// 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];
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 执行审批
|
||
*/
|
||
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
|
||
{
|
||
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();
|
||
}
|
||
|
||
/**
|
||
* 审批历史
|
||
*/
|
||
public function getApprovalHistory(int $instanceId): array
|
||
{
|
||
return Db::table('approval_records')
|
||
->where('instance_id', $instanceId)
|
||
->orderBy('step_index')
|
||
->orderByRaw('COALESCE(approved_at, created_at) asc')
|
||
->get()
|
||
->toArray();
|
||
}
|
||
|
||
/**
|
||
* 流程图数据(识别 skipped)
|
||
*/
|
||
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')->orderByRaw('COALESCE(approved_at, created_at) asc')
|
||
->get()
|
||
->groupBy('step_index');
|
||
|
||
$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 = 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';
|
||
}
|
||
}
|
||
|
||
$data[] = [
|
||
'index' => $i,
|
||
'name' => $step['name'] ?? ('Step '.$i),
|
||
'type' => $step['type'] ?? 'all',
|
||
'status' => $status,
|
||
'actions' => $actions,
|
||
];
|
||
}
|
||
return $data;
|
||
}
|
||
|
||
/**
|
||
* 定时任务:处理超时实例(实例级)
|
||
*/
|
||
public function handleTimeouts()
|
||
{
|
||
$now = date('Y-m-d H:i:s');
|
||
$expired = Db::table('approval_instances')
|
||
->where('status', 'processing')
|
||
->whereNotNull('deadline_at')
|
||
->where('deadline_at', '<=', $now)
|
||
->get();
|
||
|
||
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,
|
||
]);
|
||
|
||
// 关闭当前步所有 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);
|
||
});
|
||
}
|
||
}
|
||
|
||
// === 内部工具 ===
|
||
|
||
/**
|
||
* 解析审批人
|
||
* $avoidSelf:是否过滤发起人本人
|
||
*/
|
||
private function resolveApprovers(array $step, object $instance, bool $avoidSelf = true): array
|
||
{
|
||
$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':
|
||
// $ids = $this->getUserIdsByRole($val);
|
||
|
||
// 支持:
|
||
// - {"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':
|
||
$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));
|
||
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
|
||
{
|
||
$id = Db::table('user')->where('id', $userId)->value('report_to');
|
||
return $id ? (int)$id : null;
|
||
}
|
||
|
||
private function getDeptHeadId(int $deptId): ?int
|
||
{
|
||
$id = Db::table('departments')->where('id', $deptId)->value('head_user_id');
|
||
return $id ? (int)$id : null;
|
||
}
|
||
} |