using Admin.NET.Plugin.AiDOP.Dto.S8.OrderFlow; using Admin.NET.Plugin.AiDOP.Infrastructure; using SqlSugar; namespace Admin.NET.Plugin.AiDOP.Service.S8.OrderFlow; /// /// S8「订单执行档案」真实订单 Read Model。 /// /// 数据链: /// mdp_std_so(GROUP BY tenant_id + order_id)→ 订单 Header /// → LEFT JOIN dwd_order_review_kpi → ORDER_REVIEW 阶段事实 /// → 固定五阶段骨架 → 前端档案页。 /// /// PRINCIPLE 1 · 租户隔离:租户只来自 /// 解析的登录上下文;所有查询与 JOIN 均带 tenant_id,不接受前端传入 tenantId, /// 无默认租户、无 Factory 代偿。 /// /// PRINCIPLE 2 · 只走数据中台:本服务只读 mdp_std_so 与 /// dwd_order_review_kpi,不触碰 crm_seorder / crm_seorderentry / /// CustMaster / aidop_action_run_log /// /// 与 SEED 严格隔离:本服务完全不读 ado_s8_order_flow_*。 /// 真实订单的 Stage 2~5 一律返回空字段,绝不从 SEED 拼回来;SEED 基线由 /// 原样保留,供 fixture / 回归使用。 /// public class S8RealOrderArchiveService : ITransient { /// 本 Read Model 的数据来源标识,用于前端区分 REAL / SEED,禁止两路混用。 public const string RealDataSource = "MDP_STD_SO"; private readonly ISqlSugarClient _db; private readonly UserManager _userManager; public S8RealOrderArchiveService(ISqlSugarClient db, UserManager userManager) { _db = db; _userManager = userManager; } /// /// 订单 Header 投影。 /// /// 聚合函数只是 SQL 投影手段,不是业务规则 —— mdp_std_so 是订单行粒度, /// 而这几列已实证在同一 order_id 的所有行内取值完全一致(实测差异计数为 0), /// 因此 MAX() 只是「从若干相同值里取一个」,不隐含「取最大」的业务含义。 /// /// 行级字段禁止升格为 Headerprogress / factory_id / /// item_name / item_spec 逐行不同,故不在本投影内。 /// private const string HeaderProjection = """ SELECT `tenant_id`, `order_id`, MAX(`order_no`) AS `order_no`, MAX(`customer_id`) AS `customer_id`, MAX(`customer_no`) AS `customer_no`, MAX(`customer_name`) AS `customer_name`, MAX(`order_date`) AS `order_date`, MAX(`order_type`) AS `order_type`, MAX(`order_status`) AS `order_status`, MAX(`flow_state`) AS `flow_state` FROM `mdp_std_so` WHERE `tenant_id` = @TenantId AND `order_id` IS NOT NULL AND `order_id` <> 0 AND `deleted_flag` = 0 GROUP BY `tenant_id`, `order_id` """; /// /// 真实订单列表。一张销售订单一行。 /// ORDER_REVIEW 事实用 LEFT JOIN 挂上 —— 有订单但尚未评审的订单 /// 必须照常出现在档案里,不能被 INNER JOIN 过滤掉。 /// public async Task> GetRealOrdersAsync(AdoS8OrderFlowOrderQueryDto query) { var tenantId = AidopTenantScope.ResolveOrThrow(_userManager); var where = "1=1"; var ps = new List { new("@TenantId", tenantId) }; // 订单号 / 客户 模糊检索(order_no 仅作展示与搜索,不参与身份判定) if (!string.IsNullOrWhiteSpace(query.Keyword)) { where += " AND (h.`order_no` LIKE @Kw OR h.`customer_name` LIKE @Kw OR h.`customer_no` LIKE @Kw)"; ps.Add(new SugarParameter("@Kw", $"%{query.Keyword.Trim()}%")); } if (!string.IsNullOrWhiteSpace(query.CustomerCode)) { where += " AND h.`customer_no` = @CustNo"; ps.Add(new SugarParameter("@CustNo", query.CustomerCode.Trim())); } var rows = await _db.Ado.SqlQueryAsync($""" SELECT h.`order_id` AS SalesOrderId, h.`order_no` AS OrderNo, h.`customer_id` AS CustomerId, h.`customer_no` AS CustomerNo, h.`customer_name` AS CustomerName, h.`order_date` AS OrderDate, h.`order_type` AS OrderType, h.`order_status` AS OrderStatus, h.`flow_state` AS FlowState, k.`review_start_time` AS ReviewStartTime, k.`review_end_time` AS ReviewEndTime, k.`actual_hours` AS ActualHours, k.`calculation_status` AS CalculationStatus FROM ({HeaderProjection}) h LEFT JOIN `dwd_order_review_kpi` k ON k.`tenant_id` = h.`tenant_id` AND k.`sales_order_id` = h.`order_id` WHERE {where} ORDER BY h.`order_date` DESC, h.`order_id` DESC """, ps); return (rows ?? []).Select(MapRealOrder).ToList(); } /// /// 单张真实订单详情(含固定五阶段骨架)。 /// 定位 Authority = tenant_id + salesOrderId,不接受订单号定位。 /// public async Task GetRealOrderAsync(long salesOrderId) { if (salesOrderId <= 0) return null; var tenantId = AidopTenantScope.ResolveOrThrow(_userManager); var rows = await _db.Ado.SqlQueryAsync($""" SELECT h.`order_id` AS SalesOrderId, h.`order_no` AS OrderNo, h.`customer_id` AS CustomerId, h.`customer_no` AS CustomerNo, h.`customer_name` AS CustomerName, h.`order_date` AS OrderDate, h.`order_type` AS OrderType, h.`order_status` AS OrderStatus, h.`flow_state` AS FlowState, k.`review_start_time` AS ReviewStartTime, k.`review_end_time` AS ReviewEndTime, k.`actual_hours` AS ActualHours, k.`calculation_status` AS CalculationStatus FROM ({HeaderProjection}) h LEFT JOIN `dwd_order_review_kpi` k ON k.`tenant_id` = h.`tenant_id` AND k.`sales_order_id` = h.`order_id` WHERE h.`order_id` = @SalesOrderId LIMIT 1 """, new List { new("@TenantId", tenantId), new("@SalesOrderId", salesOrderId) }); var row = rows?.FirstOrDefault(); if (row == null) return null; var item = MapRealOrder(row); var detail = new AdoS8OrderFlowOrderDetailDto { SalesOrderId = item.SalesOrderId, OrderNo = item.OrderNo, CustomerId = item.CustomerId, OrderDate = item.OrderDate, OrderType = item.OrderType, OrderStatus = item.OrderStatus, FlowState = item.FlowState, OrderCode = item.OrderCode, CustomerCode = item.CustomerCode, CustomerName = item.CustomerName, WorkflowStatus = item.WorkflowStatus, CurrentOrderFlowCode = item.CurrentOrderFlowCode, CurrentOrderFlowName = item.CurrentOrderFlowName, CurrentStatus = item.CurrentStatus, DataSource = item.DataSource, Lifecycle = BuildFiveStageSkeleton(row), }; return detail; } // ══════════════════════════════════════════════════════════════════════ // 映射 // ══════════════════════════════════════════════════════════════════════ /// /// 行 → 列表项。 /// 没有真实 Authority 的字段一律留空/留 null:ProductName / ProductLine / /// Region / Priority / CustomerType / TargetCycleDays / ActualCycleDays / 各类偏差。 /// 绝不用 SEED、PPT 常量、随机默认值或按客户名推断来填 —— 真实订单配假 KPI 比没有更危险。 /// private static AdoS8OrderFlowOrderListItemDto MapRealOrder(RealOrderRow r) => new() { SalesOrderId = r.SalesOrderId, OrderNo = r.OrderNo, CustomerId = r.CustomerId, OrderDate = r.OrderDate, OrderType = r.OrderType, OrderStatus = r.OrderStatus, FlowState = r.FlowState, // OrderCode 在既有契约里承担「展示码」角色,REAL 下等于真实订单号;身份看 SalesOrderId。 OrderCode = r.OrderNo ?? string.Empty, CustomerCode = r.CustomerNo ?? string.Empty, CustomerName = r.CustomerName ?? string.Empty, // 本批只有 ORDER_REVIEW 有事实,当前阶段恒定指向它;不推断"订单走到哪一步"。 CurrentOrderFlowCode = OrderFlowConstants.ORDER_REVIEW_PLAN_CALC, CurrentOrderFlowName = OrderFlowConstants.ResolveName(OrderFlowConstants.ORDER_REVIEW_PLAN_CALC), CurrentStatus = string.Empty, // 订单状态用真实 Header Authority 映射,不猜。 WorkflowStatus = string.Equals(r.OrderStatus, "CLOSED", StringComparison.OrdinalIgnoreCase) ? "completed" : "in_progress", DataSource = RealDataSource, // ↓ 以下无真实 Authority,显式留空 —— 前端渲染 "--" ProductName = string.Empty, ProductLine = string.Empty, CustomerType = string.Empty, Region = string.Empty, Priority = string.Empty, ScenarioCode = string.Empty, ExceptionStatus = string.Empty, ReleaseAt = null, TargetCycleDays = null, ActualCycleDays = null, CurrentCycleDays = null, NodeVarianceDays = null, CumulativeVarianceDays = null, ResponseMinutes = null, ProcessingMinutes = null, TotalLossMinutes = null, }; /// /// 固定五阶段骨架。 /// 五阶段是 S8 的业务设计,不由「有没有数据」决定 —— 因此 Stage 2~5 /// 必须存在但字段为空;本批只有 ORDER_REVIEW 有真实事实。 /// private static List BuildFiveStageSkeleton(RealOrderRow r) { var list = new List(); for (var i = 0; i < OrderFlowConstants.All.Count; i++) { var code = OrderFlowConstants.All[i]; var isReview = code == OrderFlowConstants.ORDER_REVIEW_PLAN_CALC; list.Add(new AdoS8OrderFlowStageDto { OrderFlowCode = code, OrderFlowName = OrderFlowConstants.ResolveName(code), SortNo = i + 1, OwnerDept = null, // 仅 ORDER_REVIEW 有真实事实;其余阶段一律 null,绝不从 SEED 拼。 ActualStartAt = isReview ? r.ReviewStartTime : null, ActualEndAt = isReview ? r.ReviewEndTime : null, ActualHours = isReview ? r.ActualHours : null, CalculationStatus = isReview ? r.CalculationStatus : null, // 目标/计划/偏差全部属 TargetHours 域,本批无 Authority → null PlannedDays = null, ActualDays = null, TargetAt = null, NodeVarianceDays = null, CumulativeVarianceDays = null, // 状态色本批不评级(红黄绿属另一批次);统一 pending,由前端按时间细分「进行中 / 未到达」 Status = "pending", Substeps = [], }); } return list; } /// SQL 投影行,字段名与 SELECT 别名一一对应。 private sealed class RealOrderRow { public long SalesOrderId { get; set; } public string? OrderNo { get; set; } public long? CustomerId { get; set; } public string? CustomerNo { get; set; } public string? CustomerName { get; set; } public DateTime? OrderDate { get; set; } public int? OrderType { get; set; } public string? OrderStatus { get; set; } public string? FlowState { get; set; } public DateTime? ReviewStartTime { get; set; } public DateTime? ReviewEndTime { get; set; } public decimal? ActualHours { get; set; } public string? CalculationStatus { get; set; } } }