[報表程式 - 1 ] 單一觸發點

常常我們有一些撈報表資料的需求

我們需要從資料庫裡撈出一些符合某種情況的資料 (觸發點)

然後再根據這些資料 去衍生出更多資料 (來自更多其他的資料表)

再把所有資料拿來做一些計算

最後組裝成我們要的結果

並且以不同的方式進行呈現 (也許寫出成檔案)

 

這個例子我會分成四種情境講解

剛好對應到寫報告程式時 最常見的四種型態

這篇是1. 單一事件

完整程式碼請見 https://gitlab.com/jesperlai/Report

 

通常一個典型的報表程式流程會長得像這樣

1.【決定觸發點】我要從DB裡撈出 2001/1/1 ~ 2001/2/1 的生產記錄 (TXN)

2.【根據觸發點的資料,再串出其他資料】根據這些生產記錄 ( TXN ) 再找出其他資訊,例如: 找出客人當初下單時的一些指示 (SO) (當初說要訂購多少數量、交期是哪天)

3.【一次取全部】為了不要多次查詢 DB,所以一開始就要撈出所有資料,接著在程式裡自己每回合篩選出此回合符合的資料

4.【客製邏輯】開始組裝成客戶要的報表欄位

5.【共用函式】當中有些資料也許需經過較複雜的運算 ( 常是可以被其他程式 reuse 的 ),我想要特別獨立出來

6.【輸出】將結果輸出為 TXT、CSV、EXCEL ...

 

Program.cs

using System;
using System.Linq;

namespace Sample_1
{
    class Program
    {
        static void Main(string[] args)
        {
            {
                //為了方便講解 (通常這兩個值會從外面傳進來)----------------------
                var sTime = DateTime.Parse("2001/1/1");
                var eTime = DateTime.Parse("2001/2/20");
                //---------------------------------------------------------------

                //組裝資料
                Controller controller = new Controller(sTime, eTime);
                var result = controller.GetData();

                //寫出檔案
                RptService myRpt = new RptService();
                result = result.OrderBy(q => q.Txn_Dt).ToList();  //最後輸出資料要以過站時間排序
                myRpt.GenFile(result, @"D:\sample.csv");
            }
        }
    }
}

 

Controller.cs

using Sample_1.Extensions;
using Sample_1.Helper;
using Sample_1.Models;
using Sample_1.Utility;
using Sample_1.ViewModels;
using System;
using System.Collections.Generic;

namespace Sample_1
{
    public class Controller
    {
        private DataAccessHelper _helper;

        public Controller(DateTime pDataStartTime, DateTime pDataEndTime)
        {
            _helper = new DataAccessHelper(pDataStartTime, pDataEndTime);
        }

        public List<Report> GetData()
        {
            var src = _helper.InitializeData();
            var result = GetData(src);

            return result;
        }

        private List<Report> GetData(TriggerPointAndDataSetVM src)
        {
            var result = new List<Report>();

            //我把Trigger Point 觸發點 縮寫成tp
            foreach (var tp in src.Tp)
            {
                //該回合需用到的資料 我把this round 縮寫成 tRound
                var tRound = DataAccessHelper.GetThisRoundData(src.DataSet, tp);

                //Controller 盡量扁平 如果有複雜的運算可以放到 Utility 做
                var item = new Report
                {
                    Txn_Dt = tp.Txn_Dt,
                    Lot_No = tp.Lot_No,
                    Stage_Id = tp.Stage_Id,
                    Qty = tp.Qty.Trim(),   //有些轉型或是常用的運算 寫成extension會比較易用
                    Po_No = tRound.So.Po_No,
                    Order_Qty = tRound.So.Qty,
                    Sod = DataUtility.GetSod(tRound.So),  //假設這個欄位有很複雜的邏輯 放到Utitlity做
                };

                result.Add(item);
            }

            return result;
        }
    }
}

 

Utility資料夾

DataUtility.GetSod.cs

using Sample_1.Models;
using System;

namespace Sample_1.Utility
{
    /// <summary>
    /// 假設某些計算是很複雜的 (通常是可以被他人 reuse 的) 把他獨立出來
    /// </summary>
    public static partial class DataUtility
    {
        public static DateTime GetSod(Sales_Order so)
        {
            //假設這裡要處理一些很複雜的邏輯
            return so.Rsod.HasValue ? so.Rsod.Value : so.Sod;
        }
    }
}

 

Helper資料夾內

0_Repository.cs

