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

CI/CD 自動部署到 POS 設備

GitHub Actions CI/CD Pipeline

💡 比喻:自動化的工廠生產線 CI/CD 就像一條自動化工廠生產線: 你把原料(程式碼)放上去, 機器會自動檢查品質(Test)、組裝(Build)、包裝(Package)、 然後送到客戶手上(Deploy)。 全程不需要人工介入,又快又可靠。

GitHub Actions 工作流程

# .github/workflows/pos-deploy.yml // CI/CD 工作流程設定檔
name: POS System CI/CD # 工作流程名稱 // 在 GitHub 上顯示的名稱

on: # 觸發條件 // 什麼時候執行這個流程
  push: # 推送程式碼時觸發
    branches: [ main ] # 只在 main 分支觸發 // 避免每個分支都部署
  pull_request: # PR 時觸發 // 用於檢查程式碼品質
    branches: [ main ] # 只在 main 分支的 PR

env: # 環境變數 // 全域設定
  DOTNET_VERSION: '8.0.x' # .NET 版本 // 使用 8.0 LTS
  NODE_VERSION: '20' # Node.js 版本 // LTS 版本

jobs: # 工作定義 // 包含所有要執行的步驟
  # === 階段 1:建置和測試 === // Build + Test
  build-and-test: # 工作名稱
    runs-on: ubuntu-latest # 執行環境 // 使用最新 Ubuntu
    steps: # 步驟清單
      - uses: actions/checkout@v4 # 取出程式碼 // 從 Git 倉庫
      - name: Setup .NET # 安裝 .NET SDK
        uses: actions/setup-dotnet@v4 # 使用官方 Action
        with: # 設定參數
          dotnet-version: ${{ env.DOTNET_VERSION }} # 使用指定版本

      - name: Restore dependencies # 還原 NuGet 套件
        run: dotnet restore # 下載所有相依套件

      - name: Build # 建置專案
        run: dotnet build --no-restore --configuration Release # Release 模式建置

      - name: Test # 執行測試
        run: dotnet test --no-build --configuration Release --verbosity normal # 執行所有測試

      - name: Publish # 發佈應用程式
        run: dotnet publish -c Release -r linux-arm64 --self-contained -o ./publish # 發佈為 ARM64 Linux

      - name: Upload artifact # 上傳建置結果
        uses: actions/upload-artifact@v4 # 使用上傳 Action
        with: # 設定參數
          name: pos-app # 產出物名稱
          path: ./publish # 要上傳的路徑

  # === 階段 2:部署 === // Deploy to Pi
  deploy: # 部署工作
    needs: build-and-test # 依賴建置工作 // 建置成功才部署
    runs-on: ubuntu-latest # 執行環境
    if: github.ref == 'refs/heads/main' # 只在 main 分支部署 // PR 不部署
    steps: # 步驟清單
      - name: Download artifact # 下載建置結果
        uses: actions/download-artifact@v4 # 使用下載 Action
        with: # 設定參數
          name: pos-app # 產出物名稱
          path: ./publish # 下載到的路徑

      - name: Deploy to Raspberry Pi # 部署到 Pi
        uses: appleboy/scp-action@v0.1.7 # 使用 SCP Action // 透過 SSH 複製檔案
        with: # 設定參數
          host: ${{ secrets.PI_HOST }} # Pi 的 IP // 從 GitHub Secrets 取得
          username: ${{ secrets.PI_USER }} # SSH 帳號 // 從 Secrets 取得
          key: ${{ secrets.PI_SSH_KEY }} # SSH 私鑰 // 從 Secrets 取得
          source: './publish/*' # 來源檔案 // 建置結果
          target: '/opt/pos-app' # 目標路徑 // Pi 上的安裝目錄

      - name: Restart POS Service # 重啟 POS 服務
        uses: appleboy/ssh-action@v1.0.3 # 使用 SSH Action // 遠端執行指令
        with: # 設定參數
          host: ${{ secrets.PI_HOST }} # Pi 的 IP
          username: ${{ secrets.PI_USER }} # SSH 帳號
          key: ${{ secrets.PI_SSH_KEY }} # SSH 私鑰
          script: | # 要執行的腳本
            sudo systemctl restart pos-app # 重啟 POS 服務 // 使用 systemd
            sudo systemctl status pos-app # 檢查服務狀態 // 確認正常運作

用 C# 描述 CI/CD Pipeline

