顯示具有 工作心得 標籤的文章。 顯示所有文章
顯示具有 工作心得 標籤的文章。 顯示所有文章
2022-09-08 13:39

架構解釋

以前在專案開發時對分層架構所定下的原則,去避免不必要的地雷,以及每一層的要處裡的職權。

我會考慮分層架構,是希望提高系統的嚴謹度,以及 Method 的共用性,所以 Dao 層級就要避免功能導向,不然就是把單頁的程式拆分到多個層去而已。

 

DomainModel / ViewModel

  • 這三類都屬於 POCO 類型的物件,單純的資料載體,不允許有動作的 method,不可以外部取資料
  • 相同資料的欄位命名必須一致,反例: ProjectId, Pid, pid, PID, p_id
  • 有欄位就要有資料,明明有欄位定義卻不從 DB 取資料,這會造成不必要的雷

 

DomainModel

  • 欄位定義可與 DB 一致
  • 將 DB 的資料進行彙整,成為完整的資料體

 

ViewModel

欄位定義與 View 的表單(Form)一致,與 DomainModel 可能很相似,但有些欄位只會用在表單上,所以 DomainModel 不適合用在表單 Binding 上

例如:

  • 同意上述條款
  • 舊密碼, 新密碼, 確認密碼

 

Dao

  • 回傳 DomainModel
  • 定義單純的 BD 操作 List, GetById, Save, Insert, Update
  • 與 ORM 不同,是進一步將資料操作簡化
  • 保證 Method 的通用性,避免功能導向,例如: 有一個 Method 是專門為了A畫面功能而存在的

 

Service

  • 回傳 DomainModel
  • 驗證商業邏輯,如必要資料欄位,數值範圍等...
  • 調用一個或多個 Dao 來完成商業邏輯
  • 處理 DB 交易,來協調多個 Dao 的調用

 

Controller

  • 處理 DomainModel 到 ViewModel 的轉換
  • 調用 Service 進行 DomainModel 的資料處理
  • 調配 Responses 的結果 Html, Json, PDF ...
  • 驗證資料類型的正確,數值﹑日期
  • 負責 Access 的權限阻擋

 

View

  • View 是被動的,不能存取任何 Controller / Service / Dao
  • 負責資料呈現及格式化,如 日期﹑金額 ...
  • Ajax 只能對 Controller 調用

 

附註

  1. 同一層之間不可以互相參考,這容易發生循環參考,例如:
    • Controller 呼叫 Controller
    • Service 呼叫 Service
    • Dao 呼叫 Dao
  2. 同一層之間有相同邏輯可以抽離到 Support 類
     
  3. 建議定義命名規範,讓每一個人寫出來的程式像是同一個人寫的,好處是降低支援或接手的人的困難度
     
  4. 任何第三方的 API 都需要包裝,隔離直接相依的問題,而且可以單獨測試
     

 

架構關係全貌

 

Model 傳遞的關係

 

用 Interface 隔離實作

 

Web Api 的呼叫關係

 

2021-10-16 23:40

C# MVC 的 UserException 攔截處理

為了將錯誤訊息呈現給使用者,要完成幾件事:

  • 用 Filter 攔截 UserException
  • 將錯誤訊息存到 TempData,之後呈現在畫面上
  • 回到原本的畫面,需要產生 ViewResult
  • 針對 Ajax 請求用 400 狀態回應,並將訊息放在 http content


.Net Framework MVC

using System;
using System.Linq;
using System.Text.RegularExpressions;
using System.Web.Mvc;

namespace XXXXX.Mvc.Filters
{
    /// <summary>例外訊息過濾器</summary>
    public class ExceptionMessageActionFilter : ActionFilterAttribute
    {

        /// <summary></summary>
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            /* 自動將 Action 的 Parameters 中的 ViewModel 賦予給 ViewData
             * 不然要自己在 Action 的第一行寫 ViewData.Model = domain;
             */

            string paramName = filterContext.ActionDescriptor.GetParameters()
                .OrderBy(x => Regex.IsMatch(x.ParameterType.ToString(), @"(ViewModel|Domain)\b") ? 0 : 1)
                .Select(x => x.ParameterName)
                .FirstOrDefault();

            if (paramName != null)
            { filterContext.Controller.ViewData.Model = filterContext.ActionParameters[paramName]; }
        }


        /// <summary></summary>
        public override void OnActionExecuted(ActionExecutedContext filterContext)
        {
            base.OnActionExecuted(filterContext);

            /* 不處理子 Action */
            if (filterContext.IsChildAction) { return; }

            /* 判斷 Exception 是否已經被處理了 */
            if (filterContext.ExceptionHandled) { return; }

            /* 只針對 UserException 進行錯誤處理*/
            var ex = filterContext.Exception as UserException;
            if (ex == null) { return; }


            /* 標記 Exception 已經被處理了,讓後續的 Filter 不用再處理 */
            filterContext.ExceptionHandled = true;

            if (filterContext.HttpContext.Request.IsAjaxRequest())
            {
                /* 對 Ajax 請求的處理 */
                filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
                filterContext.HttpContext.Response.StatusCode = 400;
                filterContext.Result = new ContentResult { Content = ex.Message };
            }
            else if (ex is JwNoDataException)
            {
                /* 資料不存在的處理 */
                filterContext.Controller.TempData["StatusError"] = ex.Message;
                filterContext.Result = new HttpNotFoundResult("[" + ex.Message + "]");
            }
            else
            {
                /* 一般畫面的處理 */
                filterContext.Controller.TempData["StatusError"] = ex.Message;
                filterContext.Result = new ViewResult
                {
                    ViewData = filterContext.Controller.ViewData,
                    TempData = filterContext.Controller.TempData
                };
            }
        }

    }
}

在 FilterConfig.cs 配置

public class FilterConfig
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        //...

        filters.Add(new ExceptionMessageActionFilter());

        //...
    }
}


Net core 3.1 MVC

using System;
using System.Linq;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

namespace Orion.Mvc.Filters
{
    /// <summary>例外訊息過濾器</summary>
    public class ExceptionMessageActionFilter : ActionFilterAttribute
    {

        /// <summary></summary>
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            /* 自動將 Action 的 Arguments 中的 ViewModel 賦予給 ViewData
             * 不然要自己在 Action 的第一行寫 ViewData.Model = domain;
             */

            var arguments = filterContext.ActionArguments.Values.Where(x => x != null);

            object model = arguments
                .OrderBy(x => Regex.IsMatch(x.GetType().Name, @"(ViewModel|Domain)\b") ? 0 : 1)
                .FirstOrDefault();

            var controller = filterContext.Controller as Controller;
            controller.ViewData.Model = model;
        }


