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

實戰:DevLearn 學習平台開發全紀錄

開發者:Mike(邱瀚賢)的 Side Project 一個人從零到一,打造完整的 .NET 學習平台


📌 專案簡介

DevLearn 是一個完整的 .NET 學習平台,具備 互動式教學、遊戲化學習、老師媒合、即時聊天 等功能。 這不是教科書範例,而是一個真正上線、持續迭代的 Side Project。

技術棧總覽

前端:Razor Views + Vanilla JS + Prism.js 語法高亮 // 前端技術組合
後端:ASP.NET Core 8 MVC + EF Core 8               // 後端框架
資料庫:PostgreSQL(Railway / Azure 雲端)           // 資料庫選擇
即時通訊:SignalR(WebSocket / Long Polling)        // 即時功能
部署:Railway + Azure App Service 雙平台             // 部署方式
AI 助手:Claude Code(開發、除錯、程式碼產生)       // AI 輔助開發

🏗️ 架構設計

資料庫設計:25 個資料表

// AppDbContext.cs — 定義所有資料表 // 資料庫上下文
public class AppDbContext : DbContext // 繼承 DbContext 基底類別
{
    // 核心學習模組 // Core learning tables
    public DbSet<Chapter> Chapters { get; set; }       // 教學章節(93+ 章)
    public DbSet<Question> Questions { get; set; }     // 測驗題目
    public DbSet<UserProgress> UserProgress { get; set; } // 使用者進度

    // 遊戲化模組 // Gamification tables
    public DbSet<CheckIn> CheckIns { get; set; }       // 每日簽到
    public DbSet<SpeedRecord> SpeedRecords { get; set; } // 速度挑戰紀錄
    public DbSet<CodePuzzle> CodePuzzles { get; set; } // 填字遊戲
    public DbSet<BugChallenge> BugChallenges { get; set; } // 程式碼偵探

    // 老師媒合模組 // Teacher matching tables
    public DbSet<Teacher> Teachers { get; set; }       // 老師資料
    public DbSet<TeacherSlot> TeacherSlots { get; set; } // 老師可預約時段
    public DbSet<Booking> Bookings { get; set; }       // 預約紀錄
    public DbSet<TeacherReview> TeacherReviews { get; set; } // 學生評價

    // 社群與即時通訊 // Community & chat tables
    public DbSet<ChatMessage> ChatMessages { get; set; } // 公開聊天訊息
    public DbSet<PrivateMessage> PrivateMessages { get; set; } // 私人訊息
    public DbSet<SupportTicket> SupportTickets { get; set; } // 客服工單

    // 使用者與權限 // User & permission tables
    public DbSet<SiteUser> SiteUsers { get; set; }     // 使用者帳號
    public DbSet<AuditLog> AuditLogs { get; set; }     // 操作審計日誌
    public DbSet<ErrorLog> ErrorLogs { get; set; }     // 錯誤日誌
}

4 種角色 RBAC 權限

// 角色權限對照表 // Role-based access control
public enum UserRole // 定義使用者角色列舉
{
    Guest,   // 訪客:可瀏覽章節、公開聊天 // 最低權限
    Member,  // 會員:可測驗、簽到、收藏、私訊 // 基本權限
    Teacher, // 老師:可管理時段、回覆預約 // 教學權限
    Admin    // 管理員:完整後台 CRUD + 審核 // 最高權限
}

// Middleware 判斷角色 // 角色驗證中介軟體
public class RoleMiddleware // 自訂中介軟體類別
{
    public async Task InvokeAsync(HttpContext context) // 處理每個請求
    {
        var user = await GetCurrentUser(context); // 取得當前使用者
        context.Items["CurrentUser"] = user;     // 存入 HttpContext
        if (user?.IsBanned == true)                // 檢查是否被封鎖
        {
            context.Response.StatusCode = 403;     // 回傳 403 禁止存取
            return;                                // 中斷請求
        }
        await _next(context);                      // 繼續下一個中介軟體
    }
}

雙平台部署架構

開發環境 (localhost:5001)
    │
    ├── Railway 部署(初期)     // 免費方案,自動 CI/CD
    │     └── PostgreSQL on Railway // 雲端資料庫
    │
    └── Azure App Service(正式)// 穩定、可擴展
          └── Azure PostgreSQL    // 正式環境資料庫

部署流程:
  dotnet publish -c Release -o ./publish  // 編譯發佈版本
  cd publish && zip -r ../app.zip .       // 打包成 zip
  az webapp deploy --name devlearn        // 部署到 Azure
      --resource-group myRG               // 指定資源群組
      --src-path ../app.zip               // 指定 zip 路徑

