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

完整 POS 系統架構設計

系統架構圖

整體架構

POS 系統完整架構:

┌─────────────────────────────────────────────────────────┐
│                      ☁️ 雲端層                          │
│  ┌───────────┐  ┌───────────┐  ┌───────────┐           │
│  │ 中央 API  │  │  資料庫   │  │  報表系統  │           │
│  │  Server   │  │ (PostgreSQL)│ │ Dashboard │           │
│  └─────┬─────┘  └─────┬─────┘  └─────┬─────┘           │
│        └───────────────┴───────────────┘                 │
│                        │ HTTPS API                       │
└────────────────────────┼────────────────────────────────┘
                         │
         ┌───────────────┼───────────────┐
         │               │               │
    ┌────▼────┐     ┌────▼────┐    ┌────▼────┐
    │ 門市 A  │     │ 門市 B  │    │ 門市 C  │
    │  POS    │     │  POS    │    │  POS    │
    └────┬────┘     └────┬────┘    └────┬────┘
         │               │               │
    ┌────▼────────────────▼───────────────▼────┐
    │              邊緣 POS 層                  │
    │  ┌─────┐ ┌──────┐ ┌──────┐ ┌─────────┐  │
    │  │ 收銀 │ │ 出單 │ │ 刷卡 │ │ 條碼掃描 │  │
    │  │ 介面 │ │  機  │ │  機  │ │    器    │  │
    │  └─────┘ └──────┘ └──────┘ └─────────┘  │
    └──────────────────────────────────────────┘

資料庫設計

核心 Entity 設計

// ===== 商品相關 Entity ===== // 商品管理核心

// 商品分類 // 商品的大類別(如:飲料、食品)
public class ProductCategory // 商品分類類別
{
    public int Id { get; set; } // 分類編號
    public string Name { get; set; } = ""; // 分類名稱
    public string? Description { get; set; } // 分類描述
    public int SortOrder { get; set; } // 排序順序
    public bool IsActive { get; set; } = true; // 是否啟用
    public ICollection<Product> Products { get; set; } = new List<Product>(); // 分類下的商品
}

// 商品 // 可銷售的品項
public class Product // 商品類別
{
    public int Id { get; set; } // 商品編號
    public string Barcode { get; set; } = ""; // 商品條碼
    public string Name { get; set; } = ""; // 商品名稱
    public string? Description { get; set; } // 商品描述
    public decimal Price { get; set; } // 售價
    public decimal Cost { get; set; } // 成本
    public int CategoryId { get; set; } // 分類外鍵
    public ProductCategory Category { get; set; } = null!; // 分類導航屬性
    public string? ImageUrl { get; set; } // 商品圖片網址
    public bool IsActive { get; set; } = true; // 是否上架
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow; // 建立時間
    public ICollection<OrderItem> OrderItems { get; set; } = new List<OrderItem>(); // 訂單明細
    public ICollection<InventoryRecord> InventoryRecords { get; set; } = new List<InventoryRecord>(); // 庫存紀錄
}

訂單 Entity

// ===== 訂單相關 Entity ===== // 銷售核心

// 訂單 // 一筆銷售交易
public class Order // 訂單類別
{
    public int Id { get; set; } // 訂單編號
    public string OrderNumber { get; set; } = ""; // 訂單流水號
    public DateTime OrderDate { get; set; } = DateTime.UtcNow; // 訂單日期時間
    public decimal SubTotal { get; set; } // 小計(未稅)
    public decimal TaxAmount { get; set; } // 稅額
    public decimal DiscountAmount { get; set; } // 折扣金額
    public decimal TotalAmount { get; set; } // 應付金額
    public decimal PaidAmount { get; set; } // 實付金額
    public decimal ChangeAmount { get; set; } // 找零金額
    public string PaymentMethod { get; set; } = "cash"; // 付款方式
    public string Status { get; set; } = "completed"; // 訂單狀態
    public int? MemberId { get; set; } // 會員外鍵(可為空)
    public Member? Member { get; set; } // 會員導航屬性
    public int StoreId { get; set; } // 門市外鍵
    public Store Store { get; set; } = null!; // 門市導航屬性
    public string CashierId { get; set; } = ""; // 收銀員編號
    public string? InvoiceNumber { get; set; } // 電子發票號碼
    public ICollection<OrderItem> Items { get; set; } = new List<OrderItem>(); // 訂單明細
}

