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() 只是「从若干相同值里取一个」,不隐含「取最大」的业务含义。
///
/// 行级字段禁止升格为 Header:progress / 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; }
}
}