🎯 核心功能詳解

1. 動態章節系統(93+ 章,14 個分類)

// SeedData.cs — 從多個檔案載入章節 // 種子資料載入
public static void Initialize(AppDbContext db) // 初始化資料庫
{
    var chapters = new List<Chapter>();             // 建立章節清單
    chapters.AddRange(SeedChapters_CSharpBase.GetChapters()); // C# 基礎
    chapters.AddRange(SeedChapters_AspNet.GetChapters());     // ASP.NET
    chapters.AddRange(SeedChapters_Database.GetChapters());   // 資料庫
    chapters.AddRange(SeedChapters_Docker.GetChapters());     // Docker
    chapters.AddRange(SeedChapters_AI.GetChapters());         // AI 應用
    // ... 共 20+ 個分類檔案 // 每個檔案包含多個章節
    db.Chapters.AddRange(chapters);                // 批次寫入資料庫
    db.SaveChanges();                              // 儲存變更
}

// 每個章節的資料結構 // Chapter model
public class Chapter // 章節模型
{
    public int Id { get; set; }          // 章節編號(唯一)
    public string Category { get; set; } // 分類(csharp, aspnet, docker...)
    public int Order { get; set; }       // 排序順序
    public string Level { get; set; }    // 難度(beginner/intermediate/advanced)
    public string Icon { get; set; }     // 圖示 emoji
    public string Title { get; set; }    // 章節標題
    public string Content { get; set; }  // Markdown 內容
    public bool IsPublished { get; set; } // 是否已發佈
}

2. 遊戲化學習(5 種機制)

// 每日簽到系統 // Daily check-in system
[HttpPost] // 處理 POST 請求
public async Task<IActionResult> DoCheckIn() // 執行簽到
{
    var user = await GetOrCreateUser();     // 取得或建立使用者
    var today = DateTime.UtcNow.Date;       // 取得今天日期(UTC)
    var existing = await _db.CheckIns       // 查詢今日是否已簽到
        .FirstOrDefaultAsync(c => c.UserId == user.Id // 比對使用者
            && c.CheckInDate == today);     // 比對日期

    if (existing != null)                   // 如果已經簽到過
        return Json(new { success = false, message = "今日已簽到" }); // 回傳提示

    var streak = await CalcStreak(user.Id); // 計算連續簽到天數
    var bonus = streak >= 7 ? 20 : (streak >= 3 ? 10 : 5); // 連續獎勵加成

    _db.CheckIns.Add(new CheckIn           // 新增簽到紀錄
    {
        UserId = user.Id,                   // 使用者 ID
        CheckInDate = today,                // 簽到日期
        StreakDays = streak + 1,            // 連續天數 +1
        BonusPoints = bonus                 // 獎勵分數
    });
    user.TotalScore += bonus;               // 更新總分
    await _db.SaveChangesAsync();           // 儲存到資料庫
    return Json(new { success = true, streak = streak + 1, bonus }); // 回傳結果
}

// 速度挑戰 // Speed challenge
// 程式碼填字遊戲 // Code puzzle
// 每日一題 // Daily quiz
// 程式碼偵探(找 Bug)// Bug detective

3. 老師媒合系統

// 老師申請流程 // Teacher application process
[HttpPost] // 處理老師申請
public async Task<IActionResult> Apply(TeacherApplication model) // 提交申請
{
    var user = await GetCurrentUser();         // 取得當前使用者
    if (!user.IsRegistered)                    // 檢查是否已註冊
        return Json(new { success = false, message = "請先註冊" }); // 未註冊不可申請

    var teacher = new Teacher                  // 建立老師資料
    {
        SiteUserId = user.Id,                  // 關聯使用者 ID
        DisplayName = model.DisplayName,       // 顯示名稱
        Expertise = model.Expertise,           // 專業領域
        Introduction = model.Introduction,     // 自我介紹
        HourlyRate = model.HourlyRate,         // 時薪
        IsApproved = false                     // 預設未審核
    };
    _db.Teachers.Add(teacher);                 // 新增到資料庫
    await _db.SaveChangesAsync();              // 儲存
    return Json(new { success = true });       // 回傳成功
}

// 預約流程:學生選時段 → 送出預約 → 老師確認 → 上課 → 評價
// Booking flow: select slot → book → teacher confirm → class → review

4. 即時聊天(SignalR 三分頁)

