feat: 完成資料庫命名規範統一 - 全面實施snake_case標準

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
鄭沛軒 2025-09-30 16:57:44 +08:00
parent 923ce16f5f
commit 11b0f606d3
22 changed files with 6744 additions and 557 deletions

View File

@ -0,0 +1,213 @@
# 資料庫命名規範統一計劃
## 📋 計劃概述
本計劃旨在解決 DramaLing 專案中資料庫欄位命名不一致的問題,將所有資料庫欄位統一為 `snake_case` 命名規範。
## 🔍 現況分析
### 問題描述
目前資料庫中同時存在兩種命名方式:
- **PascalCase**: `Example`, `Word`, `Translation`, `Definition`, `Name`, `Color`
- **snake_case**: `user_id`, `created_at`, `is_favorite`, `part_of_speech`
### 問題根源
1. **歷史遺留**: 早期遷移沒有統一配置欄位命名
2. **不完整配置**: 部分屬性缺少 `.HasColumnName()` 映射
3. **維護疏漏**: 新增欄位時沒有遵循統一規範
### 已修復項目 ✅
- ✅ `Flashcard.Word``word`
- ✅ `Flashcard.Translation``translation`
- ✅ `Flashcard.Definition``definition`
- ✅ `Flashcard.Pronunciation``pronunciation`
- ✅ `Flashcard.Example``example` (原始問題)
## 🎯 統一規範標準
### 命名規則
| 層級 | 命名方式 | 範例 | 說明 |
|------|----------|------|------|
| **C# 實體屬性** | PascalCase | `UserId`, `CreatedAt`, `ExampleTranslation` | 符合 C# 慣例 |
| **資料庫欄位** | snake_case | `user_id`, `created_at`, `example_translation` | 符合資料庫慣例 |
| **表格名稱** | snake_case | `flashcards`, `user_profiles`, `daily_stats` | 保持一致性 |
### 映射規則
```csharp
// 所有屬性都需要明確映射
entity.Property(e => e.PropertyName).HasColumnName("property_name");
```
## 📝 待修復項目清單
### 1. Tag 實體
```csharp
// 目前缺少的映射:
public Guid Id { get; set; } // → "id"
public string Name { get; set; } // → "name"
public string Color { get; set; } // → "color"
```
### 2. ErrorReport 實體
```csharp
// 目前缺少的映射:
public Guid Id { get; set; } // → "id"
public string? Description { get; set; } // → "description"
public string Status { get; set; } // → "status"
```
### 3. DailyStats 實體
```csharp
// 目前缺少的映射:
public Guid Id { get; set; } // → "id"
public DateTime Date { get; set; } // → "date"
```
### 4. 其他實體
需要檢查以下實體是否有遺漏的映射:
- `SentenceAnalysisCache`
- `WordQueryUsageStats`
- `ExampleImage`
- `ImageGenerationRequest`
- `OptionsVocabulary`
### 5. 通用 Id 欄位
所有實體的 `Id` 屬性都應該映射為 `id`
## 🚀 執行步驟
### 階段一DbContext 配置更新
1. **補充 Tag 實體配置**
```csharp
private void ConfigureTagEntities(ModelBuilder modelBuilder)
{
var tagEntity = modelBuilder.Entity<Tag>();
tagEntity.Property(t => t.Id).HasColumnName("id");
tagEntity.Property(t => t.Name).HasColumnName("name");
tagEntity.Property(t => t.Color).HasColumnName("color");
// 其他現有配置...
}
```
2. **補充其他實體配置**
- 更新 `ConfigureErrorReportEntity`
- 更新 `ConfigureDailyStatsEntity`
- 新增其他實體的配置方法
### 階段二:資料庫遷移
1. **建立遷移**
```bash
dotnet ef migrations add CompleteSnakeCaseNaming
```
2. **套用遷移**
```bash
dotnet ef database update
```
### 階段三:驗證與測試
1. **檢查資料庫結構**
```sql
.schema table_name
```
2. **測試應用程式功能**
- API 端點測試
- 資料查詢測試
- 完整功能驗證
## 📋 檢核清單
### 配置檢核
- [ ] 所有實體的 `Id` 屬性都有 `.HasColumnName("id")`
- [ ] 所有多單字屬性都使用 snake_case`CreatedAt``created_at`
- [ ] 所有布林屬性都使用 `is_` 前綴(如 `IsActive``is_active`
- [ ] 外鍵屬性都使用 `_id` 後綴(如 `UserId``user_id`
### 遷移檢核
- [ ] 遷移檔案正確生成
- [ ] SQL 指令正確RENAME COLUMN
- [ ] 沒有資料遺失風險
- [ ] 回滾計劃準備完成
### 測試檢核
- [ ] 所有 API 端點正常運作
- [ ] 資料查詢結果正確
- [ ] 無效能退化
- [ ] 前端功能正常
## 🔧 長期維護建議
### 1. 編碼規範
建立明確的編碼規範文檔:
```markdown
## 資料庫命名規範
- 所有新增的實體屬性都必須配置 `.HasColumnName()`
- 資料庫欄位名稱統一使用 snake_case
- 布林欄位使用 `is_` 前綴
- 外鍵欄位使用 `_id` 後綴
```
### 2. Code Review 檢查點
在 PR 審查時檢查:
- 新增實體是否有完整的欄位映射配置
- 遷移檔案是否符合命名規範
- 是否需要更新相關文檔
### 3. 自動化檢查
考慮實施:
- **Pre-commit Hook**: 檢查新增的 DbContext 配置
- **CI/CD 檢查**: 驗證遷移檔案的正確性
- **單元測試**: 確保所有實體都有正確的欄位映射
### 4. 全局慣例配置(進階選項)
可以考慮使用 EF Core 的全局慣例:
```csharp
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Properties<string>()
.HaveColumnName(propertyInfo => propertyInfo.Name.ToSnakeCase());
}
```
## 📊 影響評估
### 優點
- ✅ 統一的命名規範
- ✅ 更好的可維護性
- ✅ 避免開發混淆
- ✅ 符合業界標準
### 風險
- ⚠️ 需要資料庫遷移
- ⚠️ 可能影響現有查詢
- ⚠️ 需要充分測試
### 緩解措施
- 📋 充分的測試計劃
- 🔄 準備回滾方案
- 📝 詳細的變更文檔
- 👥 團隊溝通協調
## 🗓️ 執行時間表
| 階段 | 預估時間 | 責任人 | 狀態 |
|------|----------|--------|------|
| 現況分析 | 0.5 天 | 開發團隊 | ✅ 完成 |
| 配置更新 | 1 天 | 後端開發 | 🚧 進行中 |
| 遷移建立 | 0.5 天 | 後端開發 | ⏳ 待執行 |
| 測試驗證 | 1 天 | 全團隊 | ⏳ 待執行 |
| 部署上線 | 0.5 天 | DevOps | ⏳ 待執行 |
## 📞 聯絡資訊
如有問題或需要協助,請聯絡:
- **技術負責人**: [待填入]
- **專案經理**: [待填入]
- **QA 負責人**: [待填入]
---
**文件版本**: v1.0
**最後更新**: 2025-09-30
**建立人**: Claude Code Assistant

View File

@ -6,19 +6,16 @@ using System.Diagnostics;
namespace DramaLing.Api.Controllers;
[ApiController]
[Route("api/ai")]
public class AIController : ControllerBase
public class AIController : BaseController
{
private readonly IAnalysisService _analysisService;
private readonly ILogger<AIController> _logger;
public AIController(
IAnalysisService analysisService,
ILogger<AIController> logger)
ILogger<AIController> logger) : base(logger)
{
_analysisService = analysisService;
_logger = logger;
}
/// <summary>
@ -28,7 +25,7 @@ public class AIController : ControllerBase
/// <returns>分析結果</returns>
[HttpPost("analyze-sentence")]
[AllowAnonymous]
public async Task<ActionResult<SentenceAnalysisResponse>> AnalyzeSentence(
public async Task<IActionResult> AnalyzeSentence(
[FromBody] SentenceAnalysisRequest request)
{
var requestId = Guid.NewGuid().ToString();
@ -45,9 +42,7 @@ public class AIController : ControllerBase
// Input validation
if (!ModelState.IsValid)
{
return BadRequest(CreateErrorResponse("INVALID_INPUT", "輸入格式錯誤",
ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(),
requestId));
return HandleModelStateErrors();
}
// 使用帶快取的分析服務
@ -61,27 +56,29 @@ public class AIController : ControllerBase
_logger.LogInformation("Sentence analysis completed for request {RequestId} in {ElapsedMs}ms",
requestId, stopwatch.ElapsedMilliseconds);
return Ok(new SentenceAnalysisResponse
var response = new SentenceAnalysisResponse
{
Success = true,
ProcessingTime = stopwatch.Elapsed.TotalSeconds,
Data = analysisData
});
};
return SuccessResponse(response);
}
catch (ArgumentException ex)
{
_logger.LogWarning(ex, "Invalid input for request {RequestId}", requestId);
return BadRequest(CreateErrorResponse("INVALID_INPUT", ex.Message, null, requestId));
return ErrorResponse("INVALID_INPUT", ex.Message, null, 400);
}
catch (InvalidOperationException ex)
{
_logger.LogError(ex, "AI service error for request {RequestId}", requestId);
return StatusCode(500, CreateErrorResponse("AI_SERVICE_ERROR", "AI服務暫時不可用", null, requestId));
return ErrorResponse("AI_SERVICE_ERROR", "AI服務暫時不可用", null, 500);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error processing request {RequestId}", requestId);
return StatusCode(500, CreateErrorResponse("INTERNAL_ERROR", "伺服器內部錯誤", null, requestId));
return ErrorResponse("INTERNAL_ERROR", "伺服器內部錯誤", null, 500);
}
}
@ -90,15 +87,17 @@ public class AIController : ControllerBase
/// </summary>
[HttpGet("health")]
[AllowAnonymous]
public ActionResult GetHealth()
public IActionResult GetHealth()
{
return Ok(new
var healthData = new
{
Status = "Healthy",
Service = "AI Analysis Service",
Timestamp = DateTime.UtcNow,
Version = "1.0"
});
};
return SuccessResponse(healthData);
}
/// <summary>
@ -106,59 +105,28 @@ public class AIController : ControllerBase
/// </summary>
[HttpGet("stats")]
[AllowAnonymous]
public async Task<ActionResult> GetAnalysisStats()
public async Task<IActionResult> GetAnalysisStats()
{
try
{
var stats = await _analysisService.GetAnalysisStatsAsync();
return Ok(new
var statsData = new
{
Success = true,
Data = new
{
TotalAnalyses = stats.TotalAnalyses,
CachedAnalyses = stats.CachedAnalyses,
CacheHitRate = stats.CacheHitRate,
AverageResponseTimeMs = stats.AverageResponseTimeMs,
LastAnalysisAt = stats.LastAnalysisAt,
ProviderUsage = stats.ProviderUsageStats
}
});
TotalAnalyses = stats.TotalAnalyses,
CachedAnalyses = stats.CachedAnalyses,
CacheHitRate = stats.CacheHitRate,
AverageResponseTimeMs = stats.AverageResponseTimeMs,
LastAnalysisAt = stats.LastAnalysisAt,
ProviderUsage = stats.ProviderUsageStats
};
return SuccessResponse(statsData);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting analysis stats");
return StatusCode(500, new { Success = false, Error = "無法取得統計資訊" });
return ErrorResponse("INTERNAL_ERROR", "無法取得統計資訊");
}
}
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
};
}
private List<string> GetSuggestionsForError(string errorCode)
{
return errorCode switch
{
"INVALID_INPUT" => new List<string> { "請檢查輸入格式", "確保文本長度在限制內" },
"RATE_LIMIT_EXCEEDED" => new List<string> { "升級到Premium帳戶以獲得無限使用", "明天重新嘗試" },
"AI_SERVICE_ERROR" => new List<string> { "請稍後重試", "如果問題持續,請聯繫客服" },
_ => new List<string> { "請稍後重試" }
};
}
}