// 訂單明細 // 訂單中的每一筆商品
public class OrderItem // 訂單明細類別
{
    public int Id { get; set; } // 明細編號
    public int OrderId { get; set; } // 訂單外鍵
    public Order Order { get; set; } = null!; // 訂單導航屬性
    public int ProductId { get; set; } // 商品外鍵
    public Product Product { get; set; } = null!; // 商品導航屬性
    public string ProductName { get; set; } = ""; // 商品名稱(快照)
    public decimal UnitPrice { get; set; } // 單價(快照)
    public int Quantity { get; set; } // 數量
    public decimal Discount { get; set; } // 折扣
    public decimal LineTotal { get; set; } // 小計
}

庫存與會員 Entity

// ===== 庫存相關 Entity ===== // 庫存管理核心

// 庫存紀錄 // 進貨/銷貨/盤點/調撥
public class InventoryRecord // 庫存紀錄類別
{
    public int Id { get; set; } // 紀錄編號
    public int ProductId { get; set; } // 商品外鍵
    public Product Product { get; set; } = null!; // 商品導航屬性
    public int StoreId { get; set; } // 門市外鍵
    public Store Store { get; set; } = null!; // 門市導航屬性
    public string Type { get; set; } = ""; // 類型:in/out/adjust/transfer
    public int Quantity { get; set; } // 數量(正數進貨,負數銷貨)
    public int BalanceAfter { get; set; } // 異動後餘額
    public string? Note { get; set; } // 備註
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow; // 建立時間
    public string OperatorId { get; set; } = ""; // 操作人員
}

// ===== 會員相關 Entity ===== // 會員管理核心

// 會員 // 註冊的客人
public class Member // 會員類別
{
    public int Id { get; set; } // 會員編號
    public string MemberNumber { get; set; } = ""; // 會員卡號
    public string Name { get; set; } = ""; // 姓名
    public string? Phone { get; set; } // 手機號碼
    public string? Email { get; set; } // 電子信箱
    public int Points { get; set; } // 累積點數
    public string Level { get; set; } = "normal"; // 會員等級
    public DateTime JoinDate { get; set; } = DateTime.UtcNow; // 入會日期
    public DateTime? LastVisitDate { get; set; } // 最後消費日期
    public bool IsActive { get; set; } = true; // 是否有效
    public ICollection<Order> Orders { get; set; } = new List<Order>(); // 消費紀錄
    public ICollection<Coupon> Coupons { get; set; } = new List<Coupon>(); // 擁有的優惠券
}

// 優惠券 // 會員優惠
public class Coupon // 優惠券類別
{
    public int Id { get; set; } // 優惠券編號
    public string Code { get; set; } = ""; // 優惠碼
    public string Name { get; set; } = ""; // 優惠券名稱
    public string Type { get; set; } = "percent"; // 類型:percent/fixed
    public decimal Value { get; set; } // 折扣值(百分比或金額)
    public decimal? MinimumAmount { get; set; } // 最低消費門檻
    public DateTime StartDate { get; set; } // 開始日期
    public DateTime EndDate { get; set; } // 結束日期
    public bool IsUsed { get; set; } // 是否已使用
    public int? MemberId { get; set; } // 所屬會員
    public Member? Member { get; set; } // 會員導航屬性
}

門市與權限 Entity

// ===== 門市相關 Entity ===== // 多店管理

// 門市 // 實體店面
public class Store // 門市類別
{
    public int Id { get; set; } // 門市編號
    public string StoreCode { get; set; } = ""; // 門市代碼
    public string Name { get; set; } = ""; // 門市名稱
    public string? Address { get; set; } // 門市地址
    public string? Phone { get; set; } // 門市電話
    public bool IsActive { get; set; } = true; // 是否營業
    public ICollection<Order> Orders { get; set; } = new List<Order>(); // 門市訂單
    public ICollection<StoreEmployee> Employees { get; set; } = new List<StoreEmployee>(); // 門市員工
}

// 門市員工 // 在門市工作的人員
public class StoreEmployee // 門市員工類別
{
    public int Id { get; set; } // 員工編號
    public string EmployeeCode { get; set; } = ""; // 員工代碼
    public string Name { get; set; } = ""; // 員工姓名
    public string Role { get; set; } = "cashier"; // 角色:manager/cashier/inventory
    public int StoreId { get; set; } // 門市外鍵
    public Store Store { get; set; } = null!; // 門市導航屬性
    public bool IsActive { get; set; } = true; // 是否在職
}

