dramaling-vocab-learning/AI生成功能前後端串接規格.md

31 KiB
Raw Blame History

AI生成功能前後端串接規格

📋 文件資訊

  • 文件名稱: AI生成功能前後端串接規格
  • 版本: v1.0
  • 建立日期: 2025-01-25
  • 最後更新: 2025-01-25
  • 負責團隊: DramaLing全端開發團隊

🎯 串接架構概述

系統架構圖

┌─────────────────┐    HTTP/JSON     ┌──────────────────┐    Gemini API    ┌─────────────────┐
│                 │    Request       │                  │    Request       │                 │
│  Frontend       │ ──────────────► │  Backend API     │ ──────────────► │  Google Gemini  │
│  (Next.js)      │                 │  (.NET Core)     │                 │  AI Service     │
│  Port 3000      │ ◄────────────── │  Port 5008       │ ◄────────────── │                 │
│                 │    Response      │                  │    Response      │                 │
└─────────────────┘                 └──────────────────┘                 └─────────────────┘
        │                                      │
        │                                      │
        ▼                                      ▼
┌─────────────────┐                 ┌──────────────────┐
│  Local Storage  │                 │  SQLite Database │
│  - auth_token   │                 │  - Cache         │
│  - user_level   │                 │  - Usage Stats   │
└─────────────────┘                 └──────────────────┘

🔄 API串接流程

完整用戶操作流程

sequenceDiagram
    participant U as 用戶
    participant F as 前端(3000)
    participant B as 後端(5008)
    participant G as Gemini API
    participant C as Cache
    participant D as Database

    U->>F: 1. 輸入英文句子
    U->>F: 2. 點擊「分析句子」

    F->>F: 3. 驗證輸入(≤300字符)
    F->>F: 4. 讀取userLevel

    F->>B: 5. POST /api/ai/analyze-sentence
    Note over F,B: Content-Type: application/json<br/>Body: {inputText, userLevel, options}

    B->>B: 6. 輸入驗證
    B->>C: 7. 檢查快取

    alt 快取命中
        C->>B: 8a. 返回快取結果
        B->>F: 9a. 返回分析結果
    else 快取未命中
        B->>G: 8b. 調用Gemini API
        G->>B: 9b. AI分析結果
        B->>B: 10. 解析和處理
        B->>C: 11. 儲存快取
        B->>D: 12. 記錄使用統計
        B->>F: 13. 返回分析結果
    end

    F->>F: 14. 處理API回應
    F->>F: 15. 渲染詞彙標記
    F->>U: 16. 顯示分析結果

    U->>F: 17. 點擊詞彙
    F->>F: 18. 顯示詞彙彈窗

    U->>F: 19. 保存詞卡
    F->>B: 20. POST /api/flashcards
    B->>D: 21. 儲存詞卡
    B->>F: 22. 返回成功狀態
    F->>U: 23. 顯示成功提示

📡 API端點詳細串接

1. 句子分析API

前端請求實現

// 位置: frontend/app/generate/page.tsx:65-87
const handleAnalyzeSentence = async () => {
  const userLevel = localStorage.getItem('userEnglishLevel') || 'A2'

  const response = await fetch('http://localhost:5008/api/ai/analyze-sentence', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      inputText: textInput,
      userLevel: userLevel,
      analysisMode: 'full',
      options: {
        includeGrammarCheck: true,
        includeVocabularyAnalysis: true,
        includeTranslation: true,
        includePhraseDetection: true,
        includeExamples: true
      }
    })
  })

  const result = await response.json()
  // 處理API回應...
}

後端處理實現

// 位置: backend/Controllers/AIController.cs:32-105
[HttpPost("analyze-sentence")]
public async Task<ActionResult<SentenceAnalysisResponse>> AnalyzeSentence(
    [FromBody] SentenceAnalysisRequest request)
{
    // 1. 輸入驗證
    if (!ModelState.IsValid) return BadRequest(...)

    // 2. 檢查快取
    var cachedResult = await _cacheService.GetCachedAnalysisAsync(request.InputText);
    if (cachedResult != null) return Ok(...)

    // 3. 調用Gemini AI
    var analysisData = await _geminiService.AnalyzeSentenceAsync(
        request.InputText, request.UserLevel, options);

    // 4. 快取結果
    await _cacheService.SetCachedAnalysisAsync(request.InputText, analysisData, TimeSpan.FromHours(24));

    // 5. 返回結果
    return Ok(new SentenceAnalysisResponse { Success = true, Data = analysisData });
}