View File

@ -0,0 +1,124 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using DramaLing.Api.Models.DTOs;
using DramaLing.Api.Services;
using System.Diagnostics;
namespace DramaLing.Api.Controllers;
[Route("api/analysis")]
[AllowAnonymous]
public class AnalysisController : BaseController
{
private readonly IAnalysisService _analysisService;
public AnalysisController(
IAnalysisService analysisService,
ILogger<AnalysisController> logger) : base(logger)
{
_analysisService = analysisService;
}
/// <summary>
/// 智能分析英文句子
/// </summary>
/// <param name="request">分析請求</param>
/// <returns>分析結果</returns>
[HttpPost("analyze")]
public async Task<IActionResult> AnalyzeSentence([FromBody] SentenceAnalysisRequest request)
{
var requestId = Guid.NewGuid().ToString();
var stopwatch = Stopwatch.StartNew();
try
{
// Input validation
if (!ModelState.IsValid)
{
return HandleModelStateErrors();
}
_logger.LogInformation("Processing sentence analysis request {RequestId}", requestId);
// 使用帶快取的分析服務
var options = request.Options ?? new AnalysisOptions();
var analysisData = await _analysisService.AnalyzeSentenceAsync(
request.InputText, options);
stopwatch.Stop();
analysisData.Metadata.ProcessingDate = DateTime.UtcNow;
_logger.LogInformation("Sentence analysis completed for request {RequestId} in {ElapsedMs}ms",
requestId, stopwatch.ElapsedMilliseconds);
var response = new SentenceAnalysisResponse
{
Success = true,
ProcessingTime = stopwatch.Elapsed.TotalSeconds,
Data = analysisData
};
return SuccessResponse(response);
}
catch (ArgumentException ex)
{
_logger.LogWarning(ex, "Invalid input for request {RequestId}", requestId);
return ErrorResponse("INVALID_INPUT", ex.Message, null, 400);
}
catch (InvalidOperationException ex)
{
_logger.LogError(ex, "AI service error for request {RequestId}", requestId);
return ErrorResponse("AI_SERVICE_ERROR", "AI服務暫時不可用", null, 500);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error processing request {RequestId}", requestId);
return ErrorResponse("INTERNAL_ERROR", "伺服器內部錯誤", null, 500);
}
}
/// <summary>
/// 健康檢查端點
/// </summary>
[HttpGet("health")]
public IActionResult GetHealth()
{
var healthData = new
{
Status = "Healthy",
Service = "Analysis Service",
Timestamp = DateTime.UtcNow,
Version = "1.0"
};
return SuccessResponse(healthData);
}
/// <summary>
/// 取得分析統計資訊
/// </summary>
[HttpGet("stats")]
public async Task<IActionResult> GetAnalysisStats()
{
try
{
var stats = await _analysisService.GetAnalysisStatsAsync();
var statsData = new
{
TotalAnalyses = stats.TotalAnalyses,
CachedAnalyses = stats.CachedAnalyses,
CacheHitRate = stats.CacheHitRate,
AverageResponseTimeMs = stats.AverageResponseTimeMs,
LastAnalysisAt = stats.LastAnalysisAt,
ProviderUsage = stats.ProviderUsageStats
};
return SuccessResponse(statsData);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting analysis stats");
return ErrorResponse("INTERNAL_ERROR", "無法取得統計資訊");
}
}
}

View File

@ -5,23 +5,20 @@ using DramaLing.Api.Services;
namespace DramaLing.Api.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class AudioController : ControllerBase
public class AudioController : BaseController
{
private readonly IAudioCacheService _audioCacheService;
private readonly IAzureSpeechService _speechService;
private readonly ILogger<AudioController> _logger;
public AudioController(
IAudioCacheService audioCacheService,
IAzureSpeechService speechService,
ILogger<AudioController> logger)
ILogger<AudioController> logger) : base(logger)
{
_audioCacheService = audioCacheService;
_speechService = speechService;
_logger = logger;
}
/// <summary>
@ -30,7 +27,7 @@ public class AudioController : ControllerBase
/// <param name="request">TTS request parameters</param>
/// <returns>Audio URL and metadata</returns>
[HttpPost("tts")]
public async Task<ActionResult<TTSResponse>> GenerateAudio([FromBody] TTSRequest request)
public async Task<IActionResult> GenerateAudio([FromBody] TTSRequest request)
{
try
{
@ -70,10 +67,10 @@ public class AudioController : ControllerBase
if (!string.IsNullOrEmpty(response.Error))
{
return StatusCode(500, response);
return ErrorResponse("TTS_ERROR", response.Error, null, 500);
}
return Ok(response);
return SuccessResponse(response);
}
catch (Exception ex)
{

View File

@ -12,26 +12,21 @@ using Microsoft.IdentityModel.Tokens;
namespace DramaLing.Api.Controllers;
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
public class AuthController : BaseController
{
private readonly DramaLingDbContext _context;
private readonly IAuthService _authService;
private readonly ILogger<AuthController> _logger;
public AuthController(
DramaLingDbContext context,
IAuthService authService,
ILogger<AuthController> logger)
ILogger<AuthController> logger) : base(logger, authService)
{
_context = context;
_authService = authService;
_logger = logger;
}
[HttpPost("register")]
public async Task<ActionResult> Register([FromBody] RegisterRequest request)
public async Task<IActionResult> Register([FromBody] RegisterRequest request)
{
try
{

View File

@ -7,19 +7,16 @@ using System.Security.Claims;
namespace DramaLing.Api.Controllers;
[Route("api/[controller]")]
[ApiController]
[AllowAnonymous] // 暫時移除認證要求,與 FlashcardsController 保持一致
public class ImageGenerationController : ControllerBase
public class ImageGenerationController : BaseController
{
private readonly IImageGenerationOrchestrator _orchestrator;
private readonly ILogger<ImageGenerationController> _logger;
public ImageGenerationController(
IImageGenerationOrchestrator orchestrator,
ILogger<ImageGenerationController> logger)
ILogger<ImageGenerationController> logger) : base(logger)
{
_orchestrator = orchestrator ?? throw new ArgumentNullException(nameof(orchestrator));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
@ -43,17 +40,17 @@ public class ImageGenerationController : ControllerBase
var result = await _orchestrator.StartGenerationAsync(flashcardId, request);
return Ok(new { success = true, data = result });
return SuccessResponse(result);
}
catch (ArgumentException ex)
{
_logger.LogWarning(ex, "Invalid request for flashcard {FlashcardId}", flashcardId);
return BadRequest(new { success = false, error = ex.Message });
return ErrorResponse("INVALID_REQUEST", ex.Message, null, 400);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to start image generation for flashcard {FlashcardId}", flashcardId);
return StatusCode(500, new { success = false, error = "Failed to start generation" });
return ErrorResponse("GENERATION_FAILED", "Failed to start generation");
}
}
@ -73,17 +70,17 @@ public class ImageGenerationController : ControllerBase
var status = await _orchestrator.GetGenerationStatusAsync(requestId);
return Ok(new { success = true, data = status });
return SuccessResponse(status);
}
catch (ArgumentException ex)
{
_logger.LogWarning(ex, "Generation request {RequestId} not found", requestId);
return NotFound(new { success = false, error = ex.Message });
return ErrorResponse("NOT_FOUND", ex.Message, null, 404);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get status for request {RequestId}", requestId);
return StatusCode(500, new { success = false, error = "Failed to get status" });
return ErrorResponse("STATUS_ERROR", "Failed to get status");
}
}
@ -105,17 +102,17 @@ public class ImageGenerationController : ControllerBase
if (cancelled)
{
return Ok(new { success = true, message = "Generation cancelled successfully" });
return SuccessResponse(new { message = "Generation cancelled successfully" });
}
else
{
return BadRequest(new { success = false, error = "Cannot cancel this request" });
return ErrorResponse("CANCEL_FAILED", "Cannot cancel this request", null, 400);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to cancel generation request {RequestId}", requestId);
return StatusCode(500, new { success = false, error = "Failed to cancel generation" });
return ErrorResponse("CANCEL_ERROR", "Failed to cancel generation");
}
}
@ -148,12 +145,12 @@ public class ImageGenerationController : ControllerBase
}
};
return Ok(new { success = true, data = history });
return SuccessResponse(history);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get generation history for user");
return StatusCode(500, new { success = false, error = "Failed to get history" });
return ErrorResponse("HISTORY_ERROR", "Failed to get history");
}
}

View File

