☕ NEW! 完成新手任務即可參加抽獎!LINE 星巴克禮券等你拿,名額有限!        🎉 推廣活動:邀請好友註冊 DevLearn,累積推薦抽 LINE 星巴克禮券! 活動詳情 →        🔥 活動期間 2026/4/1 - 5/31 |已有 0 人參加       
專案實戰 中級

實戰:電商網站開發

專案架構(三層式 + Repository Pattern)

💡 比喻:餐廳運作

  • Controller(外場服務生):接受客人點餐、送菜上桌
  • Service(廚房主廚):處理商業邏輯,決定怎麼煮
  • Repository(倉庫管理員):負責進出貨,拿食材

每一層各司其職,服務生不需要知道食材從哪來,主廚不需要知道客人坐哪桌。

專案資料夾結構

EShop/
├── Controllers/          ← 外場(接收 HTTP 請求)
│   ├── ProductController.cs
│   ├── CartController.cs
│   └── OrderController.cs
├── Services/             ← 廚房(商業邏輯)
│   ├── IProductService.cs
│   ├── ProductService.cs
│   ├── ICartService.cs
│   ├── CartService.cs
│   ├── IOrderService.cs
│   └── OrderService.cs
├── Repositories/         ← 倉庫(資料存取)
│   ├── IRepository.cs
│   ├── ProductRepository.cs
│   └── OrderRepository.cs
├── Models/               ← 食材(資料模型)
│   ├── Product.cs
│   ├── CartItem.cs
│   └── Order.cs
├── ViewModels/           ← 菜單(呈現用的模型)
│   ├── ProductListVM.cs
│   └── CheckoutVM.cs
└── Data/                 ← 冰箱(資料庫設定)
    └── AppDbContext.cs

Repository Pattern 基本實作

// 定義泛型 Repository 介面 // Define generic repository interface
public interface IRepository<T> where T : class // 泛型 Repository 介面
{
    Task<List<T>> GetAllAsync(); // 取得所有資料
    Task<T?> GetByIdAsync(int id); // 依 ID 取得單筆
    Task AddAsync(T entity); // 新增一筆資料
    Task UpdateAsync(T entity); // 更新一筆資料
    Task DeleteAsync(int id); // 刪除一筆資料
    Task<bool> ExistsAsync(int id); // 檢查是否存在
}

// 實作泛型 Repository // Implement generic repository
public class Repository<T> : IRepository<T> where T : class // 泛型 Repository 實作
{
    protected readonly AppDbContext _context; // 資料庫上下文
    protected readonly DbSet<T> _dbSet; // 資料表集合

    public Repository(AppDbContext context) // 建構函式注入 DbContext
    {
        _context = context; // 儲存 DbContext 參考
        _dbSet = context.Set<T>(); // 取得對應的 DbSet
    }

    public async Task<List<T>> GetAllAsync() // 取得所有資料的方法
    {
        return await _dbSet.ToListAsync(); // 從資料庫取得全部
    }

    public async Task<T?> GetByIdAsync(int id) // 依 ID 查詢的方法
    {
        return await _dbSet.FindAsync(id); // 用主鍵查詢
    }

    public async Task AddAsync(T entity) // 新增資料的方法
    {
        await _dbSet.AddAsync(entity); // 加入追蹤
        await _context.SaveChangesAsync(); // 儲存到資料庫
    }

    public async Task UpdateAsync(T entity) // 更新資料的方法
    {
        _dbSet.Update(entity); // 標記為已修改
        await _context.SaveChangesAsync(); // 儲存變更
    }

    public async Task DeleteAsync(int id) // 刪除資料的方法
    {
        var entity = await _dbSet.FindAsync(id); // 先找到該筆資料
        if (entity != null) // 如果找到了
        {
            _dbSet.Remove(entity); // 標記為刪除
            await _context.SaveChangesAsync(); // 執行刪除
        }
    }

    public async Task<bool> ExistsAsync(int id) // 檢查是否存在
    {
        return await _dbSet.FindAsync(id) != null; // 回傳是否找得到
    }
}

商品 CRUD 完整實作

Model 定義