        /// <summary></summary>
        public override void OnActionExecuted(ActionExecutedContext filterContext)
        {
            /* 判斷 Exception 是否已經被處理了 */
            base.OnActionExecuted(filterContext);
            if (filterContext.ExceptionHandled) { return; }


            /* 只針對 UserException 進行錯誤處理*/
            var ex = filterContext.Exception as UserException;
            if (ex == null) { return; }

            /* 標記 Exception 已經被處理了,讓後續的 Filter 不用再處理 */
            filterContext.ExceptionHandled = true;


            var controller = filterContext.Controller as Controller;

            var headers = filterContext.HttpContext.Request.Headers;
            bool isAjax = headers["X-Requested-With"] == "XMLHttpRequest";

            if (isAjax)
            {
                /* 對 Ajax 請求的處理 */
                filterContext.HttpContext.Response.StatusCode = 400;
                filterContext.Result = new ContentResult { StatusCode = 400, Content = ex.Message };
            }
            else if (ex is UserNoDataException)
            {
                /* 資料不存在的處理 */
                controller.TempData["StatusError"] = ex.Message;
                filterContext.HttpContext.Response.StatusCode = 404;
            }
            else
            {
                /* 一般畫面的處理 */
                controller.TempData["StatusError"] = ex.Message;
                filterContext.Result = new ViewResult
                {
                    ViewData = controller.ViewData,
                    TempData = controller.TempData
                };
            }
        }

    }
}

在 Startup.cs 配置

public void ConfigureServices(IServiceCollection services)
{
    //...

    IMvcBuilder mvcBuilder = services
        .AddMvc(options =>
        {
            //...
            options.Filters.Add(new ExceptionMessageActionFilter());
            //...
        })
        .AddControllersAsServices()
        ;

    //...
}


Net core 3.1 Razor Page

using System;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace XXXXX.Api.Filters
{

    public class ExceptionMessagePageFilter : AbstractPageFilter
    {
        public override void OnPageHandlerExecuted(PageHandlerExecutedContext context)
        {
            if (context.ExceptionHandled) { return; }

            /* 判斷是否有指定的 Exception */
            var ex = context.Exception;
            if (ex is UserException userEx) { handleUserException(context, userEx); return; }
            if (ex is HttpException httpEx) { handleHttpException(context, httpEx); return; }
        }


        private void handleUserException(PageHandlerExecutedContext context, UserException ex)
        {
            /* 只針對 PageModel 進行錯誤處理*/
            var page = context.HandlerInstance as PageModel;
            if (page == null) { return; }

            /* 標記 Exception 已經被處理了,讓後續的 Filter 不用再處理 */
            context.ExceptionHandled = true;

            var headers = filterContext.HttpContext.Request.Headers;
            bool isAjax = headers["X-Requested-With"] == "XMLHttpRequest";

            if (isAjax)
            {
                /* 對 Ajax 請求的處理 */
                context.HttpContext.Response.StatusCode = 400;
                context.Result = new ContentResult
                {
                    StatusCode = 400,
                    Content = ex.Message
                };
            }
            else if (ex is UserNoDataException)
            {
                /* 資料不存在的處理 */
                page.TempData["StatusError"] = ex.Message;
                context.HttpContext.Response.StatusCode = 404;
            }
            else
            {
                /* 一般畫面的處理 */
                page.TempData["StatusError"] = ex.Message;
                context.Result = page.Page();
            }
        }



        private void handleHttpException(PageHandlerExecutedContext context, HttpException ex)
        {
            context.ExceptionHandled = true;
            
            var headers = context.HttpContext.Request.Headers;
            bool isAjax = headers["X-Requested-With"] == "XMLHttpRequest";

            if (isAjax)
            {
                context.HttpContext.Response.StatusCode = ex.StatusCode;
                context.Result = new ContentResult
                {
                    StatusCode = ex.StatusCode,
                    Content = ex.Message,
                };
            }
            else
            {
                context.HttpContext.Response.StatusCode = ex.StatusCode;
                context.Result = new StatusCodeResult(ex.StatusCode);
            }
        }
    }
}

在 Startup.cs 配置

public void ConfigureServices(IServiceCollection services)
{
    //...

    IMvcBuilder mvcBuilder = services
        .AddRazorPages(options =>
        {
            //...
        })
        .AddMvcOptions(options =>
        {
            //...
            options.Filters.Add(new ExceptionMessagePageFilter());
            //...
        })
        ;

    //...
}


2021-10-16 23:09

用 Exception 作為使用者錯誤訊息的通道

自訂一個 UserException 來傳遞給使用者的訊息,然後程式裡只要簡單的 throw 訊息,接著就是在全域的錯誤處理中將訊息呈現給使用者。

這是一種非常方便的方式,因為可以無視 method 的階層深度,在程式的任何地方都可以將訊息傳到使用者面前。

原則上盡可能將錯誤轉化成 UserException,並且錯誤訊息要能讓使用者[明確知道]要如何正確操作軟體,但是只有工程師才可以修正的錯誤就應該記錄下來,並給使用者這樣的訊息[系統出現錯誤,請通知管理者],只有盡可能的修正錯誤才不會讓使用者常常打電話吵你。

2021-10-16 11:51

重構基礎-邏輯反相

利用程式具有區段的能力,我們可以將 [如果 A 成立就做 B] 轉換成 [如果 A 不成立就離開],用這個方式做邏輯反相可以讓原本看似複雜的程式簡化很多,讓程式變得清晰思慮也會變的清晰,困難的問題就變得簡單,或著就不在是問題了。

這是一段重構前的程式:

foreach (var item in list)
{
    Guid guid = new Guid(item.project_GUID.Value.ToString());
    var project = _dc.Project.FirstOrDefault(w => w.ProjectID == guid);
    if (project != null)
    {
        var mails = !String.IsNullOrEmpty(project.AlertEmail) ? project.AlertEmail.Split(new char[]{';'}) : null;
        if (mails != null)
        {
            if(!mails.Any(x => x.mail == "a@b.c"))
            {
                foreach (var mail in mails)
                {
                    if (!mailDic.ContainsKey(mail.Trim()))
                        mailDic.Add(mail, new List<int>());

                    mailDic[mail].Add(item.projectID);
                }

            }
        }
    }
}

將邏輯反相重構後的程式:

foreach (var item in list)
{
    Guid guid = new Guid(item.project_GUID.Value.ToString());
    var project = _dc.Project.FirstOrDefault(w => w.ProjectID == guid);
    if (project == null) { continue; }

    if (String.IsNullOrEmpty(project.AlertEmail)) { continue; }

    var mails = project.AlertEmail.Split(new char[]{';'});
    if(mails.Any(x => x.mail == "a@b.c")){ continue; }

    foreach (var mail in mails)
    {
        if (!mailDic.ContainsKey(mail.Trim()))
        { mailDic.Add(mail, new List<int>()); }

        mailDic[mail].Add(item.projectID);
    }
}

可以看出程式的縮排階層減少了,也簡化了程式的複雜度,重構的方法就是將 if 反相將 else 前移,也許一開始很難用這種方式寫程式,但是人腦是可以訓練的,在每次重構時進行調整改寫,習慣這種模式後很自然就會用這種[跳離]的思維去寫程式邏輯了。

當然這種方式也是有缺點的,就是有時候不這麼直覺會不好理解,但是可以增加一些註解來輔助描述,或是拆分判斷條件例如:

if (a != null || b == null) { return; }

/* 這可以拆分成兩行,不用拘泥一行完成 */