多店管理架構

中央 Server + 邊緣 POS

// 中央同步服務 // 管理雲端和邊緣 POS 的資料同步
public class CentralSyncService // 中央同步服務類別
{
    private readonly HttpClient _httpClient; // HTTP 客戶端
    private readonly ILogger<CentralSyncService> _logger; // 日誌記錄器

    // 建構函式 // 注入相依服務
    public CentralSyncService( // 建構同步服務
        HttpClient httpClient, // 注入 HTTP 客戶端
        ILogger<CentralSyncService> logger) // 注入日誌
    {
        _httpClient = httpClient; // 儲存 HTTP 客戶端
        _logger = logger; // 儲存日誌記錄器
    }

    // 上傳訂單到中央 // 邊緣 POS 將訂單同步到雲端
    public async Task<bool> SyncOrdersAsync( // 同步訂單方法
        List<Order> orders) // 要同步的訂單
    {
        try // 嘗試同步
        {
            var json = System.Text.Json.JsonSerializer.Serialize(orders); // 序列化訂單
            var content = new StringContent(json, // 建立請求內容
                System.Text.Encoding.UTF8, "application/json"); // 設定 JSON 格式

            var response = await _httpClient.PostAsync( // 發送 POST 請求
                "api/sync/orders", content); // 到同步端點

            response.EnsureSuccessStatusCode(); // 確保成功

            _logger.LogInformation( // 記錄同步成功
                "成功同步 {Count} 筆訂單", orders.Count); // 傳入數量

            return true; // 回傳成功
        }
        catch (Exception ex) // 捕捉例外
        {
            _logger.LogError(ex, "訂單同步失敗"); // 記錄錯誤
            return false; // 回傳失敗
        }
    }

    // 從中央下載最新商品 // 同步商品資料到邊緣 POS
    public async Task<List<Product>> DownloadProductsAsync() // 下載商品方法
    {
        try // 嘗試下載
        {
            var response = await _httpClient.GetAsync("api/sync/products"); // GET 商品
            response.EnsureSuccessStatusCode(); // 確保成功

            var json = await response.Content.ReadAsStringAsync(); // 讀取回應
            var products = System.Text.Json.JsonSerializer // 反序列化
                .Deserialize<List<Product>>(json) ?? new(); // 轉為商品列表

            _logger.LogInformation( // 記錄下載成功
                "下載了 {Count} 筆商品", products.Count); // 傳入數量

            return products; // 回傳商品列表
        }
        catch (Exception ex) // 捕捉例外
        {
            _logger.LogError(ex, "商品下載失敗"); // 記錄錯誤
            return new List<Product>(); // 回傳空列表
        }
    }
}

離線模式與資料同步

// 離線佇列管理 // 斷網時暫存操作,恢復後同步
public class OfflineQueueService // 離線佇列服務類別
{
    private readonly Queue<OfflineAction> _queue = new(); // 離線操作佇列
    private readonly ILogger<OfflineQueueService> _logger; // 日誌記錄器
    private bool _isOnline = true; // 網路連線狀態

    // 離線操作資料結構 // 記錄離線時的操作
    public class OfflineAction // 離線操作類別
    {
        public string ActionType { get; set; } = ""; // 操作類型
        public string Data { get; set; } = ""; // 序列化的資料
        public DateTime Timestamp { get; set; } = DateTime.UtcNow; // 操作時間
        public int RetryCount { get; set; } // 重試次數
    }

    // 建構函式 // 注入日誌
    public OfflineQueueService( // 建構離線佇列服務
        ILogger<OfflineQueueService> logger) // 注入日誌
    {
        _logger = logger; // 儲存日誌記錄器
    }

    // 加入離線佇列 // 斷網時把操作暫存起來
    public void Enqueue(string actionType, string data) // 加入佇列方法
    {
        _queue.Enqueue(new OfflineAction // 建立離線操作並加入
        {
            ActionType = actionType, // 設定操作類型
            Data = data, // 設定資料
            Timestamp = DateTime.UtcNow // 設定時間
        });

        _logger.LogInformation( // 記錄加入佇列
            "離線操作已佇列:{Type},目前佇列數:{Count}", // 格式化訊息
            actionType, _queue.Count); // 傳入參數
    }

