|
@@ -0,0 +1,294 @@
|
|
|
|
|
+using Admin.NET.Plugin.AiDOP.Dto.S8.OrderFlow;
|
|
|
|
|
+using Admin.NET.Plugin.AiDOP.Infrastructure;
|
|
|
|
|
+using SqlSugar;
|
|
|
|
|
+
|
|
|
|
|
+namespace Admin.NET.Plugin.AiDOP.Service.S8.OrderFlow;
|
|
|
|
|
+
|
|
|
|
|
+/// <summary>
|
|
|
|
|
+/// S8「订单执行档案」<b>真实订单</b> Read Model。
|
|
|
|
|
+///
|
|
|
|
|
+/// <para><b>数据链</b>:
|
|
|
|
|
+/// <c>mdp_std_so</c>(GROUP BY tenant_id + order_id)→ 订单 Header
|
|
|
|
|
+/// → LEFT JOIN <c>dwd_order_review_kpi</c> → ORDER_REVIEW 阶段事实
|
|
|
|
|
+/// → 固定五阶段骨架 → 前端档案页。</para>
|
|
|
|
|
+///
|
|
|
|
|
+/// <para><b>PRINCIPLE 1 · 租户隔离</b>:租户只来自 <see cref="AidopTenantScope.ResolveOrThrow"/>
|
|
|
|
|
+/// 解析的登录上下文;所有查询与 JOIN 均带 <c>tenant_id</c>,不接受前端传入 tenantId,
|
|
|
|
|
+/// 无默认租户、无 Factory 代偿。</para>
|
|
|
|
|
+///
|
|
|
|
|
+/// <para><b>PRINCIPLE 2 · 只走数据中台</b>:本服务只读 <c>mdp_std_so</c> 与
|
|
|
|
|
+/// <c>dwd_order_review_kpi</c>,不触碰 <c>crm_seorder</c> / <c>crm_seorderentry</c> /
|
|
|
|
|
+/// <c>CustMaster</c> / <c>aidop_action_run_log</c>。</para>
|
|
|
|
|
+///
|
|
|
|
|
+/// <para><b>与 SEED 严格隔离</b>:本服务<b>完全不读</b> <c>ado_s8_order_flow_*</c>。
|
|
|
|
|
+/// 真实订单的 Stage 2~5 一律返回空字段,绝不从 SEED 拼回来;SEED 基线由
|
|
|
|
|
+/// <see cref="S8OrderFlowService"/> 原样保留,供 fixture / 回归使用。</para>
|
|
|
|
|
+/// </summary>
|
|
|
|
|
+public class S8RealOrderArchiveService : ITransient
|
|
|
|
|
+{
|
|
|
|
|
+ /// <summary>本 Read Model 的数据来源标识,用于前端区分 REAL / SEED,禁止两路混用。</summary>
|
|
|
|
|
+ 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;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 订单 Header 投影。
|
|
|
|
|
+ ///
|
|
|
|
|
+ /// <para><b>聚合函数只是 SQL 投影手段,不是业务规则</b> —— <c>mdp_std_so</c> 是订单行粒度,
|
|
|
|
|
+ /// 而这几列已实证在同一 <c>order_id</c> 的所有行内取值完全一致(实测差异计数为 0),
|
|
|
|
|
+ /// 因此 <c>MAX()</c> 只是「从若干相同值里取一个」,不隐含「取最大」的业务含义。</para>
|
|
|
|
|
+ ///
|
|
|
|
|
+ /// <para><b>行级字段禁止升格为 Header</b>:<c>progress</c> / <c>factory_id</c> /
|
|
|
|
|
+ /// <c>item_name</c> / <c>item_spec</c> 逐行不同,故不在本投影内。</para>
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ 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`
|
|
|
|
|
+ """;
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 真实订单列表。一张销售订单一行。
|
|
|
|
|
+ /// <para>ORDER_REVIEW 事实用 <b>LEFT JOIN</b> 挂上 —— 有订单但尚未评审的订单
|
|
|
|
|
+ /// 必须照常出现在档案里,不能被 INNER JOIN 过滤掉。</para>
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ public async Task<List<AdoS8OrderFlowOrderListItemDto>> GetRealOrdersAsync(AdoS8OrderFlowOrderQueryDto query)
|
|
|
|
|
+ {
|
|
|
|
|
+ var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
|
|
|
|
|
+
|
|
|
|
|
+ var where = "1=1";
|
|
|
|
|
+ var ps = new List<SugarParameter> { 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<RealOrderRow>($"""
|
|
|
|
|
+ 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();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 单张真实订单详情(含固定五阶段骨架)。
|
|
|
|
|
+ /// <para><b>定位 Authority = <c>tenant_id + salesOrderId</c></b>,不接受订单号定位。</para>
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ public async Task<AdoS8OrderFlowOrderDetailDto?> GetRealOrderAsync(long salesOrderId)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (salesOrderId <= 0) return null;
|
|
|
|
|
+ var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
|
|
|
|
|
+
|
|
|
|
|
+ var rows = await _db.Ado.SqlQueryAsync<RealOrderRow>($"""
|
|
|
|
|
+ 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<SugarParameter>
|
|
|
|
|
+ {
|
|
|
|
|
+ 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;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ══════════════════════════════════════════════════════════════════════
|
|
|
|
|
+ // 映射
|
|
|
|
|
+ // ══════════════════════════════════════════════════════════════════════
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 行 → 列表项。
|
|
|
|
|
+ /// <para><b>没有真实 Authority 的字段一律留空/留 null</b>:ProductName / ProductLine /
|
|
|
|
|
+ /// Region / Priority / CustomerType / TargetCycleDays / ActualCycleDays / 各类偏差。
|
|
|
|
|
+ /// 绝不用 SEED、PPT 常量、随机默认值或按客户名推断来填 —— 真实订单配假 KPI 比没有更危险。</para>
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ 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,
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 固定五阶段骨架。
|
|
|
|
|
+ /// <para><b>五阶段是 S8 的业务设计,不由「有没有数据」决定</b> —— 因此 Stage 2~5
|
|
|
|
|
+ /// 必须存在但字段为空;本批只有 ORDER_REVIEW 有真实事实。</para>
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ private static List<AdoS8OrderFlowStageDto> BuildFiveStageSkeleton(RealOrderRow r)
|
|
|
|
|
+ {
|
|
|
|
|
+ var list = new List<AdoS8OrderFlowStageDto>();
|
|
|
|
|
+ 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;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>SQL 投影行,字段名与 SELECT 别名一一对应。</summary>
|
|
|
|
|
+ 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; }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|