if (a != null) { return; }
if (b == null) { return; }

程式邏輯有一個有趣的事,往往用正向邏輯無法處理的問題,改用反相邏輯就會簡單很多,例如:需要用特定的邏輯尋找符合的項目,可能因為那個特定邏輯讓效能非常的差,這時候如果改用不符合特定邏輯的項目排除,留下來的就會是符合的項目,因為是用消去法所以要尋找的數量會越算越少,所以執行效能會收斂。

2021-10-15 17:26

全域的 Exception 處理

大部分具有 Exception 機制的程式語言都有提供全域的 Exception 處理,如果你已經有用 log 去記錄錯誤的習慣的話,與其在程式裡佈滿了 try catch,不如在全域處裡中去記錄沒有被 catch 的 Exception,這樣程式就可以更乾淨了,而且程式如果發生異常的關閉時也會進入全域處裡,可以確保 Exception 妥善地被記錄。


Console, WinForm 程式

using System;
using System.Security.Permissions;
using System.Threading;
using System.Windows.Forms;
using NLog;

namespace AppThreadException
{
    static class Program
    {
        private static readonly ILogger _log = LogManager.GetCurrentClassLogger();


        [STAThread]
        [SecurityPermission(SecurityAction.Demand, Flags = SecurityPermissionFlag.ControlAppDomain)]
        public static void Main(string[] args)
        {
            /* ThreadException 用來攔截 UI 錯誤 */
            Application.ThreadException += threadExceptionHandler;

            /* UnhandledException 只能攔截錯誤,不能阻止程式關閉 */
            Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
            AppDomain.CurrentDomain.UnhandledException += unhandledExceptionHandler;

            Application.Run(new MyForm());
        }
        

        /// <summary>攔截 UI 錯誤</summary>
        private static void threadExceptionHandler(object sender, ThreadExceptionEventArgs e)
        {
            _log.Fatal(e.Exception, "操作錯誤");
            MessageBox.Show(e.Exception.Message, "操作錯誤", MessageBoxButtons.OK, MessageBoxIcon.Stop);
        }
        

        /// <summary>攔截不可挽回的錯誤,不能阻止程式關閉</summary>
        private static void unhandledExceptionHandler(object sender, UnhandledExceptionEventArgs e)
        {
            Exception ex = (Exception)e.ExceptionObject;

            _log.Fatal(ex, "執行錯誤");
            MessageBox.Show(ex.Message, "執行錯誤", MessageBoxButtons.OK, MessageBoxIcon.Stop);
        }
    }
}

Web 程式

在 Global.asax.cs 中可以設定 Application_Error 就可以攔截未處裡的 Exception,除了用這個方法記錄錯誤,還可以用現有的套件(elmah)幫我們完成。

using System;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
using NLog;

namespace MvcReport
{
    public class MvcApplication : System.Web.HttpApplication
    {
        private static readonly ILogger _log = LogManager.GetCurrentClassLogger();

        //...

        protected void Application_Error(object sender, EventArgs e)
        {
            Exception ex = Server.GetLastError();
            _log.Fatal(ex, ex.Message);
        }

        //...
    }
}

Net core 程式

Net core 的程式都是相同的方式,Web Request 的要另外配置

using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Autofac.Extensions.DependencyInjection;
using EverTop.Api;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using NLog;


namespace MyWebApp
{
    public class Program
    {
        private static readonly ILogger _log = LogManager.GetCurrentClassLogger();


        public static void Main(string[] args)
        {
            /* UnhandledException 只能攔截錯誤,不能阻止程式關閉 */
            AppDomain.CurrentDomain.UnhandledException += unhandledExceptionHandler;

            /* 用來攔截 Task 錯誤 */
            TaskScheduler.UnobservedTaskException += unobservedTaskException;

            var host = Host.CreateDefaultBuilder(args)
                .UseServiceProviderFactory(new AutofacServiceProviderFactory())
                .ConfigureWebHostDefaults(webHostBuilder => webHostBuilder
                    .UseStartup<Startup>()
                )
                .Build();

            host.Run(); /* 啟動網站 */
        }


        private static void unhandledExceptionHandler(object sender, UnhandledExceptionEventArgs e)
        {
            Exception ex = (Exception)e.ExceptionObject;
            _log.Fatal(ex, "執行錯誤");
        }

        private static void unobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
        {
            _log.Fatal(e.Exception, "執行錯誤");
            e.SetObserved();
        }
    }
}

Net core 3.1 Web 程式

在 Startup.cs 中配置 middleware 進行 Exception 攔截

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (_env.IsProduction())
    {
        app.UseExceptionHandler("/Error");
    }
    else
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseStatusCodePagesWithReExecute("/Error");


    /* 利用 middleware 進行 Exception 攔截 */
    /* 這裡的順序很重要,不然會被前面 ExceptionHandler 處理掉就拿不到 Exception */
    app.Use(async (context, next) =>
    {
        try
        {
            await next();
        }
        catch (Exception ex)
        {
            _log.Fatal(ex, "執行錯誤");
            throw; /* 把 Exception 再丟出去給別人處理 */
        }
    });
}

週期性 Thread 的處裡方式

將主要邏輯寫在另外 method 裡,這樣可以專注在 Exception 上。

private bool _runFlag = false;

public void Start()
{
    if (_runFlag) { return; }
    _runFlag = true;

    var thread = new Thread(() =>
    {
        while (_runFlag)
        {
            try
            {
                cycleHandler();
            }
            catch (Exception ex)
            {
                _log.Fatal(ex, "執行錯誤");
            }
            Thread.Sleep(1000);
        }
    });

    thread.Start();
}

public void Stop()
{
    _runFlag = false;
}

private void cycleHandler()
{
    // 主要的邏輯程式寫在這裡
}

PHP 錯誤處理

Ref: set_error_handler, set_exception_handler

<?php
function error_handler($errno, $errstr, $errfile, $errline) {
    if(error_reporting() === 0){ return; }
    throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
}
set_error_handler('error_handler', E_ALL^E_NOTICE);


function exception_handler($exception) {
    // 在這記錄 log
    throw $exception;
}
set_exception_handler('exception_handler');


throw new Exception('Uncaught Exception');
//$a = 1 / 0;

2021-10-14 17:21

Exception 是傳遞訊息的通道

void show(string tag)
{
    Console.WriteLine(tag);
}

void methodA()
{
    throw new Exception("error msg");
    show("A");
}

void methodB()
{
    methodA();
    show("B");
}

