2012年5月21日 星期一

讓系統自動發送報表給使用者 - mail, window service, reporting service

最近遇到一個狀況。朋友的老闆希望每天都收到公司的帳務報表,且希望該報表能夠加密。
所以我朋友每天早上就很認命來公司產生完報表,匯出成Excel檔,然後加密,寄信給老闆。
即使生病請假或是想出去玩請假,還是得連到公司電腦,繼續上面的步驟。
便想到如何每天自動產生報表,然後自動加密,然後寄信。
所以就來試寫看看啦。

分析需求:
  1. 首先因為是常駐程式,總不能一直開著。程式的定位就是視窗服務 ( windows service )。
  2. 然後有用到寄信服務,也就是需要 SMTP 服務,所以建議 windows server 的環境。
  3. 因為有需要產出報表,需要有 reporting service (廢話) 。



 1.首先設定 SMTP 的服務,如果有問題可參考這篇,然後進行下一步。筆者用另外已經按裝好的win 2003 伺服器來使用。
2.接下來開啟 VS2010 開新專案,選擇 Virtual C# >> Windows >> Windows 服務。
3.點擊 [請按這裡切換到程式碼檢視]。
4.設定要存取的檔案位址。
        static string strFile = @"C:\ReportUse\report.xls" ;


5.撰寫下載 Report 程式碼 GetRPFile。
        /// <summary>
        /// 取得檔案
        /// </summary>
        protected void GetRPFile()
        {

            //網址部分
            Uri uri = new Uri(@"http://localhost/ReportServer?/RPReport&TitleName=ReportName&rs:Format=Excel");
            //利用Webclient來提供驗服務
            WebClient webClient = new WebClient();
            //驗證帳號密碼
            System.Net.NetworkCredential myCr = new NetworkCredential("帳號", "密碼");
            webClient.Credentials = myCr;
            //下載檔案到目的位址,目前設定為C:\ReportUse
            webClient.DownloadFile(uri, strFile);
        }

6.因為要替下載下來的Excel檔進行加密動作,需要制方案總管的專案點選右鍵,選擇[加入參考...]

7.點選上方頁籤 [.NET] 後選擇 [Microsoft.Office.Interop.Excel] 。

8.撰寫Excel加密相關程式碼 AddPasscode。
        /// <summary>
        /// 替Excel報表加入密碼
        /// </summary>
        /// <param name="password">密碼</param>
        protected void AddPassword(string password)
        {
            //開啟目標Excel報表
            Application objExcel = new Application();
            Workbook objWorkBook = objExcel.Workbooks.Open(strFile);
            //加入密碼
            objWorkBook.Password = password;
            //存檔
            objWorkBook.Save();
            objExcel.Quit();
        }


9.撰寫一個寄信的相關類別 SendMail。
        /// <summary>
        /// 寄信程式
        /// </summary>
        /// <param name="objMail">信件地址</param>
        /// <param name="intBadMailCount">重試次數</param>
        /// <returns></returns>
        /// <remarks></remarks>
        protected bool SendMail(MailMessage objMail, int intBadMailCount = 0)
        {

            bool IsSuccess = true;
            SmtpClient objSMTP = new SmtpClient();
            //使用網路SMTP發送MAIL
            //objSMTP.DeliveryMethod = SmtpDeliveryMethod.Network
            //objSMTP.Host = "主機位置"
            //objSMTP.Credentials = New Net.NetworkCredential("帳號", "密碼")

            //使用IIS內建SMTP發送MAIL
            objSMTP.DeliveryMethod = SmtpDeliveryMethod.PickupDirectoryFromIis;
            objSMTP.Host = "Localhost";
            objSMTP.Port = 25;

            bool IsCircle = true;
            int intCount = 0;

            while (IsCircle)
            {
                try
                {
                    objSMTP.Send(objMail);
                    IsSuccess = true;
                    IsCircle = false;
                }
                catch (SmtpFailedRecipientsException ex)
                {
                    //針對發送失敗的使用者紀錄
                    for (int i = 0; i <= ex.InnerExceptions.Length; i++)
                    {
                        SmtpStatusCode status = ex.InnerExceptions[i].StatusCode;
                        if (status == SmtpStatusCode.MailboxBusy || status == SmtpStatusCode.MailboxUnavailable)
                        {
                            Console.WriteLine("信箱忙碌中");
                        }
                        else
                        {
                            Console.WriteLine("無法傳送至該信箱因{0} ", ex.InnerExceptions[i].FailedRecipient);
                        }
                    }
                    IsCircle = true;
                    IsSuccess = false;
                }
                catch (Exception ex)
                {
                    Console.WriteLine("Exception caught in RetryIfBusy(): {0}", ex.ToString());
                    IsCircle = true;
                    IsSuccess = false;
                }
                finally
                {
                    //每發送失敗
                    if (IsCircle)
                        intCount += 1;
                    if (intCount >= intBadMailCount)
                        IsCircle = false;
                }
            }

            objSMTP = null;

            return IsSuccess;

        }