// 商品 Model // Product model
public class Product // 商品類別
{
    public int Id { get; set; } // 主鍵
    [Required(ErrorMessage = "商品名稱必填")] // 驗證:必填
    [StringLength(100)] // 驗證:最長 100 字
    public string Name { get; set; } = ""; // 商品名稱

    public string? Description { get; set; } // 商品描述(可為空)

    [Required] // 驗證:必填
    [Range(1, 999999, ErrorMessage = "價格需在 1~999999 之間")] // 驗證:範圍
    public decimal Price { get; set; } // 售價

    [Range(0, int.MaxValue)] // 驗證:不可負數
    public int Stock { get; set; } // 庫存量

    public string? ImageUrl { get; set; } // 商品圖片網址

    public int CategoryId { get; set; } // 分類 ID(外鍵)
    public Category? Category { get; set; } // 導覽屬性:分類

    public DateTime CreatedAt { get; set; } = DateTime.UtcNow; // 建立時間
    public bool IsActive { get; set; } = true; // 是否上架
}

Service 層

// 商品 Service 介面 // Product service interface
public interface IProductService // 定義商品服務的合約
{
    Task<List<Product>> GetProductsAsync(string? category, string? search, int page, int pageSize); // 取得商品清單
    Task<Product?> GetProductAsync(int id); // 取得單一商品
    Task<Product> CreateProductAsync(Product product); // 建立商品
    Task UpdateProductAsync(Product product); // 更新商品
    Task DeleteProductAsync(int id); // 刪除商品
    Task<int> GetTotalCountAsync(string? category, string? search); // 取得總筆數
}

// 商品 Service 實作 // Product service implementation
public class ProductService : IProductService // 實作商品服務
{
    private readonly IRepository<Product> _repo; // 商品 Repository

    public ProductService(IRepository<Product> repo) // 建構函式注入 Repository
    {
        _repo = repo; // 儲存 Repository 參考
    }

    public async Task<List<Product>> GetProductsAsync( // 取得商品清單方法
        string? category, string? search, int page, int pageSize) // 支援篩選和分頁
    {
        var query = _context.Products // 從商品資料表開始
            .Include(p => p.Category) // 載入分類資訊
            .Where(p => p.IsActive) // 只顯示上架商品
            .AsQueryable(); // 轉為可查詢物件

        if (!string.IsNullOrEmpty(category)) // 如果有指定分類
        {
            query = query.Where(p => p.Category!.Slug == category); // 篩選該分類
        }

        if (!string.IsNullOrEmpty(search)) // 如果有搜尋關鍵字
        {
            query = query.Where(p => p.Name.Contains(search) // 搜尋名稱
                || p.Description!.Contains(search)); // 或搜尋描述
        }

        return await query // 執行查詢
            .OrderByDescending(p => p.CreatedAt) // 依建立時間降序
            .Skip((page - 1) * pageSize) // 跳過前幾筆(分頁)
            .Take(pageSize) // 取指定筆數
            .ToListAsync(); // 轉為 List 回傳
    }

    public async Task<Product> CreateProductAsync(Product product) // 建立商品方法
    {
        product.CreatedAt = DateTime.UtcNow; // 設定建立時間
        await _repo.AddAsync(product); // 透過 Repository 新增
        return product; // 回傳建立好的商品
    }

    public async Task UpdateProductAsync(Product product) // 更新商品方法
    {
        var existing = await _repo.GetByIdAsync(product.Id); // 先取得現有資料
        if (existing == null) // 如果找不到
            throw new KeyNotFoundException($"找不到商品 ID: {product.Id}"); // 拋出例外

        existing.Name = product.Name; // 更新名稱
        existing.Price = product.Price; // 更新價格
        existing.Stock = product.Stock; // 更新庫存
        existing.Description = product.Description; // 更新描述
        existing.CategoryId = product.CategoryId; // 更新分類

        await _repo.UpdateAsync(existing); // 儲存更新
    }

    public async Task DeleteProductAsync(int id) // 刪除商品方法
    {
        await _repo.DeleteAsync(id); // 透過 Repository 刪除
    }

    public async Task<Product?> GetProductAsync(int id) // 取得單一商品
    {
        return await _repo.GetByIdAsync(id); // 透過 Repository 查詢
    }