// ChatHub.cs — SignalR 即時通訊中樞 // Real-time chat hub
public class ChatHub : Hub // 繼承 SignalR Hub
{
    public async Task SendMessage(           // 發送訊息方法
        string nickname,                     // 暱稱
        string message,                      // 訊息內容
        string avatarEmoji)                  // 頭像 emoji
    {
        var time = DateTime.Now.ToString("HH:mm"); // 格式化時間
        await Clients.All.SendAsync(         // 廣播給所有連線用戶
            "ReceiveMessage",               // 事件名稱
            nickname, message,               // 暱稱和訊息
            avatarEmoji, time);              // 頭像和時間
        await SaveToDb(nickname, message, avatarEmoji); // 存入資料庫
    }

    public async Task GetRecentMessages()    // 取得歷史訊息
    {
        var msgs = await _db.ChatMessages    // 查詢聊天訊息
            .OrderByDescending(m => m.SentAt) // 按時間倒序
            .Take(50)                        // 取最近 50 筆
            .OrderBy(m => m.SentAt)          // 正序排列
            .ToListAsync();                  // 執行查詢
        await Clients.Caller.SendAsync("LoadHistory", msgs); // 回傳給請求者
    }

    // 三分頁:公開聊天 / 私人訊息 / 客服工單
    // Three tabs: Public chat / Private messages / Support tickets
}

5. 角色權限系統(RBAC + 封鎖)

// AdminController.cs — 管理員操作 // Admin dashboard
[HttpPost] // 封鎖使用者
public async Task<IActionResult> BanUser(int userId, string reason) // 封鎖帳號
{
    var user = await _db.SiteUsers.FindAsync(userId); // 查詢使用者
    if (user == null) return NotFound();     // 找不到回傳 404
    user.IsBanned = true;                    // 設定封鎖狀態
    user.BanReason = reason;                 // 記錄封鎖原因
    user.BannedAt = DateTime.UtcNow;         // 記錄封鎖時間

    _db.AuditLogs.Add(new AuditLog           // 寫入審計日誌
    {
        Action = "BanUser",                 // 操作類型
        TargetId = userId.ToString(),        // 目標使用者
        Detail = reason,                     // 詳細原因
        CreatedAt = DateTime.UtcNow          // 操作時間
    });
    await _db.SaveChangesAsync();            // 儲存變更
    return Json(new { success = true });     // 回傳成功
}

6. 自動錯誤偵測(ErrorLog + AI 修復排程)

// 前端自動回報錯誤 // Frontend auto error reporting
// _Layout.cshtml 中的全方位錯誤偵測腳本 // Error detection script
window.onerror = function(msg, url, line, col, err) { // 攔截 JS 錯誤
    fetch('/api/ErrorLog/report', {          // 呼叫後端 API
        method: 'POST',                      // 使用 POST 方法
        headers: {'Content-Type': 'application/json'}, // JSON 格式
        body: JSON.stringify({               // 組裝錯誤資料
            pageUrl: location.href,          // 發生錯誤的頁面
            errorMessage: msg,               // 錯誤訊息
            stackTrace: err?.stack || '',    // 堆疊追蹤
            source: 'frontend-js'            // 錯誤來源
        })
    });
};

// 後端 ErrorLog 控制器 // Backend error log controller
// 偵測範圍:JS 錯誤、Promise rejection、API 500、
//          資源載入失敗、慢速頁面、Console.error、
//          手機端觸控凍結、SignalR 斷線
// 共 8 大類自動偵測 // 8 categories of auto-detection

7. 管理後台(20+ 頁面)

// 管理後台功能清單 // Admin dashboard features
public class AdminController : Controller   // 管理員控制器
{
    // 儀表板:即時統計 // Dashboard: real-time stats
    // 使用者管理:列表、封鎖、角色切換 // User management
    // 章節管理:新增、編輯、發佈 // Chapter management
    // 老師審核:申請審核、資料管理 // Teacher approval
    // 預約管理:查看、處理預約 // Booking management
    // 客服工單:回覆、關閉工單 // Support tickets
    // 錯誤日誌:查看、標記修復 // Error logs
    // 審計日誌:所有操作紀錄 // Audit logs
    // AI 工作紀錄:Claude 修復歷史 // AI work history

    [HttpGet] // 儀表板首頁
    public async Task<IActionResult> Dashboard() // 載入儀表板
    {
        var stats = new DashboardStats       // 建立統計物件
        {
            TotalUsers = await _db.SiteUsers.CountAsync(),     // 總使用者數
            TotalChapters = await _db.Chapters.CountAsync(),   // 總章節數
            TodayCheckIns = await _db.CheckIns                 // 今日簽到數
                .CountAsync(c => c.CheckInDate == DateTime.UtcNow.Date), // 比對日期
            OpenTickets = await _db.SupportTickets             // 未處理工單
                .CountAsync(t => t.Status == "pending"),      // 狀態為待處理
            RecentErrors = await _db.ErrorLogs                 // 近期錯誤
                .OrderByDescending(e => e.CreatedAt)           // 按時間排序
                .Take(10).ToListAsync()                        // 取最近 10 筆
        };
        return View(stats);                  // 回傳檢視
    }
}