10.首先撰寫一個要觸發的事件 chkProcess 準備讓服務來每小時觸發一次。
        protected void chkProcess(object source, ElapsedEventArgs e)
        {
            DateTime dtNow = DateTime.Now;
            //依照時間點進行工作
            //7點進行取得檔案
            if (dtNow.Hour == 7)
            {
                GetRPFile();
            }
            //8點加入密碼
            else if (dtNow.Hour == 8)
            {
                AddPassword("Password");
            }
            //9點寄信
            else if (dtNow.Hour == 9)
            {
                MailMessage objMail = new MailMessage(new MailAddress(@"system@yahoo.com.tw", "報表系統管理者"), new MailAddress(@"user@yahoo.com.tw", "收件者"));
                objMail.Subject = dtNow.ToString("yyyy-MM-dd")+ "報表寄送";
                objMail.Body = "今天報表如附件,謝謝您。";
                objMail.Attachments.Add(new Attachment(strFile));
                SendMail(objMail,2);
            }
        }


11.設定一個 timer 固定每一個小時觸發一次。
        static System.Timers.Timer objTimer;
        protected override void OnStart(string[] args)
        {
            //設定每一小時執行一次
            objTimer.Interval = 3600000;
            objTimer.Enabled = true;
            objTimer.Elapsed += new ElapsedEventHandler(chkProcess);
            Console.ReadLine();
            //因需要長時間觸發,所以要設定GC避免回收資源
            GC.KeepAlive(objTimer);
        }


12.再設定停止服務時將 timer 關閉並回收。
        protected override void OnStop()
        {
            // 在此加入停止服務所需執行的終止程式碼。
            // 清掉計時器
            objTimer.Enabled = false;
            objTimer.Close();
            objTimer.Dispose();
            objTimer = null;
        }


13.切換至設計界面,往空白部分右鍵點選 [加入安裝程式]。

14.接著修改自動產生的兩個控制項的相關參數。


15.設定 serviceInstaller1 的顯示名稱 (DisplayName)、描述 (Description)、服務名稱 (ServiceName)、啟動類型 (StartType)。


16.以及設定 serviceProcessInstaller1 的啟動帳戶 (Account),設定完這些就差不多大功告成了。

17.將專案編譯之後,該專案所編譯出來的組件 (Assembly) 就具有「安裝能力」,只要透過 NET 提供的安裝程式工具 (Installutil.exe) 便可以將該服務順利安裝至系統內。
18.假設編譯出來的組件名稱是 testSendReportByMail.exe,那麼執行 Windows 服務安裝的指令就是:
C:\WINDOWS\Microsoft.NET\Framework\framework版本\InstallUtil.exe testSendReportByMail.exe

19.而解除按裝相關指令
C:\WINDOWS\Microsoft.NET\Framework\framework版本\InstallUtil.exe /u testSendReportByMail.exe

20.如果沒問題,之後便可以收到 Mail 。

最後因為需要釐清的觀念很多,例如如何使用 Windows Service 以及如何利用 SMTP 發送 Mail 等概念需要事先研讀一陣子。
然後 Windows Service 也比較不易除錯,要事先注意一下。



Windows Service 新增 Installer 功能並自動開啟防火牆設定
SmtpException.StatusCode 屬性

沒有留言:

張貼留言