411 lines
16 KiB
PHP
411 lines
16 KiB
PHP
<?php
|
||
namespace app\common\utils\approval\traits;
|
||
|
||
use support\Db;
|
||
use support\exception\BusinessException;
|
||
|
||
trait ApprovalResolverTrait
|
||
{
|
||
/** 解析审批人(role/user/initiator_leader/field_user/dept_head/...) */
|
||
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':
|
||
$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;
|
||
|
||
case 'group_members': {
|
||
// $gid = (int)($cfg['group_id'] ?? 0);
|
||
$gid = (int)($cfg['value'] ?? 0);
|
||
if ($gid <= 0) throw new BusinessException('group_members 需要 group_id');
|
||
$ids = $this->getGroupMemberIds($gid); // A 方案
|
||
break;
|
||
}
|
||
|
||
case 'scenario_leader_of_initiator': {
|
||
$formData = json_decode($instance->form_data ?? '{}', true) ?: [];
|
||
// $scenario = $cfg['scenario'] ?? ($formData[$cfg['scenario_field']] ?? null);
|
||
$scenario = $val ?? ($formData[$cfg['scenario_field']] ?? null);
|
||
if (!$scenario) throw new BusinessException('缺少业务场景(scenario 或 scenario_field)');
|
||
|
||
$mustBelong = array_key_exists('must_belong', $cfg) ? (bool)$cfg['must_belong'] : true;
|
||
$preferPrimary = array_key_exists('prefer_primary', $cfg) ? (bool)$cfg['prefer_primary'] : true;
|
||
|
||
$ids = $this->getLeadersByScenarioForInitiator(
|
||
(int)$instance->initiator_id,
|
||
(string)$scenario,
|
||
$mustBelong,
|
||
$preferPrimary
|
||
); // B 方案(按场景)
|
||
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));
|
||
$resolved = array_values($resolved);
|
||
}
|
||
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;
|
||
case 'group_members': {
|
||
$gid = (int)($val ?? 0);
|
||
if ($gid <= 0) throw new BusinessException('group_members 需要 group_id');
|
||
$uids = $this->getGroupMemberIds($gid); // A 方案
|
||
break;
|
||
}
|
||
case 'scenario_leader_of_initiator': {
|
||
$formData = json_decode($instance->form_data ?? '{}', true) ?: [];
|
||
$scenario = $val ?? ($formData[$cc['scenario_field']] ?? null);
|
||
if (!$scenario) throw new BusinessException('缺少业务场景(scenario 或 scenario_field)');
|
||
|
||
$mustBelong = array_key_exists('must_belong', $cc) ? (bool)$cc['must_belong'] : true;
|
||
$preferPrimary = array_key_exists('prefer_primary', $cc) ? (bool)$cc['prefer_primary'] : true;
|
||
|
||
$uids = $this->getLeadersByScenarioForInitiator(
|
||
(int)$instance->initiator_id,
|
||
(string)$scenario,
|
||
$mustBelong,
|
||
$preferPrimary
|
||
); // B 方案(按场景)
|
||
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}"),
|
||
};
|
||
}
|
||
|
||
/** 获取下一个有效步骤索引 */
|
||
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;
|
||
}
|
||
|
||
/** 聚合本步骤结论 */
|
||
private function getStepOutcome(int $instanceId, int $round, int $stepIndex, string $type): string
|
||
{
|
||
$records = Db::table('approval_records')
|
||
->where('instance_id', $instanceId)
|
||
->where('round', $round) //
|
||
->where('step_index', $stepIndex)
|
||
->get();
|
||
|
||
$pending = $approved = $rejected = $timeout = $skipped = 0;
|
||
foreach ($records as $r) {
|
||
switch ($r->status) {
|
||
case 'pending': $pending++; break;
|
||
case 'approved': $approved++; break;
|
||
case 'rejected': $rejected++; break;
|
||
case 'timeout': $timeout++; break;
|
||
case 'skipped': $skipped++; break;
|
||
}
|
||
}
|
||
|
||
$type = strtolower($type);
|
||
if ($type === 'any') {
|
||
if ($rejected > 0) return 'reject';
|
||
if ($approved > 0) return 'approve';
|
||
// 全为 skipped/timeout 且无 pending/rejected ⇒ 通过
|
||
if ($pending === 0 && $rejected === 0 && $approved === 0) return 'approve';
|
||
return 'pending';
|
||
}
|
||
if ($rejected > 0) return 'reject';
|
||
if ($pending > 0) return 'pending';
|
||
return 'approve';
|
||
}
|
||
|
||
/** 关闭本步其余待办为 skipped */
|
||
private function closeOtherPending(int $instanceId, int $round, int $stepIndex, array $keepApproverIds = []): void
|
||
{
|
||
$now = date('Y-m-d H:i:s');
|
||
$q = Db::table('approval_records')
|
||
->where('instance_id', $instanceId)
|
||
->where('round', $round) //
|
||
->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(object $instance, int $stepIndex, int $approverId): void
|
||
{
|
||
$round = (int)($instance->round ?? 1);
|
||
$now = date('Y-m-d H:i:s');
|
||
|
||
$exists = Db::table('approval_records')
|
||
->where('instance_id', (int)$instance->id)
|
||
->where('round', $round) // 限当前轮
|
||
->where('step_index', $stepIndex)
|
||
->where('approver_id', $approverId)
|
||
->where('status', 'pending')
|
||
->exists();
|
||
|
||
if ($exists) return;
|
||
|
||
Db::table('approval_records')->insert([
|
||
'instance_id' => (int)$instance->id,
|
||
'approver_id' => $approverId,
|
||
'step_index' => $stepIndex,
|
||
'round' => $round, //
|
||
'action' => null,
|
||
'status' => 'pending',
|
||
'comment' => null,
|
||
'approved_at' => null,
|
||
'created_at' => $now,
|
||
'updated_at' => $now,
|
||
]);
|
||
}
|
||
|
||
/** 取“某实例、某轮次、某步骤”里当前 pending 的审批人 */
|
||
private function getPendingApproverIds(int $instanceId, int $round, int $stepIndex): array
|
||
{
|
||
$ids = Db::table('approval_records')
|
||
->where('instance_id', $instanceId)
|
||
->where('round', $round)
|
||
->where('step_index', $stepIndex)
|
||
->where('status', 'pending')
|
||
->pluck('approver_id')->toArray();
|
||
|
||
return array_values(array_unique(array_map('intval', $ids)));
|
||
}
|
||
|
||
/** 从角色取用户ID */
|
||
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
|
||
{
|
||
$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;
|
||
}
|
||
|
||
/** A) 取组内全部成员 */
|
||
private function getGroupMemberIds(int $groupId): array
|
||
{
|
||
$rows = Db::table('user_group_members')
|
||
->where('group_id', $groupId)
|
||
->where('status', 1)
|
||
->pluck('user_id')->toArray();
|
||
|
||
return array_values(array_unique(array_map('intval', $rows)));
|
||
}
|
||
|
||
/** B) 按“场景”在发起人所在的组里找负责人 */
|
||
private function getLeadersByScenarioForInitiator(
|
||
int $initiatorId,
|
||
string $scenarioCode,
|
||
bool $mustBelong = true,
|
||
bool $preferPrimary = true
|
||
): array {
|
||
// 找出“该场景下发起人所在的所有组”
|
||
$groups = Db::table('user_group as g')
|
||
->join('user_group_members as m', 'm.group_id', '=', 'g.id')
|
||
->where('g.scenario_code', $scenarioCode)
|
||
->where('g.status', 1)
|
||
->where('m.user_id', $initiatorId)
|
||
->where('m.status', 1)
|
||
->orderByDesc('m.is_primary') // 主归属优先
|
||
->orderByDesc('g.sort') // 其次按组排序
|
||
->select('g.id', 'm.is_primary')
|
||
->get()->toArray();
|
||
|
||
if (empty($groups)) {
|
||
if ($mustBelong) {
|
||
throw new BusinessException('发起人在该业务场景下未加入任何分组');
|
||
}
|
||
// 不强制属于:允许只按“该场景下任一组”的负责人(不推荐)
|
||
$groups = Db::table('user_group as g')
|
||
->where('g.scenario_code', $scenarioCode)
|
||
->where('g.status', 1)
|
||
->orderByDesc('g.sort')
|
||
->select('g.id', Db::raw('0 as is_primary'))
|
||
->get()->toArray();
|
||
if (empty($groups)) {
|
||
throw new BusinessException('该业务场景下未配置任何分组');
|
||
}
|
||
}
|
||
|
||
// 选择一个“目标组”
|
||
$targetGroupId = null;
|
||
if ($preferPrimary) {
|
||
foreach ($groups as $g) {
|
||
if ((int)$g->is_primary === 1) { $targetGroupId = (int)$g->id; break; }
|
||
}
|
||
}
|
||
if (!$targetGroupId) {
|
||
$targetGroupId = (int)$groups[0]->id; // 用排序第一的组
|
||
}
|
||
|
||
// 取负责人(可支持多负责人)
|
||
$leaders = Db::table('user_group_members')
|
||
->where('group_id', $targetGroupId)
|
||
->where('is_leader', 1)
|
||
->where('status', 1)
|
||
->pluck('user_id')->toArray();
|
||
|
||
$ids = array_values(array_unique(array_map('intval', $leaders)));
|
||
if (empty($ids)) throw new BusinessException('目标分组未配置负责人');
|
||
|
||
return $ids;
|
||
}
|
||
|
||
public function StetApprovers(array $step, object $instance, bool $avoidSelf = true){
|
||
return$this->resolveApprovers($step, $instance, $avoidSelf);
|
||
}
|
||
}
|