💾 數據格式串接

前端到後端請求格式

SentenceAnalysisRequest

{
  "inputText": "She just join the team, so let's cut her some slack until she get used to the workflow.",
  "userLevel": "A2",
  "analysisMode": "full",
  "options": {
    "includeGrammarCheck": true,
    "includeVocabularyAnalysis": true,
    "includeTranslation": true,
    "includePhraseDetection": true,
    "includeExamples": true
  }
}

後端到前端回應格式

SentenceAnalysisResponse

{
  "success": true,
  "processingTime": 2.34,
  "data": {
    "analysisId": "uuid-string",
    "originalText": "She just join the team, so let's cut her some slack until she get used to the workflow.",
    "grammarCorrection": {
      "hasErrors": true,
      "correctedText": "She just joined the team, so let's cut her some slack until she gets used to the workflow.",
      "corrections": [
        {
          "error": "join",
          "correction": "joined",
          "type": "時態錯誤",
          "explanation": "第三人稱單數過去式應使用 'joined'"
        },
        {
          "error": "get",
          "correction": "gets",
          "type": "時態錯誤",
          "explanation": "第三人稱單數現在式應使用 'gets'"
        }
      ]
    },
    "sentenceMeaning": "她剛加入團隊,所以讓我們對她寬容一點,直到她習慣工作流程。",
    "vocabularyAnalysis": {
      "she": {
        "word": "she",
        "translation": "她",
        "definition": "female person pronoun",
        "partOfSpeech": "pronoun",
        "pronunciation": "/ʃiː/",
        "difficultyLevel": "A1",
        "isPhrase": false,
        "frequency": "very_high",
        "synonyms": ["her"],
        "example": "She is a teacher.",
        "exampleTranslation": "她是一名老師。",
        "tags": ["basic", "pronoun"]
      },
      "cut someone some slack": {
        "word": "cut someone some slack",
        "translation": "對某人寬容一點",
        "definition": "to be more lenient or forgiving with someone",
        "partOfSpeech": "idiom",
        "pronunciation": "/kʌt ˈsʌmwʌn sʌm slæk/",
        "difficultyLevel": "B2",
        "isPhrase": true,
        "frequency": "medium",
        "synonyms": ["be lenient", "be forgiving", "give leeway"],
        "example": "Cut him some slack, he's new here.",
        "exampleTranslation": "對他寬容一點,他是新來的。",
        "tags": ["idiom", "workplace", "tolerance"]
      }
    },
    "statistics": {
      "totalWords": 16,
      "uniqueWords": 15,
      "simpleWords": 8,
      "moderateWords": 4,
      "difficultWords": 3,
      "phrases": 1,
      "averageDifficulty": "A2"
    },
    "metadata": {
      "analysisModel": "gemini-pro",
      "analysisVersion": "1.0",
      "processingDate": "2025-01-25T10:30:00Z",
      "userLevel": "A2"
    }
  }
}

🔧 前端處理邏輯

API回應處理

數據解析和狀態更新

// 位置: frontend/app/generate/page.tsx:101-124
const apiData = result.data

// 設定分析結果
setSentenceAnalysis(apiData.vocabularyAnalysis || {})
setSentenceMeaning(apiData.sentenceMeaning || '')

// 處理語法修正
if (apiData.grammarCorrection) {
  setGrammarCorrection({
    hasErrors: apiData.grammarCorrection.hasErrors,
    originalText: textInput,
    correctedText: apiData.grammarCorrection.correctedText || textInput,
    corrections: apiData.grammarCorrection.corrections || []
  })

  // 使用修正後的文本作為最終文本
  setFinalText(apiData.grammarCorrection.correctedText || textInput)
} else {
  setFinalText(textInput)
}

setShowAnalysisView(true)

詞彙標記渲染

CEFR等級比較邏輯