using Sample_1.Models;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Sample_1.Helper
{

    public class Repository
    {
        public List<Lot_Txn> GetTxn(DateTime sTime, DateTime eTime)
        {
            //為了方便講解 (實際應該在DB中)-------------------------------------------------------------------------------
            var txns = new List<Lot_Txn>
            {
                new Lot_Txn { Txn_Dt = DateTime.Parse("2001/1/1"), Lot_No = "貨批1", Txn_Code = "進站", Stage_Id = "A站", Qty = 10 },
                new Lot_Txn { Txn_Dt = DateTime.Parse("2001/1/2"), Lot_No = "貨批1", Txn_Code = "出站", Stage_Id = "A站", Qty = 10 },
                new Lot_Txn { Txn_Dt = DateTime.Parse("2001/1/3"), Lot_No = "貨批1", Txn_Code = "進站", Stage_Id = "B站", Qty = 10 },
                new Lot_Txn { Txn_Dt = DateTime.Parse("2001/1/4"), Lot_No = "貨批1", Txn_Code = "出站", Stage_Id = "B站", Qty = 10 },

                new Lot_Txn { Txn_Dt = DateTime.Parse("2001/2/1"), Lot_No = "貨批2", Txn_Code = "進站", Stage_Id = "A站", Qty = 20 },
                new Lot_Txn { Txn_Dt = DateTime.Parse("2001/2/2"), Lot_No = "貨批2", Txn_Code = "出站", Stage_Id = "A站", Qty = 20 },
                new Lot_Txn { Txn_Dt = DateTime.Parse("2001/2/3"), Lot_No = "貨批2", Txn_Code = "進站", Stage_Id = "B站", Qty = 20 },
                new Lot_Txn { Txn_Dt = DateTime.Parse("2001/2/4"), Lot_No = "貨批2", Txn_Code = "出站", Stage_Id = "B站", Qty = 20 },
                new Lot_Txn { Txn_Dt = DateTime.Parse("2001/2/5"), Lot_No = "貨批2", Txn_Code = "進站", Stage_Id = "C站", Qty = 20 },
                new Lot_Txn { Txn_Dt = DateTime.Parse("2001/2/6"), Lot_No = "貨批2", Txn_Code = "出站", Stage_Id = "C站", Qty = 20 },
            };
            //------------------------------------------------------------------------------------------------------------

            return txns.Where(q => q.Txn_Dt >= sTime && q.Txn_Dt < eTime && q.Txn_Code == "出站").ToList();
        }

        public List<Sales_Order> GetSo(List<string> lotNos)
        {
            //為了方便講解 (實際應該在DB中)-------------------------------------------------------------------------------
            var sos = new List<Sales_Order>
            {
                new Sales_Order { Lot_No = "貨批1", Po_No = "訂單1", Qty = 50, Sod = DateTime.Parse("2001/1/7") },
                new Sales_Order { Lot_No = "貨批2", Po_No = "訂單2", Qty = 80, Sod = DateTime.Parse("2001/2/7"), Rsod = DateTime.Parse("2001/2/10") },
            };
            //------------------------------------------------------------------------------------------------------------

            return sos.Where(q => q.Lot_No != null && lotNos.Contains(q.Lot_No)).ToList();
        }
    }
}

 

1_DataAccessHelper.cs

using System;

namespace Sample_1.Helper
{
    public partial class DataAccessHelper
    {
        private Repository _repo;
        private DateTime _dataStartTime, _dataEndTime;

        /// <summary>
        /// 通常會傳入一些參數 為了在資料庫裡撈出一些符合的結果來寫出成報表 ex: 撈某一個區間的資料
        /// </summary>
        public DataAccessHelper(DateTime pDataStartTime, DateTime pDataEndTime)
        {
            _repo = new Repository();
            _dataStartTime = pDataStartTime;
            _dataEndTime = pDataEndTime;
        }
    }
}

 

2_DataAccessHelper.TriggerPoint.cs

using Sample_1.Models;
using Sample_1.ViewModels;
using System.Collections.Generic;

namespace Sample_1.Helper
{
    public partial class DataAccessHelper
    {
        public TriggerPointAndDataSetVM InitializeData()
        {
            var data = new TriggerPointAndDataSetVM();

            data.Tp = GetTriggerPoints();
            data.DataSet = ConcreteDataSet(data.Tp);

            return data;
        }

        private List<Lot_Txn> GetTriggerPoints()
        {
            return _repo.GetTxn(_dataStartTime, _dataEndTime);
        }
    }
}

 

3_DataAccessHelper.DataSet.cs

using Sample_1.Models;
using Sample_1.ViewModels;
using System.Collections.Generic;
using System.Linq;

namespace Sample_1.Helper
{
    public partial class DataAccessHelper
    {
        /// <summary>
        /// 其他資料都是based on 你觸發點撈出來的資料有什麼 再去補撈其他資料 
        /// 例如: 先撈出過站記錄,再透過這段時間內有哪些貨批在動 (表示要報) 再去撈出這些貨批的客戶訂單資料
        /// </summary>
        public DataSetVM ConcreteDataSet(List<Lot_Txn> triggerPoints)
        {
            DataSetVM dataSet = new DataSetVM();

            dataSet.So = GetSo(triggerPoints);

            return dataSet;
        }

        private List<Sales_Order> GetSo(List<Lot_Txn> triggerPoints)
        {
            var lotNos = triggerPoints.Select(q => q.Lot_No).Distinct().ToList();
            var result = _repo.GetSo(lotNos);

            return result;
        }
    }
}

 

4_DataAccessHelper.ThisRound.cs

using Sample_1.Models;
using Sample_1.ViewModels;
using System.Linq;

