修改审批

This commit is contained in:
mzl
2025-09-19 07:43:48 +00:00
parent cd29507c45
commit 20e3ce3b27
5 changed files with 1682 additions and 11 deletions
+25 -7
View File
@@ -69,6 +69,26 @@ class ApprovalInstances extends Model
'updated_at' => 'datetime',
];
// ====== ====== ====== 映射业务表 ====== ====== ======
protected const BIZ_CLASS = [
'stock_orders' => \app\common\model\StockOrders::class
];
public function getBizTypeAttribute($value)
{
return self::BIZ_CLASS[$value] ?? $value;
}
public function bizInfo()
{
return $this->morphTo('bizInfo', 'biz_type', 'biz_id');
}
// ====== ====== ====== 映射表业务 ====== ====== ======
public function records()
{
return $this->hasMany(ApprovalRecords::class, 'instance_id', 'id');
@@ -84,18 +104,16 @@ class ApprovalInstances extends Model
return $this->belongsTo(User::class, 'initiator_id', 'id');
}
public function bizInfo()
{
return $this->belongsTo(StockOrders::class, 'biz_id', 'id');
}
// public function bizInfo2()
// {
// return $this->belongsTo(StockOrders::class, 'biz_id', 'id');
// }
/**
* 获取当前步骤的所有审批记录(pending 状态)
*/
public function currentStepRecords()
{
return $this->approvalRecords()
->where('step_index', $this->current_step)
->where('status', 'pending');
return $this->approvalRecords()->where('step_index', $this->current_step)->where('status', 'pending');
}
}
+1 -1
View File
@@ -140,7 +140,7 @@ class StockOrdersService
{
// 启动审批
$appro = new \app\common\utils\approval\service\ApprovalFlowService();
$appro->start('stock_orders', 'stock_orders', $item->id, $item->toArray(), $item->salesperson_id);
$appro->start('stock_orders', 'stock_orders', $item->id, [], $item->salesperson_id);
// 异步生成pdf文件
\Webman\RedisQueue\Redis::send('stock-order-pdf', ['id'=>$item->id]);
}
@@ -58,7 +58,15 @@ class ApprovalFlowService
$this->insertApprovalRecord($instanceId, $firstIndex, $uid);
}
$this->sendNotification($approverIds, $instance, 'pending', false);
// $this->sendNotification($approverIds, $instance, 'pending', false);
$extra = [];
$submitNote = $formData['submit_note'] ?? '';
if (is_string($submitNote) && $submitNote !== '') {
$extra['note'] = $submitNote;
}
$this->sendNotification($approverIds, $instance, 'pending', false, $extra);
$this->notifyCCs($firstStep, $instance, 'pending');
return ['instance_id' => $instanceId];
@@ -540,7 +548,7 @@ class ApprovalFlowService
}
/** 发送通知(接你自己的消息系统即可) */
private function sendNotification(array $userIds, object $instance, string $action, bool $isCC): void
private function sendNotification2(array $userIds, object $instance, string $action, bool $isCC): void
{
$map = [
'pending' => '待审批',
@@ -572,6 +580,59 @@ class ApprovalFlowService
]);
}
/** 发送通知(支持附加文案:提交说明/申诉/讨论摘要) */
private function sendNotification(array $userIds, object $instance, string $action, bool $isCC, array $extra = []): void
{
$map = [
'pending' => '待审批',
'approve' => '已通过',
'reject' => '已拒绝',
'revoked' => '已撤回',
];
$prefix = $isCC ? '【抄送】' : '【审批】';
$text = $map[$action] ?? $action;
// 业务标题/内容
$approvalType = array_flip(\app\common\service\SysDictService::pluckKeyValueByCode('approval_type'));
// $title = $prefix.'审批通知';
// $content = "{$prefix}你有一条审批 {$text}:类型 {$instance->flow_type},业务ID {$instance->biz_id}";
switch ($instance->flow_type) {
case 'stock_orders':
// 备货单
$stockOrder = \app\common\model\StockOrders::find($instance->biz_id);
if ($stockOrder) {
$title = $prefix."备货单:{$stockOrder->order_no}";
$flowTxt = $approvalType[$instance->flow_type] ?? $instance->flow_type;
$content = "{$prefix}你有一条审批 {$text}:类型 {$flowTxt},业务单号 {$stockOrder->order_no}";
}
break;
case 'stock_orders_nullify':
// 备货单作废
break;
case 'purchase_order':
// 采购单
break;
}
// 附加说明(提交说明/申诉/讨论摘要)
$append = $extra['appeal'] ?? ($extra['note'] ?? null);
if ($append) {
if (function_exists('mb_strimwidth')) {
$append = mb_strimwidth($append, 0, 120, '...', 'UTF-8');
}
$content .= ";说明:{$append}";
}
noticePush([
'title' => $title,
'content' => $content,
'recipient' => $userIds,
'email' => 1,
]);
}
private function notifyInitiator(object $instance, string $result): void
{
$this->sendNotification([(int)$instance->initiator_id], $instance, $result, false);
@@ -1131,5 +1192,462 @@ class ApprovalFlowService
return $base;
}
// ====== ====== ====== ====== ====== ====== 重提操作 && 讨论 ====== ====== ====== ====== ====== ======
/**
* 重提交(仅对 status=rejected 的实例;支持申诉说明)
* - 合并 $patchData 到 form_data,并设置 resubmitted=1
* - 若 $appealText 非空:form_data.appeal_last = $appealText(可用于前端展示)
* - 重新计算首个命中的步骤并创建待办;通知审批人且带上申诉说明
*/
public function resubmit(int $instanceId, int $initiatorId, array $patchData = [], string $appealText = ''): array
{
return Db::transaction(function () use ($instanceId, $initiatorId, $patchData, $appealText) {
$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 !== 'rejected') 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) ?: [];
// 合并改动 & 打上重提标记
$formData = array_merge($formData, $patchData);
$formData['resubmitted'] = 1;
$appealText = trim((string)$appealText);
if ($appealText !== '') {
$formData['appeal_last'] = $appealText;
}
// 计算重提后首个命中的步骤
$firstIndex = $this->getNextStepIndex($steps, $formData, -1);
if ($firstIndex === null) 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);
}
// 清理可能遗留的 pending
Db::table('approval_records')
->where('instance_id', $instanceId)
->where('status', 'pending')
->update(['status'=>'skipped','approved_at'=>$now,'updated_at'=>$now]);
// 重新开启实例
Db::table('approval_instances')->where('id', $instanceId)->update([
'status' => 'processing',
'current_step' => $firstIndex,
'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
'finished_at' => null,
'deadline_at' => $deadlineAt,
'updated_at' => $now,
]);
// 创建待办 + 通知
$firstStep = $steps[$firstIndex];
$instanceLite = (object)[
'id' => (int)$instanceId,
'flow_type' => (string)$instance->flow_type,
'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
'initiator_id' => (int)$instance->initiator_id,
'biz_type' => (string)$instance->biz_type,
'biz_id' => (string)$instance->biz_id,
];
$approvers = $this->resolveApprovers($firstStep, $instanceLite, true);
if (empty($approvers)) throw new BusinessException('重提后首步未解析到审批人');
foreach ($approvers as $uid) {
$this->insertApprovalRecord($instanceId, $firstIndex, (int)$uid);
}
$extra = $appealText !== '' ? ['appeal' => $appealText] : [];
$this->sendNotification($approvers, $instanceLite, 'pending', false, $extra);
$this->notifyCCs($firstStep, $instanceLite, 'pending');
return [
'status' => 'resubmitted',
'instance_id' => (int)$instanceId,
'current_step' => $firstIndex,
'step_name' => $firstStep['name'] ?? ('Step '.$firstIndex),
];
});
}
/** 是否该实例参与者(发起人/任一步审批人/抄送人) */
private function isParticipantOfInstance(int $instanceId, int $userId): bool
{
$inst = Db::table('approval_instances')->where('id', $instanceId)->first();
if (!$inst) return false;
if ((int)$inst->initiator_id === $userId) return true;
$inRecords = Db::table('approval_records')
->where('instance_id', $instanceId)->where('approver_id', $userId)->exists();
if ($inRecords) return true;
$inCc = Db::table('approval_ccs')
->where('instance_id', $instanceId)->where('user_id', $userId)->exists();
return $inCc;
}
/** 是否该实例任一步的审批人 */
private function isApproverOfInstance(int $instanceId, int $userId): bool
{
return Db::table('approval_records')
->where('instance_id', $instanceId)
->where('approver_id', $userId)
->exists();
}
/** 获取讨论受众:公开=全体参与者;内部=仅审批人(排除自己) */
private function getDiscussionAudienceUserIds(int $instanceId, bool $approversOnly, int $excludeUserId): array
{
$uids = [];
if (!$approversOnly) {
$inst = Db::table('approval_instances')->where('id', $instanceId)->first();
if ($inst) $uids[] = (int)$inst->initiator_id;
$ccs = Db::table('approval_ccs')->where('instance_id', $instanceId)->pluck('user_id')->toArray();
foreach ($ccs as $u) $uids[] = (int)$u;
}
$approvers = Db::table('approval_records')
->where('instance_id', $instanceId)
->distinct()->pluck('approver_id')->toArray();
foreach ($approvers as $u) $uids[] = (int)$u;
$uids = array_values(array_unique(array_filter($uids, fn($u) => $u !== $excludeUserId)));
return $uids;
}
/**
* 发表讨论
* @param int $instanceId
* @param int $userId
* @param string $content
* @param bool $isInternal true=仅审批人可见;false=所有参与者可见
* @param array $attachments 可选附件数组
* @param string|null $userNameColumn 通知里带发言人姓名(如 'name'),不需要传 null
*/
public function postDiscussion(
int $instanceId,
int $userId,
string $content,
bool $isInternal = false,
array $attachments = [],
?string $userNameColumn = null
): array {
$content = trim($content);
if ($content === '') throw new BusinessException('讨论内容不能为空');
if (!$this->isParticipantOfInstance($instanceId, $userId)) {
throw new BusinessException('无权参与该实例的讨论');
}
if ($isInternal && !$this->isApproverOfInstance($instanceId, $userId)) {
throw new BusinessException('仅审批人可发布内部讨论');
}
$now = date('Y-m-d H:i:s');
$id = Db::table('approval_discussions')->insertGetId([
'instance_id' => $instanceId,
'user_id' => $userId,
'is_internal' => $isInternal ? 1 : 0,
'content' => $content,
'attachments' => empty($attachments) ? null : json_encode($attachments, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES),
'created_at' => $now,
'updated_at' => $now,
]);
// 通知其他人
$inst = Db::table('approval_instances')->where('id', $instanceId)->first();
if ($inst) {
$audience = $this->getDiscussionAudienceUserIds($instanceId, $isInternal, $userId);
// 发言人姓名(可选)
$speaker = (string)$userId;
if ($userNameColumn) {
$name = Db::table('user')->where('id', $userId)->value($userNameColumn);
if ($name) $speaker = $name;
}
$snippet = function_exists('mb_strimwidth') ? mb_strimwidth($content, 0, 80, '...', 'UTF-8') : $content;
$extra = ['note' => "【讨论】{$speaker}:{$snippet}"];
$this->sendNotification(
$audience,
(object)[
'id' => (int)$inst->id,
'flow_type' => (string)$inst->flow_type,
'initiator_id' => (int)$inst->initiator_id,
'form_data' => $inst->form_data,
'biz_type' => (string)$inst->biz_type,
'biz_id' => (string)$inst->biz_id,
],
'pending',
false,
$extra
);
}
return [
'id' => (int)$id,
'instance_id' => $instanceId,
'user_id' => $userId,
'is_internal' => (bool)$isInternal,
'content' => $content,
'attachments' => $attachments,
'created_at' => $now,
];
}
/**
* 获取讨论列表
* @param int $instanceId
* @param int|null $afterId 翻页:取 id > afterId 的新消息
* @param int $limit
* @param string|null $userNameColumn 带出用户名(如 'name'),不需要可传 null
*/
public function listDiscussions(int $instanceId, ?int $afterId = null, int $limit = 50, ?string $userNameColumn = null): array
{
$q = Db::table('approval_discussions as d')->where('d.instance_id', $instanceId);
if ($afterId) $q->where('d.id', '>', $afterId);
$q->orderBy('d.id', 'asc')->limit(max(1, min(200, $limit)));
if ($userNameColumn) {
$rows = $q->leftJoin('user as u', 'd.user_id', '=', 'u.id')
->select('d.*', Db::raw("u.`{$userNameColumn}` as user_name"))
->get()->toArray();
} else {
$rows = $q->select('d.*')->get()->toArray();
}
return array_map(function($r){
return [
'id' => (int)$r->id,
'instance_id' => (int)$r->instance_id,
'user_id' => (int)$r->user_id,
'is_internal' => (bool)$r->is_internal,
'content' => $r->content,
'attachments' => $r->attachments ? json_decode($r->attachments, true) : [],
'created_at' => $r->created_at,
'updated_at' => $r->updated_at,
'user_name' => property_exists($r, 'user_name') ? $r->user_name : null,
];
}, $rows);
}
// ====== ====== ====== ====== ====== ====== 转交操作 ====== ====== ====== ====== ====== ======
/**
* 转交当前待办给他人
* 仅允许【当前这条待办的审批人】把自己名下且为 pending 的记录转交给 toUserId。
* - 若目标人在本步已有 pending:合并(把本记录标记为 skipped,保留目标人的那条)
* - 否则:原地把该记录的 approver_id 改为 toUserId
* - 写入转交日志
* - 通知被转交人
*/
public function transferApproval2(int $instanceId, int $stepIndex, int $fromUserId, int $toUserId, string $reason = ''): array
{
if ($fromUserId === $toUserId) {
return ['status' => 'noop', 'msg' => '目标与来源相同,无需转交'];
}
return Db::transaction(function () use ($instanceId, $stepIndex, $fromUserId, $toUserId, $reason) {
// 锁实例,避免并发推进
$instance = Db::table('approval_instances')->where('id', $instanceId)->lockForUpdate()->first();
if (!$instance) throw new BusinessException('审批实例未找到');
if ($instance->status !== 'processing') throw new BusinessException('流程非进行中,无法转交');
// 锁定来源人的 pending 记录(必须是“自己名下”的记录)
$record = Db::table('approval_records')
->where('instance_id', $instanceId)
->where('step_index', $stepIndex)
->where('approver_id', $fromUserId)
->where('status', 'pending')
->lockForUpdate()
->first();
if (!$record) {
throw new BusinessException('未找到可转交的待办或已处理');
}
$now = date('Y-m-d H:i:s');
// 若目标人在本步已有 pending,合并:本记录设为 skipped,保留目标那条
$target = Db::table('approval_records')
->where('instance_id', $instanceId)
->where('step_index', $stepIndex)
->where('approver_id', $toUserId)
->where('status', 'pending')
->lockForUpdate()
->first();
if ($target) {
Db::table('approval_records')->where('id', $record->id)->update([
'status' => 'skipped',
'approved_at' => $now,
'updated_at' => $now,
'comment' => trim(($record->comment ?? '')."\n[转交给 {$toUserId},目标已有待办,已合并]"),
]);
$effectiveRecordId = (int)$target->id;
} else {
// 原地替换 approver_id
Db::table('approval_records')->where('id', $record->id)->update([
'approver_id' => $toUserId,
'updated_at' => $now,
'comment' => trim(($record->comment ?? '')."\n[自 {$fromUserId} 转交]"),
]);
$effectiveRecordId = (int)$record->id;
}
// 写转交日志
Db::table('approval_transfers')->insert([
'instance_id' => (int)$instanceId,
'step_index' => (int)$stepIndex,
'record_id' => $effectiveRecordId,
'from_user_id' => (int)$fromUserId,
'to_user_id' => (int)$toUserId,
'reason' => $reason,
'created_at' => $now,
]);
// 通知被转交人(沿用你的 sendNotification)
$this->sendNotification([$toUserId], (object)[
'id' => (int)$instance->id,
'flow_type' => (string)$instance->flow_type,
'biz_type' => (string)$instance->biz_type,
'biz_id' => (string)$instance->biz_id,
'initiator_id' => (int)$instance->initiator_id,
'form_data' => $instance->form_data,
], 'pending', false);
return ['status' => 'ok', 'record_id' => $effectiveRecordId];
});
}
/**
* 查看某个实例的转交流水(按时间升序)
*/
public function listTransfers(int $instanceId): array
{
$rows = Db::table('approval_transfers')
->where('instance_id', $instanceId)
->orderBy('id', 'asc')
->get()
->toArray();
return array_map(function($r){
return [
'id' => (int)$r->id,
'instance_id' => (int)$r->instance_id,
'step_index' => (int)$r->step_index,
'record_id' => (int)$r->record_id,
'from_user_id' => (int)$r->from_user_id,
'to_user_id' => (int)$r->to_user_id,
'reason' => $r->reason,
'created_at' => $r->created_at,
];
}, $rows);
}
/**
* 转交当前待办给他人(原地替换 approver_id)并在通知里追加“来源/原因”
*/
public function transferApproval(
int $instanceId,
int $stepIndex,
int $fromUserId,
int $toUserId,
string $reason = '',
?string $userNameColumn = null // 可选:传 'name' 或 'nickname' 来展示姓名
): array {
if ($fromUserId === $toUserId) {
return ['status' => 'noop', 'msg' => '目标与来源相同,无需转交'];
}
return \support\Db::transaction(function () use ($instanceId, $stepIndex, $fromUserId, $toUserId, $reason, $userNameColumn) {
// 锁实例,避免并发推进
$instance = Db::table('approval_instances')->where('id', $instanceId)->lockForUpdate()->first();
if (!$instance) throw new BusinessException('审批实例未找到');
if ($instance->status !== 'processing') throw new BusinessException('流程非进行中,无法转交');
// 锁定来源人的 pending 记录
$record = Db::table('approval_records')
->where('instance_id', $instanceId)
->where('step_index', $stepIndex)
->where('approver_id', $fromUserId)
->where('status', 'pending')
->lockForUpdate()
->first();
if (!$record) throw new BusinessException('未找到可转交的待办或已处理');
$now = date('Y-m-d H:i:s');
// 若目标已有 pending:合并(本记录置为 skipped)
$target = Db::table('approval_records')
->where('instance_id', $instanceId)
->where('step_index', $stepIndex)
->where('approver_id', $toUserId)
->where('status', 'pending')
->lockForUpdate()
->first();
if ($target) {
Db::table('approval_records')->where('id', $record->id)->update([
'status' => 'skipped',
'approved_at' => $now,
'updated_at' => $now,
'comment' => trim(($record->comment ?? '')."\n[转交给 {$toUserId},目标已有待办,已合并]"),
]);
$effectiveRecordId = (int)$target->id;
} else {
// 原地替换 approver_id
Db::table('approval_records')->where('id', $record->id)->update([
'approver_id' => $toUserId,
'updated_at' => $now,
'comment' => trim(($record->comment ?? '')."\n[自 {$fromUserId} 转交]"),
]);
$effectiveRecordId = (int)$record->id;
}
// 组织“转交来源/原因”文案(优先姓名)
$fromLabel = (string)$fromUserId;
$toLabel = (string)$toUserId;
if ($userNameColumn) {
$fromName = Db::table('user')->where('id', $fromUserId)->value($userNameColumn);
$toName = Db::table('user')->where('id', $toUserId)->value($userNameColumn);
if ($fromName) $fromLabel = $fromName;
if ($toName) $toLabel = $toName;
}
$note = "【转交】{$fromLabel} → {$toLabel}";
$r = trim($reason);
if ($r !== '') $note .= ",原因:{$r}";
// 通知被转交人(在内容里追加 note)
$this->sendNotification([$toUserId], (object)[
'id' => (int)$instance->id,
'flow_type' => (string)$instance->flow_type,
'biz_type' => (string)$instance->biz_type,
'biz_id' => (string)$instance->biz_id,
'initiator_id' => (int)$instance->initiator_id,
'form_data' => $instance->form_data,
], 'pending', false, ['note' => $note]);
// 记转交流水
Db::table('approval_transfers')->insert([
'instance_id' => (int)$instanceId,
'step_index' => (int)$stepIndex,
'record_id' => $effectiveRecordId,
'from_user_id' => (int)$fromUserId,
'to_user_id' => (int)$toUserId,
'reason' => $reason,
'created_at' => $now,
]);
return ['status' => 'ok', 'record_id' => $effectiveRecordId];
});
}
}
File diff suppressed because it is too large Load Diff