S8RealOrderArchiveService.cs 13 KB

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