| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304 |
- 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;
- namespace Admin.NET.Plugin.AiDOP.Service.S8;
- public class S8ManualReportService : ITransient
- {
- // 合法严重度白名单(与 GetFormOptionsAsync.severities 同源);前端/后端默认值 MEDIUM。
- private static readonly HashSet<string> AllowedSeverities = new(StringComparer.Ordinal)
- {
- "CRITICAL", "HIGH", "MEDIUM", "LOW"
- };
- 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 UserManager _userManager;
- public S8ManualReportService(
- SqlSugarRepository<AdoS8Exception> rep,
- SqlSugarRepository<AdoS8ExceptionTimeline> timelineRep,
- SqlSugarRepository<AdoS8Evidence> evidenceRep,
- SqlSugarRepository<AdoS8SceneConfig> sceneRep,
- SqlSugarRepository<AdoS0DepartmentMaster> deptRep,
- SqlSugarRepository<AdoS0LineMaster> lineRep,
- UserManager userManager)
- {
- _rep = rep;
- _timelineRep = timelineRep;
- _evidenceRep = evidenceRep;
- _sceneRep = sceneRep;
- _deptRep = deptRep;
- _lineRep = lineRep;
- _userManager = userManager;
- }
- 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();
- var departments = await _deptRep.AsQueryable()
- .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()
- .Where(x => x.FactoryRefId == factoryId)
- .OrderBy(x => x.Line)
- .Take(500)
- .Select(x => new { value = x.Id, label = x.Describe ?? x.Line })
- .ToListAsync();
- return new
- {
- scenes,
- severities = new[]
- {
- new { value = "CRITICAL", label = "紧急" },
- new { value = "HIGH", label = "高" },
- new { value = "MEDIUM", label = "中" },
- new { value = "LOW", label = "低" }
- },
- departments,
- lines,
- materials = Array.Empty<object>()
- };
- }
- public async Task<AdoS8ManualReportResultDto> CreateAsync(AdoS8ManualReportCreateDto dto)
- {
- if (string.IsNullOrWhiteSpace(dto.Title)) throw new S8BizException("标题必填");
- if (string.IsNullOrWhiteSpace(dto.SceneCode)) throw new S8BizException("场景必填");
- // 严重度白名单校验:空值兜底为 MEDIUM;非法值直接拒绝,避免 P3 等错位写入。
- var severity = string.IsNullOrWhiteSpace(dto.Severity) ? "MEDIUM" : dto.Severity.Trim();
- if (!AllowedSeverities.Contains(severity))
- throw new S8BizException($"严重度 {severity} 非法,仅允许 CRITICAL/HIGH/MEDIUM/LOW");
- // 提报人以服务端登录上下文为准,忽略前端传入;未登录上下文落 null。
- var currentUserId = _userManager.UserId > 0 ? _userManager.UserId : (long?)null;
- var code = $"EX-{DateTime.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,
- ReporterId = currentUserId,
- CreatedAt = DateTime.Now,
- 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);
- 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 固定 S2S6_PRODUCTION(G-01 首版唯一场景)
- /// 不做补偿、重试、对账;失败由调用方接住。
- /// </summary>
- public async Task<AdoS8Exception> CreateFromWatchAsync(S8WatchHitResult hit)
- {
- if (hit.SourceRuleId <= 0 || string.IsNullOrWhiteSpace(hit.RelatedObjectCode))
- throw new S8BizException("自动建单缺失追溯键");
- var code = $"EX-{DateTime.Now:yyyyMMdd}-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";
- var title = $"[自动] 设备 {hit.RelatedObjectCode} {hit.TriggerCondition} {hit.ThresholdValue}(当前 {hit.CurrentValue})";
- var entity = new AdoS8Exception
- {
- // 租户/工厂:与 S8WatchSchedulerService.RunOnceAsync 当前固定上下文一致。
- TenantId = 1,
- FactoryId = 1,
- ExceptionCode = code,
- Title = title,
- Description = null,
- SceneCode = S8SceneCode.S2S6Production,
- // 首版自动监控建单来源标识(字符串值,先不抽常量类)。
- SourceType = "AUTO_WATCH",
- Status = "NEW",
- Severity = string.IsNullOrWhiteSpace(hit.Severity) ? "MEDIUM" : hit.Severity,
- PriorityScore = 0,
- PriorityLevel = "P3",
- // 首版兜底口径:Hit 未提供部门时置 0 仅为保证“能建成标准异常单并进入主链”,
- // 不是最终业务部门语义;后续需由上游查询结果提供,或在专项任务中补口径。
- OccurrenceDeptId = hit.OccurrenceDeptId ?? 0,
- ResponsibleDeptId = hit.ResponsibleDeptId ?? 0,
- ReporterId = null,
- CreatedAt = DateTime.Now,
- IsDeleted = false,
- // G-01 首版唯一异常类型映射(seed 已确认 EQUIP_FAULT 属 S2S6_PRODUCTION 场景)。
- ExceptionTypeCode = "EQUIP_FAULT",
- // ModuleCode:S2S6_PRODUCTION 场景对应 S2+S6 两个模块(见 S8ModuleCode.SceneOf),
- // 无稳定“scene → 单一 module”映射;首版置空,不靠经验写死。
- ModuleCode = null,
- ProcessNodeCode = null,
- // 追溯三件套(自动建单必填口径)。
- SourceRuleId = hit.SourceRuleId,
- SourceDataSourceId = hit.DataSourceId,
- SourcePayload = hit.SourcePayload,
- RelatedObjectCode = hit.RelatedObjectCode
- };
- 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);
- 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 code = $"EX-{DateTime.Now:yyyyMMdd}-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";
- var entity = new AdoS8Exception
- {
- // 与 CreateFromWatchAsync 当前固定上下文一致;R2 不变更租户/工厂上下文语义。
- TenantId = 1,
- FactoryId = 1,
- ExceptionCode = code,
- Title = string.IsNullOrWhiteSpace(hit.Title)
- ? $"[自动] {hit.SourceObjectType} {hit.SourceObjectId}"
- : hit.Title,
- Description = null,
- SceneCode = string.IsNullOrWhiteSpace(hit.SceneCode) ? S8SceneCode.S2S6Production : hit.SceneCode,
- SourceType = "AUTO_WATCH",
- Status = "NEW",
- Severity = string.IsNullOrWhiteSpace(hit.Severity) ? "MEDIUM" : hit.Severity,
- PriorityScore = 0,
- PriorityLevel = "P3",
- OccurrenceDeptId = hit.OccurrenceDeptId ?? 0,
- ResponsibleDeptId = hit.ResponsibleDeptId ?? 0,
- ReporterId = null,
- CreatedAt = DateTime.Now,
- IsDeleted = false,
- ExceptionTypeCode = hit.ExceptionTypeCode,
- ModuleCode = null,
- 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
- };
- 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);
- 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;
- }
- }
|