// 位置: frontend/components/ClickableTextV2.tsx:107-129
const getWordClass = useCallback((word: string) => {
  const wordAnalysis = findWordAnalysis(word)
  const baseClass = "cursor-pointer transition-all duration-200 rounded relative mx-0.5 px-1 py-0.5"

  if (!wordAnalysis) return ""

  const isPhrase = getWordProperty(wordAnalysis, 'isPhrase')
  if (isPhrase) return ""

  const difficultyLevel = getWordProperty(wordAnalysis, 'difficultyLevel') || 'A1'
  const userLevel = typeof window !== 'undefined' ? localStorage.getItem('userEnglishLevel') || 'A2' : 'A2'

  const userIndex = getLevelIndex(userLevel)
  const wordIndex = getLevelIndex(difficultyLevel)

  if (userIndex > wordIndex) {
    return `${baseClass} bg-gray-50 border border-dashed border-gray-300 hover:bg-gray-100 hover:border-gray-400 text-gray-600 opacity-80`
  } else if (userIndex === wordIndex) {
    return `${baseClass} bg-green-50 border border-green-200 hover:bg-green-100 hover:shadow-lg transform hover:-translate-y-0.5 text-green-700 font-medium`
  } else {
    return `${baseClass} bg-orange-50 border border-orange-200 hover:bg-orange-100 hover:shadow-lg transform hover:-translate-y-0.5 text-orange-700 font-medium`
  }
}, [findWordAnalysis, getWordProperty, getLevelIndex])

統計卡片計算

詞彙分類統計

// 位置: frontend/app/generate/page.tsx:353-383
const vocabularyStats = useMemo(() => {
  if (!sentenceAnalysis) return null

  const userLevel = localStorage.getItem('userEnglishLevel') || 'A2'
  let simpleCount = 0, moderateCount = 0, difficultCount = 0, phraseCount = 0

  Object.entries(sentenceAnalysis).forEach(([, wordData]: [string, any]) => {
    const isPhrase = wordData?.isPhrase || wordData?.IsPhrase
    const difficultyLevel = wordData?.difficultyLevel || 'A1'

    if (isPhrase) {
      phraseCount++
    } else {
      const userIndex = getLevelIndex(userLevel)
      const wordIndex = getLevelIndex(difficultyLevel)

      if (userIndex > wordIndex) {
        simpleCount++
      } else if (userIndex === wordIndex) {
        moderateCount++
      } else {
        difficultCount++
      }
    }
  })

  return { simpleCount, moderateCount, difficultCount, phraseCount }
}, [sentenceAnalysis])

🏗️ 後端處理架構

Gemini API整合

AI分析服務實現

// 位置: backend/Services/GeminiService.cs:33-56
public async Task<SentenceAnalysisData> AnalyzeSentenceAsync(string inputText, string userLevel, AnalysisOptions options)
{
    var startTime = DateTime.UtcNow;

    try
    {
        _logger.LogInformation("Starting sentence analysis for text: {Text}, UserLevel: {UserLevel}",
            inputText.Substring(0, Math.Min(50, inputText.Length)), userLevel);

        var prompt = BuildAnalysisPrompt(inputText, userLevel, options);
        var response = await CallGeminiAPI(prompt);
        var analysisData = ParseGeminiResponse(response, inputText, userLevel);

        var processingTime = (DateTime.UtcNow - startTime).TotalSeconds;
        analysisData.Metadata.ProcessingDate = DateTime.UtcNow;

        _logger.LogInformation("Sentence analysis completed in {ProcessingTime}s", processingTime);

        return analysisData;
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Error analyzing sentence: {Text}", inputText);
        throw;
    }
}

智能Prompt構建

// 位置: backend/Services/GeminiService.cs:58-105
private string BuildAnalysisPrompt(string inputText, string userLevel, AnalysisOptions options)
{
    var userIndex = Array.IndexOf(_cefrLevels, userLevel);
    var targetLevels = GetTargetLevels(userIndex);

    return $@"
請分析以下英文句子並以JSON格式回應
句子: ""{inputText}""
學習者程度: {userLevel}

請提供完整的分析,包含:

1. 語法檢查:檢查是否有語法錯誤,如有則提供修正建議
2. 詞彙分析:分析句子中每個有意義的詞彙
3. 中文翻譯:提供自然流暢的繁體中文翻譯
4. 慣用語識別:識別句子中的慣用語和片語

詞彙分析要求:
- 為每個詞彙標註CEFR等級 (A1-C2)
- 如果是慣用語,設置 isPhrase: true
- 提供IPA發音標記
- 包含同義詞
- 提供適當的例句和翻譯

重要回應必須是有效的JSON格式不要包含任何其他文字。";
}