// 定義 CI/CD Pipeline 階段的列舉 // 每個階段代表一個步驟
public enum PipelineStage // Pipeline 階段列舉
{
    Checkout,   // 取出程式碼 // 從 Git 倉庫下載
    Restore,    // 還原套件 // NuGet restore
    Build,      // 建置專案 // 編譯程式碼
    Test,       // 執行測試 // 跑單元測試
    Publish,    // 發佈應用 // 打包成可部署的檔案
    Deploy,     // 部署到設備 // SCP 到 Pi
    Restart     // 重啟服務 // 讓新版本生效
}

// 定義 Pipeline 執行結果的類別 // 記錄每個階段的結果
public class PipelineResult // Pipeline 結果類別
{
    public PipelineStage Stage { get; set; }  // 階段名稱 // 哪個步驟
    public bool Success { get; set; }         // 是否成功 // true/false
    public string Message { get; set; } = ""; // 訊息 // 成功或錯誤描述
    public TimeSpan Duration { get; set; }    // 執行時間 // 花了多久
}

// 定義 Pipeline 執行器的類別 // 模擬 CI/CD 流程
public class PipelineRunner // Pipeline 執行器
{
    private readonly List<PipelineResult> _results = new(); // 結果清單 // 記錄所有階段

    // 執行完整 Pipeline 的方法 // 依序執行每個階段
    public async Task<bool> RunPipeline() // 執行 Pipeline 方法
    {
        var stages = Enum.GetValues<PipelineStage>(); // 取得所有階段 // 列舉值

        foreach (var stage in stages) // 逐一執行每個階段
        {
            Console.WriteLine($"▶ 執行階段:{stage}..."); // 顯示目前階段
            var startTime = DateTime.Now; // 記錄開始時間

            var success = await ExecuteStage(stage); // 執行該階段 // 回傳是否成功

            var result = new PipelineResult // 建立結果物件
            {
                Stage = stage, // 設定階段
                Success = success, // 設定結果
                Duration = DateTime.Now - startTime, // 計算執行時間
                Message = success ? "完成" : "失敗" // 設定訊息
            }; // 結果物件建立完成

            _results.Add(result); // 加入結果清單
            Console.WriteLine($"  {(success ? "✅" : "❌")} {stage}: {result.Duration.TotalSeconds:F1}s"); // 顯示結果

            if (!success) // 如果失敗
            {
                Console.WriteLine($"❌ Pipeline 在 {stage} 階段失敗!"); // 顯示失敗訊息
                return false; // 回傳失敗 // 停止後續階段
            }
        }

        Console.WriteLine("✅ Pipeline 完成!所有階段成功"); // 全部完成
        return true; // 回傳成功
    }

    // 執行單一階段的方法 // 模擬各階段操作
    private async Task<bool> ExecuteStage(PipelineStage stage) // 執行階段方法
    {
        await Task.Delay(100); // 模擬執行時間 // 實際上會花更久
        return true; // 回傳成功 // 模擬用
    }
}

自動 Build → Test → Deploy 到 Server

// 定義部署設定的類別 // 描述部署到 Pi 的參數
public class DeploymentConfig // 部署設定類別
{
    public string ServerHost { get; set; } = "";    // 伺服器 IP // Pi 的位址
    public int ServerPort { get; set; } = 22;        // SSH 埠號 // 預設 22
    public string Username { get; set; } = "";      // SSH 帳號 // 登入用
    public string SshKeyPath { get; set; } = "";    // SSH 金鑰路徑 // 免密碼登入用
    public string RemotePath { get; set; } = "/opt/pos-app"; // 遠端安裝路徑 // Pi 上的目錄
    public string ServiceName { get; set; } = "pos-app"; // 服務名稱 // systemd 服務名
    public string BuildConfig { get; set; } = "Release"; // 建置組態 // Release 模式
    public string RuntimeId { get; set; } = "linux-arm64"; // 目標平台 // ARM64 Linux
}

// 定義自動部署器的類別 // 執行自動化部署
public class AutoDeployer // 自動部署器
{
    private readonly DeploymentConfig _config; // 部署設定 // 建構時傳入

    public AutoDeployer(DeploymentConfig config) // 建構函式 // 接收設定
    {
        _config = config; // 儲存設定 // 後續步驟會用到
    }