void methodC()
{
    try
    {
        methodB();
        show("C");
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
}

這裡為了方便我們理解將程式展開成下面的樣子:

void show(string tag)
{
    Console.WriteLine(tag);
}

void methodC()
{
    try
    {
        methodB();
            void methodB()
            {
                methodA();
                    void methodA()
                    {
                        throw new Exception("error msg");
    //-----------------------------------------------------
                        show("A");
                    }
                show("B");
            }
        show("C");
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
}

從範例中我們可以看出兩個特性:


* 通透性

從 throw 的那一行開始 Exception 會一直向上傳遞,直到 catch 的區段。


* 脫離性

從 throw 的那一行之後的程式都不會被執行,會有類似 return 的效果,不同的是會對每一層的 method 都進行脫離,包含 try 的區段也會脫離。


methodC 的 catch 會捕獲從 methodA 丟出的 Exception,並且 Exception 中的 StackTrace 會紀錄 methodA, methodB, methodC,而 show() 都不會被執行。

由於大部分的程式邏輯都可以轉化成 else 就離開的程式流程,正好符合 throw 的脫離性,所以我們一開始在撰寫程式邏輯的時候,可以先專注在一般正常的流程邏輯上,之後再去補足檢查邏輯或脫離邏輯,最後在合適的位置進行 try catch 處理,或者在全域的錯誤處理中進行處理。


這裡有一個糟糕的 method,他用了個 Result 物件來裝載回傳結果以及錯誤訊息:

public class Result
{
    public int Value { get; set; }
    public string Error { get; set; }
}

public Result BadMethod(int input)
{
    var result = new Result();

    if (input > 0)
    {
        result.Value = 100 / input;
    }
    else
    {
        result.Error = "input 需要大於零";
    }
    return result;
}

如果用 Exception 可以讓程式簡單很多:

public int GoodMethod(int input)
{
    if (input <= 0)
    {
        throw new Exception("input 需要大於零");
    }

    return 100 / input;
}
2021-10-13 16:45

初學 Exception 的疑問

一開始學習 Exception 一定會有很多疑問,或者根本沒搞清楚就寫了一堆 try catch,讓程式邏輯變得亂七八糟難以閱讀,甚至小小的修改都不知道從何下手,Exception 是一種邏輯的輔助工具,可以讓你事半功倍,也可以變成噩夢。

我也是摸索了很久才對 Exception 有一定的應用認識,不敢說有多專精,畢竟每種語言跟特性都不同,但也是一定程度的應用經驗,我認為最重要的用途有兩個:


* 用 finally 去關閉資源物件

資源物件有 File, Socket,...,這類的物件在邏輯結束後一定要進行關閉,強調一定一定,實在是吃了自己埋的雷了,沒有關閉的話資源會被咬住,像 Serial Port 這種底層資源沒有關閉的話,想要重新連接就只能重開機了,這是讓人無法接受的。

FileStream file1 = null;
try
{
    file1 = File.Open("<file path>", FileMode.OpenOrCreate);
    // Do something
}
finally
{
    if (file1 != null) { file1.Close(); }
}

/*== C# 可以用 using 代替上面的程式 ==*/
using (FileStream file2 = File.Open("<file path>", FileMode.OpenOrCreate))
{
    // Do something
}


* 用 catch 去記錄 Exception

為了日後可以在日後除錯時留下線索,Exception 的記錄是很重要的,任何程式都有可能出現錯誤,不管是資料錯誤還是邏輯錯誤,有除錯的線索肯定會有很大的幫助,無論程式的大小都應該留下錯誤紀錄。

try
{
    // Do something
}
catch (Expression ex)
{
    _log.Error(ex, "An error occurred.");
}

其餘的清況,在不知道為何要用 try catch 時,最好就不要寫 try catch,基本上程式邏輯一旦出錯就只能退出,如果胡亂地進行修正讓邏輯繼續下去,可能會出現很糟糕的局面,而且資料錯誤就應該修正資料,邏輯錯誤就應該修正邏輯。

錯誤就是要讓人發現才有辦法修正,胡亂的將錯誤屏蔽掉只會埋下未來的苦果,也別用這種方式報復前東家,這只會搞到後面接手的倒楣鬼,本是同根生,相煎何太急。

2020-08-04 19:09

利用 redirect 跳轉到 預設頁 或 預設查詢

在網站開發有一些技巧可以增加後續的維護性,利用重導向來做預設內容的處裡,這樣可以統一集中的進行控制。

預設頁面最常會因為業務的策略因素進行調整,這時候散落在各地的連結都要調整,費工又容易漏。

預設查詢這個麻煩點在於參數會變動,散落在各地的連結一樣是個麻煩。


但重導向這種方式也是有損失的,將會多一個 HTTP 請求,這對 UI 反應速度很要求的狀況來說,並不是一個好方法,可能就要改用統一網址管理來處理。


利用 MVC 的 Index() 來控制預設頁面,Index 將不會有實體頁面,而是用來進行重導向。
public class UserController : Controller
{
    public ActionResult Index()
    {
        return RedirectToAction(nameof(List));
    }

    public ActionResult List(DateTime? date)
    {
        //...
        return View();
    }

}


判斷 QueryString 為空時,進行預設查詢的重導向,以 QueryString 為判斷點的好處是有時候就是要查詢全部資料,這樣就不會被預設查詢卡到。
public class UserController : Controller
{
    public ActionResult List(DateTime? date)
    {
        if (Request.QueryString.Count == 0)
        {
            RedirectToAction(nameof(List), new 
            { 
                date = DateTime.Today.ToString("yyyy-MM-dd") 
            });
        }

        //...
        return View();
    }


}


2020-07-31 20:01

資料庫設計原則

以前跟同事一起訂下的資料庫設計原則,來減少一開始設計不好造成事後要進行大改的風險。

實體關聯的設計是最需要謹慎的,這裡一旦與規格差太多,程式可能就要重寫。

欄位名稱建議在系統中是唯一的,這樣可以減少 JOIN 時還需要換名稱,FK 使用跟 PK 一樣的名稱也可以增加維護性。




資料庫設計流程


(Instance 層級)
  • 依照 Instance 建立 ER 圖,須清楚描述關聯與實體
  • 審視並釐清實體對於規格的含蓋範圍

(Table 層級)
  • 依照釐清後的含蓋範圍,修正 ER 圖
  • 審視並確認完整 ER圖

(Column 層級)
  • 定義各資料表欄位,建立 OrgTableSchema.sql
  • 審視並確認完整 Schema
  • 有問題重複上述步驟

(Develop 層級)
  • 維護 OrgTableSchema.sql
  • 使用 DbSchemaTool 工具產生 Schema.sql
  • 建立資料庫
  • 修正 Dbml
  • 更新 DB 專案


資料表與欄位定義

  • Id 是 identity 去尾的縮寫,所以 d 要小寫
  • No 改成 Num 會比較好,因為會跟 Yes, No 混淆
  • Id 是流水號,其他的建議用 Num 或 Code 表示
  • 避免用單單字做欄位名,盡量用多單字 ( Ex. IssueType, CompanyCode, SettingId, CompanyName )
  • 盡量用 流水號 或 Guid 這類無意義的 id 做 PK
  • 除了多對多的中間表,PK 都必須是單一欄位
  • 資料來源為 OptionSetting, 值為 OptionId 者,欄位名稱命名須加尾贅詞 Id,型態為 INT
  • 資料來源為 Enum, 欄位型態則為 Nvarchar(N)
  • 盡量避免資料來源為 bool 對應 bit,因為擴充性太低,資料來源改用 Enum 對應 Nvarchar(N)
  • 歷史檔與主檔的差異
    • 主檔的 ModifiedBy ModifierdDate 為歷史檔的 CreatedBy CreatedDate,歷史檔無 ModifiedBy ModifierdDate
    • 新增歷史檔的 PK , 主檔的 PK 設定為 FK
    • 其餘欄位應該與主檔相同
  • 每一個 FK 需預設建立 IX,其餘調整於開發完成後,依照使用者回報進行調整
  • 多對多的中間表如果超過 3 個附加欄位,必須用一般的方式設計


PK 必須使用 Guid 的情況

  • 如果只保留短期資料,但會有大量新增或刪除(每天一萬筆新增)
  • 如果有多系統都可以新增資料,最後要合流的狀況


設計 Table 的欄位順序

  1. PK
  2. FK [主檔]
  3. FK [選項]
  4. AK
  5. 其餘不重要的資料欄位
  6. CreatedBy
  7. CreatedDate
  8. ModifiedBy
  9. ModifierdDate


資料型態

  • 文字 : NVARCHAR 長度為預定輸入的兩倍
  • 日期 : DATETIMEOFFSET 具有時區紀錄(MSDN 建議)
  • 時間 : TIME
  • 整數 : INT
  • Enum : NVARCHAR (32)
  • Guid : UNIQUEIDENTIFIER
  • 金錢 : MONEY
  • 精確浮點數 : DECIMAL (18, 4)


鍵值規則

  • Table 規則 {ProjectName}_{TableName}
     Ex: WMS_Carrier
  • Foreign Key 規則 FK_{ProjectName}_{TableName}_{Columns}
     Ex: FK_WMS_CarrierMaterial_CarrierId
  • Unique 規則 AK_{ProjectName}_{TableName}_{Columns}
     Ex: FK_WMS_CarrierMaterial_CarrierId
  • Index 規則 IX_{ProjectName}_{TableName}_{Columns}
     Ex: IX_WMS_CarrierMaterial_CarrierId


縮寫解釋

  • PK: Primary Key
  • FK: Foreign Key
  • AK: Alternate Key
  • IX: IndeX
  • CK: ChecK
  • DF: DeFault


MSSQL 定序設定


定序 Chinese_Taiwan_Stroke_CS_AI

_CS 區分大小寫
_CI 不區分大小寫

_AS 區分腔調 a != á
_AI 不區分腔調

_KS 區分日文假名字元
_WS 區分全形與半形字元

定序會影響查詢與唯一值的判定,例如不區分大小的定序在 WHERE 'abc' = 'ABC' 會是 true。

定序選擇並沒有標準答案,我個人是採用區分大小的定序,如果規格是不區分的時候,再用程式轉大寫或小寫,這部分可以在 DAO 統一完成,雖然也會有遺漏的情況,但至少是可以由程式掌控,如果出現只有某些不區分,而大部分還是區分的時候,還是由程式掌控會比較好。



2019-07-19 16:46

WCF IP Filter

<!-- Web.config -->
<system.serviceModel>
  <extensions>
    <behaviorExtensions>
      <add name="ipFilter" type="XXX.XXX.IpFilterElement, XXX.XXX" />
    </behaviorExtensions>
  </extensions>

  <!-- .... -->

  <behaviors>
    <serviceBehaviors>
      <behavior>
        <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true" />
        <serviceDebug includeExceptionDetailInFaults="true" />
        <ipFilter allow="192.168.1.0/24, 127.0.0.1" />
      </behavior>
    </serviceBehaviors>
  </behaviors>
</system.serviceModel>

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Configuration;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Runtime.Serialization;
using System.Security.Authentication;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Configuration;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
using JustWin.API.Extensions;


public class IpFilterElement : BehaviorExtensionElement
{
    [ConfigurationProperty("allow", IsRequired = true)]
    public virtual string Allow
    {
        get { return this["allow"] as string; }
        set { this["allow"] = value; }
    }

    public override Type BehaviorType
    {
        get { return typeof(IpFilterBehaviour); }
    }

    protected override object CreateBehavior()
    {
        return new IpFilterBehaviour(Allow);
    }
}




public class IpFilterBehaviour : IDispatchMessageInspector, IServiceBehavior
{
    private readonly List<IPAddressRange> _allowList;

    public IpFilterBehaviour(string allow)
    {
        _allowList = allow.Split(',').Select(x => new IPAddressRange(x)).ToList();
    }


    void IServiceBehavior.Validate(ServiceDescription service, ServiceHostBase host)
    {
    }

    void IServiceBehavior.AddBindingParameters(ServiceDescription service, ServiceHostBase host, Collection<ServiceEndpoint> endpoints, BindingParameterCollection parameters)
    {
    }

    void IServiceBehavior.ApplyDispatchBehavior(ServiceDescription service, ServiceHostBase host)
    {
        foreach (ChannelDispatcher dispatcher in host.ChannelDispatchers)
        foreach (EndpointDispatcher endpoint in dispatcher.Endpoints)
        {
            endpoint.DispatchRuntime.MessageInspectors.Add(this);
        }
    }



    object IDispatchMessageInspector.AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
    {
        var remoteEndpoint = request.Properties[RemoteEndpointMessageProperty.Name] as RemoteEndpointMessageProperty;

        var address = IPAddress.Parse(remoteEndpoint.Address);
        if(_allowList.Any(x => x.IsMatch(address))) { return null; }

        request = null;
        return new AuthenticationException($"IP address ({remoteEndpoint.Address}) is not allowed.");
    }


    void IDispatchMessageInspector.BeforeSendReply(ref Message reply, object correlationState)
    {
        var ex = correlationState as Exception;
        if (ex == null) { return; }

        MessageFault messageFault = MessageFault.CreateFault(
            new FaultCode("Sender"),
            new FaultReason(ex.Message),
            ex,
            new NetDataContractSerializer()
        );

        reply = Message.CreateMessage(reply.Version, messageFault, null);
    }

}







public class IPAddressRange
{
    private readonly byte[] _rangeAddress;
    private readonly byte[] _rangeMask;

    public IPAddressRange(string ipAndMask)
    {
        string[] split = (ipAndMask + "/128").Split('/');

        var ip = IPAddress.Parse(split[0].Trim());

        int maskLength = int.Parse(split[1].Trim());
        if (ip.AddressFamily == AddressFamily.InterNetwork) { maskLength += 96; }

        _rangeMask = createMask(maskLength);

        _rangeAddress = ip.MapToIPv6().GetAddressBytes()
            .Select((x, i) => x & _rangeMask[i])
            .Select(x => (byte)x)
            .ToArray();
    }


    public bool IsMatch(IPAddress ip)
    {
        byte[] address = ip.MapToIPv6().GetAddressBytes();

        for (int i = 0; i < 16; i++)
        {
            if ((address[i] & _rangeMask[i]) != _rangeAddress[i]) { return false; }
        }

        return true;
    }



    private byte[] createMask(int length)
    {
        var mask = new byte[16];

        for (int i = 0; i < 16; i++)
        {
            mask[i] = 0xff;
            if (length > -8) { length -= 8; }
            if (length < 0) { mask[i] = (byte)(mask[i] << -length); }
        }
        return mask;
    }
}
2014-03-05 23:38

利用 HTTP Status Codes 傳遞 Ajax 成功失敗的狀態

一般處理 Ajax 回應時會傳送的資訊種類有:資料、成功訊息、錯誤訊息、失敗訊息以及處理狀態,傳遞的資訊種類並不一致,再加上除了資料之外,通常還希望能傳遞處理狀態,這種情況大部分會選擇是以 JSON 的方式傳遞這兩個訊息,以下是常見的幾種格式:

{ code: 1, msg: "OK" }
{ success: true, result: "data", errorMsg: "" }
{ status: 'success', result: [], errorMsg: "" }
//...

但以執行狀態跟操作行為作一個歸納,可以區分以下幾種回傳結果:
資料操作 HTTP Method 成功 錯誤/失敗
檢視(Read) GET 資料 錯誤/失敗訊息
新增(Create)
修改(Update)
刪除(Delete)
POST 成功訊息 錯誤/失敗訊息

從上面的歸納可以看出規律性,接著只要有方法可以傳送處理的狀態,以及能夠區分資料的種類,其實就單純很多,而 HTTP Status Codes 就是用來傳遞 HTTP 的處理狀態,如果利用這個方式來傳遞自訂的處理狀態,這樣 HTTP Content 就可以很單純傳遞資料,讓資料格式不受限於 JSON,還可以使用其他格式(text, xml, html),而且 XMLHttpRequest 本身就有處理 HTTP Status Codes 的能力,而 jQuery.ajax 也有提供 error status 的處理,所以可以利用這個來定義狀態的處理,在 HTTP Status Codes 有幾個已經定義狀態,很適合用來回傳處理狀態的資訊:

400 Bad Request 錯誤的請求 適用在表單內容的錯誤,如必填欄位未填、Email 格式錯誤
403 Forbidden 沒有權限,被禁止的 適用在沒有登入或權限不足
500 Internal Server Error 內部服務器錯誤 適用在程式的錯誤


jQuery 接收資訊的範例
$.ajax({
    type: "POST",
    url: document.location,
    success: function (data, textStatus, jqXHR) {
        alert(data);
    },
    error: function (jqXHR, textStatus, errorThrown) {
        alert(jqXHR.responseText);
    }
});


PHP 傳遞錯誤訊息的範例
if (php_sapi_name() == 'cgi'){
    header("Status: 400 Bad Request");
}else{
    header("HTTP/1.0 400 Bad Request");
}
exit("儲存失敗!!");


C# MVC 傳遞錯誤訊息的範例
Response.TrySkipIisCustomErrors = true;
Response.StatusCode = 400;
return Content("儲存失敗!!");
2014-02-15 14:45

[C#] delegate 到 Lambda Expressions 語法演進

一開始要看懂 Lambda Expressions 有點困難,下面會以演進方式來介紹如何做到語法省略。

首先定義一個單參數的 delegate
delegate int Del(int x);

以傳統 delegate 的語法來建構 delegate
Del a = delegate(int x) { return x + 2; };

去掉 delegate 改成 Lambda 表示式
Del a = (int x) => { return x + 2; };

由於大括號裡只有一句陳述式,而且是一個 return 的陳述式,所以可以省略大括號跟 return
Del a = (int x) => x + 2;

在 delegate 已經有定義輸入參數的型別,所以在小括號裡的型別可以省略
Del a = (x) => x + 2;

由於小括號裡面只有一個輸入參數,所以可以再進一步省略小括號
Del a = x => x + 2;

參考來源:
Lambda 運算式 (C# 程式設計手冊)
2014-02-13 23:54

HTTP GET 與 POST 的比較及使用時機

  GET POST
瀏覽器歷史紀錄 參數都會紀錄,因為都是URL的一部分 參數都不會紀錄
加入書籤 參數都會紀錄,因為都是URL的一部分 參數都不會紀錄
回上一頁/
重新載入
GET請求是重新執行,但被存儲在瀏覽器的快取,則不被重新提交到服務器 數據將被重新提交(瀏覽器通常會警告使用者該數據將需要重新提交)
編碼類型 application/x-www-form-urlencoded multipart/form-data 或 application/x-www-form-urlencoded,使用多編碼的二進制數據
參數大小限制 受限於 QueryString 長度限制(不超過 2KB 是最保險的,有些瀏覽器可以允許多達 64KB) 允許大量傳輸,包括上傳文件到服務器
參數傳輸方式 QueryString POST Data(message-body)
安全性 容易破解,因為參數是網址的一部分,所以它被紀錄在瀏覽器歷史記錄和明文服務器日誌 比較難破解
使用性 不應該被使用在發送密碼或其他敏感信息上 使用在發送密碼或其他敏感信息上
能見度 GET方法是對所有人可見(它會顯示在瀏覽器的地址欄) POST方法變量不顯示在URL中
執行速度 快,GET 比 POST 快 1.5 倍 慢,POST 多出需要發送數據的步驟
快取 瀏覽器會依據網址來快取資料,不同的網址有不同的快取 瀏覽器不會快取
自動重送 會,在回應過長時會重發請求,直到重試結束 不會,一個請求發出後會一直等待回應
適用行為 檢視(Read) 新增(Create)、修改(Update)、刪除(Delete)
情況包括 透過 <link href="">、<img src="">、<script src="">、<iframe src=""> 額外載入的 JavaScritp、CSS、圖片 透過 <form method="post"> 以及 Ajax post 發送的請求


GET 適合用在「檢視(Read)」的操作行為,由於檢視資料的操作會遠比資料異動來的更頻繁,所以需要更快的回應,而且有快取可以加快二次檢視,參數在網址上可以使每一個網址都代表一個網頁,在加入書籤的連結能夠返回對應的網頁,再者所需要的參數很少(例如:id=122&type=1),對於資料異動後快取沒更新的問題,可以在 QueryString 加上資料最後修改的時間戳記(例如:id=122&type=1&t=1392183597718)。


POST 適合用在「新增(Create)修改(Update)刪除(Delete)」的操作行為,資料異動所需要傳送的參數很可能超過 QueryString 的限制,不適合用 GET 來處理資料異動的傳送,GET 在等待過久會重新發送請求,這會造成重複的請求,如果在新增儲存就多新增一筆資料,而 POST 在一個請求發出後會一直等待回應,這可以保障在傳送的過程中請求是唯一的,新增資料的請求如果被記錄在書籤或歷史紀錄中,使用者點擊連結網址就新增一筆資料,這真是一件恐怖的事,所以在資料異動上的請求必須使用 POST 來傳送。


參考來源:
GET vs POST - Difference and Comparison | Diffen
HTTP Methods: GET vs. POST
寻根究底:Ajax请求的GET与POST方式比较
2014-02-10 23:43

[C#] 對 FirstOrDefault 新的認識

FirstOrDefault 會依據最後的型別去決定 Default 時回傳的值,例如下面的範例:
int? a = (new List<int>{2}).Select(x => x).FirstOrDefault();
// 2

int? b = (new List<int>{}).Select(x => x).FirstOrDefault();
// 0


int? c = (new List<int>{2}).Select(x => (int?)x).FirstOrDefault();
// 2

int? d = (new List<int>{}).Select(x => (int?)x).FirstOrDefault();
// null
2014-02-05 20:53

[C#] HttpUtility.ParseQueryString 的隱藏密技

在使用 Request.QueryString 發現 ToString 會產生 URL 的 query 字串,嘗試用 NameValueCollection 的 ToString 卻不是產生 URL 的 query 字串,這一整個就很奇怪,明明都是 NameValueCollection 確有不一樣的結果,透過 Reflector 發現 Request.QueryString 的 instance 型別是一個 HttpValueCollection,想說可以直接 new HttpValueCollection 出來使用,但 HttpValueCollection 卻是 System.Web 的內部 Class,外部是無法直接 new 出來使用,還好在又發現 HttpUtility.ParseQueryString 回傳的 NameValueCollection 的 instance 是 HttpValueCollection 這個型別,所以可以透過 HttpUtility.ParseQueryString 來建立 HttpValueCollection。

// using System.Web;

var qs1 = HttpUtility.ParseQueryString("id=5&type=1");
qs1.ToString(); // "id=5&type=1"

var qs2 = HttpUtility.ParseQueryString(String.Empty);
qs2["id"] = "11";
qs2["name"] = "Tom"; 

qs2.ToString(); // "id=11&name=Tom"


HttpValueCollection 的簽名
[Serializable]     
internal class HttpValueCollection : NameValueCollection     
{
}


ParseQueryString 的簽名
public static NameValueCollection ParseQueryString(string query, Encoding encoding)
{
    if (query == null)
    {
        throw new ArgumentNullException("query");
    }
    if (encoding == null)
    {
        throw new ArgumentNullException("encoding");
    }
    if ((query.Length > 0) && (query[0] == '?'))
    {
        query = query.Substring(1);
    }
    return new HttpValueCollection(query, false, true, encoding);
}
2014-02-05 20:25

[CSS] float 與 clear

float

一開始是用來定義文繞圖的呈現,後來其浮動特性很適合用來排版佈局,所以大部分的網頁都用這種方式排版。

特性:
  • 不佔用父元素的空間
  • 寬高會內縮至子元素所呈現的大小
  • 會排擠其他相鄰的 inline 元素
  • 在所定位的空間不足時會自動換行

<div style="border:1px solid #f00; float:left;">div 1</div>
<div style="background:#0f0;">div 2</div>
div1
div2

透過上面的範例可以看出因為 div1 不佔用空間而讓 div2 的位子上移了,然而呈現與 div1 重疊的效果,以及可以看到 float 排擠文字的特性,而讓 div2 的文字被擠到 div1 之後。



clear

清除在元素相鄰邊上的 float 元素

屬性:
  • none 不做任何 clear 動作
  • left 將元素向下換行,來排除具有 float:left 的相鄰元素
  • right 將元素向下換行,來排除具有 float:right 的相鄰元素
  • both 將元素向下換行,來排除具有 float:left 或 float:right 的相鄰元素

<div style="border:1px solid #f00; float:left;">div1</div>
<div style="background:#0f0; clear:left;">div2</div>
div1
div2

這個範例在 div2 加上 clear:left 的屬性,讓 div2 根據前一個具有 float:left 元素之後向下換行,來排除具有 float:left 的相鄰元素。


<div style="border:1px solid #f00; float:left;">div1</div>
<div style="background:#0f0; clear:right;">div2</div>
div1
div2

這個範例則是將 div2 換成 clear:right,可以看到 div1 與 div2 仍舊重疊在一起,這是因為 div2 前一個具有 float:right 不存在,而 div1 的 float:left 不是 div2 clear:right 排除的對象。
2011-12-16 15:04

Sphinx 增量索引的方法

之前有寫過一篇 MySQL 全文檢索引擎 - Sphinx 的文章,最近又把它拿出來用了,不過當時即時更新索引的問題,如今則找到解決的方法了,透過更新增量索引的方式達到即時更新。

簡單的說就是利用兩個索引表的合併查詢來做到,一個是完整的索引表,一個是針對當日資料變動的增量索引。

部分的設定檔如下:
# ...

source _source_base
{
    # 來源-共用的設定
}

source people_full : _source_base
{
    sql_query      = \
        SELECT \
            people_profile.id, \
            people_profile.main_name \
        FROM people_profile
        
    sql_query_killlist = \
        SELECT id FROM people_profile \
        WHERE update_date >= CURDATE()
}

# 增量索引來源,這邊只會抓出當日變動的資料
# 建議在 update_date 欄位加上 MySQL INDEX
source people_delta : people_full
{
    sql_query      = \
        SELECT \
            people_profile.id, \
            people_profile.main_name \
        FROM people_profile \
        WHERE people_profile.update_date >= CURDATE()
}


index _index_base
{
    # 索引-共用的設定
}

index people_full : _index_base
{
    source          = people_full
    path            = /var/lib/sphinxsearch/data/people_full
}

# 增量索引
index people_delta : _index_base
{
    source          = people_delta
    path            = /var/lib/sphinxsearch/data/people_delta
}

# 透過 distributed 類型來合併索引
index people
{
    type            = distributed
    local           = people_full
    local           = people_delta
}

# ...


建立 SphinxSE 表
特別注意在 CONNECTION 中所指定索引表為 people。
CREATE TABLE people_sphinx(
    id BIGINT UNSIGNED NOT NULL   COMMENT '搜尋結果的 Id',
    weight INT NOT NULL           COMMENT '搜尋結果的權重',
    query VARCHAR(3072) NOT NULL  COMMENT '搜尋的查詢條件',
  
    INDEX(query)
)ENGINE=SPHINX 
CONNECTION="sphinx://localhost:9312/people" 
COMMENT='People Sphinx搜尋連接介面';


SQL 的查詢測試
這裡使用 INNER JOIN 方式作查詢,這樣對於刪除資料的變動,就不會出現在查詢結果中。
SELECT A.id, A.main_name, B.weight
FROM people_sphinx B
INNER JOIN people_profile A
USING(id) 
WHERE B.query='馬丁尼茲;mode=any;limit=1000'


透過 PHP 更新增量索引
在資料 INSERT 或 UPDATE 時,去呼叫索引更新,這樣在第一時間就可以更新索引。
shell_exec('sudo indexer --quiet --rotate people_delta 2>&1');

如果 Server 是 Ubuntu,請記得在 vim /etc/sudoers 中賦予 apache 使用 indexer 的權限。
www-data ALL=(root) NOPASSWD: /usr/bin/indexer


在 crontab 中加上排程
利用離峰時間來更新完整的索引表,由於刪除資料的變動需要更新完整索引表才有辦法移除。
00 00 * * * indexer --quiet --rotate --all > /dev/null 2>&1
2011-12-16 06:49

[PHP] 縮圖方式的比較

這兩種的缺點是,都會根據圖片的像素大小,而佔用PHP的記憶體,會造成 Fatal error: Out of memory 的錯誤出現,有一種狀況是一個 3MB 大小的 JPEG 實際的像素大小卻是 128MB,再來 GD 支援的圖片類型有限,大約就是四五種常用類型。

優點是指需要安裝 GD 套件,這個套件不管是在 Windows 或 Linux 上很容易找到跟安裝,在處理圖片的類型明確跟尺寸不大的情況下,使用這兩個函數是不錯的。

為了改善 imageCopyResampled 效率,可以利用 imageCopyResized 做預先縮圖,例如要縮圖的大小為 100*100 時,可以先將圖片縮小成四倍或八倍,如 400*400 或 800*800,可改善超大圖造成的效率不好。

  • ImageMagick + Imagick 使用外部的程式來處理圖片,使用指令的方式或透過 Imagick 套件來處理縮圖。
缺點是透過指令的方式很不友善,且容易受到系統權限的限制,而 Imagick 套件在 Windows 上不容易找到合適的 DLL。

優點是支援下面多種格式,效率快且沒有記憶體錯誤的問題。
3FR, A, AI, ART, ARW, AVI, AVS, B, BGR, BGRA, BMP, BMP2, BMP3, BRF, BRG, C, CAL, CALS, CAPTION, CIN, CIP, CLIP, CMYK, CMYKA, CR2, CRW, CUR, CUT, DCM, DCR, DCX, DDS, DFONT, DJVU, DNG, DOT, DPX, EPDF, EPI, EPS, EPS2, EPS3, EPSF, EPSI, EPT, EPT2, EPT3, ERF, EXR, FAX, FITS, FRACTAL, FTS, G, G3, GBR, GIF, GIF87, GRADIENT, GRAY, GRB, GROUP4, HALD, HISTOGRAM, HRZ, HTM, HTML, ICB, ICO, ICON, INFO, INLINE, IPL, ISOBRL, J2C, JNG, JP2, JPC, JPEG, JPG, JPX, K, K25, KDC, LABEL, M, M2V, M4V, MAP, MAT, MATTE, MIFF, MNG, MONO, MOV, MP4, MPC, MPEG, MPG, MRW, MSL, MSVG, MTV, MVG, NEF, NULL, O, ORF, OTB, OTF, PAL, PALM, PAM, PATTERN, PBM, PCD, PCDS, PCL, PCT, PCX, PDB, PDF, PDFA, PEF, PES, PFA, PFB, PFM, PGM, PGX, PICON, PICT, PIX, PJPEG, PLASMA, PNG, PNG24, PNG32, PNG8, PNM, PPM, PREVIEW, PS, PS2, PS3, PSB, PSD, PTIF, PWP, R, RADIAL-GRADIENT, RAF, RAS, RBG, RGB, RGBA, RGBO, RLA, RLE, SCR, SCT, SFW, SGI, SHTML, SR2, SRF, STEGANO, SUN, SVG, SVGZ, TEXT, TGA, THUMBNAIL, TIFF, TIFF64, TILE, TIM, TTC, TTF, TXT, UBRL, UIL, UYVY, VDA, VICAR, VID, VIFF, VST, WBMP, WMF, WMV, WMZ, WPG, X, X3F, XBM, XC, XCF, XPM, XPS, XV, XWD, Y, YCbCr, YCbCrA, YUV
2010-05-17 21:38

Nokia Widget 實作講堂

今天去參加了第二場 Nokia 辦得活動當然要來記錄一下
這一次的講師非常的 funny 分享了很多經驗
而且他的公司名稱也很妙 有的放矢行動行銷股份有限公司
為了找他的公司網址無意間找到 Wiki 的解釋 有的放矢 - 維基詞典

再來就是其實 Nokia 的手機大部分的硬體配備都不高
並沒有想像中的好 ,有些 CPU 可能只有 20MHz
可以跑這麼順主要的原因是他有一個優秀 OS (Symbian)
並且可以完全的多工多執行緒(難怪我不小心操一下就沒電了)
有一次開著 Google Map 再來一個全時 GPS 連線
我的 Nokia 6110n 在滿電的情況下只用了半小時就沒電了 真是讓我傻眼的說
在沒有備用電源的情況下我現在都不太敢再全時 GPS 連線了


言歸正傳今天的 Workshow 到底再說什麼呢??
Widget 基本上的架構就如我們大家所熟知的 HTML + CSS + JavaScript + Ajax
但不一樣的是這裡我們可以透過 Platform Services API 去存取手機上的裝置
GPS 位址收發簡訊通訊錄行事曆 等等… 一整個就很快樂
感覺就像在寫 Firefox 的 plugin
而且在 package 時也是用 zip 壓縮
但講師說硬體資源的有限會讓你很不快樂,它只是一台手機並不是一台電腦
以前在學校時寫過 PDA 所以這種切身之痛我非常瞭解
盡可能別在手機上做大量運算,要不然它就 crash 給你看

在實做時我又再一次的見證 Eclipse 的偉大(很好又是我熟悉的 SDK 平台)
上次的 QT 也是有 Eclipse 的 plugin 而這一次的 Widget 也有 plugin
而且有很多一鍵完成的功能(雖然在 Eclipse 上本來就是這樣了)
只是 Eclipse 一直都沒有很好的 Script debug 整合
雖然 Aptana 上的 JavaScript 即時除錯已經很好用了,但卻僅限於 JavaScript
最近拿來寫 ActionScript 也是不錯用,但還是覺得不夠好
可是目前還沒找到其他的替代方案

最後在跟講師換名片時幽默的說也想買一台 Tivo
對於 Tivo 名聲心裡小小高興一下
但忠小晞我 對自己的孤陋寡聞也慚愧了一下

下次來寫一個 widget 來玩玩
如果老大可以給點 resource 就更好了


參考資料:
Nokia N97 SDK
Web Runtime Code Examples
Platform Services 2.0 JavaScript API reference
Nokia Platform Services 2.0 Download


PS:
既然參加免費的講習多少為對方打個廣告
Nokia 目前(2010/6/11止)有舉辦創意競賽最高獎金 20 萬
詳情請見:Symbian & Maemo中文資訊站
2010-05-16 14:21

三分鐘瞭解 XSS 攻擊原理

在看完酷壳寫的HTML 安全列表
突然很想寫一篇有關 XSS 的快速教學
讓更多人能瞭解何謂 XSS 安全漏洞


在瞭解 XSS 之前必須知道『網站登入(Session)』的原理

簡單的說當會員成功登入後 網站會給瀏覽器一個『令牌』
之後只要拿著這個『令牌』到網站上 就會被視為已經登入


再來下面是 XSS 最簡單的流程

簡單的說駭客透過 JavaScript 的程式碼將你的『令牌』偷走
透過這個『令牌』他也可以用你的身份順利登入網站
然後偷走你的相關資料(個人資料&交易資料)
然後再將這些資料賣給詐騙集團


相關的參考資料:
跨網站指令碼 - 維基百科
Cross-site Scripting (XSS) - OWASP
XSS(Cross Site Scripting)攻擊會讓您遺失Cookie中的資料
詳解XSS攻擊 - 網路攻防戰
HTML5 Security Cheatsheet