    public async Task<int> GetTotalCountAsync(string? category, string? search) // 取得總筆數
    {
        var query = _context.Products.Where(p => p.IsActive).AsQueryable(); // 查詢上架商品
        if (!string.IsNullOrEmpty(category)) // 如果有分類條件
            query = query.Where(p => p.Category!.Slug == category); // 加上分類篩選
        if (!string.IsNullOrEmpty(search)) // 如果有搜尋條件
            query = query.Where(p => p.Name.Contains(search)); // 加上搜尋篩選
        return await query.CountAsync(); // 回傳總筆數
    }
}

Controller 層

// 商品 Controller // Product controller
public class ProductController : Controller // 繼承 Controller 基底類別
{
    private readonly IProductService _service; // 商品服務

    public ProductController(IProductService service) // 建構函式注入服務
    {
        _service = service; // 儲存服務參考
    }

    // GET /Products?category=fruit&search=蘋果&page=1 // 商品列表頁
    public async Task<IActionResult> Index( // 列表 Action
        string? category, string? search, int page = 1) // 接收篩選參數
    {
        int pageSize = 12; // 每頁 12 筆
        var products = await _service.GetProductsAsync( // 取得商品
            category, search, page, pageSize); // 傳入篩選條件
        var total = await _service.GetTotalCountAsync(category, search); // 取得總筆數

        ViewBag.TotalPages = (int)Math.Ceiling(total / (double)pageSize); // 計算總頁數
        ViewBag.CurrentPage = page; // 目前頁碼
        ViewBag.Category = category; // 目前分類
        ViewBag.Search = search; // 目前搜尋詞

        return View(products); // 回傳 View
    }

    // GET /Products/Details/5 // 商品詳情頁
    public async Task<IActionResult> Details(int id) // 詳情 Action
    {
        var product = await _service.GetProductAsync(id); // 取得商品
        if (product == null) return NotFound(); // 找不到回 404
        return View(product); // 回傳 View
    }

    // POST /Products/Create // 建立商品
    [HttpPost] // 限定 POST 方法
    [ValidateAntiForgeryToken] // 防止 CSRF 攻擊
    [Authorize(Roles = "Admin")] // 限定 Admin 角色
    public async Task<IActionResult> Create(Product product) // 建立 Action
    {
        if (!ModelState.IsValid) return View(product); // 驗證失敗回表單
        await _service.CreateProductAsync(product); // 建立商品
        TempData["Success"] = "商品建立成功!"; // 設定成功訊息
        return RedirectToAction(nameof(Index)); // 導回列表頁
    }
}

購物車功能(Session + DB)

💡 比喻:實體購物車 vs 線上清單 在超市推購物車(Session)= 還沒結帳,離開就沒了。 加入線上願望清單(DB)= 登入後永遠都在。 我們兩個都做,未登入用 Session,登入後同步到 DB。

// 購物車項目 Model // Cart item model
public class CartItem // 購物車項目類別
{
    public int Id { get; set; } // 主鍵
    public int ProductId { get; set; } // 商品 ID
    public string ProductName { get; set; } = ""; // 商品名稱(快照)
    public decimal UnitPrice { get; set; } // 單價(快照)
    public int Quantity { get; set; } // 數量
    public string? UserId { get; set; } // 會員 ID(登入後才有)
    public string SessionId { get; set; } = ""; // Session ID(未登入用)
    public decimal Subtotal => UnitPrice * Quantity; // 小計(計算屬性)
}

// 購物車 Service // Cart service
public class CartService : ICartService // 實作購物車服務
{
    private readonly AppDbContext _context; // 資料庫上下文
    private readonly IHttpContextAccessor _http; // HTTP 上下文存取器

    public CartService(AppDbContext context, IHttpContextAccessor http) // 建構函式
    {
        _context = context; // 儲存 DbContext
        _http = http; // 儲存 HTTP 存取器
    }

    private string GetCartId() // 取得購物車識別 ID
    {
        var session = _http.HttpContext!.Session; // 取得 Session
        var cartId = session.GetString("CartId"); // 嘗試讀取 CartId
        if (string.IsNullOrEmpty(cartId)) // 如果沒有 CartId
        {
            cartId = Guid.NewGuid().ToString(); // 產生新的 GUID
            session.SetString("CartId", cartId); // 存入 Session
        }
        return cartId; // 回傳 CartId
    }