🚀 部署流程

# 1. 本地編譯發佈版本 // Build release version
dotnet publish -c Release -o ./publish     # 編譯到 publish 資料夾

# 2. 打包成 zip 檔 // Package as zip
cd publish && zip -r ../app.zip .          # 壓縮所有檔案

# 3. 部署到 Azure // Deploy to Azure App Service
az webapp deploy \                          # 使用 Azure CLI 部署
    --name devlearn-app \                   # 應用程式名稱
    --resource-group myResourceGroup \      # 資源群組名稱
    --src-path ../app.zip \                 # zip 檔路徑
    --type zip                              # 部署類型

# 4. 設定環境變數 // Configure environment variables
az webapp config appsettings set \          # 設定應用程式設定
    --name devlearn-app \                   # 應用程式名稱
    --settings \                            # 設定項目
    ConnectionStrings__Default="Host=..."  # 資料庫連線字串

# 5. 檢查部署狀態 // Check deployment status
az webapp log tail --name devlearn-app      # 即時查看日誌

🔥 遇到的挑戰與解決方案

挑戰 1:Railway 部署超時 → 改用 Azure

// 問題:Railway 免費方案有 500 小時/月限制 // Railway timeout issue
// 部署後經常 cold start 要 30+ 秒 // Cold start takes 30+ seconds
// 大量 Seed Data 初始化導致首次啟動更慢 // Seed data slows first boot

// 解決方案:遷移到 Azure App Service // Migrate to Azure
// Azure 的 Always On 功能避免 cold start // Always On prevents cold start
var builder = WebApplication.CreateBuilder(args); // 建立應用程式
builder.Services.AddResponseCompression();  // 加入回應壓縮
builder.Services.AddMemoryCache();          // 加入記憶體快取

// 加上健康檢查端點 // Health check endpoint
app.MapGet("/health", () => "OK");        // 讓 Azure 定期 ping

挑戰 2:SQLite 資料消失 → 改用 PostgreSQL

// 問題:Railway 使用 SQLite,每次重新部署資料庫都被覆蓋 // SQLite data loss
// 使用者的簽到紀錄、學習進度全部消失 // All user data lost on redeploy

// 解決方案:改用 PostgreSQL 雲端資料庫 // Switch to PostgreSQL
// Program.cs 中的連線設定 // Connection configuration
var connStr = builder.Configuration          // 讀取設定
    .GetConnectionString("Default");        // 取得連線字串
builder.Services.AddDbContext<AppDbContext>(  // 註冊 DbContext
    options => options.UseNpgsql(connStr));   // 使用 Npgsql 驅動

// appsettings.json 設定 // Application settings
// "Default": "Host=xxx;Database=devlearn;Username=xxx;Password=xxx"
// 資料庫在雲端,不會因重新部署而消失 // Cloud DB persists across deploys

挑戰 3:手機版 RWD 問題 → 漢堡選單 + safe-area

// 問題:導覽列在手機上擠成一團,聊天室輸入框被虛擬鍵盤遮住 // Mobile layout issues

// 解決方案 1:漢堡選單 // Hamburger menu solution
// CSS:隱藏桌面導覽,顯示漢堡按鈕 // Hide desktop nav, show hamburger
@@media (max-width: 768px) {               // 手機版媒體查詢
    .nav-right { display: none; }          // 隱藏桌面選單
    .hamburger { display: flex; }          // 顯示漢堡按鈕
}

// 解決方案 2:safe-area 處理 // Safe area for notch phones
.chat-input-area {                         // 聊天輸入區域
    padding-bottom: calc(                  // 計算底部間距
        10px + env(safe-area-inset-bottom, 0px) // 加上安全區域
    );
}
// 問題:使用者登入後 Session 過期,但 Cookie 還在 // Session expired but cookie exists
// 導致使用者以為自己還在登入狀態 // User thinks they're still logged in