緩存機制整合

分析結果緩存

// 位置: backend/Services/AnalysisCacheService.cs:33-60
public async Task<SentenceAnalysisCache?> GetCachedAnalysisAsync(string inputText)
{
    try
    {
        var textHash = GenerateTextHash(inputText);
        var cached = await _context.SentenceAnalysisCache
            .FirstOrDefaultAsync(c => c.InputTextHash == textHash && c.ExpiresAt > DateTime.UtcNow);

        if (cached != null)
        {
            // 更新訪問統計
            cached.AccessCount++;
            cached.LastAccessedAt = DateTime.UtcNow;
            await _context.SaveChangesAsync();

            _logger.LogInformation("Cache hit for text hash: {TextHash}", textHash);
            return cached;
        }

        _logger.LogInformation("Cache miss for text hash: {TextHash}", textHash);
        return null;
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Error getting cached analysis for text: {InputText}", inputText);
        return null;
    }
}

🎨 前端UI串接

詞彙標記渲染

ClickableTextV2組件

// 位置: frontend/components/ClickableTextV2.tsx:281-299
return (
  <div className="relative">
    <div className="text-lg" style={{lineHeight: '2.5'}}>
      {words.map((word, index) => {
        if (word.trim() === '' || /^[.,!?;:\s]+$/.test(word)) {
          return <span key={index}>{word}</span>
        }

        const className = getWordClass(word)
        const icon = getWordIcon(word)

        return (
          <span
            key={index}
            className={className}
            onClick={(e) => handleWordClick(word, e)}
          >
            {word}
            {icon}
          </span>
        )
      })}
    </div>
    <VocabPopup />
  </div>
)

統計卡片展示

四張統計卡片實現

// 位置: frontend/app/generate/page.tsx:581-605
{vocabularyStats && (
  <div className="grid grid-cols-2 sm:grid-cols-4 gap-3 sm:gap-4 mb-6">
    {/* 簡單詞彙卡片 */}
    <div className="bg-gray-50 border border-dashed border-gray-300 rounded-lg p-3 sm:p-4 text-center">
      <div className="text-xl sm:text-2xl font-bold text-gray-600 mb-1">{vocabularyStats.simpleCount}</div>
      <div className="text-gray-600 text-xs sm:text-sm font-medium">太簡單啦</div>
    </div>

    {/* 適中詞彙卡片 */}
    <div className="bg-green-50 border border-green-200 rounded-lg p-3 sm:p-4 text-center">
      <div className="text-xl sm:text-2xl font-bold text-green-700 mb-1">{vocabularyStats.moderateCount}</div>
      <div className="text-green-700 text-xs sm:text-sm font-medium">重點學習</div>
    </div>

    {/* 艱難詞彙卡片 */}
    <div className="bg-orange-50 border border-orange-200 rounded-lg p-3 sm:p-4 text-center">
      <div className="text-xl sm:text-2xl font-bold text-orange-700 mb-1">{vocabularyStats.difficultCount}</div>
      <div className="text-orange-700 text-xs sm:text-sm font-medium">有點挑戰</div>
    </div>

    {/* 慣用語卡片 */}
    <div className="bg-blue-50 border border-blue-200 rounded-lg p-3 sm:p-4 text-center">
      <div className="text-xl sm:text-2xl font-bold text-blue-700 mb-1">{vocabularyStats.phraseCount}</div>
      <div className="text-blue-700 text-xs sm:text-sm font-medium">慣用語</div>
    </div>
  </div>
)}

🔒 認證與安全串接

JWT Token處理

前端Token管理

// 位置: frontend/lib/services/auth.ts:77-81
if (response.success && response.data?.token) {
  // Store token in localStorage
  localStorage.setItem('auth_token', response.data.token);
  localStorage.setItem('user_data', JSON.stringify(response.data.user));
}

// 位置: frontend/lib/services/flashcards.ts:72-82
private async makeRequest<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
  const token = this.getAuthToken();
  const url = `${API_BASE_URL}/api${endpoint}`;

  const response = await fetch(url, {
    headers: {
      'Content-Type': 'application/json',
      ...(token && { 'Authorization': `Bearer ${token}` }),
      ...options.headers,
    },
    ...options,
  });
}

後端認證驗證

