476 lines
20 KiB
PHP
476 lines
20 KiB
PHP
<?php
|
||
namespace app\common\utils\approval\service\traits;
|
||
|
||
use support\Db;
|
||
use support\exception\BusinessException;
|
||
|
||
trait ApprovalQueryTrait
|
||
{
|
||
/** 流程图数据 */
|
||
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)
|
||
->where('round', (int)$instance->round)
|
||
->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();
|
||
|
||
if (!empty($actions)) {
|
||
$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';
|
||
}
|
||
}
|
||
|
||
$data[] = [
|
||
'index' => $i,
|
||
'name' => $step['name'] ?? ('Step '.$i),
|
||
'type' => $step['type'] ?? 'all',
|
||
'status' => $status,
|
||
'actions' => $actions,
|
||
];
|
||
}
|
||
return $data;
|
||
}
|
||
|
||
/** 查询某个审批实例的审批记录 */
|
||
public function getInstanceRecords(int $instanceId, ?string $userNameColumn = null): array
|
||
{
|
||
// 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,
|
||
];
|
||
}
|
||
|
||
/** 简单时间线 */
|
||
public function getSimpleTimeline(int $instanceId, ?string $userNameColumn = null, string $initiatorPlacement = 'prepend'): array
|
||
{
|
||
// 基础数据
|
||
$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) ?: [];
|
||
$formData = json_decode($instance->form_data ?? '{}', true) ?: [];
|
||
$curStep = (int)$instance->current_step;
|
||
$round = (int)$instance->round;
|
||
|
||
// 申请人
|
||
$initiatorId = (int)$instance->initiator_id;
|
||
$initiatorName = null;
|
||
if ($userNameColumn) {
|
||
$initiatorName = Db::table('user')->where('id', $initiatorId)->value($userNameColumn);
|
||
}
|
||
|
||
// 审批记录(仅当前轮次)
|
||
$records = Db::table('approval_records as ar')
|
||
->where('ar.instance_id', $instanceId)
|
||
->where('ar.round', $round)
|
||
->orderBy('ar.step_index')
|
||
->orderByRaw('COALESCE(ar.approved_at, ar.created_at) asc')
|
||
->select('ar.*')
|
||
->get()
|
||
->toArray();
|
||
|
||
// 一次性拉取姓名,避免 N+1
|
||
$nameMap = [];
|
||
if ($userNameColumn && !empty($records)) {
|
||
$uids = [];
|
||
foreach ($records as $r) $uids[] = (int)$r->approver_id;
|
||
$uids = array_values(array_unique($uids));
|
||
if (!empty($uids)) {
|
||
$pairs = Db::table('user')->whereIn('id', $uids)->pluck($userNameColumn, 'id')->toArray();
|
||
foreach ($pairs as $uid => $name) $nameMap[(int)$uid] = $name;
|
||
}
|
||
}
|
||
|
||
// 记录分组
|
||
$byStep = [];
|
||
foreach ($records as $r) {
|
||
$idx = (int)$r->step_index;
|
||
$byStep[$idx][] = $r;
|
||
}
|
||
|
||
$timeline = [];
|
||
|
||
// 可选:发起申请块
|
||
if ($initiatorPlacement === 'prepend') {
|
||
$appliedAt = $instance->created_at ?: null;
|
||
$timeline[] = [
|
||
'index' => -1,
|
||
'name' => '发起申请',
|
||
'type' => '-',
|
||
'status' => 'submitted',
|
||
'started_at' => $appliedAt,
|
||
'finished_at' => $appliedAt,
|
||
'duration_seconds' => 0,
|
||
'initiator_id' => $initiatorId,
|
||
'initiator_name' => $initiatorName,
|
||
];
|
||
}
|
||
|
||
// 每一步
|
||
foreach ($steps as $i => $meta) {
|
||
$name = $meta['name'] ?? ('Step '.$i);
|
||
$typeRaw = strtolower($meta['type'] ?? 'all');
|
||
$type = in_array($typeRaw, ['any','all']) ? $typeRaw : 'all';
|
||
$condStr = $meta['condition'] ?? null;
|
||
$condOk = $condStr ? $this->evaluateCondition($condStr, $formData) : true;
|
||
|
||
$recs = $byStep[$i] ?? [];
|
||
$hasRecs = !empty($recs);
|
||
|
||
$status = 'pending';
|
||
$startedAt = null;
|
||
$finishedAt = null;
|
||
$durationSec = null;
|
||
|
||
// 每一步的“审批意见”(只展示有内容的 comment)
|
||
$opinions = []; // [{approver_id, approver_name?, action, status, comment, time}...]
|
||
|
||
if ($hasRecs) {
|
||
// 统计时间
|
||
$minCreated = null;
|
||
$minApproveAt = null; $maxApproveAt = null;
|
||
$minRejectAt = null;
|
||
$maxTimeoutAt = null;
|
||
$cnt = ['pending'=>0,'approved'=>0,'rejected'=>0,'timeout'=>0];
|
||
|
||
foreach ($recs as $rr) {
|
||
// 收集“审批意见”(非空 comment 都算)
|
||
$cmt = trim((string)($rr->comment ?? ''));
|
||
if ($cmt !== '') {
|
||
$opinions[] = [
|
||
'approver_id' => (int)$rr->approver_id,
|
||
'approver_name' => $userNameColumn ? ($nameMap[(int)$rr->approver_id] ?? null) : null,
|
||
'action' => $rr->action, // approve | reject | null
|
||
'status' => $rr->status, // approved|rejected|pending|skipped|timeout
|
||
'comment' => $cmt,
|
||
'time' => $rr->approved_at ?: $rr->created_at,
|
||
];
|
||
}
|
||
|
||
if ($rr->created_at && (!$minCreated || $rr->created_at < $minCreated)) $minCreated = $rr->created_at;
|
||
if ($rr->approved_at) {
|
||
if ($rr->status === 'approved') {
|
||
if (!$minApproveAt || $rr->approved_at < $minApproveAt) $minApproveAt = $rr->approved_at;
|
||
if (!$maxApproveAt || $rr->approved_at > $maxApproveAt) $maxApproveAt = $rr->approved_at;
|
||
} elseif ($rr->status === 'rejected') {
|
||
if (!$minRejectAt || $rr->approved_at < $minRejectAt) $minRejectAt = $rr->approved_at;
|
||
} elseif ($rr->status === 'timeout') {
|
||
if (!$maxTimeoutAt || $rr->approved_at > $maxTimeoutAt) $maxTimeoutAt = $rr->approved_at;
|
||
}
|
||
}
|
||
if (isset($cnt[$rr->status])) $cnt[$rr->status]++;
|
||
}
|
||
|
||
// 时间升序排序 opinions
|
||
if (!empty($opinions)) {
|
||
usort($opinions, function($a, $b) {
|
||
return strcmp($a['time'] ?? '', $b['time'] ?? '');
|
||
});
|
||
}
|
||
|
||
$startedAt = $minCreated;
|
||
|
||
// 总结果(注意:这里按你现在的签名调用,含 round)
|
||
$outcome = $this->getStepOutcome($instanceId, $round, $i, $type); // 'pending'|'approve'|'reject'
|
||
if ($outcome === 'reject') {
|
||
$status = 'rejected';
|
||
$finishedAt = $minRejectAt;
|
||
} elseif ($outcome === 'approve') {
|
||
$status = 'approved';
|
||
$finishedAt = ($type === 'any') ? $minApproveAt : $maxApproveAt;
|
||
} else {
|
||
if ($cnt['timeout'] > 0 && $cnt['approved'] === 0 && $cnt['rejected'] === 0 && $cnt['pending'] === 0) {
|
||
$status = 'timeout';
|
||
$finishedAt = $maxTimeoutAt;
|
||
} else {
|
||
$status = 'processing';
|
||
}
|
||
}
|
||
|
||
if ($startedAt && $finishedAt) {
|
||
$durationSec = max(0, strtotime($finishedAt) - strtotime($startedAt));
|
||
}
|
||
} else {
|
||
// 无记录:根据位置与条件展示
|
||
if ($i < $curStep) {
|
||
$status = $condOk ? 'approved' : 'skipped';
|
||
} elseif ($i === $curStep && $instance->status === 'processing') {
|
||
$status = 'processing';
|
||
} else {
|
||
$status = $condOk ? 'pending' : 'skipped';
|
||
}
|
||
}
|
||
|
||
$row = [
|
||
'index' => (int)$i,
|
||
'name' => $name,
|
||
'type' => $type,
|
||
'status' => $status,
|
||
'started_at' => $startedAt,
|
||
'finished_at' => $finishedAt,
|
||
'duration_seconds' => $durationSec,
|
||
'opinions' => $opinions, // 这就是每个审批意见
|
||
];
|
||
|
||
// 把申请人信息并到第一步(可选)
|
||
if ($initiatorPlacement === 'attach' && empty($timeline)) {
|
||
$row['initiator_id'] = $initiatorId;
|
||
$row['initiator_name'] = $initiatorName;
|
||
}
|
||
|
||
$timeline[] = $row;
|
||
}
|
||
|
||
return $timeline;
|
||
}
|
||
|
||
/** 按业务ID获取“最新进展”的极简状态 */
|
||
public function getBriefStatusByBiz(string $bizId, ?string $bizType = null, ?string $userNameColumn = null): array
|
||
{
|
||
// 1) 最新实例
|
||
$q = Db::table('approval_instances')->where('biz_id', $bizId);
|
||
if ($bizType !== null && $bizType !== '') {
|
||
$q->where('biz_type', $bizType);
|
||
}
|
||
$instance = $q->orderByDesc('id')->first();
|
||
if (!$instance) {
|
||
// throw new BusinessException('未找到对应的审批实例');
|
||
return [];
|
||
}
|
||
|
||
$base = [
|
||
'instance_id' => (int)$instance->id,
|
||
'flow_type' => $instance->flow_type,
|
||
'biz_type' => $instance->biz_type,
|
||
'biz_id' => $instance->biz_id,
|
||
'status' => $instance->status,
|
||
'finished_at' => $instance->finished_at,
|
||
'updated_at' => $instance->updated_at,
|
||
];
|
||
|
||
// 2) 已完结:直接返回
|
||
if ($instance->status !== 'processing') {
|
||
return $base;
|
||
}
|
||
|
||
// 3) 进行中:取当前步骤 + 当前 pending 审批者
|
||
$flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first();
|
||
if (!$flow) {
|
||
// 容错:没有流程定义
|
||
return $base;
|
||
}
|
||
$steps = json_decode($flow->steps, true) ?: [];
|
||
$idx = (int)$instance->current_step;
|
||
$round = (int)$instance->round;
|
||
$meta = $steps[$idx] ?? [];
|
||
|
||
$typeRaw = strtolower($meta['type'] ?? 'all');
|
||
$type = in_array($typeRaw, ['any','all']) ? $typeRaw : 'all';
|
||
|
||
// 当前步的所有记录
|
||
$recsQ = Db::table('approval_records')->where('instance_id', $instance->id)->where('step_index', $idx);
|
||
|
||
// 待处理人(只列出 pending)
|
||
if ($userNameColumn) {
|
||
$pendingRows = Db::table('approval_records as ar')
|
||
->leftJoin('user as u', 'ar.approver_id', '=', 'u.id')
|
||
->where('ar.instance_id', $instance->id)
|
||
->where('ar.round', $round)
|
||
->where('ar.step_index', $idx)
|
||
->where('ar.status', 'pending')
|
||
->select('ar.approver_id', Db::raw("u.`{$userNameColumn}` as approver_name"))
|
||
->get()
|
||
->toArray();
|
||
} else {
|
||
$pendingRows = Db::table('approval_records')
|
||
->where('instance_id', $instance->id)
|
||
->where('round', $round)
|
||
->where('step_index', $idx)
|
||
->where('status', 'pending')
|
||
->select('approver_id')
|
||
->get()
|
||
->toArray();
|
||
}
|
||
|
||
$pendingApprovers = [];
|
||
foreach ($pendingRows as $r) {
|
||
$row = ['approver_id' => (int)$r->approver_id];
|
||
if ($userNameColumn && property_exists($r, 'approver_name')) {
|
||
$row['approver_name'] = $r->approver_name;
|
||
}
|
||
$pendingApprovers[] = $row;
|
||
}
|
||
|
||
// 当前步的聚合状态(进行中通常返回 processing;若全被超时任务置为 timeout,则返回 timeout)
|
||
// 先快速统计是否纯 timeout
|
||
$stats = Db::table('approval_records')
|
||
->where('instance_id', $instance->id)
|
||
->where('round', $round)
|
||
->where('step_index', $idx)
|
||
->selectRaw("SUM(CASE WHEN status='pending' THEN 1 ELSE 0 END) as c_pending")
|
||
->selectRaw("SUM(CASE WHEN status='approved' THEN 1 ELSE 0 END) as c_approved")
|
||
->selectRaw("SUM(CASE WHEN status='rejected' THEN 1 ELSE 0 END) as c_rejected")
|
||
->selectRaw("SUM(CASE WHEN status='timeout' THEN 1 ELSE 0 END) as c_timeout")
|
||
->first();
|
||
|
||
$stepStatus = 'processing';
|
||
if ($stats && (int)$stats->c_timeout > 0 && (int)$stats->c_pending === 0 && (int)$stats->c_approved === 0 && (int)$stats->c_rejected === 0) {
|
||
$stepStatus = 'timeout';
|
||
}
|
||
|
||
$base['step'] = [
|
||
'index' => $idx,
|
||
'name' => $meta['name'] ?? ('Step '.$idx),
|
||
'type' => $type, // any|all
|
||
'status' => $stepStatus, // processing|timeout
|
||
'pending_approvers' => $pendingApprovers, // 当前正在等待的审批者
|
||
];
|
||
|
||
return $base;
|
||
}
|
||
|
||
/** 登录/首屏初始化:获取当前用户的待办总数 */
|
||
public function getInitTodoCount(int $userId): int
|
||
{
|
||
$userId = (int)$userId;
|
||
if ($userId <= 0) {
|
||
throw new BusinessException('无效的用户ID');
|
||
}
|
||
|
||
$row = Db::table('approval_records as ar')
|
||
->join('approval_instances as ai', 'ai.id', '=', 'ar.instance_id')
|
||
->where('ar.approver_id', $userId)
|
||
->where('ar.status', 'pending')
|
||
->where('ai.status', 'processing')
|
||
// 只统计实例“当前轮次、当前步骤”的待办,避免历史残留干扰
|
||
->whereRaw('ar.round = ai.round')
|
||
->whereRaw('ar.step_index = ai.current_step')
|
||
->selectRaw('COUNT(*) as c')
|
||
->first();
|
||
|
||
return (int)($row->c ?? 0);
|
||
}
|
||
}
|