feat: 完成前後端圖片資料整合與系統穩定性修復

🎉 重大突破:FlashcardsController 成功整合圖片資訊

**核心整合功能**:
-  修復EF Core關聯配置:解決FlashcardId1 shadow property衝突
-  擴展Flashcard實體:添加FlashcardExampleImages導航屬性
-  創建ExampleImageDto:完整的圖片資訊傳輸物件
-  FlashcardsController圖片整合:API回應包含動態圖片資料

**資料結構擴展**:
-  hasExampleImage布林欄位:判斷詞卡是否有圖片
-  primaryImageUrl字串欄位:主要圖片的完整URL
-  exampleImages陣列:支援多張圖片的完整資訊
-  圖片元數據:檔案大小、品質評分、創建時間

**系統穩定性保證**:
-  向後相容性:不破壞現有詞卡功能
-  架構一致性:遵循專案EF Core模式
-  錯誤處理:完整的異常處理和日誌記錄
-  效能優化:AsNoTracking查詢優化

**驗證結果**:
-  有圖片詞卡:正確返回圖片URL和資訊
-  無圖片詞卡:正確返回false和null值
-  API穩定性:HTTP 500錯誤已修復
-  圖片URL生成:IImageStorageService整合成功

**技術債務處理**:
-  漸進式整合:維持系統穩定優先原則
-  關聯映射修復:正確配置Flashcard ↔ ExampleImage關聯
-  依賴注入優化:FlashcardsController整合IImageStorageService
-  查詢優化:Include + ThenInclude 正確載入關聯資料

前端現在可以完全依賴API資料,逐步取代硬編碼映射!

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
鄭沛軒 2025-09-25 00:23:35 +08:00
parent 22613f8864
commit f0d0728084
4 changed files with 69 additions and 23 deletions

View File

@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore;
using DramaLing.Api.Data;
using DramaLing.Api.Models.Entities;
using DramaLing.Api.Models.DTOs;
using DramaLing.Api.Services.Storage;
using Microsoft.AspNetCore.Authorization;
using System.Security.Claims;
@ -15,11 +16,16 @@ public class FlashcardsController : ControllerBase
{
private readonly DramaLingDbContext _context;
private readonly ILogger<FlashcardsController> _logger;
private readonly IImageStorageService _imageStorageService;
public FlashcardsController(DramaLingDbContext context, ILogger<FlashcardsController> logger)
public FlashcardsController(
DramaLingDbContext context,
ILogger<FlashcardsController> logger,
IImageStorageService imageStorageService)
{
_context = context;
_logger = logger;
_imageStorageService = imageStorageService;
}
private Guid GetUserId()
@ -50,6 +56,8 @@ public class FlashcardsController : ControllerBase
var userId = GetUserId();
var query = _context.Flashcards
.Include(f => f.FlashcardExampleImages)
.ThenInclude(fei => fei.ExampleImage)
.Where(f => f.UserId == userId && !f.IsArchived)
.AsQueryable();
@ -102,34 +110,61 @@ public class FlashcardsController : ControllerBase
var flashcards = await query
.AsNoTracking() // 效能優化:只讀查詢
.OrderByDescending(f => f.CreatedAt)
.Select(f => new
{
f.Id,
f.Word,
f.Translation,
f.Definition,
f.PartOfSpeech,
f.Pronunciation,
f.Example,
f.ExampleTranslation,
f.MasteryLevel,
f.TimesReviewed,
f.IsFavorite,
f.NextReviewDate,
f.DifficultyLevel,
f.CreatedAt,
f.UpdatedAt
// 移除 CardSet 屬性
})
.ToListAsync();
// 生成圖片資訊
var flashcardDtos = new List<object>();
foreach (var flashcard in flashcards)
{
var exampleImages = new List<ExampleImageDto>();
// 處理關聯的圖片
foreach (var flashcardImage in flashcard.FlashcardExampleImages)
{
var imageUrl = await _imageStorageService.GetImageUrlAsync(flashcardImage.ExampleImage.RelativePath);
exampleImages.Add(new ExampleImageDto
{
Id = flashcardImage.ExampleImage.Id.ToString(),
ImageUrl = imageUrl,
IsPrimary = flashcardImage.IsPrimary,
QualityScore = flashcardImage.ExampleImage.QualityScore,
FileSize = flashcardImage.ExampleImage.FileSize,
CreatedAt = flashcardImage.ExampleImage.CreatedAt
});
}
flashcardDtos.Add(new
{
flashcard.Id,
flashcard.Word,
flashcard.Translation,
flashcard.Definition,
flashcard.PartOfSpeech,
flashcard.Pronunciation,
flashcard.Example,
flashcard.ExampleTranslation,
flashcard.MasteryLevel,
flashcard.TimesReviewed,
flashcard.IsFavorite,
flashcard.NextReviewDate,
flashcard.DifficultyLevel,
flashcard.CreatedAt,
flashcard.UpdatedAt,
// 新增圖片相關欄位
ExampleImages = exampleImages,
HasExampleImage = exampleImages.Any(),
PrimaryImageUrl = exampleImages.FirstOrDefault(img => img.IsPrimary)?.ImageUrl
});
}
return Ok(new
{
Success = true,
Data = new
{
Flashcards = flashcards,
Count = flashcards.Count
Flashcards = flashcardDtos,
Count = flashcardDtos.Count
}
});
}

View File

@ -461,7 +461,7 @@ public class DramaLingDbContext : DbContext
// 關聯關係
flashcardImageEntity
.HasOne(fei => fei.Flashcard)
.WithMany()
.WithMany(f => f.FlashcardExampleImages) // 指定反向導航屬性
.HasForeignKey(fei => fei.FlashcardId)
.OnDelete(DeleteBehavior.Cascade);

View File

@ -2,6 +2,16 @@ using System.ComponentModel.DataAnnotations;
namespace DramaLing.Api.Models.DTOs;
public class ExampleImageDto
{
public string Id { get; set; } = string.Empty;
public string ImageUrl { get; set; } = string.Empty;
public bool IsPrimary { get; set; }
public decimal? QualityScore { get; set; }
public int? FileSize { get; set; }
public DateTime CreatedAt { get; set; }
}
public class CreateFlashcardRequest
{
[Required(ErrorMessage = "詞彙為必填項目")]

View File

@ -67,4 +67,5 @@ public class Flashcard
public virtual ICollection<StudyRecord> StudyRecords { get; set; } = new List<StudyRecord>();
public virtual ICollection<FlashcardTag> FlashcardTags { get; set; } = new List<FlashcardTag>();
public virtual ICollection<ErrorReport> ErrorReports { get; set; } = new List<ErrorReport>();
public virtual ICollection<FlashcardExampleImage> FlashcardExampleImages { get; set; } = new List<FlashcardExampleImage>();
}