import 'dart:convert'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:sqflite/sqflite.dart'; import 'package:path/path.dart' as p; import '../models/data_models.dart'; import 'database_helper.dart'; import 'storage_helper.dart'; /// 数据迁移帮助类:将旧版文件系统数据迁移到 SQLite 数据库 class DataMigration { final StorageHelper _storage = StorageHelper.instance; final DatabaseHelper _db = DatabaseHelper.instance; static bool _hasMigrated = false; /// 执行数据迁移(幂等,只会执行一次) Future migrateIfNeeded() async { if (_hasMigrated) return; _hasMigrated = true; try { await _migrateMovies(); await _migrateBooks(); await _migrateNotes(); debugPrint('数据迁移完成'); } catch (e, stack) { debugPrint('数据迁移失败: $e'); debugPrint('堆栈: $stack'); } } /// 迁移影视数据 Future _migrateMovies() async { final moviesDirPath = await _storage.moviesDir; final movieDirs = await _listSubdirNames(moviesDirPath); if (movieDirs.isEmpty) return; debugPrint('发现 ${movieDirs.length} 个影视目录,开始迁移...'); final db = await _db.database; for (final dirName in movieDirs) { try { final dirPath = p.join(moviesDirPath, dirName); final dataPath = '$dirPath/data.json'; final data = await _readJsonFile(dataPath); if (data == null) continue; final movie = Movie.fromJson(data); await db.insert( 'movies', _movieToMap(movie), conflictAlgorithm: ConflictAlgorithm.ignore, ); // 迁移影评 await _migrateMovieReviews(dirPath, movie.id); // 迁移海报 await _migrateMoviePosters(dirPath, movie.id); } catch (e) { debugPrint('迁移影视 $dirName 失败: $e'); } } } /// 迁移影评 Future _migrateMovieReviews(String movieDirPath, String movieId) async { final reviewsDir = p.join(movieDirPath, 'reviews'); if (!await Directory(reviewsDir).exists()) return; final files = await _listJsonFiles(reviewsDir); final db = await _db.database; for (final data in files) { try { final review = MovieReview.fromJson(data); await db.insert( 'movie_reviews', _movieReviewToMap(review), conflictAlgorithm: ConflictAlgorithm.ignore, ); } catch (e) { debugPrint('迁移影评失败: $e'); } } } /// 迁移海报 Future _migrateMoviePosters(String movieDirPath, String movieId) async { final postersDir = p.join(movieDirPath, 'posters'); if (!await Directory(postersDir).exists()) return; final files = await _listJsonFiles(postersDir); final db = await _db.database; for (final data in files) { try { final poster = MoviePoster.fromJson(data); await db.insert( 'movie_posters', _moviePosterToMap(poster), conflictAlgorithm: ConflictAlgorithm.ignore, ); } catch (e) { debugPrint('迁移海报失败: $e'); } } } /// 迁移书籍数据 Future _migrateBooks() async { final booksDirPath = await _storage.booksDir; final bookDirs = await _listSubdirNames(booksDirPath); if (bookDirs.isEmpty) return; debugPrint('发现 ${bookDirs.length} 个书籍目录,开始迁移...'); final db = await _db.database; for (final dirName in bookDirs) { try { final dirPath = p.join(booksDirPath, dirName); final dataPath = '$dirPath/data.json'; final data = await _readJsonFile(dataPath); if (data == null) continue; final book = Book.fromJson(data); await db.insert( 'books', _bookToMap(book), conflictAlgorithm: ConflictAlgorithm.ignore, ); // 迁移书评 await _migrateBookReviews(dirPath, book.id); // 迁移摘抄 await _migrateBookExcerpts(dirPath, book.id); } catch (e) { debugPrint('迁移书籍 $dirName 失败: $e'); } } } /// 迁移书评 Future _migrateBookReviews(String bookDirPath, String bookId) async { final reviewsDir = p.join(bookDirPath, 'reviews'); if (!await Directory(reviewsDir).exists()) return; final files = await _listJsonFiles(reviewsDir); final db = await _db.database; for (final data in files) { try { final review = BookReview.fromJson(data); await db.insert( 'book_reviews', _bookReviewToMap(review), conflictAlgorithm: ConflictAlgorithm.ignore, ); } catch (e) { debugPrint('迁移书评失败: $e'); } } } /// 迁移摘抄 Future _migrateBookExcerpts(String bookDirPath, String bookId) async { final excerptsDir = p.join(bookDirPath, 'excerpts'); if (!await Directory(excerptsDir).exists()) return; final files = await _listJsonFiles(excerptsDir); final db = await _db.database; for (final data in files) { try { final excerpt = BookExcerpt.fromJson(data); await db.insert( 'book_excerpts', _bookExcerptToMap(excerpt), conflictAlgorithm: ConflictAlgorithm.ignore, ); } catch (e) { debugPrint('迁移摘抄失败: $e'); } } } /// 迁移笔记数据 Future _migrateNotes() async { final notesDirPath = await _storage.notesDir; final noteDirs = await _listSubdirNames(notesDirPath); if (noteDirs.isEmpty) return; debugPrint('发现 ${noteDirs.length} 个笔记目录,开始迁移...'); final db = await _db.database; for (final dirName in noteDirs) { try { final dirPath = p.join(notesDirPath, dirName); final dataPath = '$dirPath/data.json'; final data = await _readJsonFile(dataPath); if (data == null) continue; final note = Note.fromJson(data); await db.insert( 'notes', _noteToMap(note), conflictAlgorithm: ConflictAlgorithm.ignore, ); } catch (e) { debugPrint('迁移笔记 $dirName 失败: $e'); } } } // ========== 转换方法 ========== Map _movieToMap(Movie movie) { return { 'id': movie.id, 'title': movie.title, 'poster_path': movie.posterPath, 'release_date': movie.releaseDate?.toIso8601String(), 'directors': jsonEncode(movie.directors), 'writers': jsonEncode(movie.writers), 'actors': jsonEncode(movie.actors), 'genres': jsonEncode(movie.genres), 'alternate_titles': jsonEncode(movie.alternateTitles), 'summary': movie.summary, 'rating': movie.rating, 'status': movie.status, 'watch_date': movie.watchDate?.toIso8601String(), 'created_at': movie.createdAt.toIso8601String(), 'updated_at': movie.updatedAt.toIso8601String(), 'is_deleted': movie.isDeleted ? 1 : 0, }; } Map _bookToMap(Book book) { return { 'id': book.id, 'title': book.title, 'cover_path': book.coverPath, 'authors': jsonEncode(book.authors), 'alternate_titles': jsonEncode(book.alternateTitles), 'publisher': book.publisher, 'genres': jsonEncode(book.genres), 'summary': book.summary, 'rating': book.rating, 'status': book.status, 'isbn': book.isbn, 'publish_date': book.publishDate?.toIso8601String(), 'created_at': book.createdAt.toIso8601String(), 'updated_at': book.updatedAt.toIso8601String(), 'is_deleted': book.isDeleted ? 1 : 0, }; } Map _noteToMap(Note note) { return { 'id': note.id, 'content': note.content, 'content_type': note.contentType, 'tags': jsonEncode(note.tags), 'images': jsonEncode(note.images), 'created_at': note.createdAt.toIso8601String(), 'updated_at': note.updatedAt.toIso8601String(), 'is_deleted': note.isDeleted ? 1 : 0, }; } Map _movieReviewToMap(MovieReview review) { return { 'id': review.id, 'movie_id': review.movieId, 'content': review.content, 'reviewer': review.reviewer, 'source': review.source, 'review_type': review.reviewType, 'is_deleted': review.isDeleted ? 1 : 0, 'created_at': review.createdAt.toIso8601String(), 'updated_at': review.updatedAt.toIso8601String(), }; } Map _moviePosterToMap(MoviePoster poster) { return { 'id': poster.id, 'movie_id': poster.movieId, 'poster_path': poster.posterPath, 'is_deleted': poster.isDeleted ? 1 : 0, 'created_at': poster.createdAt.toIso8601String(), }; } Map _bookReviewToMap(BookReview review) { return { 'id': review.id, 'book_id': review.bookId, 'content': review.content, 'reviewer': review.reviewer, 'source': review.source, 'review_type': review.reviewType, 'is_deleted': review.isDeleted ? 1 : 0, 'created_at': review.createdAt.toIso8601String(), 'updated_at': review.updatedAt.toIso8601String(), }; } Map _bookExcerptToMap(BookExcerpt excerpt) { return { 'id': excerpt.id, 'book_id': excerpt.bookId, 'chapter': excerpt.chapter, 'content': excerpt.content, 'comment': excerpt.comment, 'is_deleted': excerpt.isDeleted ? 1 : 0, 'created_at': excerpt.createdAt.toIso8601String(), 'updated_at': excerpt.updatedAt.toIso8601String(), }; } // ========== 辅助方法 ========== /// 列出子目录名 Future> _listSubdirNames(String dirPath) async { try { final dir = Directory(dirPath); if (!await dir.exists()) return []; final entities = await dir.list().toList(); return entities .whereType() .map((e) => p.basename(e.path)) .toList(); } catch (e) { return []; } } /// 读取 JSON 文件 Future?> _readJsonFile(String path) async { try { final file = File(path); if (!await file.exists()) return null; final content = await file.readAsString(); return jsonDecode(content) as Map; } catch (e) { return null; } } /// 列出目录中的 JSON 文件并解析 Future>> _listJsonFiles(String dirPath) async { try { final dir = Directory(dirPath); if (!await dir.exists()) return []; final files = await dir .list() .where((entity) => entity is File && entity.path.endsWith('.json')) .toList(); final results = >[]; for (final file in files) { final data = await _readJsonFile(file.path); if (data != null) results.add(data); } return results; } catch (e) { return []; } } }