This commit is contained in:
2025-09-10 13:37:45 +08:00
parent 3873238a62
commit cc4cf55e76
3 changed files with 1 additions and 753 deletions
@@ -11,7 +11,6 @@ use app\common\model\StockOrders;
use app\common\model\SysCurrency;
use support\Request;
use app\common\model\User;
use support\exception\BusinessException;
use support\Db;
class StockOrdersService
@@ -112,7 +112,7 @@ class ApprovalFlowService
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', 'msg' => '非当前审批'];
}
// return ['status' => 'noop', 'reason' => 'no_pending_record'];
return ['status' => 'noop', 'msg' => '无待处理记录'];
@@ -1,751 +0,0 @@
<?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;
}
}