namespace Sample_1.Helper
{
    public partial class DataAccessHelper
    {
        /// <summary>
        /// 因為希望不要一直頻繁開關DB 所以前面一次把所有批號的資料都撈出來了
        /// 接下來得篩選出只屬於這回合的資料 (例如 根據這回合的過站記錄的批號 從剛剛的DataSet中找出訂單 => 不是每跑一批就查一次DB )
        /// </summary>
        public static ThisRoundDataVM GetThisRoundData(DataSetVM src, Lot_Txn tp)
        {
            ThisRoundDataVM tRound = new ThisRoundDataVM();

            tRound.So = GetSo(src, tp);

            return tRound;
        }

        private static Sales_Order GetSo(DataSetVM src, Lot_Txn tp)
        {
            var Sos = src.So.Where(q => q.Lot_No == tp.Lot_No).ToList();
            return Sos.Any() ? Sos.First() : new Sales_Order();
        }
    }
}

 

Extensions 資料夾

MyExtensions.cs

using System;

namespace Sample_1.Extensions
{
    /// <summary>
    /// 有時後把一些函式包裝成擴充方法使用起來會比較直覺,程式碼看起來也比較乾淨
    /// </summary>
    public static class MyExtensions
    {
        public static int Trim(this double? value)
        {
            return Convert.ToInt32(value);
        }
    }
}

 

RptCsvService.cs

using Sample_1.Models;
using System.Collections.Generic;

namespace Sample_1
{
    public class RptService
    {
        public void GenFile(List<Report> data, string fullPath)
        {

            try
            {
                //寫一些你的程式碼來把資料寫出成檔案
                //或是使用ServiceStack來做序列化

                //File.WriteAllText(fullPath, data, new UTF8Encoding(true));
            }
            catch
            {

            }
        }
    }
}

 

Models資料夾

0_Lot_Txn.cs

using System;

namespace Sample_1.Models
{
    /// <summary>
    /// 交易過站資料表
    /// </summary>
    public class Lot_Txn
    {
        /// <summary>
        /// 過站時間
        /// </summary>
        public DateTime Txn_Dt { get; set; }
        /// <summary>
        /// 批號
        /// </summary>
        public string Lot_No { get; set; }
        /// <summary>
        /// 站碼
        /// </summary>
        public string Txn_Code { get; set; }
        /// <summary>
        /// 站別
        /// </summary>
        public string Stage_Id { get; set; }
        /// <summary>
        /// 過站數量
        /// </summary>
        public double? Qty { get; set; }
    }
}

 

1_Sales_Order.cs

using System;

namespace Sample_1.Models
{
    /// <summary>
    /// 訂單資料表
    /// </summary>
    public class Sales_Order
    {
        /// <summary>
        /// 批號
        /// </summary>
        public string Lot_No { get; set; }
        /// <summary>
        /// 原交期
        /// </summary>
        public DateTime Sod { get; set; }
        /// <summary>
        /// 新交期
        /// </summary>
        public DateTime? Rsod { get; set; }
        /// <summary>
        /// 訂單編號
        /// </summary>
        public string Po_No { get; set; }
        /// <summary>
        /// 訂單數量
        /// </summary>
        public int Qty { get; set; }
    }
}

 

2_Report.cs

using System;

namespace Sample_1.Models
{
    /// <summary>
    /// 輸出結果
    /// </summary>
    public class Report
    {
        /// <summary>
        /// 假設報表中有個欄位是輸出生產公司是誰 (固定值)
        /// </summary>
        public string Company
        {
            get
            {
                return "很棒棒工廠";
            }
        }
        /// <summary>
        /// 訂單編號
        /// </summary>
        public string Po_No { get; set; }
        /// <summary>
        /// 批號
        /// </summary>
        public string Lot_No { get; set; }
        /// <summary>
        /// 過站時間
        /// </summary>
        public DateTime Txn_Dt { get; set; }
        /// <summary>
        /// 站別
        /// </summary>
        public string Stage_Id { get; set; }
        /// <summary>
        /// 過站數量
        /// </summary>
        public int Qty { get; set; }
        /// <summary>
        /// 交期
        /// </summary>
        public DateTime? Sod { get; set; }
        /// <summary>
        /// 訂單數量
        /// </summary>
        public int Order_Qty { get; set; }
    }
}

 

ViewModels

0_TriggerPointAndDataSetVM.cs

using MySample.Console.Models;
using System.Collections.Generic;

namespace MySample.Console.ViewModels
{
    public class TriggerPointAndDataSetVM
    {
        public List<LOT_TXN> Tp { get; set; }
        public DataSetVM DataSet { get; set; }
    }
}

 

1_DataSetVM.cs

using MySample.Console.Models;
using System.Collections.Generic;

namespace MySample.Console.ViewModels
{
    public class DataSetVM
    {
        public List<SO> So { get; set; }
    }
}

 

2_ThisRoundDataVM.cs

using MySample.Console.Models;

namespace MySample.Console.ViewModels
{
    public class ThisRoundDataVM
    {
        public SO So { get; set; }
    }
}

 

希望透過這樣的架構

可以解決大家一不小心就把這種到處串資料的程式寫成幾萬行

並且常找不到哪一行壞掉的問題