    // 執行完整部署流程的方法 // Build → Test → Deploy
    public void Deploy() // 部署方法
    {
        Console.WriteLine("=== 開始自動部署流程 ==="); // 顯示開始

        // 步驟 1:建置 // dotnet publish
        Console.WriteLine("[1/4] 建置應用程式..."); // 顯示步驟
        Console.WriteLine($"  dotnet publish -c {_config.BuildConfig} -r {_config.RuntimeId}"); // 顯示指令

        // 步驟 2:測試 // dotnet test
        Console.WriteLine("[2/4] 執行測試..."); // 顯示步驟
        Console.WriteLine("  dotnet test --configuration Release"); // 顯示指令

        // 步驟 3:部署 // SCP 檔案到 Pi
        Console.WriteLine("[3/4] 部署到 Raspberry Pi..."); // 顯示步驟
        Console.WriteLine($"  scp -r ./publish/* {_config.Username}@{_config.ServerHost}:{_config.RemotePath}"); // SCP 指令

        // 步驟 4:重啟 // 重啟 systemd 服務
        Console.WriteLine("[4/4] 重啟 POS 服務..."); // 顯示步驟
        Console.WriteLine($"  ssh {_config.Username}@{_config.ServerHost} 'sudo systemctl restart {_config.ServiceName}'"); // SSH 重啟

        Console.WriteLine("=== 部署完成!==="); // 顯示完成
    }
}

Raspberry Pi 自動更新機制(Pull 或 Webhook)

💡 比喻:快遞到府 vs. 自己去取件 Pull 模式就像你每天去郵局問「有我的包裹嗎?」(定時檢查)。 Webhook 模式就像快遞員直接送到你家門口(伺服器主動通知)。 兩種都可以,但 Webhook 更即時。

// 定義更新機制的列舉 // 兩種更新方式
public enum UpdateMode // 更新模式列舉
{
    Pull,    // 拉取模式 // Pi 定時去問有沒有新版
    Webhook  // 推送模式 // 伺服器有新版主動通知 Pi
}

// 定義 Pull 模式更新器的類別 // 定時檢查更新
public class PullUpdater // Pull 更新器
{
    private readonly string _updateUrl; // 更新檢查 URL // 伺服器的版本檢查端點
    private readonly TimeSpan _checkInterval; // 檢查間隔 // 多久檢查一次

    public PullUpdater(string updateUrl, TimeSpan interval) // 建構函式
    {
        _updateUrl = updateUrl; // 設定更新 URL
        _checkInterval = interval; // 設定檢查間隔
    }

    // 開始定時檢查的方法 // 背景執行
    public async Task StartChecking() // 開始檢查方法
    {
        while (true) // 無限迴圈 // 持續檢查
        {
            Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] 檢查更新..."); // 顯示檢查訊息

            try // 嘗試檢查更新
            {
                var currentVersion = GetCurrentVersion(); // 取得目前版本 // 本地版本號
                Console.WriteLine($"  目前版本:{currentVersion}"); // 顯示目前版本

                // 模擬呼叫 API 檢查最新版本 // 實際上要用 HttpClient
                var latestVersion = "1.0.1"; // 模擬最新版本 // 從伺服器取得
                Console.WriteLine($"  最新版本:{latestVersion}"); // 顯示最新版本

                if (currentVersion != latestVersion) // 如果版本不同
                {
                    Console.WriteLine("  🔄 發現新版本,開始更新..."); // 開始更新
                    await DownloadAndApplyUpdate(latestVersion); // 下載並套用更新
                }
                else // 版本相同
                {
                    Console.WriteLine("  ✅ 已是最新版本"); // 不需要更新
                }
            }
            catch (Exception ex) // 檢查失敗
            {
                Console.WriteLine($"  ❌ 檢查更新失敗:{ex.Message}"); // 顯示錯誤
            }

            await Task.Delay(_checkInterval); // 等待下次檢查 // 按照設定的間隔
        }
    }

    private string GetCurrentVersion() => "1.0.0"; // 取得目前版本 // 簡化示範

    private async Task DownloadAndApplyUpdate(string version) // 下載並套用更新方法
    {
        Console.WriteLine($"  下載版本 {version}..."); // 顯示下載訊息
        await Task.Delay(100); // 模擬下載 // 實際上要下載檔案
        Console.WriteLine("  更新完成,重啟服務..."); // 顯示更新完成
    }
}

Docker 容器化 POS 應用

