using Admin.NET.Plugin.AiDOP.Entity;
using Admin.NET.Plugin.AiDOP.Manufacturing.Dto;
using Admin.NET.Plugin.ApprovalFlow;
using Admin.NET.Plugin.ApprovalFlow.Service;
using Yitter.IdGenerator;
namespace Admin.NET.Plugin.AiDOP.Manufacturing;
///
/// S6 Phase 1D 过程检验单(Phase 1C bill)审核流程服务(桥接:Phase 1C Complete → ApprovalFlow)。
///
/// business_id = ado_s6_process_inspection_bill.id(BizType=S6_PROCESS_INSPECTION)。复用 ApprovalFlow 引擎,不改核心。
/// 提交门槛:结果 status=COMPLETED 才能提交审核;幂等:一单最多一个 active flow。
/// N1_SUBMIT=发起人本人(提交即自动通过)→ N2_SUP_REVIEW 主管审核 → GW_RESULT(overall_result=FAIL→N3 / PASS→end)
/// → N3_QE_DISPOSITION 质量工程师处置 → end。质量结果 overall_result 全程只读、审批不改写(§20 硬门)。
/// 处置落 ado_s6_ipqc_quality_disposition(biz_type 区分,复用不建重复表)。严格租户隔离(ResolveTenantOrThrow)。
///
[ApiDescriptionSettings(Order = 305, Description = "过程检验单审核")]
[Route("api/S6ProcessInspectionReview")]
[NonUnify]
public class S6ProcessInspectionReviewService : IDynamicApiController, ITransient
{
private const long MainTenantId = 1300000000001L;
private readonly ISqlSugarClient _db;
private readonly FlowEngineService _flowEngine;
private readonly UserManager _userManager;
public S6ProcessInspectionReviewService(ISqlSugarClient db, FlowEngineService flowEngine, UserManager userManager)
{
_db = db;
_flowEngine = flowEngine;
_userManager = userManager;
}
private long ResolveTenantOrThrow()
{
var tid = _userManager.TenantId;
if (tid <= 0) throw Oops.Oh("无法确定当前租户,请重新登录或选择目标租户");
if (_userManager.SuperAdmin && tid == MainTenantId) throw Oops.Oh("超级管理员操作前必须选择目标租户");
return tid;
}
/// 审核流状态(只读,供前端按钮控制)。
[DisplayName("过程检验单审核状态")]
[HttpGet("state")]
public async Task GetState([FromQuery] long billId)
{
await LoadBillAsync(billId);
return await BuildStateAsync(billId);
}
/// 提交审核(仅 COMPLETED 可提交;幂等;发起流程并自动通过 N1→N2)。
[DisplayName("提交审核")]
[HttpPost("submit-review")]
public async Task SubmitReview([FromBody] S6SubmitReviewInput input)
{
var bill = await LoadBillAsync(input.BillId);
if (!string.Equals(bill.ResultStatus, S6ProcessInspectionFlowConst.StatusCompleted, StringComparison.OrdinalIgnoreCase))
throw Oops.Oh("INSPECTION_NOT_COMPLETED:检验未完成,无法提交审核");
var inst = await GetLatestInstanceAsync(input.BillId);
if (inst != null && inst.Status == FlowInstanceStatusEnum.Running)
throw Oops.Oh("该检验单已在审核中,请勿重复提交");
if (inst != null && inst.Status != FlowInstanceStatusEnum.Running && inst.Status != FlowInstanceStatusEnum.Rejected && inst.Status != FlowInstanceStatusEnum.Cancelled)
throw Oops.Oh("该检验单审核流程已结束");
var tran = await _db.AsTenant().UseTranAsync(async () =>
{
var instanceId = await _flowEngine.StartFlow(new StartFlowInput
{
BizType = S6ProcessInspectionFlowConst.BizType,
BizId = input.BillId,
BizNo = bill.WorkOrderNo,
Title = $"过程检验单 {bill.WorkOrderNo}",
Comment = input.Comment,
});
// N1_SUBMIT=发起人本人,取自己的 N1 待办并自动通过 → N2
var myTask = await GetMyPendingTaskAsync(instanceId);
if (myTask == null || myTask.NodeId != S6ProcessInspectionFlowConst.NodeSubmit)
throw Oops.Oh("提交审核节点异常,未取到发起人任务");
await _flowEngine.Approve(myTask.Id, input.Comment);
});
if (!tran.IsSuccess) throw tran.ErrorException;
return await BuildStateAsync(input.BillId);
}
/// 主管审核通过(N2):Approve→GW_RESULT,按 overall_result 自动分流(FAIL→N3 / PASS→end)。不改写质量结果。
[DisplayName("主管审核通过")]
[HttpPost("supervisor-approve")]
public async Task SupervisorApprove([FromBody] S6ReviewApproveInput input)
{
await LoadBillAsync(input.BillId);
var (_, myTask) = await RequireSupervisorTaskAsync(input.BillId);
var tran = await _db.AsTenant().UseTranAsync(async () =>
{
await _flowEngine.Approve(myTask.Id, input.Comment);
});
if (!tran.IsSuccess) throw tran.ErrorException;
return await BuildStateAsync(input.BillId);
}
/// 主管退回(N2→N1,意见必填)。质量结果不改写。
[DisplayName("主管退回")]
[HttpPost("supervisor-return")]
public async Task SupervisorReturn([FromBody] S6ReviewReturnInput input)
{
if (string.IsNullOrWhiteSpace(input.Comment)) throw Oops.Oh("退回必须填写意见");
await LoadBillAsync(input.BillId);
var (_, myTask) = await RequireSupervisorTaskAsync(input.BillId);
var tran = await _db.AsTenant().UseTranAsync(async () =>
{
await _flowEngine.ReturnToPrev(myTask.Id, input.Comment);
});
if (!tran.IsSuccess) throw tran.ErrorException;
return await BuildStateAsync(input.BillId);
}
/// QE 质量处置(N3):落处置记录(不改写 overall_result),Approve→end。一单一有效处置。
[DisplayName("质量处置")]
[HttpPost("qe-disposition")]
public async Task QeDisposition([FromBody] S6QeDispositionInput input)
{
var bill = await LoadBillAsync(input.BillId);
var type = (input.DispositionType ?? "").Trim().ToUpperInvariant();
if (type != S6ProcessInspectionFlowConst.DispTypeRework && type != S6ProcessInspectionFlowConst.DispTypeConcession
&& type != S6ProcessInspectionFlowConst.DispTypeScrap && type != S6ProcessInspectionFlowConst.DispTypeOther)
throw Oops.Oh("处置类型非法(REWORK/CONCESSION/SCRAP/OTHER)");
if (string.IsNullOrWhiteSpace(input.DispositionOpinion)) throw Oops.Oh("处置意见必填");
var inst = await GetLatestInstanceAsync(input.BillId);
if (inst == null || inst.Status != FlowInstanceStatusEnum.Running) throw Oops.Oh("该检验单审核流程已结束");
var myTask = await GetMyPendingTaskAsync(inst.Id);
if (myTask == null) throw Oops.Oh("当前用户没有待处理的处置任务(需质量工程师角色成员)");
if (myTask.NodeId != S6ProcessInspectionFlowConst.NodeDisposition) throw Oops.Oh("当前不在质量处置节点");
var tid = ResolveTenantOrThrow();
// 一单一有效处置(status=1 且未软删)+ InspectionId 唯一约束
var existed = await _db.Queryable().ClearFilter()
.AnyAsync(x => x.InspectionId == input.BillId && x.TenantId == tid && x.Status == 1);
if (existed) throw Oops.Oh("该检验单已存在有效质量处置记录");
var now = DateTime.Now;
var opName = _userManager.RealName ?? _userManager.Account;
var tran = await _db.AsTenant().UseTranAsync(async () =>
{
await _db.Insertable(new AdoS6IpqcQualityDisposition
{
Id = YitIdHelper.NextId(),
TenantId = tid,
InspectionId = input.BillId,
InspectionNo = bill.WorkOrderNo,
BizType = S6ProcessInspectionFlowConst.BizType,
FlowInstanceId = inst.Id,
DispositionNo = $"S6PIDISP-{bill.WorkOrderNo}-{now:yyyyMMddHHmmss}",
DispositionType = type,
DispositionQty = input.DispositionQty,
ReworkQty = input.ReworkQty,
ConcessionQty = input.ConcessionQty,
ScrapQty = input.ScrapQty,
DispositionOpinion = input.DispositionOpinion,
ResponsibilityDept = input.ResponsibilityDept,
ResponsibleUser = input.ResponsibleUser,
Status = 1,
SubmittedBy = _userManager.UserId,
SubmittedByName = opName,
SubmittedAt = now,
RowVersion = 1,
CreateTime = now,
}).ExecuteCommandAsync();
await _flowEngine.Approve(myTask.Id, input.Comment);
});
if (!tran.IsSuccess) throw tran.ErrorException;
return await BuildStateAsync(input.BillId);
}
// ───────────────────────── helpers ─────────────────────────
private sealed class BillRow
{
public long Id { get; set; }
public string? WorkOrderNo { get; set; }
public string? OverallResult { get; set; }
public string? ResultStatus { get; set; }
}
/// 加载 Phase 1C 单据 + 结果(严格租户隔离)。
private async Task LoadBillAsync(long billId)
{
if (billId <= 0) throw Oops.Oh("检验单 id 非法");
var tid = ResolveTenantOrThrow();
var row = await _db.Ado.SqlQuerySingleAsync(
"""
SELECT b.id AS Id, b.work_order_no AS WorkOrderNo, r.overall_result AS OverallResult, r.status AS ResultStatus
FROM ado_s6_process_inspection_bill b
LEFT JOIN ado_s6_process_inspection_result r ON r.bill_id=b.id AND r.tenant_id=b.tenant_id
WHERE b.id=@id AND b.tenant_id=@tid LIMIT 1
""",
new List { new("@id", billId), new("@tid", tid) });
if (row == null) throw Oops.Oh("检验单不存在");
return row;
}
private async Task GetLatestInstanceAsync(long bizId)
=> await _db.Queryable().ClearFilter()
.Where(x => x.BizType == S6ProcessInspectionFlowConst.BizType && x.BizId == bizId)
.OrderByDescending(x => x.Id).FirstAsync();
private async Task GetMyPendingTaskAsync(long instanceId)
{
var userId = _userManager.UserId;
return await _db.Queryable().ClearFilter()
.Where(x => x.InstanceId == instanceId && x.AssigneeId == userId && x.Status == FlowTaskStatusEnum.Pending)
.FirstAsync();
}
private async Task<(ApprovalFlowInstance inst, ApprovalFlowTask task)> RequireSupervisorTaskAsync(long bizId)
{
var inst = await GetLatestInstanceAsync(bizId);
if (inst == null) throw Oops.Oh("该检验单尚未发起审核");
if (inst.Status != FlowInstanceStatusEnum.Running) throw Oops.Oh("该检验单审核流程已结束");
var myTask = await GetMyPendingTaskAsync(inst.Id);
if (myTask == null) throw Oops.Oh("当前用户没有待处理的审核任务(需检验主管角色成员)");
if (myTask.NodeId != S6ProcessInspectionFlowConst.NodeSupervisor) throw Oops.Oh("当前不在主管审核节点");
return (inst, myTask);
}
private async Task HasRoleAsync(long roleId)
{
var userId = _userManager.UserId;
return await _db.Queryable().ClearFilter().AnyAsync(x => x.UserId == userId && x.RoleId == roleId);
}
private async Task BuildStateAsync(long billId)
{
var tid = ResolveTenantOrThrow();
var output = new S6ReviewStateOutput { BillId = billId };
var bill = await _db.Ado.SqlQuerySingleAsync(
"""
SELECT b.id AS Id, b.work_order_no AS WorkOrderNo, r.overall_result AS OverallResult, r.status AS ResultStatus
FROM ado_s6_process_inspection_bill b
LEFT JOIN ado_s6_process_inspection_result r ON r.bill_id=b.id AND r.tenant_id=b.tenant_id
WHERE b.id=@id AND b.tenant_id=@tid LIMIT 1
""",
new List { new("@id", billId), new("@tid", tid) });
var completed = string.Equals(bill?.ResultStatus, S6ProcessInspectionFlowConst.StatusCompleted, StringComparison.OrdinalIgnoreCase);
output.OverallResult = bill?.OverallResult;
output.ResultStatus = bill?.ResultStatus;
output.QualityText = QualityTextOf(bill?.OverallResult);
output.DispositionRequired = string.Equals(bill?.OverallResult, S6ProcessInspectionFlowConst.ResultFail, StringComparison.OrdinalIgnoreCase) ? 1 : 0;
output.Disposition = await LoadDispositionAsync(billId, tid);
output.HasDisposition = output.Disposition != null;
var inst = await GetLatestInstanceAsync(billId);
if (inst == null)
{
output.FlowStatus = "NotStarted";
output.CanSubmitReview = completed;
output.DisplayStatus = completed ? "待提交审核" : "检验中";
output.Message = completed ? "检验完成,待提交审核" : "检验未完成,暂不可提交审核";
return output;
}
output.InstanceId = inst.Id;
output.FlowStatus = inst.Status.ToString();
output.History = await LoadHistoryAsync(inst.Id, tid);
if (inst.Status != FlowInstanceStatusEnum.Running)
{
output.IsCompleted = true;
output.DisplayStatus = inst.Status == FlowInstanceStatusEnum.Approved ? "已关闭"
: inst.Status == FlowInstanceStatusEnum.Rejected ? "已驳回"
: inst.Status == FlowInstanceStatusEnum.Cancelled ? "已撤回" : "已结束";
output.Message = inst.Status == FlowInstanceStatusEnum.Approved ? "审核已闭环(CLOSED)" : $"流程已结束({inst.Status})";
output.CanSubmitReview = inst.Status is FlowInstanceStatusEnum.Rejected or FlowInstanceStatusEnum.Cancelled && completed;
return output;
}
output.CurrentNodeCode = inst.CurrentNodeId;
output.DisplayStatus = inst.CurrentNodeId switch
{
S6ProcessInspectionFlowConst.NodeSupervisor => "主管审核中",
S6ProcessInspectionFlowConst.NodeDisposition => "QE质量处置中",
_ => "审核中",
};
var myTask = await GetMyPendingTaskAsync(inst.Id);
if (myTask != null)
{
output.CurrentNodeName = NodeNameOf(myTask.NodeId) ?? myTask.NodeName;
if (myTask.NodeId == S6ProcessInspectionFlowConst.NodeSupervisor)
{
output.CurrentAssigneeRole = S6ProcessInspectionFlowConst.RoleSupervisorCode;
output.CanSupervisorApprove = true;
output.CanSupervisorReturn = true;
output.Message = "待当前用户主管审核";
}
else if (myTask.NodeId == S6ProcessInspectionFlowConst.NodeDisposition)
{
output.CurrentAssigneeRole = S6ProcessInspectionFlowConst.RoleQualityEngineerCode;
output.CanSubmitDisposition = true;
output.Message = "待当前用户提交质量处置";
}
else
{
output.Message = "流程进行中";
}
}
else
{
output.CurrentNodeName = NodeNameOf(inst.CurrentNodeId);
output.CurrentAssigneeRole = inst.CurrentNodeId switch
{
S6ProcessInspectionFlowConst.NodeDisposition => S6ProcessInspectionFlowConst.RoleQualityEngineerCode,
S6ProcessInspectionFlowConst.NodeSupervisor => S6ProcessInspectionFlowConst.RoleSupervisorCode,
_ => null,
};
output.Message = "流程进行中(当前用户无待办)";
}
return output;
}
// ── Phase 1E 展示辅助(业务中文映射;后端拥有语义)──
private static string QualityTextOf(string? overallResult) => (overallResult ?? "").ToUpperInvariant() switch
{
"PASS" => "合格",
"FAIL" => "不合格",
"PENDING" or "IN_PROGRESS" => "检验中",
"INVALID" => "无效",
_ => "—",
};
private static string? NodeNameOf(string? nodeId) => nodeId switch
{
S6ProcessInspectionFlowConst.NodeSubmit => "提交审核",
S6ProcessInspectionFlowConst.NodeSupervisor => "检验主管审核",
S6ProcessInspectionFlowConst.NodeDisposition => "质量处置",
_ => null,
};
private static string DispositionTypeTextOf(string? t) => (t ?? "").ToUpperInvariant() switch
{
"REWORK" => "返工",
"CONCESSION" => "让步接收",
"SCRAP" => "报废",
"OTHER" => "其它",
_ => t ?? "",
};
private static string ActionTextOf(int action) => action switch
{
1 => "提交",
2 => "通过",
3 => "驳回",
5 => "撤回",
6 => "退回",
9 => "取消",
_ => "操作",
};
private async Task LoadDispositionAsync(long billId, long tid)
{
var d = await _db.Queryable().ClearFilter()
.Where(x => x.InspectionId == billId && x.TenantId == tid && x.Status == 1)
.OrderByDescending(x => x.Id).FirstAsync();
if (d == null) return null;
return new S6DispositionView
{
DispositionType = d.DispositionType,
DispositionTypeText = DispositionTypeTextOf(d.DispositionType),
DispositionQty = d.DispositionQty,
DispositionOpinion = d.DispositionOpinion,
ResponsibilityDept = d.ResponsibilityDept,
ResponsibleUser = d.ResponsibleUser,
OperatorName = d.SubmittedByName,
SubmittedAt = d.SubmittedAt,
};
}
private sealed class FlowLogRow
{
public int Action { get; set; }
public string? NodeId { get; set; }
public string? OperatorName { get; set; }
public string? Comment { get; set; }
public DateTime? CreateTime { get; set; }
}
private async Task> LoadHistoryAsync(long instanceId, long tid)
{
// 复用正式 ApprovalFlowLog(不造第二套 history)。租户隔离由实例归属保证(instanceId 已按 BizType+BizId 定位)。
var rows = await _db.Ado.SqlQueryAsync(
"SELECT Action AS Action, NodeId AS NodeId, OperatorName AS OperatorName, Comment AS Comment, CreateTime AS CreateTime FROM ApprovalFlowLog WHERE InstanceId=@iid ORDER BY Id",
new List { new("@iid", instanceId) });
return rows.Select(r => new S6ReviewHistoryItem
{
Time = r.CreateTime,
OperatorName = r.OperatorName,
NodeName = NodeNameOf(r.NodeId) ?? r.NodeId,
ActionText = ActionTextOf(r.Action),
Comment = r.Comment,
}).ToList();
}
}