    public async Task AddToCartAsync(int productId, int quantity = 1) // 加入購物車方法
    {
        var product = await _context.Products.FindAsync(productId); // 查詢商品
        if (product == null) throw new KeyNotFoundException("商品不存在"); // 找不到就報錯
        if (product.Stock < quantity) throw new InvalidOperationException("庫存不足"); // 庫存不夠也報錯

        var cartId = GetCartId(); // 取得購物車 ID
        var existingItem = await _context.CartItems // 查詢購物車中是否已有此商品
            .FirstOrDefaultAsync(c => c.SessionId == cartId // 比對 Session ID
                && c.ProductId == productId); // 比對商品 ID

        if (existingItem != null) // 如果已經在購物車中
        {
            existingItem.Quantity += quantity; // 增加數量
        }
        else // 如果是新商品
        {
            _context.CartItems.Add(new CartItem // 建立新的購物車項目
            {
                ProductId = productId, // 設定商品 ID
                ProductName = product.Name, // 記錄商品名稱快照
                UnitPrice = product.Price, // 記錄當前價格快照
                Quantity = quantity, // 設定數量
                SessionId = cartId // 設定 Session ID
            });
        }

        await _context.SaveChangesAsync(); // 儲存到資料庫
    }

    public async Task<List<CartItem>> GetCartItemsAsync() // 取得購物車內容
    {
        var cartId = GetCartId(); // 取得購物車 ID
        return await _context.CartItems // 查詢購物車項目
            .Where(c => c.SessionId == cartId) // 篩選此購物車
            .ToListAsync(); // 回傳清單
    }

    public async Task<decimal> GetTotalAsync() // 計算購物車總金額
    {
        var items = await GetCartItemsAsync(); // 取得所有項目
        return items.Sum(i => i.Subtotal); // 加總所有小計
    }

    public async Task MergeCartAsync(string userId) // 登入後合併購物車
    {
        var cartId = GetCartId(); // 取得 Session 購物車 ID
        var sessionItems = await _context.CartItems // 取得 Session 中的項目
            .Where(c => c.SessionId == cartId && c.UserId == null) // 未登入的項目
            .ToListAsync(); // 轉為清單

        foreach (var item in sessionItems) // 逐一處理每個項目
        {
            item.UserId = userId; // 綁定會員 ID
        }
        await _context.SaveChangesAsync(); // 儲存變更
    }
}

會員系統(Identity Framework)

// 在 Program.cs 中設定 Identity // Configure Identity in Program.cs
builder.Services.AddIdentity<IdentityUser, IdentityRole>(options => // 加入 Identity 服務
{
    options.Password.RequireDigit = true; // 密碼需包含數字
    options.Password.RequiredLength = 8; // 密碼最少 8 字元
    options.Password.RequireUppercase = false; // 不強制大寫(對中文使用者友善)
    options.Password.RequireNonAlphanumeric = false; // 不強制特殊字元
    options.User.RequireUniqueEmail = true; // Email 不可重複
    options.SignIn.RequireConfirmedEmail = false; // 先不要求 Email 驗證
})
.AddEntityFrameworkStores<AppDbContext>() // 使用 EF Core 儲存
.AddDefaultTokenProviders(); // 加入預設 Token 產生器

// 設定 Cookie // Configure authentication cookie
builder.Services.ConfigureApplicationCookie(options => // 設定驗證 Cookie
{
    options.LoginPath = "/Account/Login"; // 未登入導向登入頁
    options.LogoutPath = "/Account/Logout"; // 登出路徑
    options.AccessDeniedPath = "/Account/AccessDenied"; // 權限不足頁面
    options.ExpireTimeSpan = TimeSpan.FromDays(7); // Cookie 有效期 7 天
});

// 註冊 ViewModel // Register view model
public class RegisterVM // 註冊用的 ViewModel
{
    [Required(ErrorMessage = "Email 必填")] // 驗證:必填
    [EmailAddress(ErrorMessage = "Email 格式不正確")] // 驗證:Email 格式
    public string Email { get; set; } = ""; // Email 欄位

