Prechádzať zdrojové kódy

feat(s8): add config wizard draft backend

YY968XX 4 mesiacov pred
rodič
commit
3172bae921

+ 57 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S8/AdoS8ConfigDraftsController.cs

@@ -0,0 +1,57 @@
+using Admin.NET.Plugin.AiDOP.Dto.S8;
+using Admin.NET.Plugin.AiDOP.Service.S8;
+
+namespace Admin.NET.Plugin.AiDOP.Controllers.S8;
+
+[ApiController]
+[Route("api/aidop/s8/config/wizard-drafts")]
+[NonUnify]
+public class AdoS8ConfigDraftsController : ControllerBase
+{
+    private readonly S8ConfigDraftService _svc;
+
+    public AdoS8ConfigDraftsController(S8ConfigDraftService svc) => _svc = svc;
+
+    [HttpGet]
+    public async Task<IActionResult> ListAsync([FromQuery] AdoS8ConfigDraftListQueryDto query) =>
+        Ok(await _svc.ListAsync(query));
+
+    [HttpGet("{id:long}")]
+    public async Task<IActionResult> GetAsync(long id)
+    {
+        try { return Ok(await _svc.GetAsync(id)); }
+        catch (S8BizException ex) { return BadRequest(new { message = ex.Message }); }
+    }
+
+    [HttpPost]
+    public async Task<IActionResult> CreateAsync([FromBody] AdoS8ConfigDraftCreateDto body)
+    {
+        try { return Ok(await _svc.CreateAsync(body)); }
+        catch (S8BizException ex) { return BadRequest(new { message = ex.Message }); }
+    }
+
+    [HttpPut("{id:long}")]
+    public async Task<IActionResult> UpdateAsync(long id, [FromBody] AdoS8ConfigDraftUpdateDto body)
+    {
+        try { return Ok(await _svc.UpdateAsync(id, body)); }
+        catch (S8BizException ex) { return BadRequest(new { message = ex.Message }); }
+    }
+
+    [HttpDelete("{id:long}")]
+    public async Task<IActionResult> DeleteAsync(long id)
+    {
+        try
+        {
+            await _svc.DeleteAsync(id);
+            return Ok(new { id, deleted = true });
+        }
+        catch (S8BizException ex) { return BadRequest(new { message = ex.Message }); }
+    }
+
+    [HttpPost("{id:long}/generate-rule")]
+    public async Task<IActionResult> GenerateRuleAsync(long id, [FromBody] AdoS8ConfigDraftGenerateRuleDto body)
+    {
+        try { return Ok(await _svc.GenerateRuleAsync(id, body)); }
+        catch (S8BizException ex) { return BadRequest(new { message = ex.Message }); }
+    }
+}

+ 84 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Dto/S8/AdoS8ConfigDraftDtos.cs