# POS 系統的 Dockerfile // 多階段建置
# === 階段 1:Build === // 用 SDK Image 編譯
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
# 設定工作目錄 // 所有操作在此目錄下
WORKDIR /src
# 複製專案檔 // 利用 Docker 層快取
COPY *.csproj ./
# 還原 NuGet 套件 // 這層變動少可以快取
RUN dotnet restore
# 複製所有原始碼 // 這層每次 build 都會變
COPY . .
# 發佈為 Release 模式 // 輸出到 /app/publish
RUN dotnet publish -c Release -o /app/publish

# === 階段 2:Runtime === // 用精簡的 Runtime Image
FROM mcr.microsoft.com/dotnet/aspnet:8.0-bookworm-slim-arm64 AS runtime
# 設定工作目錄
WORKDIR /app
# 從 build 階段複製發佈結果 // 只要成品不要原始碼
COPY --from=build /app/publish .
# 設定時區 // 台灣時區
ENV TZ=Asia/Taipei
# 對外開放的埠號 // POS 系統的 Web 埠
EXPOSE 5000
# 啟動指令 // 執行 POS 應用程式
ENTRYPOINT ["dotnet", "PosApp.dll"]

Docker Compose 整合

# docker-compose.yml // 整合所有 POS 服務
version: '3.8' # Compose 版本 // 使用 3.8

services: # 服務定義 // 所有容器
  pos-web: # POS Web 應用 // 主要的 POS 系統
    build: . # 從當前目錄建置 // 使用 Dockerfile
    ports: # 埠號對應
      - "5000:5000" # 主機 5000 → 容器 5000
    environment: # 環境變數
      - ASPNETCORE_URLS=http://+:5000 # 監聽所有介面的 5000 埠
      - ConnectionStrings__DefaultConnection=Data Source=/data/pos.db # SQLite 路徑
    volumes: # 掛載資料卷
      - pos-data:/data # 資料庫持久化 // 重啟不會遺失資料
    restart: always # 自動重啟 // 異常時自動恢復

  print-agent: # Print Agent 服務 // Node.js 列印代理
    build: ./print-agent # 從 print-agent 目錄建置
    ports: # 埠號對應
      - "9100:9100" # WebSocket 埠
    devices: # 裝置對應 // 讓容器存取 USB 印表機
      - /dev/usb/lp0:/dev/usb/lp0 # USB 印表機裝置
    privileged: true # 特權模式 // 存取硬體需要
    restart: always # 自動重啟

volumes: # 資料卷定義
  pos-data: # POS 資料卷 // 儲存 SQLite 資料庫

多台 POS 設備管理(Fleet Management)

💡 比喻:管理一群遙控汽車 如果你的店有 5 台 POS 機,每台都要更新、監控、維護。 Fleet Management 就像一個「遙控器」, 可以同時控制所有 POS 機,一次更新全部。

// 定義 POS 設備的類別 // 描述單一 POS 機的資訊
public class PosDevice // POS 設備類別
{
    public string DeviceId { get; set; } = "";   // 設備 ID // 唯一識別碼
    public string Hostname { get; set; } = "";   // 主機名稱 // 例如 pos-01
    public string IpAddress { get; set; } = "";  // IP 位址 // 網路位址
    public string Version { get; set; } = "";    // 應用版本 // 目前的版本號
    public string Status { get; set; } = "offline"; // 狀態 // online/offline/updating
    public DateTime LastSeen { get; set; }        // 最後上線時間 // 心跳檢測
    public string Location { get; set; } = "";   // 位置 // 例如「一樓櫃台」
}

// 定義設備管理器的類別 // 管理所有 POS 設備
public class FleetManager // 設備管理器
{
    private readonly List<PosDevice> _devices = new(); // 設備清單 // 所有 POS 機

    // 註冊新設備的方法 // POS 機上線時呼叫
    public void RegisterDevice(PosDevice device) // 註冊設備方法
    {
        _devices.Add(device); // 加入設備清單 // 新設備加入管理
        Console.WriteLine($"設備已註冊:{device.Hostname} ({device.IpAddress})"); // 顯示註冊訊息
    }