// 位置: backend/Controllers/AIController.cs:123-135
private string GetCurrentUserId()
{
    var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier)
                     ?? User.FindFirst("sub")
                     ?? User.FindFirst("user_id");

    if (userIdClaim?.Value == null)
    {
        throw new UnauthorizedAccessException("用戶ID未找到");
    }

    return userIdClaim.Value;
}

🔄 錯誤處理串接

前端錯誤處理

API錯誤捕獲

// 位置: frontend/app/generate/page.tsx:89-99
if (!response.ok) {
  const errorData = await response.json()
  throw new Error(errorData.error?.message || `API請求失敗: ${response.status}`)
}

const result = await response.json()

if (!result.success || !result.data) {
  throw new Error('API回應格式錯誤')
}

優雅錯誤回退

// 位置: frontend/app/generate/page.tsx:125-137
} catch (error) {
  console.error('Error in sentence analysis:', error)
  setGrammarCorrection({
    hasErrors: true,
    originalText: textInput,
    correctedText: textInput,
    corrections: []
  })
  setSentenceMeaning('分析過程中發生錯誤,請稍後再試。')
  setFinalText(textInput)
  setShowAnalysisView(true)
}

後端錯誤處理

結構化錯誤回應

// 位置: backend/Controllers/AIController.cs:137-155
private ApiErrorResponse CreateErrorResponse(string code, string message, object? details, string requestId)
{
    var suggestions = GetSuggestionsForError(code);

    return new ApiErrorResponse
    {
        Success = false,
        Error = new ApiError
        {
            Code = code,
            Message = message,
            Details = details,
            Suggestions = suggestions
        },
        RequestId = requestId,
        Timestamp = DateTime.UtcNow
    };
}

🚀 性能優化串接

前端性能優化

React記憶化

// 統計計算優化
const vocabularyStats = useMemo(() => {
  // 計算邏輯...
}, [sentenceAnalysis])

// 詞彙分類函數記憶化
const getWordClass = useCallback((word: string) => {
  // 分類邏輯...
}, [findWordAnalysis, getWordProperty, getLevelIndex])

// 彈窗定位計算優化
const calculatePopupPosition = useCallback((rect: DOMRect) => {
  // 位置計算邏輯...
}, [])

後端性能優化

快取策略

// 快取鍵生成: 文本內容 + 用戶等級
var cacheKey = $"{request.InputText}_{request.UserLevel}";

// 快取過期時間: 24小時
await _cacheService.SetCachedAnalysisAsync(cacheKey, analysisData, TimeSpan.FromHours(24));

// 智能快取清理
[BackgroundService]
public class CacheCleanupService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await _cacheService.CleanExpiredCacheAsync();
            await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
        }
    }
}

🔧 配置管理串接

環境配置

前端環境設定

// API基礎URL配置
const API_BASE_URL = 'http://localhost:5008'; // 開發環境
// const API_BASE_URL = 'https://api.dramaling.com'; // 生產環境

// 字符限制配置
const MAX_MANUAL_INPUT_LENGTH = 300;

// CEFR等級配置
const CEFR_LEVELS = ['A1', 'A2', 'B1', 'B2', 'C1', 'C2'] as const;

後端配置管理

// User Secrets配置
dotnet user-secrets set "AI:GeminiApiKey" "your-real-gemini-api-key"

// appsettings.json配置
{
  "Gemini": {
    "ApiKey": "fallback-key"  // 僅作為後備
  },
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=dramaling_test.db"
  }
}

// 環境變數配置
Environment.GetEnvironmentVariable("GEMINI_API_KEY")  // 優先級最高

📊 數據流向分析

請求數據流

1. 用戶輸入 → 前端驗證 → API請求構建
2. HTTP POST → 後端接收 → 模型驗證
3. 快取檢查 → AI服務調用 → 結果處理
4. 數據序列化 → HTTP回應 → 前端接收
5. JSON解析 → 狀態更新 → UI重新渲染

回應數據流

1. Gemini API → JSON回應 → 後端解析
2. 數據轉換 → DTO映射 → 統計計算
3. 快取儲存 → 結構化回應 → 前端接收
4. 狀態分派 → 組件更新 → 詞彙標記
5. 彈窗渲染 → 統計卡片 → 用戶互動

🧪 測試串接規格

前端測試

單元測試

