| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800 |
- using Admin.NET.Plugin.AiDOP.Dto.S8;
- using Admin.NET.Plugin.AiDOP.Entity.S0.Manufacturing;
- using Admin.NET.Plugin.AiDOP.Entity.S0.Warehouse;
- using Admin.NET.Plugin.AiDOP.Entity.S8;
- using Admin.NET.Plugin.AiDOP.Infrastructure.S8;
- using Admin.NET.Plugin.AiDOP.Service.S8.Rules;
- using Admin.NET.Plugin.ApprovalFlow.Service;
- using Microsoft.Extensions.Logging;
- namespace Admin.NET.Plugin.AiDOP.Service.S8;
- public class S8ManualReportService : ITransient
- {
- // S8-SEVERITY-FOLLOW-SERIOUS-STANDARDIZE-EXEC-1:业务枚举只保留 FOLLOW/SERIOUS。
- // 旧值 LOW/MEDIUM/HIGH/CRITICAL 仍可作为兼容输入(接收后由 S8SeverityCode.Normalize 归一)。
- private static readonly HashSet<string> AllowedSeverities = new(StringComparer.OrdinalIgnoreCase)
- {
- "FOLLOW", "SERIOUS",
- "LOW", "MEDIUM", "HIGH", "CRITICAL", // legacy compat
- };
- // S8-PROCESS-NODE-S1S7-ALIGN-1:process_node_code 当前阶段对齐 S1-S7 订单主流程。
- // 优先 module_code(已是 S1-S7),其次按 scene_code 反推;都无法识别则 null。
- // S8-PROCESS-NODE-MODULE-CODE-ALIGNMENT-EXEC-1:保留函数挂起待用——本阶段所有建单点不再调用,
- // 由 module_code 承担 S1-S7 主流程归属;未来引入更细流程节点(如 S2.PLAN / S6.WO_RELEASE)时恢复使用。
- private static string? ResolveProcessNodeCode(string? sceneCode, string? moduleCode)
- {
- if (!string.IsNullOrWhiteSpace(moduleCode) && S8ModuleCode.All.Contains(moduleCode))
- return moduleCode;
- var fromScene = S8ModuleCode.FromScene(sceneCode);
- if (!string.IsNullOrWhiteSpace(fromScene) && S8ModuleCode.All.Contains(fromScene))
- return fromScene;
- return null;
- }
- /// <summary>
- /// S8-EXCEPTION-CREATION-MODULE-CODE-FIX-1:建单 module_code 派生统一入口。
- /// 优先级:显式 module → hit module → 单模块 sceneCode(S1-S7)→ exception_type.scene_code。
- /// 严格 S1-S7 校验,不接受 legacy 复合 scene;最终无法确定时返回 null(caller 决定记日志或拒绝)。
- /// </summary>
- private async Task<string?> ResolveModuleCodeAsync(
- string? explicitModuleCode,
- string? hitModuleCode,
- string? sceneCode,
- string? exceptionTypeCode)
- {
- var byExplicit = S8ModuleCode.Normalize(explicitModuleCode);
- if (byExplicit != null) return byExplicit;
- var byHit = S8ModuleCode.Normalize(hitModuleCode);
- if (byHit != null) return byHit;
- var byScene = S8ModuleCode.FromCanonicalScene(sceneCode);
- if (byScene != null) return byScene;
- if (!string.IsNullOrWhiteSpace(exceptionTypeCode))
- {
- var typeScene = await _typeRep.AsQueryable().ClearFilter()
- .Where(t => t.TypeCode == exceptionTypeCode && t.Enabled)
- .OrderByDescending(t => t.FactoryId)
- .Select(t => t.SceneCode)
- .FirstAsync();
- var byType = S8ModuleCode.FromCanonicalScene(typeScene);
- if (byType != null) return byType;
- }
- return null;
- }
- private readonly SqlSugarRepository<AdoS8Exception> _rep;
- private readonly SqlSugarRepository<AdoS8ExceptionTimeline> _timelineRep;
- private readonly SqlSugarRepository<AdoS8Evidence> _evidenceRep;
- private readonly SqlSugarRepository<AdoS8SceneConfig> _sceneRep;
- private readonly SqlSugarRepository<AdoS0DepartmentMaster> _deptRep;
- private readonly SqlSugarRepository<AdoS0LineMaster> _lineRep;
- private readonly SqlSugarRepository<AdoS8ExceptionType> _typeRep;
- private readonly SqlSugarRepository<AdoS0EmployeeMaster> _empRep;
- // S8-AUTO-WATCH-DEPT-RESOLVE-1(P2):自动建单部门 resolver 需读取 watch_rule.params_json 里的默认部门字段。
- private readonly SqlSugarRepository<AdoS8WatchRule> _ruleRep;
- private readonly UserManager _userManager;
- private readonly FlowEngineService _flowEngine;
- private readonly ILogger<S8ManualReportService> _logger;
- public S8ManualReportService(
- SqlSugarRepository<AdoS8Exception> rep,
- SqlSugarRepository<AdoS8ExceptionTimeline> timelineRep,
- SqlSugarRepository<AdoS8Evidence> evidenceRep,
- SqlSugarRepository<AdoS8SceneConfig> sceneRep,
- SqlSugarRepository<AdoS0DepartmentMaster> deptRep,
- SqlSugarRepository<AdoS0LineMaster> lineRep,
- SqlSugarRepository<AdoS8ExceptionType> typeRep,
- SqlSugarRepository<AdoS0EmployeeMaster> empRep,
- SqlSugarRepository<AdoS8WatchRule> ruleRep,
- UserManager userManager,
- FlowEngineService flowEngine,
- ILogger<S8ManualReportService> logger)
- {
- _rep = rep;
- _timelineRep = timelineRep;
- _evidenceRep = evidenceRep;
- _sceneRep = sceneRep;
- _deptRep = deptRep;
- _lineRep = lineRep;
- _typeRep = typeRep;
- _empRep = empRep;
- _ruleRep = ruleRep;
- _userManager = userManager;
- _flowEngine = flowEngine;
- _logger = logger;
- }
- // S8-SLA-TIMEOUT-RUNTIME-1(P3):按 exception_type.sla_minutes 计算 sla_deadline。
- // typeCode 空 / type 缺失 / sla_minutes <= 0 → 返回 null(不阻断建单,仅 LogWarning)。
- // 不写 timeout_flag;timeout_flag 已降级为 legacy 字段,当前超时由读端基于 sla_deadline + status 在线计算。
- private async Task<DateTime?> ResolveSlaDeadlineAsync(string? exceptionTypeCode, DateTime createdAt)
- {
- if (string.IsNullOrWhiteSpace(exceptionTypeCode)) return null;
- var slaMinutes = await _typeRep.AsQueryable().ClearFilter()
- .Where(t => t.TypeCode == exceptionTypeCode)
- .OrderByDescending(t => t.FactoryId)
- .Select(t => (int?)t.SlaMinutes)
- .FirstAsync();
- if (slaMinutes == null)
- {
- _logger.LogWarning("s8_sla_type_not_found exceptionTypeCode={TypeCode}", exceptionTypeCode);
- return null;
- }
- if (slaMinutes.Value <= 0) return null;
- return createdAt.AddMinutes(slaMinutes.Value);
- }
- // S8-AUTO-WATCH-DEPT-RESOLVE-1(P2):自动建单部门解析顺序 hit → watch_rule.params_json default → 未归属。
- // 单字段独立解析(occurrence / responsible 各自走优先级)。任一字段最终为 null → 调用方按"throw 让 scheduler 跳过"处理。
- // 不允许写 0 作为最终值;不硬编码 1=质量部 / 2=生产部;不猜测业务对象部门派生(待后续增强)。
- // 协议常量:未归属部门 codename = D-UNASSIGNED;按 factory_ref_id 唯一存在;本批不创建该基线,缺失时 resolver 返回 null。
- private const string UnassignedDepartmentCode = "D-UNASSIGNED";
- private sealed class AutoWatchDeptResolution
- {
- public long? OccurrenceDeptId { get; set; }
- public long? ResponsibleDeptId { get; set; }
- public string OccurrenceSource { get; set; } = "failed";
- public string ResponsibleSource { get; set; } = "failed";
- }
- private async Task<AutoWatchDeptResolution> ResolveAutoWatchDepartmentsAsync(
- string path,
- long factoryId,
- long? hitOccurrenceDeptId,
- long? hitResponsibleDeptId,
- long? ruleId,
- string? ruleCode,
- string? moduleCode,
- string? sceneCode,
- string? exceptionTypeCode,
- string? sourceObjectType,
- string? sourceObjectId,
- string? relatedObjectCode,
- string? dedupKey)
- {
- var result = new AutoWatchDeptResolution();
- // 1) hit dept 优先
- var occOk = await ValidateDeptForFactoryAsync(hitOccurrenceDeptId, factoryId);
- var respOk = await ValidateDeptForFactoryAsync(hitResponsibleDeptId, factoryId);
- if (occOk) { result.OccurrenceDeptId = hitOccurrenceDeptId; result.OccurrenceSource = "hit"; }
- if (respOk) { result.ResponsibleDeptId = hitResponsibleDeptId; result.ResponsibleSource = "hit"; }
- // 2) watch_rule.params_json 默认部门
- if ((!occOk || !respOk) && ruleId.HasValue && ruleId.Value > 0)
- {
- var paramsJson = await _ruleRep.AsQueryable()
- .Where(x => x.Id == ruleId.Value)
- .Select(x => x.ParamsJson)
- .FirstAsync();
- var (paramsOcc, paramsResp) = ParseParamsDefaultDepts(paramsJson);
- if (!occOk && paramsOcc.HasValue)
- {
- if (await ValidateDeptForFactoryAsync(paramsOcc, factoryId))
- {
- result.OccurrenceDeptId = paramsOcc;
- result.OccurrenceSource = "watch_rule_params";
- }
- else
- {
- _logger.LogWarning(
- "s8_auto_watch_default_dept_invalid path={Path} ruleId={RuleId} ruleCode={RuleCode} field=defaultOccurrenceDeptId value={Value} factoryId={FactoryId}",
- path, ruleId, ruleCode, paramsOcc, factoryId);
- }
- }
- if (!respOk && paramsResp.HasValue)
- {
- if (await ValidateDeptForFactoryAsync(paramsResp, factoryId))
- {
- result.ResponsibleDeptId = paramsResp;
- result.ResponsibleSource = "watch_rule_params";
- }
- else
- {
- _logger.LogWarning(
- "s8_auto_watch_default_dept_invalid path={Path} ruleId={RuleId} ruleCode={RuleCode} field=defaultResponsibleDeptId value={Value} factoryId={FactoryId}",
- path, ruleId, ruleCode, paramsResp, factoryId);
- }
- }
- }
- // 3) 未归属部门 fallback
- long? unassignedId = null;
- if (result.OccurrenceDeptId == null || result.ResponsibleDeptId == null)
- {
- unassignedId = await _deptRep.AsQueryable().ClearFilter()
- .Where(x => x.Department == UnassignedDepartmentCode && x.FactoryRefId == factoryId && x.IsActive)
- .Select(x => (long?)x.Id)
- .FirstAsync();
- if (result.OccurrenceDeptId == null && unassignedId.HasValue)
- {
- result.OccurrenceDeptId = unassignedId;
- result.OccurrenceSource = "unassigned";
- _logger.LogWarning(
- "s8_auto_watch_dept_unassigned_fallback path={Path} ruleId={RuleId} ruleCode={RuleCode} field=occurrence factoryId={FactoryId} unassignedDeptId={UnassignedId} sourceObjectType={SourceObjectType} sourceObjectId={SourceObjectId} relatedObjectCode={RelatedObjectCode} dedupKey={DedupKey}",
- path, ruleId, ruleCode, factoryId, unassignedId, sourceObjectType, sourceObjectId, relatedObjectCode, dedupKey);
- }
- if (result.ResponsibleDeptId == null && unassignedId.HasValue)
- {
- result.ResponsibleDeptId = unassignedId;
- result.ResponsibleSource = "unassigned";
- _logger.LogWarning(
- "s8_auto_watch_dept_unassigned_fallback path={Path} ruleId={RuleId} ruleCode={RuleCode} field=responsible factoryId={FactoryId} unassignedDeptId={UnassignedId} sourceObjectType={SourceObjectType} sourceObjectId={SourceObjectId} relatedObjectCode={RelatedObjectCode} dedupKey={DedupKey}",
- path, ruleId, ruleCode, factoryId, unassignedId, sourceObjectType, sourceObjectId, relatedObjectCode, dedupKey);
- }
- }
- return result;
- }
- private async Task<bool> ValidateDeptForFactoryAsync(long? deptId, long factoryId)
- {
- if (!deptId.HasValue || deptId.Value <= 0 || factoryId <= 0) return false;
- return await _deptRep.AsQueryable().ClearFilter()
- .Where(x => x.Id == deptId.Value && x.FactoryRefId == factoryId && x.IsActive)
- .AnyAsync();
- }
- private static (long? Occurrence, long? Responsible) ParseParamsDefaultDepts(string? paramsJson)
- {
- if (string.IsNullOrWhiteSpace(paramsJson)) return (null, null);
- try
- {
- using var doc = System.Text.Json.JsonDocument.Parse(paramsJson);
- long? occ = ReadJsonLong(doc.RootElement, "defaultOccurrenceDeptId");
- long? resp = ReadJsonLong(doc.RootElement, "defaultResponsibleDeptId");
- return (occ, resp);
- }
- catch (System.Text.Json.JsonException) { return (null, null); }
- }
- private static long? ReadJsonLong(System.Text.Json.JsonElement root, string property)
- {
- if (root.ValueKind != System.Text.Json.JsonValueKind.Object) return null;
- if (!root.TryGetProperty(property, out var el)) return null;
- if (el.ValueKind == System.Text.Json.JsonValueKind.Number && el.TryGetInt64(out var v)) return v;
- if (el.ValueKind == System.Text.Json.JsonValueKind.String && long.TryParse(el.GetString(), out var sv)) return sv;
- return null;
- }
- // S8-REPORTER-IDSPACE-FIX-1(P0-B-1):把当前登录 SysUser.Id 反查 EmployeeMaster.RecID。
- // 用于 reporter_id idspace 与 assignee_id / verifier_id 对齐。
- // 协议:sys_user_id + factory_ref_id 双键命中;EmployeeMaster.tenant_id 与 SysUser.TenantId 历史错位 →
- // ClearFilter,由 factoryRefId 做硬边界(与 S8MasterDataAdapter / S8TaskFlowService 同口径)。
- // 未绑定时返回 null,调用方负责诊断日志。
- private async Task<long?> ResolveCurrentEmployeeIdAsync(long factoryId, long sysUserId)
- {
- if (sysUserId <= 0 || factoryId <= 0) return null;
- var emp = await _empRep.AsQueryable().ClearFilter()
- .Where(x => x.SysUserId == sysUserId && x.FactoryRefId == factoryId && x.IsActive)
- .Select(x => new { x.Id })
- .FirstAsync();
- return emp?.Id;
- }
- // S8-MANUAL-REPORT-DEFAULT-OCCURRENCE-DEPT-1(P1):取当前登录员工的业务部门作为默认发生部门。
- // 链路:SysUser.Id → EmployeeMaster.sys_user_id → EmployeeMaster.Department(string)
- // + EmployeeMaster.FactoryRefId → DepartmentMaster.Department + FactoryRefId → RecID。
- // 不绑定 / 部门为空 / 部门未匹配 / 命中多条:均返回 (null,null) + warning,绝不报错也不阻断页面。
- // 仅作为前端 form 默认初值;用户仍可改,CreateAsync 的 dept=0 guard 不变。
- private async Task<(long? DeptId, string? DeptName)> ResolveCurrentEmployeeDefaultDeptAsync(long factoryId, long sysUserId)
- {
- if (sysUserId <= 0 || factoryId <= 0) return (null, null);
- var emp = await _empRep.AsQueryable().ClearFilter()
- .Where(x => x.SysUserId == sysUserId && x.FactoryRefId == factoryId && x.IsActive)
- .Select(x => new { x.Id, x.Department, x.FactoryRefId })
- .FirstAsync();
- if (emp == null)
- {
- _logger.LogWarning(
- "s8_manual_report_default_dept_unbound_employee sysUserId={SysUserId} factoryId={FactoryId}",
- sysUserId, factoryId);
- return (null, null);
- }
- if (string.IsNullOrWhiteSpace(emp.Department))
- {
- _logger.LogWarning(
- "s8_manual_report_default_dept_employee_dept_empty sysUserId={SysUserId} factoryId={FactoryId} employeeId={EmployeeId}",
- sysUserId, factoryId, emp.Id);
- return (null, null);
- }
- var depts = await _deptRep.AsQueryable().ClearFilter()
- .Where(x => x.Department == emp.Department && x.FactoryRefId == emp.FactoryRefId && x.IsActive)
- .Select(x => new { x.Id, x.Department, x.Descr })
- .Take(2)
- .ToListAsync();
- if (depts.Count == 0)
- {
- _logger.LogWarning(
- "s8_manual_report_default_dept_unmatched sysUserId={SysUserId} factoryId={FactoryId} employeeId={EmployeeId} departmentCode={DeptCode}",
- sysUserId, factoryId, emp.Id, emp.Department);
- return (null, null);
- }
- if (depts.Count > 1)
- {
- _logger.LogWarning(
- "s8_manual_report_default_dept_ambiguous sysUserId={SysUserId} factoryId={FactoryId} employeeId={EmployeeId} departmentCode={DeptCode} matchedCount={Count}",
- sysUserId, factoryId, emp.Id, emp.Department, depts.Count);
- return (null, null);
- }
- var d = depts[0];
- return (d.Id, string.IsNullOrWhiteSpace(d.Descr) ? d.Department : d.Descr);
- }
- /// <summary>
- /// 主动提报推断 ExceptionTypeCode:场景下取启用且 SortNo 最小的一条。
- /// baseline 异常类型当前 tenant_id=0/factory_id=0(全局基线),所以匹配条件为
- /// (tenantId 命中 OR 0) AND (factoryId 命中 OR 0)。ClearFilter 兜底全局多租户过滤器。
- /// 找不到返回 null(保持兼容)。
- /// </summary>
- private async Task<string?> InferExceptionTypeCodeAsync(long tenantId, long factoryId, string sceneCode)
- {
- if (string.IsNullOrWhiteSpace(sceneCode)) return null;
- return await _typeRep.AsQueryable().ClearFilter()
- .Where(x => (x.TenantId == tenantId || x.TenantId == 0)
- && (x.FactoryId == factoryId || x.FactoryId == 0)
- && x.SceneCode == sceneCode && x.Enabled)
- .OrderBy(x => x.SortNo)
- .Select(x => x.TypeCode)
- .FirstAsync();
- }
- /// <summary>
- /// TB001 异常提报审批流:自动监控 + 主动提报后软触发,失败仅 warn 日志,不阻断建单。
- /// </summary>
- private async Task TryStartIntakeFlowAsync(AdoS8Exception entity)
- {
- try
- {
- await _flowEngine.StartFlow(new StartFlowInput
- {
- BizType = "EXCEPTION_REPORT",
- BizId = entity.Id,
- BizNo = entity.ExceptionCode,
- Title = $"异常提报 - {entity.ExceptionCode}",
- Comment = entity.SourceType == "AUTO_WATCH" ? "自动监控触发" : "主动提报触发",
- BizData = new Dictionary<string, object>
- {
- ["sceneCode"] = entity.SceneCode ?? "",
- ["exceptionTypeCode"] = entity.ExceptionTypeCode ?? "",
- ["sourceType"] = entity.SourceType ?? ""
- }
- });
- }
- catch (Exception ex)
- {
- _logger.LogWarning(ex, "TB001 异常提报审批流触发失败 ExceptionId={Id} ExceptionCode={Code}", entity.Id, entity.ExceptionCode);
- }
- }
- public async Task<object> GetFormOptionsAsync(long tenantId, long factoryId)
- {
- var scenes = await _sceneRep.AsQueryable()
- .Where(x => x.TenantId == tenantId && x.FactoryId == factoryId && x.Enabled)
- .OrderBy(x => x.SortNo)
- .Select(x => new { value = x.SceneCode, label = x.SceneName })
- .ToListAsync();
- // ClearFilter:DepartmentMaster.tenant_id 属 S0 域租户,不与登录 token TenantId 一致;
- // 用 factory_ref_id 做硬边界,安全等价。同 BUG-S8-EMPLOYEES-TENANT-FILTER 协议。
- var departments = await _deptRep.AsQueryable().ClearFilter()
- .Where(x => x.FactoryRefId == factoryId)
- .OrderBy(x => x.Department)
- .Take(500)
- .Select(x => new { value = x.Id, label = x.Descr ?? x.Department })
- .ToListAsync();
- var lines = await _lineRep.AsQueryable().ClearFilter()
- .Where(x => x.FactoryRefId == factoryId)
- .OrderBy(x => x.Line)
- .Take(500)
- .Select(x => new { value = x.Id, label = x.Describe ?? x.Line })
- .ToListAsync();
- // S8-MANUAL-REPORT-DEFAULT-OCCURRENCE-DEPT-1(P1):默认发生部门 = 当前登录员工的业务部门。
- // 未登录 / 未绑定 / 部门不可解析 → 返回 null,前端继续要求用户手动选择。
- var sysUserId = _userManager.UserId;
- var (defaultOccDeptId, defaultOccDeptName) = sysUserId > 0
- ? await ResolveCurrentEmployeeDefaultDeptAsync(factoryId, sysUserId)
- : (null, null);
- return new
- {
- scenes,
- // S8-SEVERITY-FOLLOW-SERIOUS-STANDARDIZE-EXEC-1:业务枚举 FOLLOW/SERIOUS 两档。
- severities = S8SeverityCode.Options(),
- departments,
- lines,
- materials = Array.Empty<object>(),
- defaultOccurrenceDeptId = defaultOccDeptId,
- defaultOccurrenceDeptName = defaultOccDeptName,
- };
- }
- public async Task<AdoS8ManualReportResultDto> CreateAsync(AdoS8ManualReportCreateDto dto)
- {
- if (string.IsNullOrWhiteSpace(dto.Title)) throw new S8BizException("标题必填");
- if (string.IsNullOrWhiteSpace(dto.SceneCode)) throw new S8BizException("场景必填");
- // S8-MANUAL-REPORT-DEPT-ZERO-GUARD-1(P0-B-3):人工提报禁止 dept=0 入库。
- // 字段类型 long(非空),未填默认 0;guard 命中 <=0 直接拒绝,绝不转 null/默认部门/未归属。
- // 范围仅限本入口;自动建单(CreateFromWatchAsync/CreateFromHitAsync)的 ?? 0 风险归 P0-B-4。
- if (dto.OccurrenceDeptId <= 0) throw new S8BizException("发生部门不能为空,请选择有效发生部门");
- if (dto.ResponsibleDeptId <= 0) throw new S8BizException("处理部门不能为空,请选择有效处理部门");
- // S8-SEVERITY-FOLLOW-SERIOUS-STANDARDIZE-EXEC-1:白名单接受新值(FOLLOW/SERIOUS)+ 旧值兼容;
- // 通过 Normalize 写入 DB 一律为 FOLLOW/SERIOUS。
- var rawSeverity = string.IsNullOrWhiteSpace(dto.Severity) ? S8SeverityCode.Follow : dto.Severity.Trim();
- if (!AllowedSeverities.Contains(rawSeverity))
- throw new S8BizException($"严重度 {rawSeverity} 非法,仅允许 FOLLOW/SERIOUS");
- var severity = S8SeverityCode.Normalize(rawSeverity);
- // 提报人以服务端登录上下文为准,忽略前端传入;未登录上下文落 null。
- // currentUserId 仍是 SysUser.Id,仅用作 timeline.OperatorId(与 S8TaskFlowService 同口径);
- // S8-REPORTER-IDSPACE-FIX-1:ReporterId 写入改用 EmployeeMaster.RecID(与 assignee/verifier 同 idspace)。
- var currentUserId = _userManager.UserId > 0 ? _userManager.UserId : (long?)null;
- long? currentEmployeeId = null;
- if (currentUserId.HasValue)
- {
- currentEmployeeId = await ResolveCurrentEmployeeIdAsync(dto.FactoryId, currentUserId.Value);
- if (currentEmployeeId == null)
- {
- _logger.LogWarning(
- "manual_report_reporter_unbound sysUserId={SysUserId} factoryId={FactoryId} title={Title}",
- currentUserId.Value, dto.FactoryId, dto.Title);
- }
- }
- // 主动提报无前端 type 字段,按场景兜底推断;保证不进"未分类"桶。
- var inferredType = await InferExceptionTypeCodeAsync(dto.TenantId, dto.FactoryId, dto.SceneCode.Trim());
- // S8-EXCEPTION-CREATION-MODULE-CODE-FIX-1:手工提报 module_code 严格按 S1-S7 派生;
- // dto 暂不携带显式 module_code,按 scene → exception_type.scene 链路降级。
- var resolvedModule = await ResolveModuleCodeAsync(
- explicitModuleCode: null,
- hitModuleCode: null,
- sceneCode: dto.SceneCode.Trim(),
- exceptionTypeCode: inferredType);
- if (resolvedModule == null)
- {
- _logger.LogWarning(
- "manual_report_module_code_unresolved sceneCode={SceneCode} exceptionTypeCode={TypeCode} title={Title}",
- dto.SceneCode, inferredType, dto.Title);
- }
- // S8-SLA-TIMEOUT-RUNTIME-1(P3):CreatedAt 与 SlaDeadline 共用同一 now,避免漂移。
- var now = DateTime.Now;
- var slaDeadline = await ResolveSlaDeadlineAsync(inferredType, now);
- var code = $"EX-{now:yyyyMMdd}-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";
- var entity = new AdoS8Exception
- {
- TenantId = dto.TenantId,
- FactoryId = dto.FactoryId,
- ExceptionCode = code,
- Title = dto.Title.Trim(),
- Description = dto.Description,
- SceneCode = dto.SceneCode.Trim(),
- SourceType = "MANUAL",
- Status = "NEW",
- Severity = severity,
- PriorityScore = 0,
- PriorityLevel = "P3",
- OccurrenceDeptId = dto.OccurrenceDeptId,
- ResponsibleDeptId = dto.ResponsibleDeptId,
- // S8-REPORTER-IDSPACE-FIX-1:reporter_id idspace = EmployeeMaster.RecID(与 assignee/verifier 同)。
- ReporterId = currentEmployeeId,
- ExceptionTypeCode = inferredType,
- ModuleCode = resolvedModule,
- // S8-PROCESS-NODE-MODULE-CODE-ALIGNMENT-EXEC-1:当前阶段 process_node_code 留空,
- // module_code 承担 S1-S7 主流程归属;process_node_code 留给未来更细流程节点。
- ProcessNodeCode = null,
- // S8-MANUAL-RELATED-OBJECT-FILL-1:手工提报支持自由文本关联对象编码(订单项/类订单项),
- // 空白归 null;不写 source_object_type / source_object_id / dedup_key(仍由自动监控链路独占)。
- RelatedObjectCode = string.IsNullOrWhiteSpace(dto.RelatedObjectCode) ? null : dto.RelatedObjectCode.Trim(),
- CreatedAt = now,
- // S8-SLA-TIMEOUT-RUNTIME-1:sla_deadline 由 exception_type.sla_minutes 决定;缺配置 → null。
- SlaDeadline = slaDeadline,
- IsDeleted = false
- };
- await _rep.AsTenant().UseTranAsync(async () =>
- {
- entity = await _rep.AsInsertable(entity).ExecuteReturnEntityAsync();
- await _timelineRep.InsertAsync(new AdoS8ExceptionTimeline
- {
- ExceptionId = entity.Id,
- ActionCode = "CREATE",
- ActionLabel = "创建",
- FromStatus = null,
- ToStatus = "NEW",
- OperatorId = currentUserId,
- ActionRemark = "主动提报",
- CreatedAt = DateTime.Now
- });
- }, ex => throw ex);
- await TryStartIntakeFlowAsync(entity);
- return new AdoS8ManualReportResultDto
- {
- ExceptionId = entity.Id,
- ExceptionCode = entity.ExceptionCode,
- TaskId = entity.Id
- };
- }
- /// <summary>
- /// G01-06:自动建单分支(非第二套创建主链)。
- /// 这是本服务内的自动监控建单路径,与 <see cref="CreateAsync"/> 并列,
- /// 复用同一仓储(_rep / _timelineRep)、同一事务边界、同一 ExceptionCode 生成规则、
- /// 同一时间线主链(ActionCode="CREATE"、ToStatus="NEW");仅差异点:
- /// - SourceType 标识为自动监控来源
- /// - 填入 SourceRuleId / SourceDataSourceId / SourcePayload / RelatedObjectCode 追溯
- /// - ExceptionTypeCode 固定 EQUIP_FAULT(G-01 首版唯一映射)
- /// - SceneCode 固定 S2(G-01 首版唯一场景,迁移后从 S2S6_PRODUCTION 切到单模块 S2)
- /// 不做补偿、重试、对账;失败由调用方接住。
- /// </summary>
- public async Task<AdoS8Exception> CreateFromWatchAsync(S8WatchHitResult hit)
- {
- if (hit.SourceRuleId <= 0 || string.IsNullOrWhiteSpace(hit.RelatedObjectCode))
- throw new S8BizException("自动建单缺失追溯键");
- // S8-SLA-TIMEOUT-RUNTIME-1(P3):CreatedAt 与 SlaDeadline 共用同一 now。
- var now = DateTime.Now;
- var code = $"EX-{now:yyyyMMdd}-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";
- var title = $"[自动] 设备 {hit.RelatedObjectCode} {hit.TriggerCondition} {hit.ThresholdValue}(当前 {hit.CurrentValue})";
- // S8-EXCEPTION-CREATION-MODULE-CODE-FIX-1:固定 SceneCode=S2 + ExceptionTypeCode=EQUIP_FAULT,
- // 派生链路通过 ResolveModuleCodeAsync 严格按 S1-S7 走(结果稳定为 S2,但消除 FromScene 的 legacy 兼容路径)。
- var resolvedModule = await ResolveModuleCodeAsync(
- explicitModuleCode: null,
- hitModuleCode: null,
- sceneCode: S8SceneCode.S2,
- exceptionTypeCode: "EQUIP_FAULT");
- if (resolvedModule == null)
- {
- _logger.LogWarning(
- "auto_watch_module_code_unresolved sceneCode={SceneCode} exceptionTypeCode={TypeCode} ruleId={RuleId}",
- S8SceneCode.S2, "EQUIP_FAULT", hit.SourceRuleId);
- }
- // S8-AUTO-WATCH-DEPT-RESOLVE-1(P2):部门解析顺序 hit → params → unassigned;resolver 失败 → throw。
- const long fixedTenantId = 1;
- const long fixedFactoryId = 1;
- var deptResolution = await ResolveAutoWatchDepartmentsAsync(
- path: nameof(CreateFromWatchAsync),
- factoryId: fixedFactoryId,
- hitOccurrenceDeptId: hit.OccurrenceDeptId,
- hitResponsibleDeptId: hit.ResponsibleDeptId,
- ruleId: hit.SourceRuleId,
- ruleCode: hit.SourceRuleCode,
- moduleCode: resolvedModule,
- sceneCode: S8SceneCode.S2,
- exceptionTypeCode: "EQUIP_FAULT",
- sourceObjectType: null,
- sourceObjectId: null,
- relatedObjectCode: hit.RelatedObjectCode,
- dedupKey: null);
- if (!deptResolution.OccurrenceDeptId.HasValue || !deptResolution.ResponsibleDeptId.HasValue)
- {
- _logger.LogWarning(
- "s8_auto_watch_dept_resolve_failed path={Path} ruleId={RuleId} ruleCode={RuleCode} factoryId={FactoryId} occSource={OccSource} respSource={RespSource} relatedObjectCode={RelatedObjectCode}",
- nameof(CreateFromWatchAsync), hit.SourceRuleId, hit.SourceRuleCode, fixedFactoryId,
- deptResolution.OccurrenceSource, deptResolution.ResponsibleSource, hit.RelatedObjectCode);
- throw new S8BizException($"自动建单部门解析失败:缺少未归属部门基线(factory_ref_id={fixedFactoryId}, codename={UnassignedDepartmentCode})");
- }
- var entity = new AdoS8Exception
- {
- // 租户/工厂:与 S8WatchSchedulerService.RunOnceAsync 当前固定上下文一致。
- TenantId = fixedTenantId,
- FactoryId = fixedFactoryId,
- ExceptionCode = code,
- Title = title,
- Description = null,
- SceneCode = S8SceneCode.S2,
- // 首版自动监控建单来源标识(字符串值,先不抽常量类)。
- SourceType = "AUTO_WATCH",
- Status = "NEW",
- Severity = S8SeverityCode.Normalize(hit.Severity),
- PriorityScore = 0,
- PriorityLevel = "P3",
- // S8-AUTO-WATCH-DEPT-RESOLVE-1:经 resolver 严格校验后写入;不再使用 ?? 0 兜底。
- OccurrenceDeptId = deptResolution.OccurrenceDeptId.Value,
- ResponsibleDeptId = deptResolution.ResponsibleDeptId.Value,
- ReporterId = null,
- CreatedAt = now,
- // S8-SLA-TIMEOUT-RUNTIME-1:sla_deadline 由 exception_type.sla_minutes 决定(EQUIP_FAULT)。
- SlaDeadline = await ResolveSlaDeadlineAsync("EQUIP_FAULT", now),
- IsDeleted = false,
- // G-01 首版唯一异常类型映射(baseline 已迁后 EQUIP_FAULT 属 S2 制造协同场景)。
- ExceptionTypeCode = "EQUIP_FAULT",
- ModuleCode = resolvedModule,
- // S8-PROCESS-NODE-MODULE-CODE-ALIGNMENT-EXEC-1:当前阶段 process_node_code 留空,
- // module_code 承担 S1-S7 主流程归属;process_node_code 留给未来更细流程节点。
- ProcessNodeCode = null,
- // 追溯三件套(自动建单必填口径)。
- SourceRuleId = hit.SourceRuleId,
- SourceDataSourceId = hit.DataSourceId,
- SourcePayload = hit.SourcePayload,
- RelatedObjectCode = hit.RelatedObjectCode
- };
- _logger.LogInformation(
- "s8_auto_watch_dept_resolved path={Path} ruleId={RuleId} ruleCode={RuleCode} factoryId={FactoryId} occDeptId={OccDeptId} occSource={OccSource} respDeptId={RespDeptId} respSource={RespSource}",
- nameof(CreateFromWatchAsync), hit.SourceRuleId, hit.SourceRuleCode, fixedFactoryId,
- entity.OccurrenceDeptId, deptResolution.OccurrenceSource, entity.ResponsibleDeptId, deptResolution.ResponsibleSource);
- await _rep.AsTenant().UseTranAsync(async () =>
- {
- entity = await _rep.AsInsertable(entity).ExecuteReturnEntityAsync();
- await _timelineRep.InsertAsync(new AdoS8ExceptionTimeline
- {
- ExceptionId = entity.Id,
- ActionCode = "CREATE",
- ActionLabel = "创建",
- FromStatus = null,
- ToStatus = "NEW",
- OperatorId = null,
- ActionRemark = "自动建单",
- CreatedAt = DateTime.Now
- });
- }, ex => throw ex);
- await TryStartIntakeFlowAsync(entity);
- return entity;
- }
- /// <summary>
- /// R2 自动建单分支(TIMEOUT 等新 evaluator 走此路径)。
- /// 与 <see cref="CreateFromWatchAsync"/> 并列:复用同一仓储 / 事务 / 时间线 ActionCode;
- /// 差异点:消费 <see cref="S8RuleHit"/> 一份命中模型,把 R2 新列(DedupKey / LastDetectedAt /
- /// SourceRuleCode / SourceObjectType / SourceObjectId)落齐;ExceptionTypeCode 由 hit 自带,
- /// 不再硬编码 EQUIP_FAULT。RecoveredAt 本轮不写。
- /// 调用方负责前置检查 ExceptionTypeCode 是否在 baseline;本方法不再二次校验。
- /// </summary>
- public async Task<AdoS8Exception> CreateFromHitAsync(S8RuleHit hit)
- {
- if (hit.SourceRuleId <= 0 || string.IsNullOrWhiteSpace(hit.RelatedObjectCode))
- throw new S8BizException("自动建单缺失追溯键");
- var effectiveScene = string.IsNullOrWhiteSpace(hit.SceneCode) ? S8SceneCode.S2 : hit.SceneCode;
- // S8-EXCEPTION-CREATION-MODULE-CODE-FIX-1:R2 自动建单 module_code 派生统一走严格 S1-S7 链路;
- // hit.ModuleCode(evaluator 显式)→ rule/hit.SceneCode(当前 DB 100% S1-S7)→ exception_type.scene_code。
- var resolvedModule = await ResolveModuleCodeAsync(
- explicitModuleCode: null,
- hitModuleCode: hit.ModuleCode,
- sceneCode: effectiveScene,
- exceptionTypeCode: hit.ExceptionTypeCode);
- if (resolvedModule == null)
- {
- _logger.LogWarning(
- "auto_watch_hit_module_code_unresolved sceneCode={SceneCode} hitModule={HitModule} typeCode={TypeCode} ruleCode={RuleCode}",
- hit.SceneCode, hit.ModuleCode, hit.ExceptionTypeCode, hit.SourceRuleCode);
- }
- // S8-AUTO-WATCH-DEPT-RESOLVE-1(P2):部门解析顺序 hit → params → unassigned;resolver 失败 → throw。
- const long fixedTenantId = 1;
- const long fixedFactoryId = 1;
- var deptResolution = await ResolveAutoWatchDepartmentsAsync(
- path: nameof(CreateFromHitAsync),
- factoryId: fixedFactoryId,
- hitOccurrenceDeptId: hit.OccurrenceDeptId,
- hitResponsibleDeptId: hit.ResponsibleDeptId,
- ruleId: hit.SourceRuleId,
- ruleCode: hit.SourceRuleCode,
- moduleCode: resolvedModule,
- sceneCode: effectiveScene,
- exceptionTypeCode: hit.ExceptionTypeCode,
- sourceObjectType: hit.SourceObjectType,
- sourceObjectId: hit.SourceObjectId,
- relatedObjectCode: hit.RelatedObjectCode,
- dedupKey: hit.DedupKey);
- if (!deptResolution.OccurrenceDeptId.HasValue || !deptResolution.ResponsibleDeptId.HasValue)
- {
- _logger.LogWarning(
- "s8_auto_watch_dept_resolve_failed path={Path} ruleId={RuleId} ruleCode={RuleCode} factoryId={FactoryId} occSource={OccSource} respSource={RespSource} sourceObjectType={SourceObjectType} sourceObjectId={SourceObjectId} dedupKey={DedupKey}",
- nameof(CreateFromHitAsync), hit.SourceRuleId, hit.SourceRuleCode, fixedFactoryId,
- deptResolution.OccurrenceSource, deptResolution.ResponsibleSource, hit.SourceObjectType, hit.SourceObjectId, hit.DedupKey);
- throw new S8BizException($"自动建单部门解析失败:缺少未归属部门基线(factory_ref_id={fixedFactoryId}, codename={UnassignedDepartmentCode})");
- }
- // S8-SLA-TIMEOUT-RUNTIME-1(P3):CreatedAt 与 SlaDeadline 共用同一 now。
- var now = DateTime.Now;
- var code = $"EX-{now:yyyyMMdd}-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";
- var slaDeadline = await ResolveSlaDeadlineAsync(hit.ExceptionTypeCode, now);
- var entity = new AdoS8Exception
- {
- // 与 CreateFromWatchAsync 当前固定上下文一致;R2 不变更租户/工厂上下文语义。
- TenantId = fixedTenantId,
- FactoryId = fixedFactoryId,
- ExceptionCode = code,
- Title = string.IsNullOrWhiteSpace(hit.Title)
- ? $"[自动] {hit.SourceObjectType} {hit.SourceObjectId}"
- : hit.Title,
- Description = null,
- SceneCode = effectiveScene,
- SourceType = "AUTO_WATCH",
- Status = "NEW",
- Severity = S8SeverityCode.Normalize(hit.Severity),
- PriorityScore = 0,
- PriorityLevel = "P3",
- // S8-AUTO-WATCH-DEPT-RESOLVE-1:经 resolver 严格校验后写入;不再使用 ?? 0 兜底。
- OccurrenceDeptId = deptResolution.OccurrenceDeptId.Value,
- ResponsibleDeptId = deptResolution.ResponsibleDeptId.Value,
- ReporterId = null,
- CreatedAt = now,
- // S8-SLA-TIMEOUT-RUNTIME-1:sla_deadline 来自 hit.ExceptionTypeCode 对应 sla_minutes。
- SlaDeadline = slaDeadline,
- IsDeleted = false,
- ExceptionTypeCode = hit.ExceptionTypeCode,
- ModuleCode = resolvedModule,
- // S8-PROCESS-NODE-MODULE-CODE-ALIGNMENT-EXEC-1:当前阶段 process_node_code 留空,
- // module_code 承担 S1-S7 主流程归属;process_node_code 留给未来更细流程节点。
- ProcessNodeCode = null,
- SourceRuleId = hit.SourceRuleId,
- SourceDataSourceId = hit.DataSourceId == 0 ? null : hit.DataSourceId,
- SourcePayload = hit.SourcePayload,
- RelatedObjectCode = hit.RelatedObjectCode,
- // R2 新列回填
- DedupKey = hit.DedupKey,
- LastDetectedAt = hit.DetectedAt,
- RecoveredAt = null,
- SourceRuleCode = hit.SourceRuleCode,
- SourceObjectType = hit.SourceObjectType,
- SourceObjectId = hit.SourceObjectId
- };
- _logger.LogInformation(
- "s8_auto_watch_dept_resolved path={Path} ruleId={RuleId} ruleCode={RuleCode} factoryId={FactoryId} occDeptId={OccDeptId} occSource={OccSource} respDeptId={RespDeptId} respSource={RespSource} sourceObjectType={SourceObjectType} sourceObjectId={SourceObjectId} dedupKey={DedupKey}",
- nameof(CreateFromHitAsync), hit.SourceRuleId, hit.SourceRuleCode, fixedFactoryId,
- entity.OccurrenceDeptId, deptResolution.OccurrenceSource, entity.ResponsibleDeptId, deptResolution.ResponsibleSource, hit.SourceObjectType, hit.SourceObjectId, hit.DedupKey);
- await _rep.AsTenant().UseTranAsync(async () =>
- {
- entity = await _rep.AsInsertable(entity).ExecuteReturnEntityAsync();
- await _timelineRep.InsertAsync(new AdoS8ExceptionTimeline
- {
- ExceptionId = entity.Id,
- ActionCode = "CREATE",
- ActionLabel = "创建",
- FromStatus = null,
- ToStatus = "NEW",
- OperatorId = null,
- ActionRemark = "自动建单(R2)",
- CreatedAt = DateTime.Now
- });
- }, ex => throw ex);
- await TryStartIntakeFlowAsync(entity);
- return entity;
- }
- public async Task<AdoS8Exception?> GetAsync(long id) =>
- await _rep.GetByIdAsync(id);
- public async Task<AdoS8Evidence> AddAttachmentAsync(long id, AdoS8AttachmentCreateDto dto)
- {
- var entity = await _rep.GetFirstAsync(x => x.Id == id && !x.IsDeleted)
- ?? throw new S8BizException("异常不存在");
- if (string.IsNullOrWhiteSpace(dto.FileName) || string.IsNullOrWhiteSpace(dto.FileUrl))
- throw new S8BizException("附件名称和地址必填");
- var evidence = new AdoS8Evidence
- {
- ExceptionId = id,
- EvidenceType = string.IsNullOrWhiteSpace(dto.EvidenceType) ? "file" : dto.EvidenceType,
- FileName = dto.FileName.Trim(),
- FileUrl = dto.FileUrl.Trim(),
- SourceSystem = dto.SourceSystem,
- UploadedBy = dto.UploadedBy,
- UploadedAt = DateTime.Now,
- IsDeleted = false
- };
- await _evidenceRep.InsertAsync(evidence);
- return evidence;
- }
- }
|