    // 取得所有設備狀態的方法 // 儀表板用
    public void PrintDashboard() // 顯示儀表板方法
    {
        Console.WriteLine("╔════════════════════════════════════════════════════╗"); // 表格上框
        Console.WriteLine("║          POS 設備管理儀表板                        ║"); // 標題
        Console.WriteLine("╠════════════════════════════════════════════════════╣"); // 分隔線
        Console.WriteLine("║ 名稱       IP              版本     狀態   位置   ║"); // 欄位標題
        Console.WriteLine("╠════════════════════════════════════════════════════╣"); // 分隔線

        foreach (var d in _devices) // 逐一列出設備
        {
            var statusIcon = d.Status switch // 根據狀態顯示圖示
            {
                "online" => "🟢",   // 上線 // 綠色圓點
                "offline" => "🔴",  // 離線 // 紅色圓點
                "updating" => "🟡", // 更新中 // 黃色圓點
                _ => "⚪"             // 未知 // 白色圓點
            }; // switch 結束
            Console.WriteLine($"║ {d.Hostname,-10} {d.IpAddress,-15} {d.Version,-8} {statusIcon,-4} {d.Location,-6} ║"); // 設備資訊
        }

        Console.WriteLine("╚════════════════════════════════════════════════════╝"); // 表格下框
    }

    // 批次更新所有設備的方法 // 一次更新全部 POS 機
    public async Task UpdateAll(string newVersion) // 批次更新方法
    {
        Console.WriteLine($"開始批次更新到 v{newVersion}..."); // 顯示更新開始

        foreach (var device in _devices) // 逐一更新設備
        {
            device.Status = "updating"; // 設定狀態為更新中
            Console.WriteLine($"  更新 {device.Hostname}..."); // 顯示更新進度

            await Task.Delay(200); // 模擬更新過程 // 實際上是 SCP + 重啟

            device.Version = newVersion; // 更新版本號
            device.Status = "online"; // 恢復上線狀態
            Console.WriteLine($"  ✅ {device.Hostname} 更新完成"); // 顯示完成
        }

        Console.WriteLine($"全部 {_devices.Count} 台設備更新完成!"); // 顯示全部完成
    }
}

監控與遠端維護(SSH Tunnel、VPN)

// 定義監控設定的類別 // 描述監控和遠端維護的設定
public class MonitoringConfig // 監控設定類別
{
    public int HealthCheckIntervalSeconds { get; set; } = 30; // 健康檢查間隔 // 30 秒一次
    public string SshTunnelHost { get; set; } = ""; // SSH Tunnel 伺服器 // 跳板機
    public int SshTunnelPort { get; set; } = 22;    // SSH Tunnel 埠號
    public string VpnServer { get; set; } = "";    // VPN 伺服器 // 安全連線用
    public bool EnableAlerts { get; set; } = true;  // 是否啟用警報 // 異常時通知
}

// 定義健康檢查結果的類別 // 單次檢查的結果
public class HealthCheckResult // 健康檢查結果
{
    public string DeviceId { get; set; } = "";  // 設備 ID // 哪台機器
    public DateTime CheckTime { get; set; }     // 檢查時間 // 什麼時候檢查的
    public bool IsHealthy { get; set; }         // 是否健康 // 正常/異常
    public double CpuUsage { get; set; }        // CPU 使用率 // 百分比
    public double MemoryUsage { get; set; }     // 記憶體使用率 // 百分比
    public double DiskUsage { get; set; }       // 硬碟使用率 // 百分比
    public bool PrinterOnline { get; set; }     // 印表機是否在線 // 列印服務狀態
    public bool NetworkOnline { get; set; }     // 網路是否在線 // 連線狀態
}

// 定義設備監控器的類別 // 持續監控所有 POS 設備
public class DeviceMonitor // 設備監控器
{
    private readonly MonitoringConfig _config; // 監控設定 // 建構時傳入

    public DeviceMonitor(MonitoringConfig config) // 建構函式
    {
        _config = config; // 儲存設定
    }

    // 執行健康檢查的方法 // 檢查單一設備
    public HealthCheckResult CheckDevice(PosDevice device) // 檢查設備方法
    {
        var result = new HealthCheckResult // 建立檢查結果
        {
            DeviceId = device.DeviceId, // 設備 ID
            CheckTime = DateTime.Now,   // 檢查時間
            CpuUsage = 35.2,           // 模擬 CPU 使用率 // 實際要用 SSH 查詢
            MemoryUsage = 62.8,        // 模擬記憶體使用率
            DiskUsage = 45.0,          // 模擬硬碟使用率
            PrinterOnline = true,      // 印表機在線
            NetworkOnline = true       // 網路在線
        }; // 結果建立完成

        result.IsHealthy = result.CpuUsage < 90 // CPU 低於 90%
            && result.MemoryUsage < 90  // 記憶體低於 90%
            && result.DiskUsage < 90    // 硬碟低於 90%
            && result.PrinterOnline     // 印表機在線
            && result.NetworkOnline;    // 網路在線

        if (!result.IsHealthy && _config.EnableAlerts) // 如果不健康且啟用警報
        {
            Console.WriteLine($"⚠️ 警報:{device.Hostname} 狀態異常!"); // 顯示警報
            Console.WriteLine($"  CPU: {result.CpuUsage}% | RAM: {result.MemoryUsage}% | Disk: {result.DiskUsage}%"); // 詳細數據
        }

        return result; // 回傳檢查結果
    }
}