// 測試API調用
describe('handleAnalyzeSentence', () => {
  it('should call API with correct parameters', async () => {
    // Mock fetch
    global.fetch = jest.fn(() =>
      Promise.resolve({
        ok: true,
        json: () => Promise.resolve(mockApiResponse),
      })
    );

    await handleAnalyzeSentence();

    expect(fetch).toHaveBeenCalledWith(
      'http://localhost:5008/api/ai/analyze-sentence',
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          inputText: 'test sentence',
          userLevel: 'A2',
          analysisMode: 'full'
        })
      }
    );
  });
});

整合測試

// 測試完整流程
describe('AI Analysis Integration', () => {
  it('should complete full analysis workflow', async () => {
    // 1. 輸入文本
    fireEvent.change(screen.getByRole('textbox'), {
      target: { value: 'She just join the team' }
    });

    // 2. 點擊分析按鈕
    fireEvent.click(screen.getByText('🔍 分析句子'));

    // 3. 等待API回應
    await waitFor(() => {
      expect(screen.getByText('語法修正建議')).toBeInTheDocument();
    });

    // 4. 驗證詞彙標記
    expect(screen.getByText('she')).toHaveClass('bg-gray-50');
    expect(screen.getByText('join')).toHaveClass('bg-orange-50');
  });
});

後端測試

API端點測試

[Test]
public async Task AnalyzeSentence_WithValidInput_ReturnsSuccessResponse()
{
    // Arrange
    var request = new SentenceAnalysisRequest
    {
        InputText = "She just join the team",
        UserLevel = "A2",
        AnalysisMode = "full"
    };

    // Act
    var response = await _controller.AnalyzeSentence(request);

    // Assert
    var okResult = Assert.IsType<OkObjectResult>(response.Result);
    var analysisResponse = Assert.IsType<SentenceAnalysisResponse>(okResult.Value);

    Assert.True(analysisResponse.Success);
    Assert.NotNull(analysisResponse.Data);
    Assert.NotEmpty(analysisResponse.Data.VocabularyAnalysis);
}

Gemini服務測試

[Test]
public async Task GeminiService_WithValidApiKey_CallsRealAPI()
{
    // Arrange
    var service = new GeminiService(_httpClient, _configuration, _logger);

    // Act
    var result = await service.AnalyzeSentenceAsync("test sentence", "A2", new AnalysisOptions());

    // Assert
    Assert.NotNull(result);
    Assert.NotEmpty(result.VocabularyAnalysis);
    Assert.Equal("gemini-pro", result.Metadata.AnalysisModel);
}

🔧 開發環境串接

啟動順序

開發環境啟動腳本

# 1. 啟動後端 (Port 5008)
cd backend/DramaLing.Api
dotnet run

# 2. 啟動前端 (Port 3000)
cd frontend
npm run dev

# 3. 設置Gemini API Key
dotnet user-secrets set "AI:GeminiApiKey" "your-real-api-key"

健康檢查

# 檢查後端健康狀態
curl http://localhost:5008/health
curl http://localhost:5008/api/ai/health

# 檢查前端運行狀態
curl http://localhost:3000

調試工具

前端調試

// 在瀏覽器Console中
console.log('API回應:', result.data);
console.log('詞彙分析:', sentenceAnalysis);
console.log('統計資料:', vocabularyStats);

// Network面板查看
// 檢查API請求和回應內容
// 驗證請求頭和載荷格式

後端調試

// 日誌輸出
_logger.LogInformation("Processing sentence analysis request {RequestId} for user {UserId}", requestId, userId);
_logger.LogInformation("Gemini API response: {Response}", response);
_logger.LogInformation("Sentence analysis completed in {ProcessingTime}s", processingTime);

// SQL查詢調試
// 檢查Entity Framework查詢日誌
// 監控數據庫性能指標

📈 監控與分析

性能監控

前端性能指標

// 性能測量
const startTime = performance.now();
await handleAnalyzeSentence();
const endTime = performance.now();
console.log(`分析耗時: ${endTime - startTime}ms`);

// 記憶體使用監控
const observer = new PerformanceObserver((list) => {
  list.getEntries().forEach((entry) => {
    console.log('Performance:', entry.name, entry.duration);
  });
});
observer.observe({ entryTypes: ['measure'] });

後端性能指標