    [Required(ErrorMessage = "密碼必填")] // 驗證:必填
    [MinLength(8, ErrorMessage = "密碼至少 8 個字元")] // 驗證:最少 8 字
    [DataType(DataType.Password)] // 指定為密碼類型
    public string Password { get; set; } = ""; // 密碼欄位

    [Compare("Password", ErrorMessage = "密碼不一致")] // 驗證:與密碼比對
    [DataType(DataType.Password)] // 指定為密碼類型
    public string ConfirmPassword { get; set; } = ""; // 確認密碼欄位
}

訂單流程(狀態機設計)

💡 比喻:快遞包裹追蹤 你的包裹狀態:已下單 → 已付款 → 出貨中 → 配送中 → 已送達。 每個狀態只能往下一步走,不能跳步也不能倒退(除了取消)。

// 訂單狀態列舉 // Order status enum
public enum OrderStatus // 訂單狀態
{
    Pending,    // 待付款:剛建立訂單
    Paid,       // 已付款:付款成功
    Processing, // 處理中:賣家準備出貨
    Shipped,    // 已出貨:交給物流
    Delivered,  // 已送達:買家收到
    Cancelled,  // 已取消:訂單取消
    Refunded    // 已退款:退款完成
}

// 訂單狀態機 // Order state machine
public class OrderStateMachine // 訂單狀態機類別
{
    // 定義合法的狀態轉換 // Define valid state transitions
    private static readonly Dictionary<OrderStatus, List<OrderStatus>> _transitions = // 狀態轉換表
        new() // 初始化狀態轉換規則
        {
            [OrderStatus.Pending] = new() { OrderStatus.Paid, OrderStatus.Cancelled }, // 待付款可以→已付款或取消
            [OrderStatus.Paid] = new() { OrderStatus.Processing, OrderStatus.Refunded }, // 已付款可以→處理中或退款
            [OrderStatus.Processing] = new() { OrderStatus.Shipped, OrderStatus.Refunded }, // 處理中可以→已出貨或退款
            [OrderStatus.Shipped] = new() { OrderStatus.Delivered }, // 已出貨只能→已送達
            [OrderStatus.Delivered] = new() { OrderStatus.Refunded }, // 已送達可以→退款
            [OrderStatus.Cancelled] = new(), // 已取消:終態,不能再變
            [OrderStatus.Refunded] = new(), // 已退款:終態,不能再變
        };

    public static bool CanTransition(OrderStatus from, OrderStatus to) // 檢查能否轉換狀態
    {
        return _transitions.ContainsKey(from) // 確認來源狀態存在
            && _transitions[from].Contains(to); // 確認目標狀態合法
    }

    public static void Transition(Order order, OrderStatus newStatus) // 執行狀態轉換
    {
        if (!CanTransition(order.Status, newStatus)) // 檢查是否可以轉換
        {
            throw new InvalidOperationException( // 不合法就拋出例外
                $"無法從 {order.Status} 變更為 {newStatus}"); // 說明錯誤原因
        }
        order.Status = newStatus; // 更新訂單狀態
        order.UpdatedAt = DateTime.UtcNow; // 記錄更新時間
    }
}

// 建立訂單的 Service 方法 // Order creation service method
public async Task<Order> CreateOrderAsync(string userId) // 建立訂單
{
    var cartItems = await _cartService.GetCartItemsAsync(); // 取得購物車項目
    if (!cartItems.Any()) // 如果購物車是空的
        throw new InvalidOperationException("購物車是空的"); // 拋出例外

    var order = new Order // 建立新訂單
    {
        UserId = userId, // 設定會員 ID
        OrderDate = DateTime.UtcNow, // 設定訂單日期
        Status = OrderStatus.Pending, // 初始狀態:待付款
        Items = cartItems.Select(ci => new OrderItem // 將購物車轉為訂單明細
        {
            ProductId = ci.ProductId, // 商品 ID
            ProductName = ci.ProductName, // 商品名稱快照
            Quantity = ci.Quantity, // 數量
            UnitPrice = ci.UnitPrice // 單價快照
        }).ToList(), // 轉為清單
        TotalAmount = cartItems.Sum(ci => ci.Subtotal) // 計算訂單總金額
    };

    _context.Orders.Add(order); // 加入訂單
    await _context.SaveChangesAsync(); // 儲存到資料庫
    await _cartService.ClearCartAsync(); // 清空購物車
    return order; // 回傳訂單
}