資料同步(離線→上線自動同步)

💡 比喻:出差回來整理報帳 POS 設備離線期間(網路斷了)的交易就像出差時的花費, 先用小本子記下來,等回到公司再整理報帳。 資料同步就是把「小本子上的記錄」匯入公司的系統。

// 定義同步項目的類別 // 需要同步的單筆資料
public class SyncItem // 同步項目
{
    public string Id { get; set; } = Guid.NewGuid().ToString(); // 唯一 ID // UUID 格式
    public string Type { get; set; } = "";       // 資料類型 // order, product, inventory
    public string JsonData { get; set; } = "";   // JSON 資料 // 序列化的資料內容
    public DateTime CreatedAt { get; set; } = DateTime.Now; // 建立時間 // 離線時記錄
    public bool IsSynced { get; set; } = false;  // 是否已同步 // 同步後設為 true
    public int RetryCount { get; set; } = 0;     // 重試次數 // 同步失敗累計
}

// 定義資料同步管理器的類別 // 處理離線→上線的資料同步
public class SyncManager // 同步管理器
{
    private readonly List<SyncItem> _pendingItems = new(); // 待同步清單 // 離線期間累積的資料
    private readonly string _serverUrl; // 伺服器 URL // 總部伺服器的位址

    public SyncManager(string serverUrl) // 建構函式 // 接收伺服器位址
    {
        _serverUrl = serverUrl; // 儲存伺服器 URL
    }

    // 加入待同步項目的方法 // 離線時呼叫
    public void AddPending(string type, string jsonData) // 加入待同步方法
    {
        var item = new SyncItem // 建立同步項目
        {
            Type = type,       // 設定資料類型
            JsonData = jsonData // 設定 JSON 資料
        }; // 項目建立完成

        _pendingItems.Add(item); // 加入待同步清單
        Console.WriteLine($"新增待同步項目:{type}(共 {_pendingItems.Count} 筆待同步)"); // 顯示訊息
    }

    // 執行同步的方法 // 網路恢復時呼叫
    public async Task SyncAll() // 同步全部方法
    {
        var unsyncedItems = _pendingItems // 篩選未同步的項目
            .Where(i => !i.IsSynced) // 只處理未同步的
            .OrderBy(i => i.CreatedAt) // 按建立時間排序 // 先進先出
            .ToList(); // 轉為清單

        if (unsyncedItems.Count == 0) // 沒有待同步的
        {
            Console.WriteLine("沒有待同步的項目"); // 顯示訊息
            return; // 結束
        }

        Console.WriteLine($"開始同步 {unsyncedItems.Count} 筆資料..."); // 顯示開始

        var successCount = 0; // 成功計數 // 統計用
        var failCount = 0;    // 失敗計數 // 統計用

        foreach (var item in unsyncedItems) // 逐筆同步
        {
            try // 嘗試同步
            {
                Console.WriteLine($"  同步 {item.Type} ({item.Id[..8]}...)"); // 顯示同步項目
                await Task.Delay(50); // 模擬 API 呼叫 // 實際要用 HttpClient POST

                item.IsSynced = true; // 標記為已同步
                successCount++; // 成功計數加 1
            }
            catch (Exception ex) // 同步失敗
            {
                item.RetryCount++; // 重試次數加 1
                failCount++; // 失敗計數加 1
                Console.WriteLine($"  ❌ 同步失敗(第 {item.RetryCount} 次):{ex.Message}"); // 顯示錯誤

                if (item.RetryCount >= 5) // 重試超過 5 次
                {
                    Console.WriteLine($"  ⚠️ 項目 {item.Id[..8]} 已超過重試上限,標記為需人工處理"); // 需要人工介入
                }
            }
        }

        Console.WriteLine($"同步完成!成功 {successCount} 筆,失敗 {failCount} 筆"); // 顯示結果
    }
}

安全考量(HTTPS、防火牆、VPN)