// 請求處理時間
var stopwatch = Stopwatch.StartNew();
// ... 處理邏輯
stopwatch.Stop();
_logger.LogInformation("Request processed in {ElapsedMs}ms", stopwatch.ElapsedMilliseconds);

// 資料庫查詢性能
services.AddDbContext<DramaLingDbContext>(options =>
{
    options.UseSqlite(connectionString)
           .EnableSensitiveDataLogging()
           .LogTo(Console.WriteLine, LogLevel.Information);
});

🔮 未來擴展串接

批次分析支援

前端批次請求

interface BatchAnalysisRequest {
  sentences: string[];
  userLevel: string;
  analysisMode: string;
}

const handleBatchAnalysis = async (sentences: string[]) => {
  const response = await fetch('/api/ai/batch-analyze', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      sentences,
      userLevel: getUserLevel(),
      analysisMode: 'full'
    })
  });
};

後端批次處理

[HttpPost("batch-analyze")]
public async Task<ActionResult<BatchAnalysisResponse>> BatchAnalyze(
    [FromBody] BatchAnalysisRequest request)
{
    var results = new List<SentenceAnalysisData>();

    foreach (var sentence in request.Sentences)
    {
        var analysis = await _geminiService.AnalyzeSentenceAsync(
            sentence, request.UserLevel, request.Options);
        results.Add(analysis);
    }

    return Ok(new BatchAnalysisResponse { Results = results });
}

即時分析支援

WebSocket連接

// 前端WebSocket客戶端
const ws = new WebSocket('ws://localhost:5008/ws/analysis');

ws.onmessage = (event) => {
  const partialResult = JSON.parse(event.data);
  updateAnalysisProgress(partialResult);
};

// 即時分析請求
ws.send(JSON.stringify({
  type: 'analyze',
  inputText: textInput,
  userLevel: userLevel
}));

SignalR整合

// 後端SignalR Hub
public class AnalysisHub : Hub
{
    public async Task StartAnalysis(string inputText, string userLevel)
    {
        await Clients.Caller.SendAsync("AnalysisStarted");

        // 分段回傳結果
        await Clients.Caller.SendAsync("GrammarCheckComplete", grammarResult);
        await Clients.Caller.SendAsync("VocabularyAnalysisComplete", vocabResult);
        await Clients.Caller.SendAsync("AnalysisComplete", finalResult);
    }
}

📋 串接檢查清單

開發階段檢查

前端檢查項目

  • API端點URL正確配置
  • 請求格式符合後端期望
  • 錯誤處理涵蓋所有情況
  • 認證Token正確傳遞
  • 響應數據正確解析
  • UI狀態正確更新
  • 性能優化已實施

後端檢查項目

  • API端點路由正確設置
  • 輸入驗證完整實現
  • Gemini API正確調用
  • 數據模型匹配前端需求
  • 錯誤處理完善
  • 緩存機制有效
  • 日誌記錄詳細

生產環境檢查

安全性檢查

  • API Key安全存儲
  • JWT認證正確實施
  • CORS策略適當配置
  • 輸入驗證防止注入
  • 錯誤訊息不洩露敏感資訊

效能檢查

  • API回應時間 < 5秒
  • 快取命中率 > 70%
  • 記憶體使用穩定
  • 資料庫查詢優化
  • 並發處理正常

📞 故障排除指南

常見串接問題

CORS錯誤

錯誤: Access to fetch blocked by CORS policy
解決: 檢查後端CORS設定是否包含前端域名

認證失敗

錯誤: 401 Unauthorized
解決: 檢查JWT Token是否有效、格式是否正確

數據格式不匹配

錯誤: Cannot read property 'vocabularyAnalysis' of undefined
解決: 檢查後端回應格式是否符合前端期望

API超時

錯誤: Network timeout
解決: 檢查Gemini API是否可達、增加超時設定

調試步驟

  1. 檢查網路連通性: curl http://localhost:5008/health
  2. 驗證API端點: curl -X POST http://localhost:5008/api/ai/analyze-sentence
  3. 檢查前端日誌: 瀏覽器開發者工具Console
  4. 檢查後端日誌: dotnet應用程式輸出
  5. 驗證數據庫: 檢查SQLite文件和表結構

文件版本: v1.0 對應實現: main分支 commit 03c1756 最後驗證: 2025-01-25 下次檢查: 功能更新時