金流串接概念(綠界 ECPay 範例)

// 綠界金流串接服務 // ECPay integration service
public class EcpayService // 綠界金流服務類別
{
    private readonly IConfiguration _config; // 設定檔

    public EcpayService(IConfiguration config) // 建構函式注入設定
    {
        _config = config; // 儲存設定參考
    }

    public Dictionary<string, string> BuildPaymentForm(Order order) // 建立付款表單參數
    {
        var merchantId = _config["ECPay:MerchantID"]; // 取得特店編號
        var hashKey = _config["ECPay:HashKey"]; // 取得 HashKey
        var hashIv = _config["ECPay:HashIV"]; // 取得 HashIV

        var parameters = new Dictionary<string, string> // 建立參數字典
        {
            ["MerchantID"] = merchantId!, // 特店編號
            ["MerchantTradeNo"] = $"ORDER{order.Id:D10}", // 交易編號(補零到 10 位)
            ["MerchantTradeDate"] = DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"), // 交易時間
            ["PaymentType"] = "aio", // 付款類型:all in one
            ["TotalAmount"] = ((int)order.TotalAmount).ToString(), // 總金額(整數)
            ["TradeDesc"] = "小農直售平台訂單", // 交易描述
            ["ItemName"] = string.Join("#", order.Items // 商品名稱用 # 分隔
                .Select(i => $"{i.ProductName} x{i.Quantity}")), // 格式:名稱 x 數量
            ["ReturnURL"] = _config["ECPay:ReturnURL"]!, // 付款結果通知網址
            ["OrderResultURL"] = _config["ECPay:OrderResultURL"]!, // 付款完成導回網址
            ["ChoosePayment"] = "ALL" // 付款方式:全部開放
        };

        // 產生檢查碼 // Generate check mac value
        var checkMac = GenerateCheckMacValue(parameters, hashKey!, hashIv!); // 計算檢查碼
        parameters["CheckMacValue"] = checkMac; // 加入檢查碼

        return parameters; // 回傳完整參數
    }

    private string GenerateCheckMacValue( // 產生綠界檢查碼
        Dictionary<string, string> parameters, // 參數字典
        string hashKey, string hashIv) // 金鑰
    {
        var raw = string.Join("&", parameters // 將參數排序並組合
            .OrderBy(p => p.Key) // 依 Key 排序
            .Select(p => $"{p.Key}={p.Value}")); // 組合 Key=Value

        raw = $"HashKey={hashKey}&{raw}&HashIV={hashIv}"; // 前後加上金鑰
        raw = WebUtility.UrlEncode(raw).ToLower(); // URL 編碼後轉小寫

        using var sha256 = SHA256.Create(); // 建立 SHA256 雜湊
        var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(raw)); // 計算雜湊值
        return BitConverter.ToString(hash).Replace("-", "").ToUpper(); // 轉為大寫 16 進位
    }
}

部署到 Railway

// appsettings.Production.json 設定 // Production settings
// Railway 會提供 DATABASE_URL 環境變數 // Railway provides DATABASE_URL env var
// 在 Program.cs 中讀取 // Read in Program.cs

var connectionString = Environment.GetEnvironmentVariable("DATABASE_URL") // 讀取環境變數
    ?? builder.Configuration.GetConnectionString("DefaultConnection"); // 或用設定檔

builder.Services.AddDbContext<AppDbContext>(options => // 設定 DbContext
{
    if (connectionString!.StartsWith("postgres://")) // 如果是 PostgreSQL 格式
    {
        options.UseNpgsql(ConvertPostgresUrl(connectionString)); // 轉換並使用 Npgsql
    }
    else // 如果是一般格式
    {
        options.UseSqlServer(connectionString); // 使用 SQL Server
    }
});