// 定義安全設定的類別 // POS 系統的安全相關設定
public class SecurityConfig // 安全設定類別
{
    public bool EnableHttps { get; set; } = true;   // 啟用 HTTPS // 加密傳輸
    public bool EnableFirewall { get; set; } = true; // 啟用防火牆 // 限制網路存取
    public bool EnableVpn { get; set; } = false;    // 啟用 VPN // 遠端管理用
    public int SessionTimeoutMinutes { get; set; } = 30; // 工作階段逾時 // 閒置自動登出
    public List<string> AllowedPorts { get; set; } = new() // 允許的埠號 // 防火牆規則
    {
        "22",   // SSH // 遠端管理
        "443",  // HTTPS // Web 加密連線
        "5000", // POS Web // 應用程式埠
        "9100"  // Print Agent // 列印服務埠
    }; // 允許的埠號清單
}

// 定義安全檢查器的類別 // 檢查 POS 系統的安全狀態
public class SecurityAuditor // 安全檢查器
{
    // 執行安全稽核的方法 // 檢查所有安全設定
    public List<string> Audit(SecurityConfig config) // 安全稽核方法
    {
        var findings = new List<string>(); // 發現清單 // 收集所有問題

        if (!config.EnableHttps) // 如果沒啟用 HTTPS
            findings.Add("⚠️ 高風險:未啟用 HTTPS,交易資料可能被攔截"); // 加入警告

        if (!config.EnableFirewall) // 如果沒啟用防火牆
            findings.Add("⚠️ 高風險:未啟用防火牆,POS 設備暴露在網路中"); // 加入警告

        if (config.AllowedPorts.Contains("80")) // 如果允許 HTTP(不安全)
            findings.Add("⚠️ 中風險:開放了 HTTP port 80,建議只用 HTTPS"); // 加入警告

        if (config.SessionTimeoutMinutes > 60) // 如果工作階段逾時太長
            findings.Add("⚠️ 低風險:工作階段逾時設定過長,建議 30 分鐘內"); // 加入警告

        if (findings.Count == 0) // 沒有發現問題
            findings.Add("✅ 所有安全設定都正常"); // 加入通過訊息

        return findings; // 回傳稽核結果
    }
}

實際部署案例分析

POS 系統實際部署案例:小型咖啡廳

📍 店家規模:
   - 1 間店面,2 台 POS 設備
   - 1 台熱感應印表機
   - 1 個錢箱
   - 觸控螢幕

🔧 硬體配置:
   ┌─────────────────────────────────────────────┐
   │ POS 機 1(前台)                              │
   │ ├── Raspberry Pi 4 (4GB)                     │
   │ ├── 10 吋觸控螢幕                             │
   │ ├── 熱感應印表機(USB)                       │
   │ ├── 錢箱(RJ-11 接印表機)                   │
   │ └── 條碼掃描器(USB)                         │
   │                                              │
   │ POS 機 2(外帶區)                            │
   │ ├── Raspberry Pi 4 (4GB)                     │
   │ ├── 7 吋觸控螢幕                              │
   │ └── 條碼掃描器(USB)                         │
   │                                              │
   │ 伺服器(後台管理)                            │
   │ ├── 雲端 VPS 或本地 NAS                      │
   │ ├── 資料庫(PostgreSQL)                     │
   │ └── 管理後台(報表、庫存)                   │
   └─────────────────────────────────────────────┘

💰 成本估算:
   Pi 4 (4GB) x2     = NT$ 4,000
   觸控螢幕 x2        = NT$ 6,000
   熱感應印表機 x1     = NT$ 3,000
   錢箱 x1            = NT$ 2,000
   條碼掃描器 x2       = NT$ 2,000
   SD 卡 x2           = NT$ 600
   ─────────────────────────────
   總計               ≈ NT$ 17,600

   vs. 市售 POS 系統  ≈ NT$ 50,000+(還不含月租費)
   省下超過 60% 的成本!

用 C# 模擬部署規劃

// 定義部署規劃的類別 // 描述完整的部署方案
public class DeploymentPlan // 部署規劃類別
{
    public string StoreName { get; set; } = ""; // 店家名稱 // 部署的對象
    public int PosCount { get; set; }          // POS 數量 // 幾台收銀機
    public List<PosDevice> Devices { get; set; } = new(); // 設備清單 // 所有 POS 機
    public decimal EstimatedCost { get; set; } // 預估成本 // 硬體+軟體

