namespace Admin.NET.Plugin.ApprovalFlow.Service;
///
/// 审批任务服务
///
[ApiDescriptionSettings(ApprovalFlowConst.GroupName, Order = 90)]
public class FlowTaskService : IDynamicApiController, ITransient
{
private readonly FlowEngineService _engine;
private readonly SqlSugarRepository _taskRep;
private readonly SqlSugarRepository _instanceRep;
private readonly UserManager _userManager;
public FlowTaskService(
FlowEngineService engine,
SqlSugarRepository taskRep,
SqlSugarRepository instanceRep,
UserManager userManager)
{
_engine = engine;
_taskRep = taskRep;
_instanceRep = instanceRep;
_userManager = userManager;
}
// ── 查询 ──
///
/// 我的待办(分页)
///
[HttpPost]
[ApiDescriptionSettings(Name = "MyPendingPage")]
[DisplayName("我的待办")]
public async Task> MyPendingPage(TaskPageInput input)
{
var userId = _userManager.UserId;
return await _taskRep.AsQueryable()
.InnerJoin((t, i) => t.InstanceId == i.Id)
.Where((t, i) => t.AssigneeId == userId && t.Status == FlowTaskStatusEnum.Pending)
.WhereIF(!string.IsNullOrWhiteSpace(input.BizType), (t, i) => i.BizType == input.BizType)
.OrderByDescending((t, i) => t.CreateTime)
.Select((t, i) => new TaskPageOutput
{
Id = t.Id,
InstanceId = t.InstanceId,
NodeId = t.NodeId,
NodeName = t.NodeName,
Status = t.Status,
Title = i.Title,
BizType = i.BizType,
BizId = i.BizId,
BizNo = i.BizNo,
InitiatorName = i.InitiatorName,
StartTime = i.StartTime,
CreateTime = t.CreateTime,
IsDelegate = t.IsDelegate,
DelegateForUserName = t.DelegateForUserName,
})
.ToPagedListAsync(input.Page, input.PageSize);
}
///
/// 我的已办(分页)
///
[HttpPost]
[ApiDescriptionSettings(Name = "MyDonePage")]
[DisplayName("我的已办")]
public async Task> MyDonePage(TaskPageInput input)
{
var userId = _userManager.UserId;
return await _taskRep.AsQueryable()
.InnerJoin((t, i) => t.InstanceId == i.Id)
.Where((t, i) => t.AssigneeId == userId && t.Status != FlowTaskStatusEnum.Pending)
.WhereIF(!string.IsNullOrWhiteSpace(input.BizType), (t, i) => i.BizType == input.BizType)
.OrderByDescending((t, i) => t.ActionTime)
.Select((t, i) => new TaskPageOutput
{
Id = t.Id,
InstanceId = t.InstanceId,
NodeId = t.NodeId,
NodeName = t.NodeName,
Status = t.Status,
Comment = t.Comment,
ActionTime = t.ActionTime,
Title = i.Title,
BizType = i.BizType,
BizId = i.BizId,
BizNo = i.BizNo,
InitiatorName = i.InitiatorName,
StartTime = i.StartTime,
CreateTime = t.CreateTime,
IsDelegate = t.IsDelegate,
DelegateForUserName = t.DelegateForUserName,
})
.ToPagedListAsync(input.Page, input.PageSize);
}
///
/// 我发起的(分页)
///
[HttpPost]
[ApiDescriptionSettings(Name = "MyInitiatedPage")]
[DisplayName("我发起的")]
public async Task> MyInitiatedPage(InstancePageInput input)
{
var userId = _userManager.UserId;
var page = await _instanceRep.AsQueryable()
.Where(i => i.InitiatorId == userId)
.WhereIF(!string.IsNullOrWhiteSpace(input.BizType), i => i.BizType == input.BizType)
.WhereIF(input.Status.HasValue, i => i.Status == input.Status!.Value)
.OrderByDescending(i => i.CreateTime)
.Select(i => new InstancePageOutput
{
Id = i.Id,
BizType = i.BizType,
BizId = i.BizId,
BizNo = i.BizNo,
Title = i.Title,
Status = i.Status,
CurrentNodeId = i.CurrentNodeId,
StartTime = i.StartTime,
EndTime = i.EndTime,
InitiatorName = i.InitiatorName,
})
.ToPagedListAsync(input.Page, input.PageSize);
var runningIds = page.Items.Where(x => x.Status == FlowInstanceStatusEnum.Running).Select(x => x.Id).ToList();
if (runningIds.Count > 0)
{
var pendingAssignees = await _taskRep.AsQueryable()
.Where(t => runningIds.Contains(t.InstanceId) && t.Status == FlowTaskStatusEnum.Pending)
.Select(t => new { t.InstanceId, t.AssigneeName })
.ToListAsync();
var grouped = pendingAssignees.GroupBy(x => x.InstanceId)
.ToDictionary(g => g.Key, g => string.Join(", ", g.Select(x => x.AssigneeName).Distinct()));
foreach (var item in page.Items)
{
if (grouped.TryGetValue(item.Id, out var names))
item.CurrentAssigneeName = names;
}
}
return page;
}
///
/// 待办数量
///
[HttpGet]
[ApiDescriptionSettings(Name = "MyPendingCount")]
[DisplayName("待办数量")]
public async Task MyPendingCount()
{
var userId = _userManager.UserId;
return await _taskRep.AsQueryable()
.Where(t => t.AssigneeId == userId && t.Status == FlowTaskStatusEnum.Pending)
.CountAsync();
}
// ── 操作 ──
///
/// 只能经专用业务入口审批的业务类型 → 对应的引导文案。
///
/// 背景:这些业务在专用服务里额外建立了通用审批链没有的 authority
/// (租户解析 → 业务单归属校验 → 当前节点守卫 → UseTranAsync 事务边界),
/// 走通用 flowTask/approve|reject 会把这四层全部绕过(引擎侧 GetPendingTask
/// 只校验「任务存在 / Pending / 指派人==当前用户」,不校验业务类型与租户)。
/// 仅隐藏前端按钮不足以闭环——手工 HTTP 仍可直达,故在网关层 fail-closed。
///
/// 这里刻意硬编码 BizType 字符串:ApprovalFlow 插件不引用 AiDOP 插件
/// (依赖方向为 AiDOP → ApprovalFlow,反向会成环),无法引用业务侧常量。
///
private static readonly Dictionary DedicatedEntryOnlyBizTypes = new()
{
["S5_IQC_INSPBILL"] = "来料检验单请在「来料检验单详情」页完成审核,不支持在审批中心直接同意/拒绝。",
// S6 过程检验单:N3_QE_DISPOSITION 的处置记录(ado_s6_ipqc_quality_disposition)是由
// S6ProcessInspectionReview.qe-disposition 在同一事务里 INSERT 后才推进流程的。走通用审批会把
// 流程直接推到 end 而永不写处置记录 —— 流程显示已闭环、处置轨迹却为空,且事后无法补记。
// 另:专用 supervisor-return 要求退回意见必填且回退到 N1,通用 Reject 会把实例直接置为终态。
["S6_PROCESS_INSPECTION"] = "过程检验单请在「过程检验」业务页完成主管审核或质量处置,不支持在审批中心直接同意/拒绝。",
};
///
/// 通用审批动作前置守卫:按 taskId 从库中反查权威 BizType,命中名单即拒绝。
///
/// 只做「拒绝」不做「放行授权」,故不引入新的租户判定
/// (ApprovalFlowTask / ApprovalFlowInstance 无 TenantId 列,租户边界仍由
/// 各业务专用入口的业务表 tenant 谓词负责)。
/// 任务/实例不存在时不在此抛错,交由引擎沿用既有错误语义。
///
private async Task EnsureDedicatedEntryOnlyAsync(long taskId)
{
var task = await _taskRep.GetByIdAsync(taskId);
if (task == null) return;
var instance = await _instanceRep.GetByIdAsync(task.InstanceId);
if (instance?.BizType == null) return;
if (DedicatedEntryOnlyBizTypes.TryGetValue(instance.BizType, out var hint))
throw Oops.Oh(hint);
}
///
/// 同意
///
[HttpPost]
[ApiDescriptionSettings(Name = "Approve")]
[DisplayName("同意")]
public async Task Approve(TaskActionInput input)
{
await EnsureDedicatedEntryOnlyAsync(input.TaskId);
await _engine.Approve(input.TaskId, input.Comment);
}
///
/// 拒绝
///
[HttpPost]
[ApiDescriptionSettings(Name = "Reject")]
[DisplayName("拒绝")]
public async Task Reject(TaskActionInput input)
{
await EnsureDedicatedEntryOnlyAsync(input.TaskId);
await _engine.Reject(input.TaskId, input.Comment);
}
///
/// 转办
///
[HttpPost]
[ApiDescriptionSettings(Name = "Transfer")]
[DisplayName("转办")]
public async Task Transfer(TaskTransferInput input)
{
await _engine.Transfer(input.TaskId, input.TargetUserId, input.Comment);
}
///
/// 撤回
///
[HttpPost]
[ApiDescriptionSettings(Name = "Withdraw")]
[DisplayName("撤回")]
public async Task Withdraw(TaskWithdrawInput input)
{
await _engine.Withdraw(input.InstanceId);
}
///
/// 退回上一步
///
[HttpPost]
[ApiDescriptionSettings(Name = "ReturnToPrev")]
[DisplayName("退回上一步")]
public async Task ReturnToPrev(TaskActionInput input)
{
await _engine.ReturnToPrev(input.TaskId, input.Comment);
}
///
/// 加签
///
[HttpPost]
[ApiDescriptionSettings(Name = "AddSign")]
[DisplayName("加签")]
public async Task AddSign(TaskAddSignInput input)
{
await _engine.AddSign(input.TaskId, input.TargetUserId, input.Comment);
}
///
/// 升级
///
[HttpPost]
[ApiDescriptionSettings(Name = "Escalate")]
[DisplayName("升级")]
public async Task Escalate(TaskActionInput input)
{
await _engine.Escalate(input.TaskId, input.Comment);
}
///
/// 获取指定任务所属节点的升级配置
///
[HttpGet]
[ApiDescriptionSettings(Name = "GetEscalationConfig")]
[DisplayName("获取升级配置")]
public async Task GetEscalationConfig([FromQuery] long taskId)
{
var task = await _taskRep.GetByIdAsync(taskId);
if (task == null) return new EscalationConfigOutput { Enabled = false };
var instance = await _instanceRep.GetByIdAsync(task.InstanceId);
if (instance == null) return new EscalationConfigOutput { Enabled = false };
var flowData = System.Text.Json.JsonSerializer.Deserialize(instance.FlowJsonSnapshot ?? "{}");
var node = flowData?.Nodes?.FirstOrDefault(n => n.Id == task.NodeId);
var props = node?.Properties;
return new EscalationConfigOutput
{
Enabled = props?.EnableManualEscalation == true
&& !string.IsNullOrWhiteSpace(props?.EscalationApproverType)
&& !string.IsNullOrWhiteSpace(props?.EscalationApproverIds),
EscalationApproverNames = props?.EscalationApproverNames,
};
}
///
/// 催办
///
[HttpPost]
[ApiDescriptionSettings(Name = "Urge")]
[DisplayName("催办")]
public async Task Urge(TaskUrgeInput input)
{
await _engine.Urge(input.InstanceId);
}
///
/// 立即触发一次超时任务扫描(等价于 FlowTimeoutJob 的单次执行)
/// 仅 SuperAdmin / SysAdmin 可调用,用于运维排障与 E2E 自动化验收。
///
[HttpPost]
[ApiDescriptionSettings(Name = "TriggerTimeoutScan")]
[DisplayName("立即扫描超时任务")]
public async Task TriggerTimeoutScan()
{
if (!_userManager.SuperAdmin && !_userManager.SysAdmin)
throw Oops.Oh("仅超级管理员/系统管理员可触发该操作");
return await _engine.ScanTimeoutTasks();
}
///
/// 批量同意
///
[HttpPost]
[ApiDescriptionSettings(Name = "BatchApprove")]
[DisplayName("批量同意")]
public async Task BatchApprove(BatchActionInput input)
{
var result = new BatchActionResult();
foreach (var taskId in input.TaskIds)
{
try
{
await EnsureDedicatedEntryOnlyAsync(taskId);
await _engine.Approve(taskId, input.Comment);
result.SuccessCount++;
}
catch (Exception ex)
{
result.FailCount++;
result.FailItems.Add(new BatchActionFailItem { TaskId = taskId, Error = ex.Message });
}
}
return result;
}
///
/// 批量拒绝
///
[HttpPost]
[ApiDescriptionSettings(Name = "BatchReject")]
[DisplayName("批量拒绝")]
public async Task BatchReject(BatchActionInput input)
{
var result = new BatchActionResult();
foreach (var taskId in input.TaskIds)
{
try
{
await EnsureDedicatedEntryOnlyAsync(taskId);
await _engine.Reject(taskId, input.Comment);
result.SuccessCount++;
}
catch (Exception ex)
{
result.FailCount++;
result.FailItems.Add(new BatchActionFailItem { TaskId = taskId, Error = ex.Message });
}
}
return result;
}
}