完整 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; // 嘗試同步到雲端
}