    // 同步離線佇列 // 恢復網路後逐一處理
    public async Task FlushQueueAsync( // 清空佇列方法
        Func<OfflineAction, Task<bool>> processor) // 處理每個操作的委派
    {
        _logger.LogInformation( // 記錄開始同步
            "開始同步離線佇列,共 {Count} 筆", _queue.Count); // 顯示數量

        while (_queue.Count > 0) // 逐一處理佇列
        {
            var action = _queue.Peek(); // 查看佇列前端
            var success = await processor(action); // 處理操作

            if (success) // 如果成功
            {
                _queue.Dequeue(); // 從佇列移除
            }
            else // 如果失敗
            {
                action.RetryCount++; // 增加重試次數
                if (action.RetryCount > 5) // 超過重試上限
                {
                    _queue.Dequeue(); // 放棄此操作
                    _logger.LogError( // 記錄放棄
                        "離線操作重試失敗已放棄:{Type}", // 格式化訊息
                        action.ActionType); // 傳入類型
                }
                break; // 停止處理(可能又斷線了)
            }
        }
    }
}

報表系統

銷售報表服務

// 報表服務 // 產生各種銷售報表
public class ReportService // 報表服務類別
{
    private readonly ILogger<ReportService> _logger; // 日誌記錄器

    // 日報資料模型 // 每日銷售摘要
    public class DailyReport // 日報類別
    {
        public DateTime Date { get; set; } // 報表日期
        public int TotalTransactions { get; set; } // 交易筆數
        public decimal TotalRevenue { get; set; } // 營業額
        public decimal TotalCost { get; set; } // 總成本
        public decimal GrossProfit { get; set; } // 毛利
        public decimal AverageOrderValue { get; set; } // 客單價
        public Dictionary<string, decimal> PaymentBreakdown { get; set; } = new(); // 付款方式分布
        public List<TopSellingItem> TopItems { get; set; } = new(); // 暢銷商品
    }

    // 暢銷品項 // 銷售排行
    public class TopSellingItem // 暢銷品項類別
    {
        public string ProductName { get; set; } = ""; // 商品名稱
        public int QuantitySold { get; set; } // 銷售數量
        public decimal Revenue { get; set; } // 銷售金額
    }

    // 建構函式 // 注入日誌
    public ReportService(ILogger<ReportService> logger) // 注入 Logger
    {
        _logger = logger; // 儲存日誌記錄器
    }

    // 產生日報 // 統計當日銷售數據
    public DailyReport GenerateDailyReport( // 產生日報方法
        DateTime date, // 報表日期
        List<Order> orders) // 當日訂單
    {
        var report = new DailyReport // 建立日報
        {
            Date = date, // 設定日期
            TotalTransactions = orders.Count, // 計算交易筆數
            TotalRevenue = orders.Sum(o => o.TotalAmount), // 計算總營收
            AverageOrderValue = orders.Count > 0 // 計算客單價
                ? orders.Average(o => o.TotalAmount) : 0 // 有訂單才計算平均
        };

        // 付款方式分布 // 各種付款方式的金額統計
        report.PaymentBreakdown = orders // 依付款方式分組
            .GroupBy(o => o.PaymentMethod) // 分組
            .ToDictionary( // 轉為字典
                g => g.Key, // 鍵為付款方式
                g => g.Sum(o => o.TotalAmount)); // 值為金額合計

        _logger.LogInformation( // 記錄報表產生
            "日報 {Date:MM/dd}:{Count} 筆, ${Revenue}", // 格式化訊息
            date, report.TotalTransactions, report.TotalRevenue); // 傳入參數

        return report; // 回傳日報
    }
}

權限管理

// POS 權限管理 // 依角色控制功能存取
public class PosAuthorizationService // POS 授權服務類別
{
    // 權限定義 // 各角色可執行的操作
    private readonly Dictionary<string, HashSet<string>> _rolePermissions = new() // 角色權限字典
    {
        ["manager"] = new HashSet<string> // 店長權限
        {
            "sale", "refund", "void", // 銷售、退款、作廢
            "report", "inventory", // 報表、庫存
            "member", "settings", "discount" // 會員、設定、折扣
        },
        ["cashier"] = new HashSet<string> // 收銀員權限
        {
            "sale", "member" // 只能銷售和查會員
        },
        ["inventory"] = new HashSet<string> // 倉管權限
        {
            "inventory", "report" // 庫存和報表
        }
    };