@ -6,19 +6,16 @@ namespace DramaLing.Api.Controllers;
/// <summary>
/// 選項詞彙庫服務測試控制器 (僅用於開發測試)
/// </summary>
[ApiController]
[Route("api/test/[controller]")]
public class OptionsVocabularyTestController : ControllerBase
public class OptionsVocabularyTestController : BaseController
{
private readonly IOptionsVocabularyService _optionsVocabularyService;
private readonly ILogger<OptionsVocabularyTestController> _logger;
public OptionsVocabularyTestController(
IOptionsVocabularyService optionsVocabularyService,
ILogger<OptionsVocabularyTestController> logger)
ILogger<OptionsVocabularyTestController> logger) : base(logger)
{
_optionsVocabularyService = optionsVocabularyService;
_logger = logger;
}
/// <summary>

View File

@ -6,14 +6,13 @@ using System.Security.Claims;
namespace DramaLing.Api.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class StatsController : ControllerBase
public class StatsController : BaseController
{
private readonly DramaLingDbContext _context;
public StatsController(DramaLingDbContext context)
public StatsController(DramaLingDbContext context, ILogger<StatsController> logger) : base(logger)
{
_context = context;
}

View File

@ -49,14 +49,19 @@ public class DramaLingDbContext : DbContext
modelBuilder.Entity<FlashcardExampleImage>().ToTable("flashcard_example_images");
modelBuilder.Entity<ImageGenerationRequest>().ToTable("image_generation_requests");
modelBuilder.Entity<OptionsVocabulary>().ToTable("options_vocabularies");
modelBuilder.Entity<SentenceAnalysisCache>().ToTable("sentence_analysis_cache");
modelBuilder.Entity<WordQueryUsageStats>().ToTable("word_query_usage_stats");
// 配置屬性名稱 (snake_case)
ConfigureUserEntity(modelBuilder);
ConfigureUserSettingsEntity(modelBuilder);
ConfigureFlashcardEntity(modelBuilder);
// ConfigureStudyEntities 已移除 - StudyRecord 實體已清理
ConfigureTagEntities(modelBuilder);
ConfigureErrorReportEntity(modelBuilder);
ConfigureDailyStatsEntity(modelBuilder);
ConfigureSentenceAnalysisCacheEntity(modelBuilder);
ConfigureWordQueryUsageStatsEntity(modelBuilder);
ConfigureAudioEntities(modelBuilder);
ConfigureImageGenerationEntities(modelBuilder);
ConfigureOptionsVocabularyEntity(modelBuilder);
@ -79,6 +84,7 @@ public class DramaLingDbContext : DbContext
private void ConfigureUserEntity(ModelBuilder modelBuilder)
{
var userEntity = modelBuilder.Entity<User>();
userEntity.Property(u => u.Id).HasColumnName("id");
userEntity.Property(u => u.Username).HasColumnName("username");
userEntity.Property(u => u.Email).HasColumnName("email");
userEntity.Property(u => u.PasswordHash).HasColumnName("password_hash");
@ -108,8 +114,14 @@ public class DramaLingDbContext : DbContext
private void ConfigureFlashcardEntity(ModelBuilder modelBuilder)
{
var flashcardEntity = modelBuilder.Entity<Flashcard>();
flashcardEntity.Property(f => f.Id).HasColumnName("id");
flashcardEntity.Property(f => f.UserId).HasColumnName("user_id");
flashcardEntity.Property(f => f.Word).HasColumnName("word");
flashcardEntity.Property(f => f.Translation).HasColumnName("translation");
flashcardEntity.Property(f => f.Definition).HasColumnName("definition");
flashcardEntity.Property(f => f.PartOfSpeech).HasColumnName("part_of_speech");
flashcardEntity.Property(f => f.Pronunciation).HasColumnName("pronunciation");
flashcardEntity.Property(f => f.Example).HasColumnName("example");
flashcardEntity.Property(f => f.ExampleTranslation).HasColumnName("example_translation");
// 已刪除的復習相關屬性配置
// EasinessFactor, IntervalDays, NextReviewDate, MasteryLevel,
@ -126,7 +138,10 @@ public class DramaLingDbContext : DbContext
private void ConfigureTagEntities(ModelBuilder modelBuilder)
{
var tagEntity = modelBuilder.Entity<Tag>();
tagEntity.Property(t => t.Id).HasColumnName("id");
tagEntity.Property(t => t.UserId).HasColumnName("user_id");
tagEntity.Property(t => t.Name).HasColumnName("name");
tagEntity.Property(t => t.Color).HasColumnName("color");
tagEntity.Property(t => t.UsageCount).HasColumnName("usage_count");
tagEntity.Property(t => t.CreatedAt).HasColumnName("created_at");
@ -138,10 +153,13 @@ public class DramaLingDbContext : DbContext
private void ConfigureErrorReportEntity(ModelBuilder modelBuilder)
{
var errorEntity = modelBuilder.Entity<ErrorReport>();
errorEntity.Property(e => e.Id).HasColumnName("id");
errorEntity.Property(e => e.UserId).HasColumnName("user_id");
errorEntity.Property(e => e.FlashcardId).HasColumnName("flashcard_id");
errorEntity.Property(e => e.ReportType).HasColumnName("report_type");
errorEntity.Property(e => e.Description).HasColumnName("description");
errorEntity.Property(e => e.StudyMode).HasColumnName("study_mode");
errorEntity.Property(e => e.Status).HasColumnName("status");
errorEntity.Property(e => e.AdminNotes).HasColumnName("admin_notes");
errorEntity.Property(e => e.ResolvedAt).HasColumnName("resolved_at");
errorEntity.Property(e => e.ResolvedBy).HasColumnName("resolved_by");
@ -151,7 +169,9 @@ public class DramaLingDbContext : DbContext
private void ConfigureDailyStatsEntity(ModelBuilder modelBuilder)
{
var statsEntity = modelBuilder.Entity<DailyStats>();
statsEntity.Property(d => d.Id).HasColumnName("id");
statsEntity.Property(d => d.UserId).HasColumnName("user_id");
statsEntity.Property(d => d.Date).HasColumnName("date");
statsEntity.Property(d => d.WordsStudied).HasColumnName("words_studied");
statsEntity.Property(d => d.WordsCorrect).HasColumnName("words_correct");
statsEntity.Property(d => d.StudyTimeSeconds).HasColumnName("study_time_seconds");
@ -161,6 +181,54 @@ public class DramaLingDbContext : DbContext
statsEntity.Property(d => d.CreatedAt).HasColumnName("created_at");
}
private void ConfigureUserSettingsEntity(ModelBuilder modelBuilder)
{
var settingsEntity = modelBuilder.Entity<UserSettings>();
settingsEntity.Property(us => us.Id).HasColumnName("id");
settingsEntity.Property(us => us.UserId).HasColumnName("user_id");
settingsEntity.Property(us => us.DailyGoal).HasColumnName("daily_goal");
settingsEntity.Property(us => us.ReminderTime).HasColumnName("reminder_time");
settingsEntity.Property(us => us.ReminderEnabled).HasColumnName("reminder_enabled");
settingsEntity.Property(us => us.DifficultyPreference).HasColumnName("difficulty_preference");
settingsEntity.Property(us => us.AutoPlayAudio).HasColumnName("auto_play_audio");
settingsEntity.Property(us => us.ShowPronunciation).HasColumnName("show_pronunciation");
settingsEntity.Property(us => us.CreatedAt).HasColumnName("created_at");
settingsEntity.Property(us => us.UpdatedAt).HasColumnName("updated_at");
}
private void ConfigureSentenceAnalysisCacheEntity(ModelBuilder modelBuilder)
{
var cacheEntity = modelBuilder.Entity<SentenceAnalysisCache>();
cacheEntity.Property(sac => sac.Id).HasColumnName("id");
cacheEntity.Property(sac => sac.InputTextHash).HasColumnName("input_text_hash");
cacheEntity.Property(sac => sac.InputText).HasColumnName("input_text");
cacheEntity.Property(sac => sac.CorrectedText).HasColumnName("corrected_text");
cacheEntity.Property(sac => sac.HasGrammarErrors).HasColumnName("has_grammar_errors");
cacheEntity.Property(sac => sac.GrammarCorrections).HasColumnName("grammar_corrections");
cacheEntity.Property(sac => sac.AnalysisResult).HasColumnName("analysis_result");
cacheEntity.Property(sac => sac.HighValueWords).HasColumnName("high_value_words");
cacheEntity.Property(sac => sac.IdiomsDetected).HasColumnName("idioms_detected");
cacheEntity.Property(sac => sac.CreatedAt).HasColumnName("created_at");
cacheEntity.Property(sac => sac.ExpiresAt).HasColumnName("expires_at");
cacheEntity.Property(sac => sac.AccessCount).HasColumnName("access_count");
cacheEntity.Property(sac => sac.LastAccessedAt).HasColumnName("last_accessed_at");
}
private void ConfigureWordQueryUsageStatsEntity(ModelBuilder modelBuilder)
{
var statsEntity = modelBuilder.Entity<WordQueryUsageStats>();
statsEntity.Property(wq => wq.Id).HasColumnName("id");
statsEntity.Property(wq => wq.UserId).HasColumnName("user_id");
statsEntity.Property(wq => wq.Date).HasColumnName("date");
statsEntity.Property(wq => wq.SentenceAnalysisCount).HasColumnName("sentence_analysis_count");
statsEntity.Property(wq => wq.HighValueWordClicks).HasColumnName("high_value_word_clicks");
statsEntity.Property(wq => wq.LowValueWordClicks).HasColumnName("low_value_word_clicks");
statsEntity.Property(wq => wq.TotalApiCalls).HasColumnName("total_api_calls");
statsEntity.Property(wq => wq.UniqueWordsQueried).HasColumnName("unique_words_queried");
statsEntity.Property(wq => wq.CreatedAt).HasColumnName("created_at");
statsEntity.Property(wq => wq.UpdatedAt).HasColumnName("updated_at");
}
private void ConfigureRelationships(ModelBuilder modelBuilder)
{
// User relationships
@ -255,8 +323,10 @@ public class DramaLingDbContext : DbContext
{
// AudioCache configuration
var audioCacheEntity = modelBuilder.Entity<AudioCache>();
audioCacheEntity.Property(ac => ac.Id).HasColumnName("id");
audioCacheEntity.Property(ac => ac.TextHash).HasColumnName("text_hash");
audioCacheEntity.Property(ac => ac.TextContent).HasColumnName("text_content");
audioCacheEntity.Property(ac => ac.Accent).HasColumnName("accent");
audioCacheEntity.Property(ac => ac.VoiceId).HasColumnName("voice_id");
audioCacheEntity.Property(ac => ac.AudioUrl).HasColumnName("audio_url");
audioCacheEntity.Property(ac => ac.FileSize).HasColumnName("file_size");
@ -274,6 +344,7 @@ public class DramaLingDbContext : DbContext
// PronunciationAssessment configuration
var pronunciationEntity = modelBuilder.Entity<PronunciationAssessment>();
pronunciationEntity.Property(pa => pa.Id).HasColumnName("id");
pronunciationEntity.Property(pa => pa.UserId).HasColumnName("user_id");
pronunciationEntity.Property(pa => pa.FlashcardId).HasColumnName("flashcard_id");
pronunciationEntity.Property(pa => pa.TargetText).HasColumnName("target_text");
@ -296,6 +367,7 @@ public class DramaLingDbContext : DbContext
// UserAudioPreferences configuration
var audioPrefsEntity = modelBuilder.Entity<UserAudioPreferences>();
audioPrefsEntity.Property(uap => uap.UserId).HasColumnName("user_id");
audioPrefsEntity.Property(uap => uap.PreferredAccent).HasColumnName("preferred_accent");
audioPrefsEntity.Property(uap => uap.PreferredVoiceMale).HasColumnName("preferred_voice_male");
audioPrefsEntity.Property(uap => uap.PreferredVoiceFemale).HasColumnName("preferred_voice_female");
@ -336,6 +408,7 @@ public class DramaLingDbContext : DbContext
{
// ExampleImage configuration
var exampleImageEntity = modelBuilder.Entity<ExampleImage>();
exampleImageEntity.Property(ei => ei.Id).HasColumnName("id");
exampleImageEntity.Property(ei => ei.RelativePath).HasColumnName("relative_path");
exampleImageEntity.Property(ei => ei.AltText).HasColumnName("alt_text");
exampleImageEntity.Property(ei => ei.GeminiPrompt).HasColumnName("gemini_prompt");
@ -371,6 +444,7 @@ public class DramaLingDbContext : DbContext
// ImageGenerationRequest configuration
var generationRequestEntity = modelBuilder.Entity<ImageGenerationRequest>();
generationRequestEntity.Property(igr => igr.Id).HasColumnName("id");
generationRequestEntity.Property(igr => igr.UserId).HasColumnName("user_id");
generationRequestEntity.Property(igr => igr.FlashcardId).HasColumnName("flashcard_id");
generationRequestEntity.Property(igr => igr.OverallStatus).HasColumnName("overall_status");
@ -433,6 +507,8 @@ public class DramaLingDbContext : DbContext
var optionsVocabEntity = modelBuilder.Entity<OptionsVocabulary>();
// Configure column names (snake_case)
optionsVocabEntity.Property(ov => ov.Id).HasColumnName("id");
optionsVocabEntity.Property(ov => ov.Word).HasColumnName("word");
optionsVocabEntity.Property(ov => ov.CEFRLevel).HasColumnName("cefr_level");
optionsVocabEntity.Property(ov => ov.PartOfSpeech).HasColumnName("part_of_speech");
optionsVocabEntity.Property(ov => ov.WordLength).HasColumnName("word_length");

View File

@ -69,27 +69,35 @@ public static class ServiceCollectionExtensions
services.AddSingleton<ICacheStrategyManager, CacheStrategyManager>();
services.AddScoped<IDatabaseCacheManager, DatabaseCacheManager>();
// 快取提供者
services.AddScoped<ICacheProvider>(provider =>
new MemoryCacheProvider(
provider.GetRequiredService<Microsoft.Extensions.Caching.Memory.IMemoryCache>(),
provider.GetRequiredService<ILogger<MemoryCacheProvider>>()));
services.AddScoped<ICacheProvider>(provider =>
// 快取提供者 - 使用具名註冊
services.AddScoped<ICacheService>(provider =>
{
var memoryCache = provider.GetRequiredService<Microsoft.Extensions.Caching.Memory.IMemoryCache>();
var logger = provider.GetRequiredService<ILogger<HybridCacheService>>();
var databaseCacheManager = provider.GetRequiredService<IDatabaseCacheManager>();
var strategyManager = provider.GetRequiredService<ICacheStrategyManager>();
var memoryProvider = new MemoryCacheProvider(
memoryCache,
provider.GetRequiredService<ILogger<MemoryCacheProvider>>());
ICacheProvider? distributedProvider = null;
var distributedCache = provider.GetService<Microsoft.Extensions.Caching.Distributed.IDistributedCache>();
if (distributedCache != null)
{
return new DistributedCacheProvider(
distributedProvider = new DistributedCacheProvider(
distributedCache,
provider.GetRequiredService<ICacheSerializer>(),
provider.GetRequiredService<ILogger<DistributedCacheProvider>>());
}
return null!;
});
// 主要快取服務
services.AddScoped<ICacheService, HybridCacheService>();
return new HybridCacheService(
memoryProvider,
distributedProvider,
databaseCacheManager,
strategyManager,
logger);
});
return services;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,428 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DramaLing.Api.Migrations
{
/// <inheritdoc />
public partial class FixFlashcardColumnNaming : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_pronunciation_assessments_study_sessions_study_session_id",
table: "pronunciation_assessments");
migrationBuilder.DropTable(
name: "study_records");
migrationBuilder.DropTable(
name: "test_results");
migrationBuilder.DropTable(
name: "study_cards");
migrationBuilder.DropTable(
name: "study_sessions");
migrationBuilder.DropIndex(
name: "IX_PronunciationAssessment_Session",
table: "pronunciation_assessments");
migrationBuilder.DropColumn(
name: "study_session_id",
table: "pronunciation_assessments");
migrationBuilder.DropColumn(
name: "FilledQuestionText",
table: "flashcards");
migrationBuilder.DropColumn(
name: "LastQuestionType",
table: "flashcards");
migrationBuilder.DropColumn(
name: "Repetitions",
table: "flashcards");
migrationBuilder.DropColumn(
name: "ReviewHistory",
table: "flashcards");
migrationBuilder.DropColumn(
name: "Synonyms",
table: "flashcards");
migrationBuilder.DropColumn(
name: "easiness_factor",
table: "flashcards");
migrationBuilder.DropColumn(
name: "interval_days",
table: "flashcards");
migrationBuilder.DropColumn(
name: "last_reviewed_at",
table: "flashcards");
migrationBuilder.DropColumn(
name: "mastery_level",
table: "flashcards");
migrationBuilder.DropColumn(
name: "next_review_date",
table: "flashcards");
migrationBuilder.DropColumn(
name: "times_correct",
table: "flashcards");
migrationBuilder.DropColumn(
name: "times_reviewed",
table: "flashcards");
migrationBuilder.RenameColumn(
name: "Word",
table: "flashcards",
newName: "word");
migrationBuilder.RenameColumn(
name: "Translation",
table: "flashcards",
newName: "translation");
migrationBuilder.RenameColumn(
name: "Pronunciation",
table: "flashcards",
newName: "pronunciation");
migrationBuilder.RenameColumn(
name: "Example",
table: "flashcards",
newName: "example");
migrationBuilder.RenameColumn(
name: "Definition",
table: "flashcards",
newName: "definition");
migrationBuilder.AlterColumn<string>(
name: "definition",
table: "flashcards",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "word",
table: "flashcards",
newName: "Word");
migrationBuilder.RenameColumn(
name: "translation",
table: "flashcards",
newName: "Translation");
migrationBuilder.RenameColumn(
name: "pronunciation",
table: "flashcards",
newName: "Pronunciation");
migrationBuilder.RenameColumn(
name: "example",
table: "flashcards",
newName: "Example");
migrationBuilder.RenameColumn(
name: "definition",
table: "flashcards",
newName: "Definition");
migrationBuilder.AddColumn<Guid>(
name: "study_session_id",
table: "pronunciation_assessments",
type: "TEXT",
nullable: true);
migrationBuilder.AlterColumn<string>(
name: "Definition",
table: "flashcards",
type: "TEXT",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AddColumn<string>(
name: "FilledQuestionText",
table: "flashcards",
type: "TEXT",
maxLength: 1000,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "LastQuestionType",
table: "flashcards",
type: "TEXT",
maxLength: 50,
nullable: true);
migrationBuilder.AddColumn<int>(
name: "Repetitions",
table: "flashcards",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<string>(
name: "ReviewHistory",
table: "flashcards",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Synonyms",
table: "flashcards",
type: "TEXT",
maxLength: 2000,
nullable: true);
migrationBuilder.AddColumn<float>(
name: "easiness_factor",
table: "flashcards",
type: "REAL",
nullable: false,
defaultValue: 0f);
migrationBuilder.AddColumn<int>(
name: "interval_days",
table: "flashcards",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<DateTime>(
name: "last_reviewed_at",
table: "flashcards",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "mastery_level",
table: "flashcards",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<DateTime>(
name: "next_review_date",
table: "flashcards",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<int>(
name: "times_correct",
table: "flashcards",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "times_reviewed",
table: "flashcards",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.CreateTable(
name: "study_sessions",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false),
user_id = table.Column<Guid>(type: "TEXT", nullable: false),
average_response_time_ms = table.Column<int>(type: "INTEGER", nullable: false),
CompletedCards = table.Column<int>(type: "INTEGER", nullable: false),
CompletedTests = table.Column<int>(type: "INTEGER", nullable: false),
correct_count = table.Column<int>(type: "INTEGER", nullable: false),
CurrentCardIndex = table.Column<int>(type: "INTEGER", nullable: false),
CurrentTestType = table.Column<string>(type: "TEXT", maxLength: 50, nullable: true),
duration_seconds = table.Column<int>(type: "INTEGER", nullable: false),
ended_at = table.Column<DateTime>(type: "TEXT", nullable: true),
session_type = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
started_at = table.Column<DateTime>(type: "TEXT", nullable: false),
Status = table.Column<int>(type: "INTEGER", nullable: false),
total_cards = table.Column<int>(type: "INTEGER", nullable: false),
TotalTests = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_study_sessions", x => x.Id);
table.ForeignKey(
name: "FK_study_sessions_user_profiles_user_id",
column: x => x.user_id,
principalTable: "user_profiles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "study_cards",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false),
FlashcardId = table.Column<Guid>(type: "TEXT", nullable: false),
StudySessionId = table.Column<Guid>(type: "TEXT", nullable: false),
CompletedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
IsCompleted = table.Column<bool>(type: "INTEGER", nullable: false),
Order = table.Column<int>(type: "INTEGER", nullable: false),
PlannedTests = table.Column<string>(type: "TEXT", nullable: false),
PlannedTestsJson = table.Column<string>(type: "TEXT", nullable: false),
StartedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
Word = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_study_cards", x => x.Id);
table.ForeignKey(
name: "FK_study_cards_flashcards_FlashcardId",
column: x => x.FlashcardId,
principalTable: "flashcards",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_study_cards_study_sessions_StudySessionId",
column: x => x.StudySessionId,
principalTable: "study_sessions",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "study_records",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false),
flashcard_id = table.Column<Guid>(type: "TEXT", nullable: false),
session_id = table.Column<Guid>(type: "TEXT", nullable: false),
user_id = table.Column<Guid>(type: "TEXT", nullable: false),
is_correct = table.Column<bool>(type: "INTEGER", nullable: false),
NewEasinessFactor = table.Column<float>(type: "REAL", nullable: false),
NewIntervalDays = table.Column<int>(type: "INTEGER", nullable: false),
NewRepetitions = table.Column<int>(type: "INTEGER", nullable: false),
NextReviewDate = table.Column<DateTime>(type: "TEXT", nullable: false),
PreviousEasinessFactor = table.Column<float>(type: "REAL", nullable: false),
PreviousIntervalDays = table.Column<int>(type: "INTEGER", nullable: false),
PreviousRepetitions = table.Column<int>(type: "INTEGER", nullable: false),
quality_rating = table.Column<int>(type: "INTEGER", nullable: false),
response_time_ms = table.Column<int>(type: "INTEGER", nullable: true),
studied_at = table.Column<DateTime>(type: "TEXT", nullable: false),
study_mode = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
user_answer = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_study_records", x => x.Id);
table.ForeignKey(
name: "FK_study_records_flashcards_flashcard_id",
column: x => x.flashcard_id,
principalTable: "flashcards",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_study_records_study_sessions_session_id",
column: x => x.session_id,
principalTable: "study_sessions",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_study_records_user_profiles_user_id",
column: x => x.user_id,
principalTable: "user_profiles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "test_results",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false),
StudyCardId = table.Column<Guid>(type: "TEXT", nullable: false),
CompletedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
ConfidenceLevel = table.Column<int>(type: "INTEGER", nullable: true),
IsCorrect = table.Column<bool>(type: "INTEGER", nullable: false),
ResponseTimeMs = table.Column<int>(type: "INTEGER", nullable: false),
TestType = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
UserAnswer = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_test_results", x => x.Id);
table.ForeignKey(
name: "FK_test_results_study_cards_StudyCardId",
column: x => x.StudyCardId,
principalTable: "study_cards",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_PronunciationAssessment_Session",
table: "pronunciation_assessments",
column: "study_session_id");
migrationBuilder.CreateIndex(
name: "IX_study_cards_FlashcardId",
table: "study_cards",
column: "FlashcardId");
migrationBuilder.CreateIndex(
name: "IX_study_cards_StudySessionId",
table: "study_cards",
column: "StudySessionId");
migrationBuilder.CreateIndex(
name: "IX_study_records_flashcard_id",
table: "study_records",
column: "flashcard_id");
migrationBuilder.CreateIndex(
name: "IX_study_records_session_id",
table: "study_records",
column: "session_id");
migrationBuilder.CreateIndex(
name: "IX_StudyRecord_UserCard_TestType_Unique",
table: "study_records",
columns: new[] { "user_id", "flashcard_id", "study_mode" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_study_sessions_user_id",
table: "study_sessions",
column: "user_id");
migrationBuilder.CreateIndex(
name: "IX_test_results_StudyCardId",
table: "test_results",
column: "StudyCardId");
migrationBuilder.AddForeignKey(
name: "FK_pronunciation_assessments_study_sessions_study_session_id",
table: "pronunciation_assessments",
column: "study_session_id",
principalTable: "study_sessions",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,516 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DramaLing.Api.Migrations
{
/// <inheritdoc />
public partial class CompleteSnakeCaseNaming : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_user_settings_user_profiles_UserId",
table: "user_settings");
migrationBuilder.DropForeignKey(
name: "FK_WordQueryUsageStats_user_profiles_UserId",
table: "WordQueryUsageStats");
migrationBuilder.RenameColumn(
name: "Date",
table: "WordQueryUsageStats",
newName: "date");
migrationBuilder.RenameColumn(
name: "Id",
table: "WordQueryUsageStats",
newName: "id");
migrationBuilder.RenameColumn(
name: "UserId",
table: "WordQueryUsageStats",
newName: "user_id");
migrationBuilder.RenameColumn(
name: "UpdatedAt",
table: "WordQueryUsageStats",
newName: "updated_at");
migrationBuilder.RenameColumn(
name: "UniqueWordsQueried",
table: "WordQueryUsageStats",
newName: "unique_words_queried");
migrationBuilder.RenameColumn(
name: "TotalApiCalls",
table: "WordQueryUsageStats",
newName: "total_api_calls");
migrationBuilder.RenameColumn(
name: "SentenceAnalysisCount",
table: "WordQueryUsageStats",
newName: "sentence_analysis_count");
migrationBuilder.RenameColumn(
name: "LowValueWordClicks",
table: "WordQueryUsageStats",
newName: "low_value_word_clicks");
migrationBuilder.RenameColumn(
name: "HighValueWordClicks",
table: "WordQueryUsageStats",
newName: "high_value_word_clicks");
migrationBuilder.RenameColumn(
name: "CreatedAt",
table: "WordQueryUsageStats",
newName: "created_at");
migrationBuilder.RenameColumn(
name: "Id",
table: "user_settings",
newName: "id");
migrationBuilder.RenameColumn(
name: "UserId",
table: "user_settings",
newName: "user_id");
migrationBuilder.RenameColumn(
name: "UpdatedAt",
table: "user_settings",
newName: "updated_at");
migrationBuilder.RenameColumn(
name: "ShowPronunciation",
table: "user_settings",
newName: "show_pronunciation");
migrationBuilder.RenameColumn(
name: "ReminderTime",
table: "user_settings",
newName: "reminder_time");
migrationBuilder.RenameColumn(
name: "ReminderEnabled",
table: "user_settings",
newName: "reminder_enabled");
migrationBuilder.RenameColumn(
name: "DifficultyPreference",
table: "user_settings",
newName: "difficulty_preference");
migrationBuilder.RenameColumn(
name: "DailyGoal",
table: "user_settings",
newName: "daily_goal");
migrationBuilder.RenameColumn(
name: "CreatedAt",
table: "user_settings",
newName: "created_at");
migrationBuilder.RenameColumn(
name: "AutoPlayAudio",
table: "user_settings",
newName: "auto_play_audio");
migrationBuilder.RenameIndex(
name: "IX_user_settings_UserId",
table: "user_settings",
newName: "IX_user_settings_user_id");
migrationBuilder.RenameColumn(
name: "Id",
table: "user_profiles",
newName: "id");
migrationBuilder.RenameColumn(
name: "Name",
table: "tags",
newName: "name");
migrationBuilder.RenameColumn(
name: "Color",
table: "tags",
newName: "color");
migrationBuilder.RenameColumn(
name: "Id",
table: "tags",
newName: "id");
migrationBuilder.RenameColumn(
name: "Id",
table: "SentenceAnalysisCache",
newName: "id");
migrationBuilder.RenameColumn(
name: "LastAccessedAt",
table: "SentenceAnalysisCache",
newName: "last_accessed_at");
migrationBuilder.RenameColumn(
name: "InputTextHash",
table: "SentenceAnalysisCache",
newName: "input_text_hash");
migrationBuilder.RenameColumn(
name: "InputText",
table: "SentenceAnalysisCache",
newName: "input_text");
migrationBuilder.RenameColumn(
name: "IdiomsDetected",
table: "SentenceAnalysisCache",
newName: "idioms_detected");
migrationBuilder.RenameColumn(
name: "HighValueWords",
table: "SentenceAnalysisCache",
newName: "high_value_words");
migrationBuilder.RenameColumn(
name: "HasGrammarErrors",
table: "SentenceAnalysisCache",
newName: "has_grammar_errors");
migrationBuilder.RenameColumn(
name: "GrammarCorrections",
table: "SentenceAnalysisCache",
newName: "grammar_corrections");
migrationBuilder.RenameColumn(
name: "ExpiresAt",
table: "SentenceAnalysisCache",
newName: "expires_at");
migrationBuilder.RenameColumn(
name: "CreatedAt",
table: "SentenceAnalysisCache",
newName: "created_at");
migrationBuilder.RenameColumn(
name: "CorrectedText",
table: "SentenceAnalysisCache",
newName: "corrected_text");
migrationBuilder.RenameColumn(
name: "AnalysisResult",
table: "SentenceAnalysisCache",
newName: "analysis_result");
migrationBuilder.RenameColumn(
name: "AccessCount",
table: "SentenceAnalysisCache",
newName: "access_count");
migrationBuilder.RenameColumn(
name: "Id",
table: "flashcards",
newName: "id");
migrationBuilder.RenameColumn(
name: "Status",
table: "error_reports",
newName: "status");
migrationBuilder.RenameColumn(
name: "Description",
table: "error_reports",
newName: "description");
migrationBuilder.RenameColumn(
name: "Id",
table: "error_reports",
newName: "id");
migrationBuilder.RenameColumn(
name: "Date",
table: "daily_stats",
newName: "date");
migrationBuilder.RenameColumn(
name: "Id",
table: "daily_stats",
newName: "id");
migrationBuilder.RenameIndex(
name: "IX_daily_stats_user_id_Date",
table: "daily_stats",
newName: "IX_daily_stats_user_id_date");
migrationBuilder.AddForeignKey(
name: "FK_user_settings_user_profiles_user_id",
table: "user_settings",
column: "user_id",
principalTable: "user_profiles",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_WordQueryUsageStats_user_profiles_user_id",
table: "WordQueryUsageStats",
column: "user_id",
principalTable: "user_profiles",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_user_settings_user_profiles_user_id",
table: "user_settings");
migrationBuilder.DropForeignKey(
name: "FK_WordQueryUsageStats_user_profiles_user_id",
table: "WordQueryUsageStats");
migrationBuilder.RenameColumn(
name: "date",
table: "WordQueryUsageStats",
newName: "Date");
migrationBuilder.RenameColumn(
name: "id",
table: "WordQueryUsageStats",
newName: "Id");
migrationBuilder.RenameColumn(
name: "user_id",
table: "WordQueryUsageStats",
newName: "UserId");
migrationBuilder.RenameColumn(
name: "updated_at",
table: "WordQueryUsageStats",
newName: "UpdatedAt");
migrationBuilder.RenameColumn(
name: "unique_words_queried",
table: "WordQueryUsageStats",
newName: "UniqueWordsQueried");
migrationBuilder.RenameColumn(
name: "total_api_calls",
table: "WordQueryUsageStats",
newName: "TotalApiCalls");
migrationBuilder.RenameColumn(
name: "sentence_analysis_count",
table: "WordQueryUsageStats",
newName: "SentenceAnalysisCount");
migrationBuilder.RenameColumn(
name: "low_value_word_clicks",
table: "WordQueryUsageStats",
newName: "LowValueWordClicks");
migrationBuilder.RenameColumn(
name: "high_value_word_clicks",
table: "WordQueryUsageStats",
newName: "HighValueWordClicks");
migrationBuilder.RenameColumn(
name: "created_at",
table: "WordQueryUsageStats",
newName: "CreatedAt");
migrationBuilder.RenameColumn(
name: "id",
table: "user_settings",
newName: "Id");
migrationBuilder.RenameColumn(
name: "user_id",
table: "user_settings",
newName: "UserId");
migrationBuilder.RenameColumn(
name: "updated_at",
table: "user_settings",
newName: "UpdatedAt");
migrationBuilder.RenameColumn(
name: "show_pronunciation",
table: "user_settings",
newName: "ShowPronunciation");
migrationBuilder.RenameColumn(
name: "reminder_time",
table: "user_settings",
newName: "ReminderTime");
migrationBuilder.RenameColumn(
name: "reminder_enabled",
table: "user_settings",
newName: "ReminderEnabled");
migrationBuilder.RenameColumn(
name: "difficulty_preference",
table: "user_settings",
newName: "DifficultyPreference");
migrationBuilder.RenameColumn(
name: "daily_goal",
table: "user_settings",
newName: "DailyGoal");
migrationBuilder.RenameColumn(
name: "created_at",
table: "user_settings",
newName: "CreatedAt");
migrationBuilder.RenameColumn(
name: "auto_play_audio",
table: "user_settings",
newName: "AutoPlayAudio");
migrationBuilder.RenameIndex(
name: "IX_user_settings_user_id",
table: "user_settings",
newName: "IX_user_settings_UserId");
migrationBuilder.RenameColumn(
name: "id",
table: "user_profiles",
newName: "Id");
migrationBuilder.RenameColumn(
name: "name",
table: "tags",
newName: "Name");
migrationBuilder.RenameColumn(
name: "color",
table: "tags",
newName: "Color");
migrationBuilder.RenameColumn(
name: "id",
table: "tags",
newName: "Id");
migrationBuilder.RenameColumn(
name: "id",
table: "SentenceAnalysisCache",
newName: "Id");
migrationBuilder.RenameColumn(
name: "last_accessed_at",
table: "SentenceAnalysisCache",
newName: "LastAccessedAt");
migrationBuilder.RenameColumn(
name: "input_text_hash",
table: "SentenceAnalysisCache",
newName: "InputTextHash");
migrationBuilder.RenameColumn(
name: "input_text",
table: "SentenceAnalysisCache",
newName: "InputText");
migrationBuilder.RenameColumn(
name: "idioms_detected",
table: "SentenceAnalysisCache",
newName: "IdiomsDetected");
migrationBuilder.RenameColumn(
name: "high_value_words",
table: "SentenceAnalysisCache",
newName: "HighValueWords");
migrationBuilder.RenameColumn(
name: "has_grammar_errors",
table: "SentenceAnalysisCache",
newName: "HasGrammarErrors");
migrationBuilder.RenameColumn(
name: "grammar_corrections",
table: "SentenceAnalysisCache",
newName: "GrammarCorrections");
migrationBuilder.RenameColumn(
name: "expires_at",
table: "SentenceAnalysisCache",
newName: "ExpiresAt");
migrationBuilder.RenameColumn(
name: "created_at",
table: "SentenceAnalysisCache",
newName: "CreatedAt");
migrationBuilder.RenameColumn(
name: "corrected_text",
table: "SentenceAnalysisCache",
newName: "CorrectedText");
migrationBuilder.RenameColumn(
name: "analysis_result",
table: "SentenceAnalysisCache",
newName: "AnalysisResult");
migrationBuilder.RenameColumn(
name: "access_count",
table: "SentenceAnalysisCache",
newName: "AccessCount");
migrationBuilder.RenameColumn(
name: "id",
table: "flashcards",
newName: "Id");
migrationBuilder.RenameColumn(
name: "status",
table: "error_reports",
newName: "Status");
migrationBuilder.RenameColumn(
name: "description",
table: "error_reports",
newName: "Description");
migrationBuilder.RenameColumn(
name: "id",
table: "error_reports",
newName: "Id");
migrationBuilder.RenameColumn(
name: "date",
table: "daily_stats",
newName: "Date");
migrationBuilder.RenameColumn(
name: "id",
table: "daily_stats",
newName: "Id");
migrationBuilder.RenameIndex(
name: "IX_daily_stats_user_id_date",
table: "daily_stats",
newName: "IX_daily_stats_user_id_Date");
migrationBuilder.AddForeignKey(
name: "FK_user_settings_user_profiles_UserId",
table: "user_settings",
column: "UserId",
principalTable: "user_profiles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_WordQueryUsageStats_user_profiles_UserId",
table: "WordQueryUsageStats",
column: "UserId",
principalTable: "user_profiles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,122 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DramaLing.Api.Migrations
{
/// <inheritdoc />
public partial class FinalPascalCaseFieldsFix : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_user_audio_preferences_user_profiles_UserId",
table: "user_audio_preferences");
migrationBuilder.RenameColumn(
name: "UserId",
table: "user_audio_preferences",
newName: "user_id");
migrationBuilder.RenameColumn(
name: "Id",
table: "pronunciation_assessments",
newName: "id");
migrationBuilder.RenameColumn(
name: "Word",
table: "options_vocabularies",
newName: "word");
migrationBuilder.RenameColumn(
name: "Id",
table: "options_vocabularies",
newName: "id");
migrationBuilder.RenameColumn(
name: "Id",
table: "image_generation_requests",
newName: "id");
migrationBuilder.RenameColumn(
name: "Id",
table: "example_images",
newName: "id");
migrationBuilder.RenameColumn(
name: "Accent",
table: "audio_cache",
newName: "accent");
migrationBuilder.RenameColumn(
name: "Id",
table: "audio_cache",
newName: "id");
migrationBuilder.AddForeignKey(
name: "FK_user_audio_preferences_user_profiles_user_id",
table: "user_audio_preferences",
column: "user_id",
principalTable: "user_profiles",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_user_audio_preferences_user_profiles_user_id",
table: "user_audio_preferences");
migrationBuilder.RenameColumn(
name: "user_id",
table: "user_audio_preferences",
newName: "UserId");
migrationBuilder.RenameColumn(
name: "id",
table: "pronunciation_assessments",
newName: "Id");
migrationBuilder.RenameColumn(
name: "word",
table: "options_vocabularies",
newName: "Word");
migrationBuilder.RenameColumn(
name: "id",
table: "options_vocabularies",
newName: "Id");
migrationBuilder.RenameColumn(
name: "id",
table: "image_generation_requests",
newName: "Id");
migrationBuilder.RenameColumn(
name: "id",
table: "example_images",
newName: "Id");
migrationBuilder.RenameColumn(
name: "accent",
table: "audio_cache",
newName: "Accent");
migrationBuilder.RenameColumn(
name: "id",
table: "audio_cache",
newName: "Id");
migrationBuilder.AddForeignKey(
name: "FK_user_audio_preferences_user_profiles_UserId",
table: "user_audio_preferences",
column: "UserId",
principalTable: "user_profiles",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,94 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DramaLing.Api.Migrations
{
/// <inheritdoc />
public partial class FixTableNamesToSnakeCase : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_WordQueryUsageStats_user_profiles_user_id",
table: "WordQueryUsageStats");
migrationBuilder.DropPrimaryKey(
name: "PK_WordQueryUsageStats",
table: "WordQueryUsageStats");
migrationBuilder.DropPrimaryKey(
name: "PK_SentenceAnalysisCache",
table: "SentenceAnalysisCache");
migrationBuilder.RenameTable(
name: "WordQueryUsageStats",
newName: "word_query_usage_stats");
migrationBuilder.RenameTable(
name: "SentenceAnalysisCache",
newName: "sentence_analysis_cache");
migrationBuilder.AddPrimaryKey(
name: "PK_word_query_usage_stats",
table: "word_query_usage_stats",
column: "id");
migrationBuilder.AddPrimaryKey(
name: "PK_sentence_analysis_cache",
table: "sentence_analysis_cache",
column: "id");
migrationBuilder.AddForeignKey(
name: "FK_word_query_usage_stats_user_profiles_user_id",
table: "word_query_usage_stats",
column: "user_id",
principalTable: "user_profiles",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_word_query_usage_stats_user_profiles_user_id",
table: "word_query_usage_stats");
migrationBuilder.DropPrimaryKey(
name: "PK_word_query_usage_stats",
table: "word_query_usage_stats");
migrationBuilder.DropPrimaryKey(
name: "PK_sentence_analysis_cache",
table: "sentence_analysis_cache");
migrationBuilder.RenameTable(
name: "word_query_usage_stats",
newName: "WordQueryUsageStats");
migrationBuilder.RenameTable(
name: "sentence_analysis_cache",
newName: "SentenceAnalysisCache");
migrationBuilder.AddPrimaryKey(
name: "PK_WordQueryUsageStats",
table: "WordQueryUsageStats",
column: "id");
migrationBuilder.AddPrimaryKey(
name: "PK_SentenceAnalysisCache",
table: "SentenceAnalysisCache",
column: "id");
migrationBuilder.AddForeignKey(
name: "FK_WordQueryUsageStats_user_profiles_user_id",
table: "WordQueryUsageStats",
column: "user_id",
principalTable: "user_profiles",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
}
}
}

View File

@ -21,12 +21,14 @@ namespace DramaLing.Api.Migrations
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<string>("Accent")
.IsRequired()
.HasMaxLength(2)
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("accent");
b.Property<int>("AccessCount")
.HasColumnType("INTEGER")
@ -86,7 +88,8 @@ namespace DramaLing.Api.Migrations
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<int>("AiApiCalls")
.HasColumnType("INTEGER")
@ -101,7 +104,8 @@ namespace DramaLing.Api.Migrations
.HasColumnName("created_at");
b.Property<DateOnly>("Date")
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("date");
b.Property<int>("SessionCount")
.HasColumnType("INTEGER")
@ -135,7 +139,8 @@ namespace DramaLing.Api.Migrations
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<string>("AdminNotes")
.HasColumnType("TEXT")
@ -146,7 +151,8 @@ namespace DramaLing.Api.Migrations
.HasColumnName("created_at");
b.Property<string>("Description")
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("description");
b.Property<Guid>("FlashcardId")
.HasColumnType("TEXT")
@ -169,7 +175,8 @@ namespace DramaLing.Api.Migrations
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("status");
b.Property<string>("StudyMode")
.HasMaxLength(50)
@ -195,7 +202,8 @@ namespace DramaLing.Api.Migrations
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<int>("AccessCount")
.HasColumnType("INTEGER")
@ -299,40 +307,30 @@ namespace DramaLing.Api.Migrations
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("Definition")
.IsRequired()
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("definition");
b.Property<string>("DifficultyLevel")
.HasMaxLength(10)
.HasColumnType("TEXT")
.HasColumnName("difficulty_level");
b.Property<float>("EasinessFactor")
.HasColumnType("REAL")
.HasColumnName("easiness_factor");
b.Property<string>("Example")
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("example");
b.Property<string>("ExampleTranslation")
.HasColumnType("TEXT")
.HasColumnName("example_translation");
b.Property<string>("FilledQuestionText")
.HasMaxLength(1000)
.HasColumnType("TEXT");
b.Property<int>("IntervalDays")
.HasColumnType("INTEGER")
.HasColumnName("interval_days");
b.Property<bool>("IsArchived")
.HasColumnType("INTEGER")
.HasColumnName("is_archived");
@ -341,22 +339,6 @@ namespace DramaLing.Api.Migrations
.HasColumnType("INTEGER")
.HasColumnName("is_favorite");
b.Property<string>("LastQuestionType")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<DateTime?>("LastReviewedAt")
.HasColumnType("TEXT")
.HasColumnName("last_reviewed_at");
b.Property<int>("MasteryLevel")
.HasColumnType("INTEGER")
.HasColumnName("mastery_level");
b.Property<DateTime>("NextReviewDate")
.HasColumnType("TEXT")
.HasColumnName("next_review_date");
b.Property<string>("PartOfSpeech")
.HasMaxLength(50)
.HasColumnType("TEXT")
@ -364,29 +346,13 @@ namespace DramaLing.Api.Migrations
b.Property<string>("Pronunciation")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<int>("Repetitions")
.HasColumnType("INTEGER");
b.Property<string>("ReviewHistory")
.HasColumnType("TEXT");
b.Property<string>("Synonyms")
.HasMaxLength(2000)
.HasColumnType("TEXT");
b.Property<int>("TimesCorrect")
.HasColumnType("INTEGER")
.HasColumnName("times_correct");
b.Property<int>("TimesReviewed")
.HasColumnType("INTEGER")
.HasColumnName("times_reviewed");
.HasColumnType("TEXT")
.HasColumnName("pronunciation");
b.Property<string>("Translation")
.IsRequired()
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("translation");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT")
@ -399,7 +365,8 @@ namespace DramaLing.Api.Migrations
b.Property<string>("Word")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("word");
b.HasKey("Id");
@ -462,7 +429,8 @@ namespace DramaLing.Api.Migrations
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime?>("CompletedAt")
.HasColumnType("TEXT")
@ -582,7 +550,8 @@ namespace DramaLing.Api.Migrations
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<string>("CEFRLevel")
.IsRequired()
@ -617,7 +586,8 @@ namespace DramaLing.Api.Migrations
b.Property<string>("Word")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("word");
b.Property<int>("WordLength")
.HasColumnType("INTEGER")
@ -645,7 +615,8 @@ namespace DramaLing.Api.Migrations
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<decimal>("AccuracyScore")
.HasColumnType("TEXT")
@ -689,10 +660,6 @@ namespace DramaLing.Api.Migrations
.HasColumnType("TEXT")
.HasColumnName("prosody_score");
b.Property<Guid?>("StudySessionId")
.HasColumnType("TEXT")
.HasColumnName("study_session_id");
b.Property<string>("Suggestions")
.HasColumnType("TEXT")
.HasColumnName("suggestions");
@ -710,9 +677,6 @@ namespace DramaLing.Api.Migrations
b.HasIndex("FlashcardId");
b.HasIndex("StudySessionId")
.HasDatabaseName("IX_PronunciationAssessment_Session");
b.HasIndex("UserId", "FlashcardId")
.HasDatabaseName("IX_PronunciationAssessment_UserFlashcard");
@ -723,49 +687,62 @@ namespace DramaLing.Api.Migrations
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<int>("AccessCount")
.HasColumnType("INTEGER");
.HasColumnType("INTEGER")
.HasColumnName("access_count");
b.Property<string>("AnalysisResult")
.IsRequired()
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("analysis_result");
b.Property<string>("CorrectedText")
.HasMaxLength(1000)
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("corrected_text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("expires_at");
b.Property<string>("GrammarCorrections")
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("grammar_corrections");
b.Property<bool>("HasGrammarErrors")
.HasColumnType("INTEGER");
.HasColumnType("INTEGER")
.HasColumnName("has_grammar_errors");
b.Property<string>("HighValueWords")
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("high_value_words");
b.Property<string>("IdiomsDetected")
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("idioms_detected");
b.Property<string>("InputText")
.IsRequired()
.HasMaxLength(1000)
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("input_text");
b.Property<string>("InputTextHash")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("input_text_hash");
b.Property<DateTime?>("LastAccessedAt")
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("last_accessed_at");
b.HasKey("Id");
@ -778,209 +755,21 @@ namespace DramaLing.Api.Migrations
b.HasIndex("InputTextHash", "ExpiresAt")
.HasDatabaseName("IX_SentenceAnalysisCache_Hash_Expires");
b.ToTable("SentenceAnalysisCache");
});
modelBuilder.Entity("DramaLing.Api.Models.Entities.StudyCard", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime?>("CompletedAt")
.HasColumnType("TEXT");
b.Property<Guid>("FlashcardId")
.HasColumnType("TEXT");
b.Property<bool>("IsCompleted")
.HasColumnType("INTEGER");
b.Property<int>("Order")
.HasColumnType("INTEGER");
b.Property<string>("PlannedTests")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("PlannedTestsJson")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("StartedAt")
.HasColumnType("TEXT");
b.Property<Guid>("StudySessionId")
.HasColumnType("TEXT");
b.Property<string>("Word")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("FlashcardId");
b.HasIndex("StudySessionId");
b.ToTable("study_cards", (string)null);
});
modelBuilder.Entity("DramaLing.Api.Models.Entities.StudyRecord", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid>("FlashcardId")
.HasColumnType("TEXT")
.HasColumnName("flashcard_id");
b.Property<bool>("IsCorrect")
.HasColumnType("INTEGER")
.HasColumnName("is_correct");
b.Property<float>("NewEasinessFactor")
.HasColumnType("REAL");
b.Property<int>("NewIntervalDays")
.HasColumnType("INTEGER");
b.Property<int>("NewRepetitions")
.HasColumnType("INTEGER");
b.Property<DateTime>("NextReviewDate")
.HasColumnType("TEXT");
b.Property<float>("PreviousEasinessFactor")
.HasColumnType("REAL");
b.Property<int>("PreviousIntervalDays")
.HasColumnType("INTEGER");
b.Property<int>("PreviousRepetitions")
.HasColumnType("INTEGER");
b.Property<int>("QualityRating")
.HasColumnType("INTEGER")
.HasColumnName("quality_rating");
b.Property<int?>("ResponseTimeMs")
.HasColumnType("INTEGER")
.HasColumnName("response_time_ms");
b.Property<Guid>("SessionId")
.HasColumnType("TEXT")
.HasColumnName("session_id");
b.Property<DateTime>("StudiedAt")
.HasColumnType("TEXT")
.HasColumnName("studied_at");
b.Property<string>("StudyMode")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("study_mode");
b.Property<string>("UserAnswer")
.HasColumnType("TEXT")
.HasColumnName("user_answer");
b.Property<Guid>("UserId")
.HasColumnType("TEXT")
.HasColumnName("user_id");
b.HasKey("Id");
b.HasIndex("FlashcardId");
b.HasIndex("SessionId");
b.HasIndex("UserId", "FlashcardId", "StudyMode")
.IsUnique()
.HasDatabaseName("IX_StudyRecord_UserCard_TestType_Unique");
b.ToTable("study_records", (string)null);
});
modelBuilder.Entity("DramaLing.Api.Models.Entities.StudySession", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<int>("AverageResponseTimeMs")
.HasColumnType("INTEGER")
.HasColumnName("average_response_time_ms");
b.Property<int>("CompletedCards")
.HasColumnType("INTEGER");
b.Property<int>("CompletedTests")
.HasColumnType("INTEGER");
b.Property<int>("CorrectCount")
.HasColumnType("INTEGER")
.HasColumnName("correct_count");
b.Property<int>("CurrentCardIndex")
.HasColumnType("INTEGER");
b.Property<string>("CurrentTestType")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<int>("DurationSeconds")
.HasColumnType("INTEGER")
.HasColumnName("duration_seconds");
b.Property<DateTime?>("EndedAt")
.HasColumnType("TEXT")
.HasColumnName("ended_at");
b.Property<string>("SessionType")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("session_type");
b.Property<DateTime>("StartedAt")
.HasColumnType("TEXT")
.HasColumnName("started_at");
b.Property<int>("Status")
.HasColumnType("INTEGER");
b.Property<int>("TotalCards")
.HasColumnType("INTEGER")
.HasColumnName("total_cards");
b.Property<int>("TotalTests")
.HasColumnType("INTEGER");
b.Property<Guid>("UserId")
.HasColumnType("TEXT")
.HasColumnName("user_id");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("study_sessions", (string)null);
b.ToTable("sentence_analysis_cache", (string)null);
});
modelBuilder.Entity("DramaLing.Api.Models.Entities.Tag", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<string>("Color")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("color");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
@ -989,7 +778,8 @@ namespace DramaLing.Api.Migrations
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("name");
b.Property<int>("UsageCount")
.HasColumnType("INTEGER")
@ -1006,47 +796,12 @@ namespace DramaLing.Api.Migrations
b.ToTable("tags", (string)null);
});
modelBuilder.Entity("DramaLing.Api.Models.Entities.TestResult", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("CompletedAt")
.HasColumnType("TEXT");
b.Property<int?>("ConfidenceLevel")
.HasColumnType("INTEGER");
b.Property<bool>("IsCorrect")
.HasColumnType("INTEGER");
b.Property<int>("ResponseTimeMs")
.HasColumnType("INTEGER");
b.Property<Guid>("StudyCardId")
.HasColumnType("TEXT");
b.Property<string>("TestType")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("UserAnswer")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("StudyCardId");
b.ToTable("test_results", (string)null);
});
modelBuilder.Entity("DramaLing.Api.Models.Entities.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<string>("AvatarUrl")
.HasColumnType("TEXT")
@ -1127,7 +882,8 @@ namespace DramaLing.Api.Migrations
modelBuilder.Entity("DramaLing.Api.Models.Entities.UserAudioPreferences", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("user_id");
b.Property<bool>("AutoPlayEnabled")
.HasColumnType("INTEGER")
@ -1180,36 +936,46 @@ namespace DramaLing.Api.Migrations
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<bool>("AutoPlayAudio")
.HasColumnType("INTEGER");
.HasColumnType("INTEGER")
.HasColumnName("auto_play_audio");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<int>("DailyGoal")
.HasColumnType("INTEGER");
.HasColumnType("INTEGER")
.HasColumnName("daily_goal");
b.Property<string>("DifficultyPreference")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("difficulty_preference");
b.Property<bool>("ReminderEnabled")
.HasColumnType("INTEGER");
.HasColumnType("INTEGER")
.HasColumnName("reminder_enabled");
b.Property<TimeOnly>("ReminderTime")
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("reminder_time");
b.Property<bool>("ShowPronunciation")
.HasColumnType("INTEGER");
.HasColumnType("INTEGER")
.HasColumnName("show_pronunciation");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("updated_at");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("user_id");
b.HasKey("Id");
@ -1223,34 +989,44 @@ namespace DramaLing.Api.Migrations
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<DateOnly>("Date")
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("date");
b.Property<int>("HighValueWordClicks")
.HasColumnType("INTEGER");
.HasColumnType("INTEGER")
.HasColumnName("high_value_word_clicks");
b.Property<int>("LowValueWordClicks")
.HasColumnType("INTEGER");
.HasColumnType("INTEGER")
.HasColumnName("low_value_word_clicks");
b.Property<int>("SentenceAnalysisCount")
.HasColumnType("INTEGER");
.HasColumnType("INTEGER")
.HasColumnName("sentence_analysis_count");
b.Property<int>("TotalApiCalls")
.HasColumnType("INTEGER");
.HasColumnType("INTEGER")
.HasColumnName("total_api_calls");
b.Property<int>("UniqueWordsQueried")
.HasColumnType("INTEGER");
.HasColumnType("INTEGER")
.HasColumnName("unique_words_queried");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("updated_at");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
.HasColumnType("TEXT")
.HasColumnName("user_id");
b.HasKey("Id");
@ -1261,7 +1037,7 @@ namespace DramaLing.Api.Migrations
.IsUnique()
.HasDatabaseName("IX_WordQueryUsageStats_UserDate");
b.ToTable("WordQueryUsageStats");
b.ToTable("word_query_usage_stats", (string)null);
});
modelBuilder.Entity("DramaLing.Api.Models.Entities.DailyStats", b =>
@ -1383,11 +1159,6 @@ namespace DramaLing.Api.Migrations
.HasForeignKey("FlashcardId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("DramaLing.Api.Models.Entities.StudySession", "StudySession")
.WithMany()
.HasForeignKey("StudySessionId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("DramaLing.Api.Models.Entities.User", "User")
.WithMany()
.HasForeignKey("UserId")
@ -1396,65 +1167,6 @@ namespace DramaLing.Api.Migrations
b.Navigation("Flashcard");
b.Navigation("StudySession");
b.Navigation("User");
});
modelBuilder.Entity("DramaLing.Api.Models.Entities.StudyCard", b =>
{
b.HasOne("DramaLing.Api.Models.Entities.Flashcard", "Flashcard")
.WithMany()
.HasForeignKey("FlashcardId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DramaLing.Api.Models.Entities.StudySession", "StudySession")
.WithMany("StudyCards")
.HasForeignKey("StudySessionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Flashcard");
b.Navigation("StudySession");
});
modelBuilder.Entity("DramaLing.Api.Models.Entities.StudyRecord", b =>
{
b.HasOne("DramaLing.Api.Models.Entities.Flashcard", "Flashcard")
.WithMany("StudyRecords")
.HasForeignKey("FlashcardId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DramaLing.Api.Models.Entities.StudySession", "Session")
.WithMany("StudyRecords")
.HasForeignKey("SessionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DramaLing.Api.Models.Entities.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Flashcard");
b.Navigation("Session");
b.Navigation("User");
});
modelBuilder.Entity("DramaLing.Api.Models.Entities.StudySession", b =>
{
b.HasOne("DramaLing.Api.Models.Entities.User", "User")
.WithMany("StudySessions")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
@ -1469,17 +1181,6 @@ namespace DramaLing.Api.Migrations
b.Navigation("User");
});
modelBuilder.Entity("DramaLing.Api.Models.Entities.TestResult", b =>
{
b.HasOne("DramaLing.Api.Models.Entities.StudyCard", "StudyCard")
.WithMany("TestResults")
.HasForeignKey("StudyCardId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("StudyCard");
});
modelBuilder.Entity("DramaLing.Api.Models.Entities.UserAudioPreferences", b =>
{
b.HasOne("DramaLing.Api.Models.Entities.User", "User")
@ -1525,20 +1226,6 @@ namespace DramaLing.Api.Migrations
b.Navigation("FlashcardExampleImages");
b.Navigation("FlashcardTags");
b.Navigation("StudyRecords");
});
modelBuilder.Entity("DramaLing.Api.Models.Entities.StudyCard", b =>
{
b.Navigation("TestResults");
});
modelBuilder.Entity("DramaLing.Api.Models.Entities.StudySession", b =>
{
b.Navigation("StudyCards");
b.Navigation("StudyRecords");
});
modelBuilder.Entity("DramaLing.Api.Models.Entities.Tag", b =>
@ -1555,8 +1242,6 @@ namespace DramaLing.Api.Migrations
b.Navigation("Flashcards");
b.Navigation("Settings");
b.Navigation("StudySessions");
});
#pragma warning restore 612, 618
}

View File

@ -9,19 +9,37 @@ public class GeminiOptions
[Required(ErrorMessage = "Gemini API Key is required")]
public string ApiKey { get; set; } = string.Empty;
/// <summary>
/// API 請求超時時間(秒)
/// </summary>
[Range(1, 120, ErrorMessage = "Timeout must be between 1 and 120 seconds")]
public int TimeoutSeconds { get; set; } = 30;
/// <summary>
/// API 請求最大重試次數
/// </summary>
[Range(1, 10, ErrorMessage = "Max retries must be between 1 and 10")]
public int MaxRetries { get; set; } = 3;
/// <summary>
/// AI 回應最大 Token 數量
/// </summary>
[Range(100, 10000, ErrorMessage = "Max tokens must be between 100 and 10000")]
public int MaxOutputTokens { get; set; } = 2000;
/// <summary>
/// AI 回應的隨機性程度0.0-2.0
/// </summary>
[Range(0.0, 2.0, ErrorMessage = "Temperature must be between 0 and 2")]
public double Temperature { get; set; } = 0.7;
public string Model { get; set; } = "gemini-1.5-flash";
/// <summary>
/// 使用的 Gemini 模型名稱
/// </summary>
public string Model { get; set; } = "gemini-2.0-flash";
/// <summary>
/// Gemini API 基本 URL
/// </summary>
public string BaseUrl { get; set; } = "https://generativelanguage.googleapis.com";
}

View File

@ -26,17 +26,27 @@ builder.Services.Configure<ReplicateOptions>(
builder.Configuration.GetSection(ReplicateOptions.SectionName));
builder.Services.AddSingleton<IValidateOptions<ReplicateOptions>, ReplicateOptionsValidator>();
// 在開發環境設定測試用的API Key
if (builder.Environment.IsDevelopment() &&
string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GEMINI_API_KEY")))
// 在開發環境設定 API Key - 修復配置讀取邏輯
if (builder.Environment.IsDevelopment())
{
builder.Services.PostConfigure<GeminiOptions>(options =>
{
// 只有當 ApiKey 未設置時才嘗試從其他來源讀取
if (string.IsNullOrEmpty(options.ApiKey))
{
options.ApiKey = Environment.GetEnvironmentVariable("GEMINI_API_KEY")
?? builder.Configuration["Gemini:ApiKey"]
?? "test-key";
// 優先從環境變數讀取
var envApiKey = Environment.GetEnvironmentVariable("GEMINI_API_KEY");
if (!string.IsNullOrEmpty(envApiKey))
{
options.ApiKey = envApiKey;
}
// 如果環境變數沒有Configuration 應該已經包含 user-secrets
// 這裡只是作為後備,不應該覆蓋已經從 user-secrets 載入的設定
else if (string.IsNullOrEmpty(builder.Configuration["Gemini:ApiKey"]))
{
// 只有在真的沒有任何配置時才使用測試金鑰
options.ApiKey = "test-key";
}
}
});
}

View File

@ -19,15 +19,6 @@
"Frontend": {
"Urls": ["http://localhost:3000", "http://localhost:3001"]
},
"Gemini": {
"ApiKey": "",
"TimeoutSeconds": 30,
"MaxRetries": 3,
"MaxOutputTokens": 2000,
"Temperature": 0.7,
"Model": "gemini-1.5-flash",
"BaseUrl": "https://generativelanguage.googleapis.com"
},
"Replicate": {
"ApiKey": "",
"BaseUrl": "https://api.replicate.com/v1",