@@ -0,0 +1,84 @@
+using Admin.NET.Plugin.AiDOP.Entity.S8;
+
+namespace Admin.NET.Plugin.AiDOP.Dto.S8;
+
+public class AdoS8ConfigDraftCreateDto
+{
+    public long TenantId { get; set; } = 1;
+    public long FactoryId { get; set; } = 1;
+    public string DraftCode { get; set; } = string.Empty;
+    public string? DraftName { get; set; }
+    public string WizardJson { get; set; } = string.Empty;
+    public int? CurrentStep { get; set; }
+    public string? Mechanism { get; set; }
+    public string? StageCode { get; set; }
+    public string? OrderFlowCode { get; set; }
+    public string? ExceptionTypeCode { get; set; }
+    public string? Remark { get; set; }
+}
+
+public class AdoS8ConfigDraftUpdateDto
+{
+    public string? DraftName { get; set; }
+    public string? WizardJson { get; set; }
+    public int? CurrentStep { get; set; }
+    public string? Mechanism { get; set; }
+    public string? StageCode { get; set; }
+    public string? OrderFlowCode { get; set; }
+    public string? ExceptionTypeCode { get; set; }
+    public string? Remark { get; set; }
+}
+
+public class AdoS8ConfigDraftListQueryDto
+{
+    public long TenantId { get; set; } = 1;
+    public long FactoryId { get; set; } = 1;
+    public string? Status { get; set; }
+    public int Page { get; set; } = 1;
+    public int PageSize { get; set; } = 20;
+}
+
+public class AdoS8ConfigDraftListItemDto
+{
+    public long Id { get; set; }
+    public long TenantId { get; set; }
+    public long FactoryId { get; set; }
+    public string DraftCode { get; set; } = string.Empty;
+    public string? DraftName { get; set; }
+    public string Status { get; set; } = string.Empty;
+    public int CurrentStep { get; set; }
+    public string? Mechanism { get; set; }
+    public string? StageCode { get; set; }
+    public string? OrderFlowCode { get; set; }
+    public string? ExceptionTypeCode { get; set; }
+    public long? GeneratedRuleId { get; set; }
+    public string? Remark { get; set; }
+    public DateTime CreatedAt { get; set; }
+    public DateTime? UpdatedAt { get; set; }
+}
+
+public class AdoS8ConfigDraftListResultDto
+{
+    public List<AdoS8ConfigDraftListItemDto> Items { get; set; } = new();
+    public int Total { get; set; }
+    public int Page { get; set; }
+    public int PageSize { get; set; }
+}
+
+public class AdoS8ConfigDraftDetailDto : AdoS8ConfigDraftListItemDto
+{
+    public string WizardJson { get; set; } = string.Empty;
+}
+
+public class AdoS8ConfigDraftGenerateRuleDto
+{
+    /// <summary>前端构造好的完整 watch_rule payload;首版不从 wizard_json 重建。</summary>
+    public AdoS8WatchRule? RulePayload { get; set; }
+}
+
+public class AdoS8ConfigDraftGenerateRuleResultDto
+{
+    public long RuleId { get; set; }
+    public string RuleCode { get; set; } = string.Empty;
+    public string DraftStatus { get; set; } = string.Empty;
+}

+ 59 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S8/AdoS8ConfigDraft.cs