    // 檢查權限 // 確認員工是否有權執行操作
    public bool HasPermission( // 檢查權限方法
        string role, string action) // 角色和操作
    {
        if (!_rolePermissions.ContainsKey(role)) // 如果角色不存在
            return false; // 回傳無權限

        return _rolePermissions[role].Contains(action); // 檢查是否包含該操作
    }
}

🤔 我這樣寫為什麼會錯?

❌ 錯誤:訂單金額在前端計算

// 錯誤寫法 // 讓前端算金額,後端直接存
[HttpPost("api/orders")] // 建立訂單 API
public IActionResult CreateOrder( // 建立訂單方法
    [FromBody] Order order) // 直接接收前端送來的訂單(含金額)
{
    // 直接存入資料庫 ← 前端送什麼就存什麼! // 客人可以竄改金額
    _db.Orders.Add(order); // 直接儲存
    _db.SaveChanges(); // 存入資料庫
    return Ok(); // 回傳成功
}

private dynamic _db = null!; // 資料庫(示意)

✅ 正確:後端重新計算金額

// 正確寫法 // 後端根據商品 ID 和數量重新計算
[HttpPost("api/orders")] // 建立訂單 API
public IActionResult CreateOrder( // 建立訂單方法
    [FromBody] CreateOrderRequest request) // 只接收商品 ID 和數量
{
    var order = new Order(); // 建立新訂單
    decimal total = 0; // 初始化總金額

    foreach (var item in request.Items) // 逐筆處理商品
    {
        var product = _db2.Products.Find(item.ProductId); // 從資料庫查商品
        if (product == null) continue; // 商品不存在就跳過

        var lineTotal = product.Price * item.Quantity; // 後端計算小計
        total += lineTotal; // 累加總金額

        order.Items.Add(new OrderItem // 加入訂單明細
        {
            ProductId = product.Id, // 商品 ID
            ProductName = product.Name, // 商品名稱(快照)
            UnitPrice = product.Price, // 單價(從資料庫取)
            Quantity = item.Quantity, // 數量
            LineTotal = lineTotal // 小計
        });
    }

    order.TotalAmount = total; // 後端計算的總金額
    _db2.Orders.Add(order); // 儲存訂單
    _db2.SaveChanges(); // 存入資料庫
    return Ok(order); // 回傳訂單
}

// 前端只需送這些 // 不包含金額
public class CreateOrderRequest // 建立訂單請求類別
{
    public List<OrderItemRequest> Items { get; set; } = new(); // 商品列表
}

public class OrderItemRequest // 訂單商品請求
{
    public int ProductId { get; set; } // 商品 ID
    public int Quantity { get; set; } // 數量
}

private dynamic _db2 = null!; // 資料庫(示意)

❌ 錯誤:離線時拒絕所有操作

// 錯誤寫法 // 斷網就不能用 ← POS 怎麼能停擺!
public class BadPosService // 錯誤的 POS 服務
{
    public bool ProcessSale() // 處理銷售方法
    {
        if (!IsOnline()) // 如果沒有網路
            throw new Exception("無法連線,請稍後再試"); // 直接拒絕
        return true; // 有網路才處理
    }

    private bool IsOnline() => false; // 檢查網路狀態
}

✅ 正確:離線模式繼續服務

// 正確寫法 // 斷網也能繼續收銀
public class ResilientPosService // 有韌性的 POS 服務
{
    private readonly OfflineQueueService _offlineQueue; // 離線佇列

    public ResilientPosService( // 建構函式
        OfflineQueueService offlineQueue) // 注入離線佇列
    {
        _offlineQueue = offlineQueue; // 儲存離線佇列
    }

    public bool ProcessSale(Order order) // 處理銷售方法
    {
        // 先存到本地資料庫 // 確保資料不會遺失
        SaveToLocalDb(order); // 存入本地 SQLite

        // 嘗試同步到雲端 // 失敗就加入離線佇列
        if (!TrySyncToCloud(order)) // 如果同步失敗
        {
            _offlineQueue.Enqueue("order", // 加入離線佇列
                System.Text.Json.JsonSerializer.Serialize(order)); // 序列化訂單
        }

        return true; // 不管有沒有網路都回傳成功
    }

    private void SaveToLocalDb(Order o) { } // 存到本地資料庫
    private bool TrySyncToCloud(Order o) => false; // 嘗試同步到雲端
}

💡 大家的想法 · 0

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