// 解決方案:Cookie-first 中介軟體 // Cookie-first middleware
public class CookieFirstMiddleware          // 自訂中介軟體
{
    public async Task InvokeAsync(HttpContext context) // 處理請求
    {
        var sessionId = context.Session      // 嘗試從 Session 取得
            .GetString("SessionId");        // 使用者識別碼
        var cookieId = context.Request       // 嘗試從 Cookie 取得
            .Cookies["UserSession"];         // 使用者識別碼

        if (string.IsNullOrEmpty(sessionId)  // 如果 Session 為空
            && !string.IsNullOrEmpty(cookieId)) // 但 Cookie 有值
        {
            context.Session.SetString(       // 從 Cookie 恢復 Session
                "SessionId", cookieId);      // 保持使用者登入狀態
        }
        await _next(context);               // 繼續處理請求
    }
}

💡 開發心得:用 Claude Code 做 AI 開發助手

// Claude Code 在這個專案中扮演的角色 // Claude Code's role in this project

// 1. 程式碼產生:快速產出 Controller、View、Model // Code generation
// 2. 除錯協助:分析錯誤日誌,提供修復建議 // Debugging help
// 3. 架構設計:討論資料表設計、API 設計 // Architecture design
// 4. 文件撰寫:產生教學章節內容(包含這一章!)// Documentation
// 5. 程式碼審查:發現潛在 Bug 和效能問題 // Code review

// 開發效率提升要點 // Development efficiency tips
var tips = new List<string>                 // 建立心得清單
{
    "明確描述需求,AI 才能給出精確回答",   // 清楚的 prompt 很重要
    "先理解 AI 產出的程式碼,再貼入專案",   // 不要盲目複製貼上
    "利用 AI 產出測試案例,提高品質",       // 用 AI 寫測試
    "讓 AI 幫忙重構,但架構決策自己來",     // AI 是助手不是主導
    "定期回顧 AI 建議,學習新的寫法"        // 從 AI 身上學習
};

// 最終心得:AI 不會取代開發者 // Final thought
// 但會用 AI 的開發者,會取代不會用的 // AI-skilled devs will outpace others
// DevLearn 就是最好的證明 🚀 // DevLearn is living proof

🤔 我這樣寫為什麼會錯?

常見錯誤 1:部署時忘記設定環境變數

// ❌ 錯誤寫法:把連線字串寫死在程式碼裡 // Hardcoded connection string
var connStr = "Host=localhost;Database=dev;Password=123"; // 寫死的連線字串
// 部署到雲端後還是連 localhost,當然連不上! // Will fail on cloud deployment

// ✅ 正確寫法:從環境變數或設定檔讀取 // Read from config
var connStr = builder.Configuration          // 從設定中讀取
    .GetConnectionString("Default");        // 取得連線字串
// 雲端透過環境變數覆寫 // Override via environment variables on cloud

常見錯誤 2:SignalR 在手機上連不上

// ❌ 錯誤寫法:只用 WebSocket // WebSocket only
var connection = new signalR.HubConnectionBuilder() // 建立 SignalR 連線
    .withUrl('/chathub')                     // 只用預設 WebSocket
    .build();                                // 手機瀏覽器可能不支援

// ✅ 正確寫法:加上 Long Polling 作為備援 // Add Long Polling fallback
var connection = new signalR.HubConnectionBuilder() // 建立 SignalR 連線
    .withUrl('/chathub', {                   // 指定傳輸方式
        transport: signalR.HttpTransportType.LongPolling // 使用 Long Polling
    })
    .withAutomaticReconnect()                // 加上自動重連
    .build();                                // 確保手機端也能連線

常見錯誤 3:忘記處理 null 值

// ❌ 錯誤寫法:直接存取可能為 null 的物件 // Accessing nullable without check
var nickname = user.Nickname;                // 如果 user 是 null 就炸了

// ✅ 正確寫法:使用 null 條件運算子 // Use null conditional operator
var nickname = user?.Nickname ?? "訪客";    // 如果 null 就用預設值
// 或者先檢查 // Or check first
if (user == null)                            // 先判斷是否為 null
    return Json(new { error = "未登入" });   // 回傳錯誤訊息

📊 專案數據總結

總程式碼行數:   50,000+ 行          // Total lines of code
教學章節:       93+ 章(14 分類)   // Teaching chapters
資料表:         25 個               // Database tables
Controller:     15+ 個              // API controllers
View 頁面:      30+ 個              // Razor views
管理後台頁面:   20+ 個              // Admin pages
遊戲化機制:     5 種                // Gamification types
即時功能:       聊天 + 私訊 + 客服  // Real-time features
AI 輔助修復:    自動偵測 + 排程修復 // Auto error detection
部署方式:       Azure App Service   // Deployment platform
開發期間:       持續迭代中 🔄       // Ongoing development

這個專案證明了:一個人 + AI 助手,也能打造出完整的全端應用。 重點不是程式碼多完美,而是不斷迭代、持續改進。

💡 大家的想法 · 0

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