// 轉換 Railway 的 PostgreSQL URL // Convert Railway PostgreSQL URL
static string ConvertPostgresUrl(string url) // URL 格式轉換方法
{
    var uri = new Uri(url); // 解析 URL
    var userInfo = uri.UserInfo.Split(':'); // 分離帳號密碼
    return $"Host={uri.Host};" + // 主機
           $"Port={uri.Port};" + // 連接埠
           $"Database={uri.AbsolutePath.TrimStart('/')};" + // 資料庫名稱
           $"Username={userInfo[0]};" + // 使用者名稱
           $"Password={userInfo[1]};" + // 密碼
           $"SSL Mode=Require;Trust Server Certificate=true"; // SSL 設定
}

🤔 我這樣寫為什麼會錯?

❌ 錯誤 1:Controller 直接操作 DbContext

// ❌ 錯誤:Controller 直接查資料庫 // Mistake: Controller directly queries DB
public class BadController : Controller // 不好的 Controller
{
    private readonly AppDbContext _context; // 直接注入 DbContext
    public async Task<IActionResult> Index() // 直接在 Controller 查詢
    {
        var products = await _context.Products.ToListAsync(); // 這裡沒有商業邏輯層
        return View(products); // 小專案可以但不好擴充
    }
}

// ✅ 正確:透過 Service 層 // Correct: go through Service layer
public class GoodController : Controller // 正確的 Controller
{
    private readonly IProductService _service; // 注入服務介面
    public async Task<IActionResult> Index() // 透過 Service 取資料
    {
        var products = await _service.GetProductsAsync(null, null, 1, 12); // 呼叫 Service 方法
        return View(products); // 回傳 View
    }
}

❌ 錯誤 2:購物車沒有存價格快照

// ❌ 錯誤:結帳時才去查價格 // Mistake: query price at checkout time
public decimal CalculateTotal(List<CartItem> items) // 計算總金額
{
    decimal total = 0; // 初始化總額
    foreach (var item in items) // 逐一計算
    {
        var product = _context.Products.Find(item.ProductId); // 查詢當前價格
        total += product!.Price * item.Quantity; // 用當前價格計算
        // 問題:如果商品在購物車期間漲價,客人會嚇到! // Bug: price may have changed!
    }
    return total; // 回傳可能不正確的金額
}

// ✅ 正確:加入購物車時就記錄價格 // Correct: record price when adding to cart
public async Task AddToCartAsync(int productId, int qty) // 加入購物車
{
    var product = await _context.Products.FindAsync(productId); // 查詢商品
    _context.CartItems.Add(new CartItem // 建立購物車項目
    {
        ProductId = productId, // 商品 ID
        UnitPrice = product!.Price, // 記錄當時的價格快照
        ProductName = product.Name, // 記錄當時的名稱快照
        Quantity = qty // 數量
    });
    await _context.SaveChangesAsync(); // 儲存
}

❌ 錯誤 3:訂單狀態沒有檢查就直接改

// ❌ 錯誤:不檢查就直接改狀態 // Mistake: changing status without validation
public async Task UpdateStatus(int orderId, OrderStatus newStatus) // 更新訂單狀態
{
    var order = await _context.Orders.FindAsync(orderId); // 查詢訂單
    order!.Status = newStatus; // 直接改(已送達可以變回待付款?!)
    await _context.SaveChangesAsync(); // 儲存不合理的狀態
}

// ✅ 正確:用狀態機檢查 // Correct: use state machine
public async Task UpdateStatus(int orderId, OrderStatus newStatus) // 更新訂單狀態
{
    var order = await _context.Orders.FindAsync(orderId); // 查詢訂單
    OrderStateMachine.Transition(order!, newStatus); // 透過狀態機檢查並更新
    await _context.SaveChangesAsync(); // 儲存合法的狀態變更
}

📋 本章重點

層級 負責的事 不該做的事
Controller 接收請求、回傳結果 不該有商業邏輯
Service 商業邏輯、規則驗證 不該直接操作 HTTP
Repository 資料存取(CRUD) 不該有業務規則

🎯 下一步:用相同的架構來做部落格 CMS 系統!

💡 大家的想法 · 0

載入中...
💬 即時聊天室 🟢 0 人在線
😀 😎 🤓 💻 🎮 🎸 🔥
➕ 新問題
📋 我的工單
💬 LINE 社群
🔒
需要註冊才能使用此功能
註冊帳號即可解鎖測驗、遊戲、簽到、筆記下載等所有功能,完全免費!
免費註冊