@@ -0,0 +1,59 @@
+namespace Admin.NET.Plugin.AiDOP.Entity.S8;
+
+[SugarTable("ado_s8_config_draft", "S8 统一配置器草稿")]
+[SugarIndex("idx_s8_config_draft_tfs", nameof(TenantId), OrderByType.Asc, nameof(FactoryId), OrderByType.Asc, nameof(Status), OrderByType.Asc)]
+[SugarIndex("uk_s8_config_draft_tenant_factory_code", nameof(TenantId), OrderByType.Asc, nameof(FactoryId), OrderByType.Asc, nameof(DraftCode), OrderByType.Asc, IsUnique = true)]
+[SugarIndex("idx_s8_config_draft_generated_rule", nameof(GeneratedRuleId), OrderByType.Asc)]
+public class AdoS8ConfigDraft
+{
+    [SugarColumn(ColumnName = "id", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
+    public long Id { get; set; }
+
+    [SugarColumn(ColumnName = "tenant_id", ColumnDataType = "bigint")]
+    public long TenantId { get; set; }
+
+    [SugarColumn(ColumnName = "factory_id", ColumnDataType = "bigint")]
+    public long FactoryId { get; set; }
+
+    [SugarColumn(ColumnName = "draft_code", Length = 64)]
+    public string DraftCode { get; set; } = string.Empty;
+
+    [SugarColumn(ColumnName = "draft_name", Length = 128, IsNullable = true)]
+    public string? DraftName { get; set; }
+
+    /// <summary>草稿状态:IN_PROGRESS / GENERATED。</summary>
+    [SugarColumn(ColumnName = "status", Length = 32)]
+    public string Status { get; set; } = "IN_PROGRESS";
+
+    [SugarColumn(ColumnName = "current_step")]
+    public int CurrentStep { get; set; }
+
+    [SugarColumn(ColumnName = "mechanism", Length = 32, IsNullable = true)]
+    public string? Mechanism { get; set; }
+
+    [SugarColumn(ColumnName = "stage_code", Length = 16, IsNullable = true)]
+    public string? StageCode { get; set; }
+
+    [SugarColumn(ColumnName = "order_flow_code", Length = 64, IsNullable = true)]
+    public string? OrderFlowCode { get; set; }
+
+    [SugarColumn(ColumnName = "exception_type_code", Length = 64, IsNullable = true)]
+    public string? ExceptionTypeCode { get; set; }
+
+    /// <summary>向导态序列化 JSON;列表不返回此字段。</summary>
+    [SugarColumn(ColumnName = "wizard_json", ColumnDataType = "mediumtext")]
+    public string WizardJson { get; set; } = string.Empty;
+
+    /// <summary>generate-rule 成功后回写 ado_s8_watch_rule.id;幂等返回依据。</summary>
+    [SugarColumn(ColumnName = "generated_rule_id", ColumnDataType = "bigint", IsNullable = true)]
+    public long? GeneratedRuleId { get; set; }
+
+    [SugarColumn(ColumnName = "remark", Length = 512, IsNullable = true)]
+    public string? Remark { get; set; }
+
+    [SugarColumn(ColumnName = "created_at")]
+    public DateTime CreatedAt { get; set; } = DateTime.Now;
+
+    [SugarColumn(ColumnName = "updated_at", IsNullable = true)]
+    public DateTime? UpdatedAt { get; set; }
+}

+ 244 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/S8ConfigDraftService.cs

@@ -0,0 +1,244 @@
+using Admin.NET.Plugin.AiDOP.Dto.S8;
+using Admin.NET.Plugin.AiDOP.Entity.S8;
+
+namespace Admin.NET.Plugin.AiDOP.Service.S8;
+
+internal static class S8ConfigDraftStatus
+{
+    public const string InProgress = "IN_PROGRESS";
+    public const string Generated = "GENERATED";
+}
+
+public class S8ConfigDraftService : ITransient
+{
+    private readonly SqlSugarRepository<AdoS8ConfigDraft> _rep;
+    private readonly S8WatchRuleService _watchRuleService;
+
+    public S8ConfigDraftService(
+        SqlSugarRepository<AdoS8ConfigDraft> rep,
+        S8WatchRuleService watchRuleService)
+    {
+        _rep = rep;
+        _watchRuleService = watchRuleService;
+    }
+
+    public async Task<AdoS8ConfigDraftListResultDto> ListAsync(AdoS8ConfigDraftListQueryDto query)
+    {
+        var page = query.Page < 1 ? 1 : query.Page;
+        var pageSize = query.PageSize < 1 ? 20 : Math.Min(query.PageSize, 200);
+        var status = string.IsNullOrWhiteSpace(query.Status) ? null : query.Status.Trim();
+
+        var q = _rep.AsQueryable()
+            .Where(x => x.TenantId == query.TenantId && x.FactoryId == query.FactoryId);
+        if (status != null)
+            q = q.Where(x => x.Status == status);
+
+        RefAsync<int> total = 0;
+        var rows = await q
+            .OrderBy(x => x.Id, OrderByType.Desc)
+            .Select(x => new AdoS8ConfigDraftListItemDto
+            {
+                Id = x.Id,
+                TenantId = x.TenantId,
+                FactoryId = x.FactoryId,
+                DraftCode = x.DraftCode,
+                DraftName = x.DraftName,
+                Status = x.Status,
+                CurrentStep = x.CurrentStep,
+                Mechanism = x.Mechanism,
+                StageCode = x.StageCode,
+                OrderFlowCode = x.OrderFlowCode,
+                ExceptionTypeCode = x.ExceptionTypeCode,
+                GeneratedRuleId = x.GeneratedRuleId,
+                Remark = x.Remark,
+                CreatedAt = x.CreatedAt,
+                UpdatedAt = x.UpdatedAt
+            })
+            .ToPageListAsync(page, pageSize, total);
+
+        return new AdoS8ConfigDraftListResultDto
+        {
+            Items = rows,
+            Total = total.Value,
+            Page = page,
+            PageSize = pageSize
+        };
+    }
+
+    public async Task<AdoS8ConfigDraftDetailDto> GetAsync(long id)
+    {
+        var e = await _rep.GetByIdAsync(id) ?? throw new S8BizException("草稿不存在");
+        return ToDetail(e);
+    }
+
+    public async Task<AdoS8ConfigDraftDetailDto> CreateAsync(AdoS8ConfigDraftCreateDto dto)
+    {
+        if (string.IsNullOrWhiteSpace(dto.DraftCode))
+            throw new S8BizException("草稿编码必填");
+        if (string.IsNullOrWhiteSpace(dto.WizardJson))
+            throw new S8BizException("草稿 wizard_json 必填");
+
+        var draftCode = dto.DraftCode.Trim();
+        var exists = await _rep.AsQueryable()
+            .AnyAsync(x => x.TenantId == dto.TenantId && x.FactoryId == dto.FactoryId && x.DraftCode == draftCode);
+        if (exists) throw new S8BizException("草稿编码已存在");
+
+        var entity = new AdoS8ConfigDraft
+        {
+            TenantId = dto.TenantId,
+            FactoryId = dto.FactoryId,
+            DraftCode = draftCode,
+            DraftName = NormalizeOrNull(dto.DraftName),
+            WizardJson = dto.WizardJson,
+            CurrentStep = dto.CurrentStep ?? 0,
+            Mechanism = NormalizeOrNull(dto.Mechanism),
+            StageCode = NormalizeOrNull(dto.StageCode),
+            OrderFlowCode = NormalizeOrNull(dto.OrderFlowCode),
+            ExceptionTypeCode = NormalizeOrNull(dto.ExceptionTypeCode),
+            Remark = NormalizeOrNull(dto.Remark),
+            Status = S8ConfigDraftStatus.InProgress,
+            CreatedAt = DateTime.Now
+        };
+        entity.Id = await _rep.AsInsertable(entity).ExecuteReturnBigIdentityAsync();
+        return ToDetail(entity);
+    }
+
+    public async Task<AdoS8ConfigDraftDetailDto> UpdateAsync(long id, AdoS8ConfigDraftUpdateDto dto)
+    {
+        var e = await _rep.GetByIdAsync(id) ?? throw new S8BizException("草稿不存在");
+        if (e.Status == S8ConfigDraftStatus.Generated)
+            throw new S8BizException("草稿已生成正式规则,禁止再编辑");
+
+        if (dto.DraftName != null) e.DraftName = NormalizeOrNull(dto.DraftName);
+        if (dto.WizardJson != null)
+        {
+            if (string.IsNullOrWhiteSpace(dto.WizardJson))
+                throw new S8BizException("wizard_json 不能为空字符串");
+            e.WizardJson = dto.WizardJson;
+        }
+        if (dto.CurrentStep.HasValue) e.CurrentStep = dto.CurrentStep.Value;
+        if (dto.Mechanism != null) e.Mechanism = NormalizeOrNull(dto.Mechanism);
+        if (dto.StageCode != null) e.StageCode = NormalizeOrNull(dto.StageCode);
+        if (dto.OrderFlowCode != null) e.OrderFlowCode = NormalizeOrNull(dto.OrderFlowCode);
+        if (dto.ExceptionTypeCode != null) e.ExceptionTypeCode = NormalizeOrNull(dto.ExceptionTypeCode);
+        if (dto.Remark != null) e.Remark = NormalizeOrNull(dto.Remark);
+
+        e.UpdatedAt = DateTime.Now;
+        await _rep.UpdateAsync(e);
+        return ToDetail(e);
+    }
+
+    public async Task DeleteAsync(long id)
+    {
+        var e = await _rep.GetByIdAsync(id) ?? throw new S8BizException("草稿不存在");
+        await _rep.DeleteByIdAsync(e.Id);
+    }
+
+    public async Task<AdoS8ConfigDraftGenerateRuleResultDto> GenerateRuleAsync(long id, AdoS8ConfigDraftGenerateRuleDto dto)
+    {
+        var draft = await _rep.GetByIdAsync(id) ?? throw new S8BizException("草稿不存在");
+
+        if (draft.GeneratedRuleId.HasValue)
+        {
+            return new AdoS8ConfigDraftGenerateRuleResultDto
+            {
+                RuleId = draft.GeneratedRuleId.Value,
+                RuleCode = await ResolveExistingRuleCodeAsync(draft.GeneratedRuleId.Value),
+                DraftStatus = draft.Status
+            };
+        }
+        if (draft.Status == S8ConfigDraftStatus.Generated)
+            throw new S8BizException("草稿状态异常:已标记 GENERATED 但未回写规则 ID");
+
+        var payload = dto.RulePayload ?? throw new S8BizException("rulePayload 必填");
+        if (string.IsNullOrWhiteSpace(payload.RuleCode))
+            throw new S8BizException("rulePayload.ruleCode 必填");
+
+        // 安全收口:强制对齐草稿租户/工厂、强制 enabled=false。
+        payload.Id = 0;
+        payload.TenantId = draft.TenantId;
+        payload.FactoryId = draft.FactoryId;
+        payload.Enabled = false;
+
+        var db = _rep.Context;
+        await db.Ado.BeginTranAsync();
+        try
+        {
+            var created = await _watchRuleService.CreateAsync(payload);
+
+            // S8WatchRuleService.CreateAsync 内部用 InsertAsync 不回填 Id;按 RuleCode 反查真实 id。
+            var newRuleId = created.Id;
+            if (newRuleId <= 0)
+            {
+                newRuleId = await db.Queryable<AdoS8WatchRule>()
+                    .Where(x => x.TenantId == draft.TenantId
+                                && x.FactoryId == draft.FactoryId
+                                && x.RuleCode == created.RuleCode)
+                    .Select(x => x.Id)
+                    .FirstAsync();
+            }
+            if (newRuleId <= 0)
+                throw new S8BizException("生成规则失败:无法定位新规则 id");
+
+            await db.Updateable<AdoS8ConfigDraft>()
+                .SetColumns(x => new AdoS8ConfigDraft
+                {
+                    GeneratedRuleId = newRuleId,
+                    Status = S8ConfigDraftStatus.Generated,
+                    UpdatedAt = DateTime.Now
+                })
+                .Where(x => x.Id == draft.Id)
+                .ExecuteCommandAsync();
+
+            await db.Ado.CommitTranAsync();
+
+            return new AdoS8ConfigDraftGenerateRuleResultDto
+            {
+                RuleId = newRuleId,
+                RuleCode = created.RuleCode,
+                DraftStatus = S8ConfigDraftStatus.Generated
+            };
+        }
+        catch
+        {
+            await db.Ado.RollbackTranAsync();
+            throw;
+        }
+    }
+
+    private async Task<string> ResolveExistingRuleCodeAsync(long ruleId)
+    {
+        var rule = await _rep.Context.Queryable<AdoS8WatchRule>()
+            .Where(x => x.Id == ruleId)
+            .Select(x => new { x.RuleCode })
+            .FirstAsync();
+        return rule?.RuleCode ?? string.Empty;
+    }
+
+    private static AdoS8ConfigDraftDetailDto ToDetail(AdoS8ConfigDraft e) => new()
+    {
+        Id = e.Id,
+        TenantId = e.TenantId,
+        FactoryId = e.FactoryId,
+        DraftCode = e.DraftCode,
+        DraftName = e.DraftName,
+        Status = e.Status,
+        CurrentStep = e.CurrentStep,
+        Mechanism = e.Mechanism,
+        StageCode = e.StageCode,
+        OrderFlowCode = e.OrderFlowCode,
+        ExceptionTypeCode = e.ExceptionTypeCode,
+        GeneratedRuleId = e.GeneratedRuleId,
+        Remark = e.Remark,
+        CreatedAt = e.CreatedAt,
+        UpdatedAt = e.UpdatedAt,
+        WizardJson = e.WizardJson
+    };
+
+    private static string? NormalizeOrNull(string? value)
+    {
+        if (string.IsNullOrWhiteSpace(value)) return null;
+        var trimmed = value.Trim();
+        return trimmed.Length == 0 ? null : trimmed;
+    }
+}

+ 1 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Startup.cs

@@ -139,6 +139,7 @@ public class Startup : AppStartup
                 typeof(AdoS8RuleDetectionState),
                 typeof(AdoS8Dimension),
                 typeof(AdoS8DimensionNode),
+                typeof(AdoS8ConfigDraft),
                 typeof(ContractReview),
                 typeof(ContractReviewFlow),
                 typeof(ProductDesign),