|
|
@@ -1,36 +1,47 @@
|
|
|
+using Admin.NET.Plugin.AiDOP.DataPlatform;
|
|
|
+using Admin.NET.Plugin.AiDOP.DataPlatform.Executors;
|
|
|
+
|
|
|
namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse;
|
|
|
|
|
|
/// <summary>
|
|
|
-/// S5 生产领料单 数据中台只读同步转换服务(DOP 内部,头级单表,独立于 S5MdpSyncTransformService 的 KPI 管线)。
|
|
|
+/// S5 生产领料单 数据中台只读同步转换服务(DOP 内部,头级单表)。
|
|
|
///
|
|
|
-/// 源:aidopdev NbrMaster(m),业务类型 Type='SM';维表 DepartmentMaster(d) LEFT JOIN(Domain+Department)。
|
|
|
-/// 链路:NbrMaster(SM) 只读 → mdp_std_production_issue(typed)。
|
|
|
-/// 不经 stg、不建明细、不建 dwd(只读列表);SM=领料单业务类型有 WorkOrderPickBillService 代码实证(工单下达 Type=SM)。
|
|
|
+/// B-① 双源通用管线(2026-07-29 由 bespoke 直读 NbrMaster 迁入):
|
|
|
+/// 源(NbrMaster) --MdpDbPullExecutor--> mdp_stg_production_issue(raw_data JSON) --transform--> mdp_std_production_issue。
|
|
|
+/// 入站实体:S5_PRODUCTION_ISSUE_MASTER(源=AIDOPDEV_MYSQL) / S5_PRODUCTION_ISSUE_MASTER_SQLSERVER(源=DOPDEMORQ_SQLSERVER)。
|
|
|
+/// 维表 DepartmentMaster 仍本库 LEFT JOIN(与 mode-A 采购/生产收货同构;department_desc 富化,部门 code 由事实表保留)。
|
|
|
///
|
|
|
/// 约束:
|
|
|
-/// - 只读源表,仅写 mdp_std_production_issue;绝不 INSERT/UPDATE/DELETE NbrMaster;不碰 WorkOrderPickBillService 写路径。
|
|
|
-/// - 头过滤 m.Type='SM' AND m.IsActive=1(领料无 IsReturn 语义)。
|
|
|
-/// - status_desc 用旧系统领料 SQL 口径:PretreatmentState 非空取其值,Status='C'→'已下架','A'→'备料中',否则空。
|
|
|
-/// - 领料人 issue_user 取 m.User1(列表 SQL 口径),原样落库。
|
|
|
-/// - trans_type 展示:TransType='Z61'→'补料',否则'正常';trans_type_text:TransType='PrevProcess'→'需要前处理',否则空(旧系统口径)。
|
|
|
-/// - 不做 danjia/jiage/库存事务/状态流转;SM 为 0 行时转换成功完成、处理数为 0,不报错。
|
|
|
+/// - 只读源/贴源,仅写 mdp_stg_production_issue / mdp_std_production_issue;绝不写 NbrMaster;不碰 WorkOrderPickBillService 写路径。
|
|
|
+/// - 头过滤 Type='SM' AND IsActive(领料无 IsReturn 语义);SM=领料单业务类型(Type SM/WOI/WOD/CA 互斥)。
|
|
|
+/// - 跨源类型兼容经 MdpJsonSql(datetime ISO-T / bit true-false / JSON null 归一),不改 MDP 核心。
|
|
|
+/// - 切源单 active source + FULL Replace(决策1A);不做 merge、不做回写/outbox(决策2A)。
|
|
|
+/// - SM 为 0 行时转换成功完成、处理数为 0,不报错。
|
|
|
/// </summary>
|
|
|
public class ProductionIssueMdpSyncService : ITransient
|
|
|
{
|
|
|
private const string JobCode = "S5_PRODUCTION_ISSUE_MDP_SYNC";
|
|
|
+ private const string InboundEntityCode = "S5_PRODUCTION_ISSUE_MASTER";
|
|
|
+
|
|
|
+ // 双源(dopdemorq SQL Server):第二源与其入站实体。实体默认 status=0(就位不启用)。
|
|
|
+ private const string SqlServerSourceCode = "DOPDEMORQ_SQLSERVER";
|
|
|
+ private const string SqlServerEntityCode = "S5_PRODUCTION_ISSUE_MASTER_SQLSERVER";
|
|
|
|
|
|
private readonly ISqlSugarClient _db;
|
|
|
+ private readonly MdpSourcePullDispatcher _pullDispatcher;
|
|
|
|
|
|
- public ProductionIssueMdpSyncService(ISqlSugarClient db)
|
|
|
+ public ProductionIssueMdpSyncService(ISqlSugarClient db, MdpSourcePullDispatcher pullDispatcher)
|
|
|
{
|
|
|
_db = db;
|
|
|
+ _pullDispatcher = pullDispatcher;
|
|
|
}
|
|
|
|
|
|
- /// <summary>全量同步转换:源(SM 头) → 标准层(头)。</summary>
|
|
|
+ /// <summary>全量:本库 NbrMaster 执行器灌 stg → 标准层(读 stg)。</summary>
|
|
|
public async Task<ProductionIssueMdpSyncResult> RunFullAsync(CancellationToken cancellationToken = default, string triggerType = "AUTO")
|
|
|
{
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
await EnsureTablesAsync();
|
|
|
+ await EnsureStgTableAsync();
|
|
|
|
|
|
var now = DateTime.Now;
|
|
|
var batchId = $"S5_PROD_ISSUE_FULL_{now:yyyyMMddHHmmss}";
|
|
|
@@ -39,6 +50,14 @@ public class ProductionIssueMdpSyncService : ITransient
|
|
|
|
|
|
try
|
|
|
{
|
|
|
+ var pullCtx = new MdpPullContext
|
|
|
+ {
|
|
|
+ TenantId = 0,
|
|
|
+ FullRefresh = true,
|
|
|
+ TaskCode = "S5_PRODUCTION_ISSUE_INBOUND",
|
|
|
+ BatchId = $"{batchId}_PULL"
|
|
|
+ };
|
|
|
+ await PopulateStgAsync(InboundEntityCode, pullCtx, cancellationToken);
|
|
|
result.HeadRows = await TransformHeadStandardAsync(batchId, now);
|
|
|
await MarkRunSuccessAsync(runLogId, now, result);
|
|
|
return result;
|
|
|
@@ -50,6 +69,136 @@ public class ProductionIssueMdpSyncService : ITransient
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /// <summary>双模式入站:执行器抽头落 stg,再跑标准层(读 stg)。</summary>
|
|
|
+ public async Task<ProductionIssueInboundResult> RunInboundAsync(
|
|
|
+ long tenantId = 0, bool fullRefresh = false, CancellationToken cancellationToken = default)
|
|
|
+ {
|
|
|
+ cancellationToken.ThrowIfCancellationRequested();
|
|
|
+ await EnsureTablesAsync();
|
|
|
+ await EnsureStgTableAsync();
|
|
|
+
|
|
|
+ var now = DateTime.Now;
|
|
|
+ var pullCtx = new MdpPullContext
|
|
|
+ {
|
|
|
+ TenantId = tenantId,
|
|
|
+ FullRefresh = fullRefresh,
|
|
|
+ TaskCode = "S5_PRODUCTION_ISSUE_INBOUND",
|
|
|
+ BatchId = $"S5_PROD_ISSUE_IN_{now:yyyyMMddHHmmss}"
|
|
|
+ };
|
|
|
+ var pull = await PopulateStgAsync(InboundEntityCode, pullCtx, cancellationToken);
|
|
|
+
|
|
|
+ var batchId = $"S5_PROD_ISSUE_STD_{now:yyyyMMddHHmmss}";
|
|
|
+ var runLogId = await InsertRunLogAsync(batchId, now, "INBOUND");
|
|
|
+ var result = new ProductionIssueMdpSyncResult { BatchId = batchId, RunLogId = runLogId };
|
|
|
+ try
|
|
|
+ {
|
|
|
+ result.HeadRows = await TransformHeadStandardAsync(batchId, now);
|
|
|
+ await MarkRunSuccessAsync(runLogId, now, result);
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ await MarkRunFailedAsync(runLogId, now, ex.Message);
|
|
|
+ throw;
|
|
|
+ }
|
|
|
+
|
|
|
+ return new ProductionIssueInboundResult
|
|
|
+ {
|
|
|
+ PullBatchId = pullCtx.BatchId,
|
|
|
+ RowsPulled = pull.RowsPulled,
|
|
|
+ RowsWrittenStg = pull.RowsWritten,
|
|
|
+ NewCursor = pull.NewCursor,
|
|
|
+ TransformBatchId = result.BatchId,
|
|
|
+ StdRows = result.HeadRows
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 双源切换 FULL Replace(Phase 1):从指定 DB 源(默认 DOPDEMORQ_SQLSERVER)PullAll 全量灌 stg,
|
|
|
+ /// 成功后在单事务内以「仅当前源」结果 FULL 重建 mdp_std_production_issue(按 tenant 精确隔离),消除旧源独有业务键残留。
|
|
|
+ /// PullAllByEntityCodeAsync 抽尽 + FullRefresh=true;transform 仅当前 source_system;Pull 成功后才进入 destructive replace。
|
|
|
+ /// SQLSERVER 实体默认 status=0(就位不启用);本批 FULL-only,不启自动增量。
|
|
|
+ /// </summary>
|
|
|
+ public async Task<ProductionIssueInboundResult> RunSourceSwitchFullAsync(
|
|
|
+ string sourceCode = SqlServerSourceCode,
|
|
|
+ string entityCode = SqlServerEntityCode,
|
|
|
+ long tenantId = 0,
|
|
|
+ CancellationToken cancellationToken = default)
|
|
|
+ {
|
|
|
+ cancellationToken.ThrowIfCancellationRequested();
|
|
|
+ await EnsureTablesAsync();
|
|
|
+ await EnsureStgTableAsync();
|
|
|
+
|
|
|
+ var now = DateTime.Now;
|
|
|
+ var pullCtx = new MdpPullContext
|
|
|
+ {
|
|
|
+ TenantId = tenantId,
|
|
|
+ FullRefresh = true,
|
|
|
+ TaskCode = "S5_PRODUCTION_ISSUE_INBOUND",
|
|
|
+ BatchId = $"S5_PROD_ISSUE_SW_{now:yyyyMMddHHmmss}"
|
|
|
+ };
|
|
|
+ var pull = await _pullDispatcher.PullAllByEntityCodeAsync(entityCode, pullCtx, cancellationToken);
|
|
|
+
|
|
|
+ var batchId = $"S5_PROD_ISSUE_SWSTD_{now:yyyyMMddHHmmss}";
|
|
|
+ var runLogId = await InsertRunLogAsync(batchId, now, "SOURCE_SWITCH");
|
|
|
+ var result = new ProductionIssueMdpSyncResult { BatchId = batchId, RunLogId = runLogId };
|
|
|
+ try
|
|
|
+ {
|
|
|
+ result.HeadRows = await MdpStdFullReplace.ReplaceAsync(
|
|
|
+ _db, "mdp_std_production_issue", tenantId, extraWhere: null,
|
|
|
+ insertScopedAsync: () => TransformHeadStandardAsync(batchId, now, sourceCode),
|
|
|
+ cancellationToken);
|
|
|
+ await MarkRunSuccessAsync(runLogId, now, result);
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ await MarkRunFailedAsync(runLogId, now, ex.Message);
|
|
|
+ throw;
|
|
|
+ }
|
|
|
+
|
|
|
+ return new ProductionIssueInboundResult
|
|
|
+ {
|
|
|
+ PullBatchId = pullCtx.BatchId,
|
|
|
+ RowsPulled = pull.RowsPulled,
|
|
|
+ RowsWrittenStg = pull.RowsWritten,
|
|
|
+ NewCursor = pull.NewCursor,
|
|
|
+ TransformBatchId = batchId,
|
|
|
+ StdRows = result.HeadRows
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task<(int RowsPulled, int RowsWritten, string? NewCursor)> PopulateStgAsync(
|
|
|
+ string entityCode, MdpPullContext pullCtx, CancellationToken cancellationToken)
|
|
|
+ {
|
|
|
+ var r = await _pullDispatcher.PullByEntityCodeAsync(entityCode, pullCtx, cancellationToken);
|
|
|
+ return (r.RowsPulled, r.RowsWritten, r.NewCursor);
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task EnsureStgTableAsync()
|
|
|
+ {
|
|
|
+ await _db.Ado.ExecuteCommandAsync(
|
|
|
+ """
|
|
|
+ CREATE TABLE IF NOT EXISTS mdp_stg_production_issue (
|
|
|
+ id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
|
|
+ tenant_id BIGINT NOT NULL,
|
|
|
+ source_system VARCHAR(50) NULL,
|
|
|
+ source_table VARCHAR(200),
|
|
|
+ source_row_id VARCHAR(200),
|
|
|
+ source_biz_key VARCHAR(300) NULL,
|
|
|
+ raw_data JSON,
|
|
|
+ sync_batch_id VARCHAR(100),
|
|
|
+ sync_time DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
+ process_status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
|
|
|
+ process_message VARCHAR(500) NULL,
|
|
|
+ create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
+ update_time DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
|
+ UNIQUE KEY uk_source_key (source_system, source_table, source_biz_key),
|
|
|
+ KEY idx_batch (sync_batch_id),
|
|
|
+ KEY idx_src (source_table, source_row_id),
|
|
|
+ KEY idx_tenant (tenant_id)
|
|
|
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='S5生产领料执行器贴源层'
|
|
|
+ """);
|
|
|
+ }
|
|
|
+
|
|
|
/// <summary>防御式建表(与 UpdateScripts DDL 同构,幂等)。</summary>
|
|
|
private async Task EnsureTablesAsync()
|
|
|
{
|
|
|
@@ -99,43 +248,63 @@ public class ProductionIssueMdpSyncService : ITransient
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
- /// 标准化头:NbrMaster(SM) + DepartmentMaster -> mdp_std_production_issue。返回处理头行数。
|
|
|
- /// SQL 由旧系统领料头 SQL 翻译(SQL Server -> MySQL):IsNull->IFNULL、upper() 保留、
|
|
|
- /// rtrim(a+' '+b)->TRIM(CONCAT(...))、with(nolock) 去除、convert(varchar,EffDate,23)-> DATE(EffDate)。
|
|
|
+ /// 标准化头:stg(NbrMaster,SM) + DepartmentMaster(本库) → mdp_std_production_issue。
|
|
|
+ /// sourceSystem 非空时仅转当前源(切源 FULL Replace 用);为空时全源(本库全量)。跨源类型经 MdpJsonSql 兼容。
|
|
|
/// </summary>
|
|
|
- private async Task<int> TransformHeadStandardAsync(string batchId, DateTime now)
|
|
|
+ private async Task<int> TransformHeadStandardAsync(string batchId, DateTime now, string? sourceSystem = null)
|
|
|
{
|
|
|
+ var srcClause = sourceSystem == null ? "" : " AND m.source_system=@Src";
|
|
|
+ // 表达式片段(跨源类型兼容)
|
|
|
+ var domainE = MdpJsonSql.Str("m", "Domain");
|
|
|
+ var deptE = MdpJsonSql.Str("m", "Department");
|
|
|
+ var statusUpper = $"UPPER(IFNULL({MdpJsonSql.Str("m", "Status")},''))";
|
|
|
+ var pretreatE = MdpJsonSql.Str("m", "PretreatmentState");
|
|
|
+ var transTypeE = MdpJsonSql.Str("m", "TransType");
|
|
|
+
|
|
|
+ var countPars = new List<SugarParameter>();
|
|
|
+ if (sourceSystem != null) countPars.Add(new SugarParameter("@Src", sourceSystem));
|
|
|
var rows = await _db.Ado.GetIntAsync(
|
|
|
- "SELECT COUNT(1) FROM NbrMaster m WHERE m.Type='SM' AND m.IsActive=1");
|
|
|
- await _db.Ado.ExecuteCommandAsync(
|
|
|
- """
|
|
|
+ $"SELECT COUNT(1) FROM mdp_stg_production_issue m WHERE m.source_table='NbrMaster' " +
|
|
|
+ $"AND {MdpJsonSql.Str("m", "Type")}='SM' AND {MdpJsonSql.BoolTrue("m", "IsActive")}{srcClause}",
|
|
|
+ countPars);
|
|
|
+
|
|
|
+ var insertSql =
|
|
|
+ $"""
|
|
|
INSERT INTO mdp_std_production_issue
|
|
|
(tenant_id, factory_id, source_system, domain, rec_id, nbr, issue_date, status, status_desc,
|
|
|
work_ord, department, department_desc, qty_ord, prod_line, applicant_name, issue_user,
|
|
|
user1, remark, create_user, source_create_time, pretreatment_state, trans_type, trans_type_text,
|
|
|
eff_date, address, source_biz_key, sync_batch_id, sync_time)
|
|
|
SELECT
|
|
|
- IFNULL(m.tenant_id, 0), 1, 'AIDOP', m.Domain, m.RecID, m.Nbr, m.Date,
|
|
|
- UPPER(IFNULL(m.Status, '')),
|
|
|
+ IFNULL({MdpJsonSql.Int("m", "tenant_id")}, IFNULL(m.tenant_id, 0)), 1, IFNULL(NULLIF(m.source_system,''), 'AIDOP'),
|
|
|
+ {domainE}, {MdpJsonSql.Int("m", "RecID")}, {MdpJsonSql.Str("m", "Nbr")},
|
|
|
+ {MdpJsonSql.DateTimeSec("m", "Date")},
|
|
|
+ {statusUpper},
|
|
|
CASE
|
|
|
- WHEN IFNULL(m.PretreatmentState,'')<>'' THEN m.PretreatmentState
|
|
|
- WHEN UPPER(IFNULL(m.Status,''))='C' THEN '已下架'
|
|
|
- WHEN UPPER(IFNULL(m.Status,''))='A' THEN '备料中'
|
|
|
+ WHEN IFNULL({pretreatE},'')<>'' THEN {pretreatE}
|
|
|
+ WHEN {statusUpper}='C' THEN '已下架'
|
|
|
+ WHEN {statusUpper}='A' THEN '备料中'
|
|
|
ELSE ''
|
|
|
END,
|
|
|
- m.WorkOrd, m.Department, TRIM(CONCAT(m.Department, ' ', IFNULL(d.Descr, ''))),
|
|
|
- m.QtyOrd, m.ProdLine, m.Name, CAST(m.User1 AS CHAR),
|
|
|
- CAST(m.User1 AS CHAR), m.Remark, m.CreateUser, m.CreateTime,
|
|
|
- IFNULL(m.PretreatmentState, ''),
|
|
|
- CASE WHEN m.TransType='Z61' THEN '补料' ELSE '正常' END,
|
|
|
- CASE WHEN m.TransType='PrevProcess' THEN '需要前处理' ELSE '' END,
|
|
|
- m.EffDate, m.Address,
|
|
|
- CONCAT(m.Domain, ':', m.RecID), @BatchId, @Now
|
|
|
- FROM NbrMaster m
|
|
|
- LEFT JOIN DepartmentMaster d ON d.Domain = m.Domain AND d.Department = m.Department
|
|
|
- WHERE m.Type='SM' AND m.IsActive=1
|
|
|
+ {MdpJsonSql.Str("m", "WorkOrd")}, {deptE},
|
|
|
+ TRIM(CONCAT(IFNULL({deptE},''), ' ', IFNULL(d.Descr, ''))),
|
|
|
+ {MdpJsonSql.Dec("m", "QtyOrd", 18, 5)}, {MdpJsonSql.Str("m", "ProdLine")}, {MdpJsonSql.Str("m", "Name")},
|
|
|
+ {MdpJsonSql.Str("m", "User1")},
|
|
|
+ {MdpJsonSql.Str("m", "User1")}, {MdpJsonSql.Str("m", "Remark")}, {MdpJsonSql.Str("m", "CreateUser")},
|
|
|
+ {MdpJsonSql.DateTimeSec("m", "CreateTime")},
|
|
|
+ IFNULL({pretreatE}, ''),
|
|
|
+ CASE WHEN {transTypeE}='Z61' THEN '补料' ELSE '正常' END,
|
|
|
+ CASE WHEN {transTypeE}='PrevProcess' THEN '需要前处理' ELSE '' END,
|
|
|
+ {MdpJsonSql.DateTimeSec("m", "EffDate")}, {MdpJsonSql.Str("m", "Address")},
|
|
|
+ IFNULL(NULLIF(m.source_biz_key,''), CONCAT(IFNULL({domainE},''), ':', IFNULL({MdpJsonSql.Str("m", "RecID")},''))),
|
|
|
+ @BatchId, @Now
|
|
|
+ FROM mdp_stg_production_issue m
|
|
|
+ LEFT JOIN DepartmentMaster d ON d.Domain = {domainE} AND d.Department = {deptE}
|
|
|
+ WHERE m.source_table='NbrMaster'
|
|
|
+ AND {MdpJsonSql.Str("m", "Type")}='SM'
|
|
|
+ AND {MdpJsonSql.BoolTrue("m", "IsActive")}{srcClause}
|
|
|
ON DUPLICATE KEY UPDATE
|
|
|
- factory_id=VALUES(factory_id), nbr=VALUES(nbr), issue_date=VALUES(issue_date),
|
|
|
+ factory_id=VALUES(factory_id), source_system=VALUES(source_system), nbr=VALUES(nbr), issue_date=VALUES(issue_date),
|
|
|
status=VALUES(status), status_desc=VALUES(status_desc), work_ord=VALUES(work_ord),
|
|
|
department=VALUES(department), department_desc=VALUES(department_desc), qty_ord=VALUES(qty_ord),
|
|
|
prod_line=VALUES(prod_line), applicant_name=VALUES(applicant_name), issue_user=VALUES(issue_user),
|
|
|
@@ -144,9 +313,14 @@ public class ProductionIssueMdpSyncService : ITransient
|
|
|
trans_type=VALUES(trans_type), trans_type_text=VALUES(trans_type_text),
|
|
|
eff_date=VALUES(eff_date), address=VALUES(address),
|
|
|
sync_batch_id=VALUES(sync_batch_id), sync_time=VALUES(sync_time), update_time=CURRENT_TIMESTAMP
|
|
|
- """,
|
|
|
- new SugarParameter("@BatchId", batchId),
|
|
|
- new SugarParameter("@Now", now));
|
|
|
+ """;
|
|
|
+ var insPars = new List<SugarParameter>
|
|
|
+ {
|
|
|
+ new("@BatchId", batchId),
|
|
|
+ new("@Now", now)
|
|
|
+ };
|
|
|
+ if (sourceSystem != null) insPars.Add(new SugarParameter("@Src", sourceSystem));
|
|
|
+ await _db.Ado.ExecuteCommandAsync(insertSql, insPars);
|
|
|
return rows;
|
|
|
}
|
|
|
|
|
|
@@ -210,3 +384,14 @@ public sealed class ProductionIssueMdpSyncResult
|
|
|
public string BatchId { get; set; } = string.Empty;
|
|
|
public int HeadRows { get; set; }
|
|
|
}
|
|
|
+
|
|
|
+/// <summary>生产领料双源入站结果(stg 抽数 + std 转换)。</summary>
|
|
|
+public sealed class ProductionIssueInboundResult
|
|
|
+{
|
|
|
+ public string PullBatchId { get; set; } = string.Empty;
|
|
|
+ public int RowsPulled { get; set; }
|
|
|
+ public int RowsWrittenStg { get; set; }
|
|
|
+ public string? NewCursor { get; set; }
|
|
|
+ public string TransformBatchId { get; set; } = string.Empty;
|
|
|
+ public int StdRows { get; set; }
|
|
|
+}
|