    // 產生部署報告的方法 // 給店家看的評估報告
    public void GenerateReport() // 產生報告方法
    {
        Console.WriteLine("╔══════════════════════════════════════╗"); // 報告框線
        Console.WriteLine($"║  POS 系統部署規劃報告              ║"); // 報告標題
        Console.WriteLine($"║  店家:{StoreName,-28}║"); // 店家名稱
        Console.WriteLine("╠══════════════════════════════════════╣"); // 分隔線
        Console.WriteLine($"║  POS 設備數量:{PosCount} 台         ║"); // POS 數量
        Console.WriteLine($"║  預估總成本:NT$ {EstimatedCost:N0}  ║"); // 預估成本

        foreach (var device in Devices) // 逐一列出設備
        {
            Console.WriteLine($"║  {device.Hostname}: {device.Location,-20}║"); // 設備名稱和位置
        }

        Console.WriteLine("╚══════════════════════════════════════╝"); // 報告底框
    }
}

🤔 我這樣寫為什麼會錯?

❌ 錯誤 1:把密碼和金鑰寫死在程式碼裡

// ❌ 錯誤:密碼和金鑰寫死在原始碼 // 推上 GitHub 全世界都看得到
var config = new DeploymentConfig // 部署設定
{
    ServerHost = "192.168.1.100", // Pi 的 IP
    Username = "admin",           // ❌ 帳號寫在程式碼裡
    SshKeyPath = "/home/user/.ssh/id_rsa" // ❌ 金鑰路徑寫在程式碼裡
}; // 這些資訊推上 Git 就洩漏了

// ✅ 正確:使用環境變數或 Secrets // 永遠不把敏感資訊放在程式碼裡
var secureConfig = new DeploymentConfig // 安全的部署設定
{
    ServerHost = Environment.GetEnvironmentVariable("PI_HOST") ?? "", // 從環境變數讀取 // 不會進版控
    Username = Environment.GetEnvironmentVariable("PI_USER") ?? "",  // 從環境變數讀取
    SshKeyPath = Environment.GetEnvironmentVariable("PI_SSH_KEY") ?? "" // 從環境變數讀取
}; // 敏感資訊不會出現在 Git 歷史中

❌ 錯誤 2:Docker 容器直接用 root 執行

# ❌ 錯誤:用 root 身份執行應用程式 // 安全隱患
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY --from=build /app/publish .
# 沒有切換使用者 // 預設就是 root // 如果應用程式被入侵,攻擊者就有 root 權限
ENTRYPOINT ["dotnet", "PosApp.dll"]

# ✅ 正確:建立專用使用者 // 最小權限原則
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY --from=build /app/publish .
# 建立非 root 使用者 // 專用的應用程式帳號
RUN adduser --disabled-password --gecos '' appuser
# 切換到非 root 使用者 // 降低安全風險
USER appuser
ENTRYPOINT ["dotnet", "PosApp.dll"]

❌ 錯誤 3:同步時沒有處理衝突

// ❌ 錯誤:同步時直接覆蓋 // 離線期間的修改會被蓋掉
public async Task SyncNaive(string jsonData) // 天真的同步方法
{
    // 直接 POST 到伺服器 // 不管伺服器上的資料有沒有被別人改過
    Console.WriteLine("直接覆蓋伺服器資料..."); // 暴力覆蓋 // 可能遺失其他 POS 機的交易
}

// ✅ 正確:使用時間戳和衝突偵測 // 確保不會遺失資料
public async Task SyncWithConflictCheck(SyncItem item) // 有衝突檢查的同步方法
{
    Console.WriteLine($"檢查衝突:{item.Type} ({item.Id[..8]})"); // 顯示檢查訊息

    // 1. 先查詢伺服器上的最新版本 // 比較時間戳
    Console.WriteLine("  查詢伺服器版本..."); // 取得伺服器資料

    // 2. 如果伺服器版本比本地舊,直接同步 // 本地是最新的
    // 3. 如果伺服器版本比本地新,需要合併 // 有人在我離線時改了資料
    // 4. 對於訂單資料,通常用「追加」而非「覆蓋」// 新訂單直接加入
    Console.WriteLine("  使用追加模式同步訂單資料"); // 訂單只增不改 // 最安全的方式

    item.IsSynced = true; // 標記為已同步
    Console.WriteLine("  ✅ 同步完成(無衝突)"); // 顯示完成
}

💡 大家的想法 · 0

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