| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294 |
- 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; }
- }
- }
|