S8RealOrderArchiveService.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. using Admin.NET.Plugin.AiDOP.Dto.S8.OrderFlow;
  2. using Admin.NET.Plugin.AiDOP.Infrastructure;
  3. using SqlSugar;
  4. namespace Admin.NET.Plugin.AiDOP.Service.S8.OrderFlow;
  5. /// <summary>
  6. /// S8「订单执行档案」<b>真实订单</b> Read Model。
  7. ///
  8. /// <para><b>数据链</b>:
  9. /// <c>mdp_std_so</c>(GROUP BY tenant_id + order_id)→ 订单 Header
  10. /// → LEFT JOIN <c>dwd_order_review_kpi</c> → ORDER_REVIEW 阶段事实
  11. /// → LEFT JOIN <c>dwd_product_design_kpi</c> → PRODUCT_DESIGN 阶段事实
  12. /// → 固定五阶段骨架 → 前端档案页。</para>
  13. ///
  14. /// <para><b>PRINCIPLE 1 · 租户隔离</b>:租户只来自 <see cref="AidopTenantScope.ResolveOrThrow"/>
  15. /// 解析的登录上下文;所有查询与 JOIN 均带 <c>tenant_id</c>,不接受前端传入 tenantId,
  16. /// 无默认租户、无 Factory 代偿。</para>
  17. ///
  18. /// <para><b>PRINCIPLE 2 · 只走数据中台</b>:本服务只读 <c>mdp_std_so</c> 与
  19. /// <c>dwd_order_review_kpi</c>,不触碰 <c>crm_seorder</c> / <c>crm_seorderentry</c> /
  20. /// <c>CustMaster</c> / <c>aidop_action_run_log</c>。</para>
  21. ///
  22. /// <para><b>与 SEED 严格隔离</b>:本服务<b>完全不读</b> <c>ado_s8_order_flow_*</c>。
  23. /// 真实订单的 Stage 2~5 一律返回空字段,绝不从 SEED 拼回来;SEED 基线由
  24. /// <see cref="S8OrderFlowService"/> 原样保留,供 fixture / 回归使用。</para>
  25. /// </summary>
  26. public class S8RealOrderArchiveService : ITransient
  27. {
  28. /// <summary>本 Read Model 的数据来源标识,用于前端区分 REAL / SEED,禁止两路混用。</summary>
  29. public const string RealDataSource = "MDP_STD_SO";
  30. private readonly ISqlSugarClient _db;
  31. private readonly UserManager _userManager;
  32. public S8RealOrderArchiveService(ISqlSugarClient db, UserManager userManager)
  33. {
  34. _db = db;
  35. _userManager = userManager;
  36. }
  37. /// <summary>
  38. /// 订单 Header 投影。
  39. ///
  40. /// <para><b>聚合函数只是 SQL 投影手段,不是业务规则</b> —— <c>mdp_std_so</c> 是订单行粒度,
  41. /// 而这几列已实证在同一 <c>order_id</c> 的所有行内取值完全一致(实测差异计数为 0),
  42. /// 因此 <c>MAX()</c> 只是「从若干相同值里取一个」,不隐含「取最大」的业务含义。</para>
  43. ///
  44. /// <para><b>行级字段禁止升格为 Header</b>:<c>progress</c> / <c>factory_id</c> /
  45. /// <c>item_name</c> / <c>item_spec</c> 逐行不同,故不在本投影内。</para>
  46. /// </summary>
  47. private const string HeaderProjection = """
  48. SELECT `tenant_id`,
  49. `order_id`,
  50. MAX(`order_no`) AS `order_no`,
  51. MAX(`customer_id`) AS `customer_id`,
  52. MAX(`customer_no`) AS `customer_no`,
  53. MAX(`customer_name`) AS `customer_name`,
  54. MAX(`order_date`) AS `order_date`,
  55. MAX(`order_type`) AS `order_type`,
  56. MAX(`order_status`) AS `order_status`,
  57. MAX(`flow_state`) AS `flow_state`
  58. FROM `mdp_std_so`
  59. WHERE `tenant_id` = @TenantId
  60. AND `order_id` IS NOT NULL
  61. AND `order_id` <> 0
  62. AND `deleted_flag` = 0
  63. GROUP BY `tenant_id`, `order_id`
  64. """;
  65. /// <summary>
  66. /// 真实订单列表。一张销售订单一行。
  67. /// <para>ORDER_REVIEW 事实用 <b>LEFT JOIN</b> 挂上 —— 有订单但尚未评审的订单
  68. /// 必须照常出现在档案里,不能被 INNER JOIN 过滤掉。</para>
  69. /// </summary>
  70. public async Task<List<AdoS8OrderFlowOrderListItemDto>> GetRealOrdersAsync(AdoS8OrderFlowOrderQueryDto query)
  71. {
  72. var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
  73. var where = "1=1";
  74. var ps = new List<SugarParameter> { new("@TenantId", tenantId) };
  75. // 订单号 / 客户 模糊检索(order_no 仅作展示与搜索,不参与身份判定)
  76. if (!string.IsNullOrWhiteSpace(query.Keyword))
  77. {
  78. where += " AND (h.`order_no` LIKE @Kw OR h.`customer_name` LIKE @Kw OR h.`customer_no` LIKE @Kw)";
  79. ps.Add(new SugarParameter("@Kw", $"%{query.Keyword.Trim()}%"));
  80. }
  81. if (!string.IsNullOrWhiteSpace(query.CustomerCode))
  82. {
  83. where += " AND h.`customer_no` = @CustNo";
  84. ps.Add(new SugarParameter("@CustNo", query.CustomerCode.Trim()));
  85. }
  86. var rows = await _db.Ado.SqlQueryAsync<RealOrderRow>($"""
  87. SELECT h.`order_id` AS SalesOrderId,
  88. h.`order_no` AS OrderNo,
  89. h.`customer_id` AS CustomerId,
  90. h.`customer_no` AS CustomerNo,
  91. h.`customer_name` AS CustomerName,
  92. h.`order_date` AS OrderDate,
  93. h.`order_type` AS OrderType,
  94. h.`order_status` AS OrderStatus,
  95. h.`flow_state` AS FlowState,
  96. k.`review_start_time` AS ReviewStartTime,
  97. k.`review_end_time` AS ReviewEndTime,
  98. k.`actual_hours` AS ActualHours,
  99. k.`calculation_status` AS CalculationStatus,
  100. p.`product_design_start_time` AS DesignStartTime,
  101. p.`product_design_end_time` AS DesignEndTime,
  102. p.`actual_hours` AS DesignActualHours,
  103. p.`calculation_status` AS DesignCalculationStatus
  104. FROM ({HeaderProjection}) h
  105. LEFT JOIN `dwd_order_review_kpi` k
  106. ON k.`tenant_id` = h.`tenant_id`
  107. AND k.`sales_order_id` = h.`order_id`
  108. LEFT JOIN `dwd_product_design_kpi` p
  109. ON p.`tenant_id` = h.`tenant_id`
  110. AND p.`sales_order_id` = h.`order_id`
  111. WHERE {where}
  112. ORDER BY h.`order_date` DESC, h.`order_id` DESC
  113. """, ps);
  114. return (rows ?? []).Select(MapRealOrder).ToList();
  115. }
  116. /// <summary>
  117. /// 单张真实订单详情(含固定五阶段骨架)。
  118. /// <para><b>定位 Authority = <c>tenant_id + salesOrderId</c></b>,不接受订单号定位。</para>
  119. /// </summary>
  120. public async Task<AdoS8OrderFlowOrderDetailDto?> GetRealOrderAsync(long salesOrderId)
  121. {
  122. if (salesOrderId <= 0) return null;
  123. var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
  124. var rows = await _db.Ado.SqlQueryAsync<RealOrderRow>($"""
  125. SELECT h.`order_id` AS SalesOrderId,
  126. h.`order_no` AS OrderNo,
  127. h.`customer_id` AS CustomerId,
  128. h.`customer_no` AS CustomerNo,
  129. h.`customer_name` AS CustomerName,
  130. h.`order_date` AS OrderDate,
  131. h.`order_type` AS OrderType,
  132. h.`order_status` AS OrderStatus,
  133. h.`flow_state` AS FlowState,
  134. k.`review_start_time` AS ReviewStartTime,
  135. k.`review_end_time` AS ReviewEndTime,
  136. k.`actual_hours` AS ActualHours,
  137. k.`calculation_status` AS CalculationStatus,
  138. p.`product_design_start_time` AS DesignStartTime,
  139. p.`product_design_end_time` AS DesignEndTime,
  140. p.`actual_hours` AS DesignActualHours,
  141. p.`calculation_status` AS DesignCalculationStatus
  142. FROM ({HeaderProjection}) h
  143. LEFT JOIN `dwd_order_review_kpi` k
  144. ON k.`tenant_id` = h.`tenant_id`
  145. AND k.`sales_order_id` = h.`order_id`
  146. LEFT JOIN `dwd_product_design_kpi` p
  147. ON p.`tenant_id` = h.`tenant_id`
  148. AND p.`sales_order_id` = h.`order_id`
  149. WHERE h.`order_id` = @SalesOrderId
  150. LIMIT 1
  151. """, new List<SugarParameter>
  152. {
  153. new("@TenantId", tenantId),
  154. new("@SalesOrderId", salesOrderId)
  155. });
  156. var row = rows?.FirstOrDefault();
  157. if (row == null) return null;
  158. var item = MapRealOrder(row);
  159. var detail = new AdoS8OrderFlowOrderDetailDto
  160. {
  161. SalesOrderId = item.SalesOrderId,
  162. OrderNo = item.OrderNo,
  163. CustomerId = item.CustomerId,
  164. OrderDate = item.OrderDate,
  165. OrderType = item.OrderType,
  166. OrderStatus = item.OrderStatus,
  167. FlowState = item.FlowState,
  168. OrderCode = item.OrderCode,
  169. CustomerCode = item.CustomerCode,
  170. CustomerName = item.CustomerName,
  171. WorkflowStatus = item.WorkflowStatus,
  172. CurrentOrderFlowCode = item.CurrentOrderFlowCode,
  173. CurrentOrderFlowName = item.CurrentOrderFlowName,
  174. CurrentStatus = item.CurrentStatus,
  175. DataSource = item.DataSource,
  176. Lifecycle = BuildFiveStageSkeleton(row),
  177. };
  178. return detail;
  179. }
  180. // ══════════════════════════════════════════════════════════════════════
  181. // 映射
  182. // ══════════════════════════════════════════════════════════════════════
  183. /// <summary>
  184. /// 行 → 列表项。
  185. /// <para><b>没有真实 Authority 的字段一律留空/留 null</b>:ProductName / ProductLine /
  186. /// Region / Priority / CustomerType / TargetCycleDays / ActualCycleDays / 各类偏差。
  187. /// 绝不用 SEED、PPT 常量、随机默认值或按客户名推断来填 —— 真实订单配假 KPI 比没有更危险。</para>
  188. /// </summary>
  189. private static AdoS8OrderFlowOrderListItemDto MapRealOrder(RealOrderRow r) => new()
  190. {
  191. SalesOrderId = r.SalesOrderId,
  192. OrderNo = r.OrderNo,
  193. CustomerId = r.CustomerId,
  194. OrderDate = r.OrderDate,
  195. OrderType = r.OrderType,
  196. OrderStatus = r.OrderStatus,
  197. FlowState = r.FlowState,
  198. // OrderCode 在既有契约里承担「展示码」角色,REAL 下等于真实订单号;身份看 SalesOrderId。
  199. OrderCode = r.OrderNo ?? string.Empty,
  200. CustomerCode = r.CustomerNo ?? string.Empty,
  201. CustomerName = r.CustomerName ?? string.Empty,
  202. // 本批只有 ORDER_REVIEW 有事实,当前阶段恒定指向它;不推断"订单走到哪一步"。
  203. CurrentOrderFlowCode = OrderFlowConstants.ORDER_REVIEW_PLAN_CALC,
  204. CurrentOrderFlowName = OrderFlowConstants.ResolveName(OrderFlowConstants.ORDER_REVIEW_PLAN_CALC),
  205. CurrentStatus = string.Empty,
  206. // 订单状态用真实 Header Authority 映射,不猜。
  207. WorkflowStatus = string.Equals(r.OrderStatus, "CLOSED", StringComparison.OrdinalIgnoreCase)
  208. ? "completed"
  209. : "in_progress",
  210. DataSource = RealDataSource,
  211. // ↓ 以下无真实 Authority,显式留空 —— 前端渲染 "--"
  212. ProductName = string.Empty,
  213. ProductLine = string.Empty,
  214. CustomerType = string.Empty,
  215. Region = string.Empty,
  216. Priority = string.Empty,
  217. ScenarioCode = string.Empty,
  218. ExceptionStatus = string.Empty,
  219. ReleaseAt = null,
  220. TargetCycleDays = null,
  221. ActualCycleDays = null,
  222. CurrentCycleDays = null,
  223. NodeVarianceDays = null,
  224. CumulativeVarianceDays = null,
  225. ResponseMinutes = null,
  226. ProcessingMinutes = null,
  227. TotalLossMinutes = null,
  228. };
  229. /// <summary>
  230. /// 固定五阶段骨架。
  231. /// <para><b>五阶段是 S8 的业务设计,不由「有没有数据」决定</b> —— 因此 Stage 2~5
  232. /// 必须存在但字段为空;本批只有 ORDER_REVIEW 有真实事实。</para>
  233. /// </summary>
  234. private static List<AdoS8OrderFlowStageDto> BuildFiveStageSkeleton(RealOrderRow r)
  235. {
  236. var list = new List<AdoS8OrderFlowStageDto>();
  237. for (var i = 0; i < OrderFlowConstants.All.Count; i++)
  238. {
  239. var code = OrderFlowConstants.All[i];
  240. var isReview = code == OrderFlowConstants.ORDER_REVIEW_PLAN_CALC;
  241. var isDesign = code == OrderFlowConstants.PRODUCT_DESIGN;
  242. list.Add(new AdoS8OrderFlowStageDto
  243. {
  244. OrderFlowCode = code,
  245. OrderFlowName = OrderFlowConstants.ResolveName(code),
  246. SortNo = i + 1,
  247. OwnerDept = null,
  248. // 目前只有 ORDER_REVIEW 与 PRODUCT_DESIGN 有真实事实;
  249. // Stage 3~5(材料采购 / 本体生产 / 总装发货)一律 null —— 即便这些域
  250. // 已经有真实业务数据(工单 / MRP / 采购 / 排产),本批也不顺手点亮。
  251. ActualStartAt = isReview ? r.ReviewStartTime : isDesign ? r.DesignStartTime : null,
  252. ActualEndAt = isReview ? r.ReviewEndTime : isDesign ? r.DesignEndTime : null,
  253. ActualHours = isReview ? r.ActualHours : isDesign ? r.DesignActualHours : null,
  254. CalculationStatus = isReview ? r.CalculationStatus : isDesign ? r.DesignCalculationStatus : null,
  255. // 目标/计划/偏差全部属 TargetHours 域,本批无 Authority → null
  256. PlannedDays = null,
  257. ActualDays = null,
  258. TargetAt = null,
  259. NodeVarianceDays = null,
  260. CumulativeVarianceDays = null,
  261. // 状态色本批不评级(红黄绿属另一批次);统一 pending,由前端按时间细分「进行中 / 未到达」
  262. Status = "pending",
  263. Substeps = [],
  264. });
  265. }
  266. return list;
  267. }
  268. /// <summary>SQL 投影行,字段名与 SELECT 别名一一对应。</summary>
  269. private sealed class RealOrderRow
  270. {
  271. public long SalesOrderId { get; set; }
  272. public string? OrderNo { get; set; }
  273. public long? CustomerId { get; set; }
  274. public string? CustomerNo { get; set; }
  275. public string? CustomerName { get; set; }
  276. public DateTime? OrderDate { get; set; }
  277. public int? OrderType { get; set; }
  278. public string? OrderStatus { get; set; }
  279. public string? FlowState { get; set; }
  280. public DateTime? ReviewStartTime { get; set; }
  281. public DateTime? ReviewEndTime { get; set; }
  282. public decimal? ActualHours { get; set; }
  283. public string? CalculationStatus { get; set; }
  284. public DateTime? DesignStartTime { get; set; }
  285. public DateTime? DesignEndTime { get; set; }
  286. public decimal? DesignActualHours { get; set; }
  287. public string? DesignCalculationStatus { get; set; }
  288. }
  289. }