generated from dellevin/template
优化新增人物功能
This commit is contained in:
@@ -81,7 +81,7 @@ class DatabaseHelper {
|
||||
|
||||
return await openDatabase(
|
||||
path,
|
||||
version: 38,
|
||||
version: 39,
|
||||
onCreate: _createDB,
|
||||
onUpgrade: _onUpgrade,
|
||||
);
|
||||
@@ -401,6 +401,10 @@ class DatabaseHelper {
|
||||
await db.execute('ALTER TABLE playlists ADD COLUMN sort_order INTEGER DEFAULT 0');
|
||||
}
|
||||
}
|
||||
if (oldVersion < 39) {
|
||||
// 创建人物表和关联表
|
||||
await _createPeopleTables(db);
|
||||
}
|
||||
}
|
||||
Future<void> _upgradeBooksTableV26(Database db) async {
|
||||
final columns = await db.rawQuery('PRAGMA table_info(books)');
|
||||
@@ -1033,6 +1037,74 @@ class DatabaseHelper {
|
||||
await db.execute(
|
||||
'CREATE INDEX idx_playlist_items_playlist ON playlist_items(playlist_id)',
|
||||
);
|
||||
|
||||
// 人物表
|
||||
await _createPeopleTables(db);
|
||||
}
|
||||
|
||||
/// 创建人物表和关联表
|
||||
Future<void> _createPeopleTables(Database db) async {
|
||||
await db.execute('''
|
||||
CREATE TABLE IF NOT EXISTS people (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
gender TEXT,
|
||||
birth_date TEXT,
|
||||
birth_place TEXT,
|
||||
alternate_names TEXT DEFAULT '[]',
|
||||
occupation TEXT DEFAULT '[]',
|
||||
summary TEXT,
|
||||
photo_path TEXT,
|
||||
cover_offset REAL DEFAULT 0,
|
||||
is_deleted INTEGER DEFAULT 0,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)
|
||||
''');
|
||||
|
||||
await db.execute('''
|
||||
CREATE TABLE IF NOT EXISTS movie_people (
|
||||
id TEXT PRIMARY KEY,
|
||||
movie_id TEXT NOT NULL,
|
||||
person_id TEXT NOT NULL,
|
||||
role_type TEXT NOT NULL,
|
||||
character_name TEXT,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
FOREIGN KEY (movie_id) REFERENCES movies (id),
|
||||
FOREIGN KEY (person_id) REFERENCES people (id)
|
||||
)
|
||||
''');
|
||||
|
||||
await db.execute('''
|
||||
CREATE TABLE IF NOT EXISTS book_people (
|
||||
id TEXT PRIMARY KEY,
|
||||
book_id TEXT NOT NULL,
|
||||
person_id TEXT NOT NULL,
|
||||
role_type TEXT NOT NULL,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
FOREIGN KEY (book_id) REFERENCES books (id),
|
||||
FOREIGN KEY (person_id) REFERENCES people (id)
|
||||
)
|
||||
''');
|
||||
|
||||
await db.execute('''
|
||||
CREATE TABLE IF NOT EXISTS game_people (
|
||||
id TEXT PRIMARY KEY,
|
||||
game_id TEXT NOT NULL,
|
||||
person_id TEXT NOT NULL,
|
||||
role_type TEXT NOT NULL,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
FOREIGN KEY (game_id) REFERENCES games (id),
|
||||
FOREIGN KEY (person_id) REFERENCES people (id)
|
||||
)
|
||||
''');
|
||||
|
||||
await db.execute('CREATE INDEX IF NOT EXISTS idx_movie_people_movie ON movie_people(movie_id)');
|
||||
await db.execute('CREATE INDEX IF NOT EXISTS idx_movie_people_person ON movie_people(person_id)');
|
||||
await db.execute('CREATE INDEX IF NOT EXISTS idx_book_people_book ON book_people(book_id)');
|
||||
await db.execute('CREATE INDEX IF NOT EXISTS idx_book_people_person ON book_people(person_id)');
|
||||
await db.execute('CREATE INDEX IF NOT EXISTS idx_game_people_game ON game_people(game_id)');
|
||||
await db.execute('CREATE INDEX IF NOT EXISTS idx_game_people_person ON game_people(person_id)');
|
||||
}
|
||||
|
||||
// 关闭数据库
|
||||
|
||||
87
lib/data/person/book_person_dao.dart
Normal file
87
lib/data/person/book_person_dao.dart
Normal file
@@ -0,0 +1,87 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../../models/data_models.dart';
|
||||
import '../database_helper.dart';
|
||||
|
||||
/// 书籍↔人物关联数据访问对象
|
||||
class BookPersonDao {
|
||||
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
|
||||
|
||||
Future<T> _wrap<T>(String op, Future<T> Function() fn) async {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (e) {
|
||||
debugPrint('[BookPersonDao] $op error: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取某本书的所有关联人物
|
||||
Future<List<BookPerson>> getByBookId(String bookId) => _wrap('getByBookId', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final maps = await db.query(
|
||||
'book_people',
|
||||
where: 'book_id = ?',
|
||||
whereArgs: [bookId],
|
||||
orderBy: 'role_type, sort_order',
|
||||
);
|
||||
return maps.map((m) => BookPerson.fromJson(m)).toList();
|
||||
});
|
||||
|
||||
// 获取某个人物参与的所有书籍关联
|
||||
Future<List<BookPerson>> getByPersonId(String personId) => _wrap('getByPersonId', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final maps = await db.query(
|
||||
'book_people',
|
||||
where: 'person_id = ?',
|
||||
whereArgs: [personId],
|
||||
orderBy: 'sort_order',
|
||||
);
|
||||
return maps.map((m) => BookPerson.fromJson(m)).toList();
|
||||
});
|
||||
|
||||
// 添加关联
|
||||
Future<int> insert(BookPerson bookPerson) => _wrap('insert', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.insert('book_people', bookPerson.toJson());
|
||||
});
|
||||
|
||||
// 批量添加关联
|
||||
Future<void> insertAll(List<BookPerson> items) => _wrap('insertAll', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final batch = db.batch();
|
||||
for (final item in items) {
|
||||
batch.insert('book_people', item.toJson());
|
||||
}
|
||||
await batch.commit(noResult: true);
|
||||
});
|
||||
|
||||
// 删除某本书的所有关联
|
||||
Future<int> deleteByBookId(String bookId) => _wrap('deleteByBookId', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.delete('book_people', where: 'book_id = ?', whereArgs: [bookId]);
|
||||
});
|
||||
|
||||
// 删除某个人物的所有书籍关联
|
||||
Future<int> deleteByPersonId(String personId) => _wrap('deleteByPersonId', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.delete('book_people', where: 'person_id = ?', whereArgs: [personId]);
|
||||
});
|
||||
|
||||
// 删除单条关联
|
||||
Future<int> deleteById(String id) => _wrap('deleteById', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.delete('book_people', where: 'id = ?', whereArgs: [id]);
|
||||
});
|
||||
|
||||
// 检查关联是否已存在(避免重复插入)
|
||||
Future<bool> existsRelation(String bookId, String personId, String roleType) => _wrap('existsRelation', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final maps = await db.query(
|
||||
'book_people',
|
||||
where: 'book_id = ? AND person_id = ? AND role_type = ?',
|
||||
whereArgs: [bookId, personId, roleType],
|
||||
limit: 1,
|
||||
);
|
||||
return maps.isNotEmpty;
|
||||
});
|
||||
}
|
||||
87
lib/data/person/game_person_dao.dart
Normal file
87
lib/data/person/game_person_dao.dart
Normal file
@@ -0,0 +1,87 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../../models/data_models.dart';
|
||||
import '../database_helper.dart';
|
||||
|
||||
/// 游戏↔人物关联数据访问对象
|
||||
class GamePersonDao {
|
||||
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
|
||||
|
||||
Future<T> _wrap<T>(String op, Future<T> Function() fn) async {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (e) {
|
||||
debugPrint('[GamePersonDao] $op error: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取某游戏的所有关联人物
|
||||
Future<List<GamePerson>> getByGameId(String gameId) => _wrap('getByGameId', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final maps = await db.query(
|
||||
'game_people',
|
||||
where: 'game_id = ?',
|
||||
whereArgs: [gameId],
|
||||
orderBy: 'sort_order',
|
||||
);
|
||||
return maps.map((m) => GamePerson.fromJson(m)).toList();
|
||||
});
|
||||
|
||||
// 获取某个人物参与的所有游戏关联
|
||||
Future<List<GamePerson>> getByPersonId(String personId) => _wrap('getByPersonId', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final maps = await db.query(
|
||||
'game_people',
|
||||
where: 'person_id = ?',
|
||||
whereArgs: [personId],
|
||||
orderBy: 'sort_order',
|
||||
);
|
||||
return maps.map((m) => GamePerson.fromJson(m)).toList();
|
||||
});
|
||||
|
||||
// 添加关联
|
||||
Future<int> insert(GamePerson gamePerson) => _wrap('insert', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.insert('game_people', gamePerson.toJson());
|
||||
});
|
||||
|
||||
// 批量添加关联
|
||||
Future<void> insertAll(List<GamePerson> items) => _wrap('insertAll', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final batch = db.batch();
|
||||
for (final item in items) {
|
||||
batch.insert('game_people', item.toJson());
|
||||
}
|
||||
await batch.commit(noResult: true);
|
||||
});
|
||||
|
||||
// 删除某游戏的所有关联
|
||||
Future<int> deleteByGameId(String gameId) => _wrap('deleteByGameId', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.delete('game_people', where: 'game_id = ?', whereArgs: [gameId]);
|
||||
});
|
||||
|
||||
// 删除某个人物的所有游戏关联
|
||||
Future<int> deleteByPersonId(String personId) => _wrap('deleteByPersonId', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.delete('game_people', where: 'person_id = ?', whereArgs: [personId]);
|
||||
});
|
||||
|
||||
// 删除单条关联
|
||||
Future<int> deleteById(String id) => _wrap('deleteById', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.delete('game_people', where: 'id = ?', whereArgs: [id]);
|
||||
});
|
||||
|
||||
// 检查关联是否已存在(避免重复插入)
|
||||
Future<bool> existsRelation(String gameId, String personId, String roleType) => _wrap('existsRelation', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final maps = await db.query(
|
||||
'game_people',
|
||||
where: 'game_id = ? AND person_id = ? AND role_type = ?',
|
||||
whereArgs: [gameId, personId, roleType],
|
||||
limit: 1,
|
||||
);
|
||||
return maps.isNotEmpty;
|
||||
});
|
||||
}
|
||||
110
lib/data/person/movie_person_dao.dart
Normal file
110
lib/data/person/movie_person_dao.dart
Normal file
@@ -0,0 +1,110 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../../models/data_models.dart';
|
||||
import '../database_helper.dart';
|
||||
|
||||
/// 影视↔人物关联数据访问对象
|
||||
class MoviePersonDao {
|
||||
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
|
||||
|
||||
Future<T> _wrap<T>(String op, Future<T> Function() fn) async {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (e) {
|
||||
debugPrint('[MoviePersonDao] $op error: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取某部影视的所有关联人物
|
||||
Future<List<MoviePerson>> getByMovieId(String movieId) => _wrap('getByMovieId', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final maps = await db.query(
|
||||
'movie_people',
|
||||
where: 'movie_id = ?',
|
||||
whereArgs: [movieId],
|
||||
orderBy: 'role_type, sort_order',
|
||||
);
|
||||
return maps.map((m) => MoviePerson.fromJson(m)).toList();
|
||||
});
|
||||
|
||||
// 获取某个人物参与的所有影视关联
|
||||
Future<List<MoviePerson>> getByPersonId(String personId) => _wrap('getByPersonId', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final maps = await db.query(
|
||||
'movie_people',
|
||||
where: 'person_id = ?',
|
||||
whereArgs: [personId],
|
||||
orderBy: 'sort_order',
|
||||
);
|
||||
return maps.map((m) => MoviePerson.fromJson(m)).toList();
|
||||
});
|
||||
|
||||
// 按影视+角色类型获取人物
|
||||
Future<List<MoviePerson>> getByMovieAndRole(String movieId, String roleType) => _wrap('getByMovieAndRole', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final maps = await db.query(
|
||||
'movie_people',
|
||||
where: 'movie_id = ? AND role_type = ?',
|
||||
whereArgs: [movieId, roleType],
|
||||
orderBy: 'sort_order',
|
||||
);
|
||||
return maps.map((m) => MoviePerson.fromJson(m)).toList();
|
||||
});
|
||||
|
||||
// 添加关联
|
||||
Future<int> insert(MoviePerson moviePerson) => _wrap('insert', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.insert('movie_people', moviePerson.toJson());
|
||||
});
|
||||
|
||||
// 批量添加关联
|
||||
Future<void> insertAll(List<MoviePerson> items) => _wrap('insertAll', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final batch = db.batch();
|
||||
for (final item in items) {
|
||||
batch.insert('movie_people', item.toJson());
|
||||
}
|
||||
await batch.commit(noResult: true);
|
||||
});
|
||||
|
||||
// 更新关联(如修改角色名)
|
||||
Future<int> update(MoviePerson moviePerson) => _wrap('update', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.update(
|
||||
'movie_people',
|
||||
moviePerson.toJson(),
|
||||
where: 'id = ?',
|
||||
whereArgs: [moviePerson.id],
|
||||
);
|
||||
});
|
||||
|
||||
// 删除某部影视的所有关联
|
||||
Future<int> deleteByMovieId(String movieId) => _wrap('deleteByMovieId', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.delete('movie_people', where: 'movie_id = ?', whereArgs: [movieId]);
|
||||
});
|
||||
|
||||
// 删除某个人物的所有影视关联
|
||||
Future<int> deleteByPersonId(String personId) => _wrap('deleteByPersonId', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.delete('movie_people', where: 'person_id = ?', whereArgs: [personId]);
|
||||
});
|
||||
|
||||
// 删除单条关联
|
||||
Future<int> deleteById(String id) => _wrap('deleteById', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.delete('movie_people', where: 'id = ?', whereArgs: [id]);
|
||||
});
|
||||
|
||||
// 检查关联是否已存在(避免重复插入)
|
||||
Future<bool> existsRelation(String movieId, String personId, String roleType) => _wrap('existsRelation', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final maps = await db.query(
|
||||
'movie_people',
|
||||
where: 'movie_id = ? AND person_id = ? AND role_type = ?',
|
||||
whereArgs: [movieId, personId, roleType],
|
||||
limit: 1,
|
||||
);
|
||||
return maps.isNotEmpty;
|
||||
});
|
||||
}
|
||||
313
lib/data/person/person_dao.dart
Normal file
313
lib/data/person/person_dao.dart
Normal file
@@ -0,0 +1,313 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import '../../models/data_models.dart';
|
||||
import '../database_helper.dart';
|
||||
|
||||
/// 人物数据访问对象
|
||||
class PersonDao {
|
||||
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
|
||||
|
||||
Future<T> _wrap<T>(String op, Future<T> Function() fn) async {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (e) {
|
||||
debugPrint('[PersonDao] $op error: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有人物(未删除的)
|
||||
Future<List<Person>> getAllPeople() => _wrap('getAllPeople', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final maps = await db.query(
|
||||
'people',
|
||||
where: 'is_deleted = ?',
|
||||
whereArgs: [0],
|
||||
orderBy: 'updated_at DESC',
|
||||
);
|
||||
return maps.map((m) => Person.fromJson(m)).toList();
|
||||
});
|
||||
|
||||
// 搜索人物(名称或别名)
|
||||
Future<List<Person>> searchPeople(String keyword) => _wrap('searchPeople', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final like = '%$keyword%';
|
||||
final maps = await db.query(
|
||||
'people',
|
||||
where: '(name LIKE ? OR alternate_names LIKE ?) AND is_deleted = ?',
|
||||
whereArgs: [like, like, 0],
|
||||
orderBy: 'updated_at DESC',
|
||||
);
|
||||
return maps.map((m) => Person.fromJson(m)).toList();
|
||||
});
|
||||
|
||||
// 按职业筛选
|
||||
Future<List<Person>> getPeopleByOccupation(String occupation) => _wrap('getPeopleByOccupation', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final maps = await db.query(
|
||||
'people',
|
||||
where: 'occupation LIKE ? AND is_deleted = ?',
|
||||
whereArgs: ['%$occupation%', 0],
|
||||
orderBy: 'updated_at DESC',
|
||||
);
|
||||
return maps.map((m) => Person.fromJson(m))
|
||||
.where((p) => p.occupation.contains(occupation))
|
||||
.toList();
|
||||
});
|
||||
|
||||
// 根据名称精确查找人物(未删除)
|
||||
Future<Person?> getPersonByName(String name) => _wrap('getPersonByName', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final maps = await db.query(
|
||||
'people',
|
||||
where: 'name = ? AND is_deleted = ?',
|
||||
whereArgs: [name, 0],
|
||||
limit: 1,
|
||||
);
|
||||
if (maps.isEmpty) return null;
|
||||
return Person.fromJson(maps.first);
|
||||
});
|
||||
|
||||
// 根据ID获取人物
|
||||
Future<Person?> getPersonById(String id) => _wrap('getPersonById', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final maps = await db.query(
|
||||
'people',
|
||||
where: 'id = ? AND is_deleted = ?',
|
||||
whereArgs: [id, 0],
|
||||
);
|
||||
if (maps.isEmpty) return null;
|
||||
return Person.fromJson(maps.first);
|
||||
});
|
||||
|
||||
// 添加人物
|
||||
Future<int> insertPerson(Person person) => _wrap('insertPerson', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.insert('people', person.toJson());
|
||||
});
|
||||
|
||||
// 更新人物
|
||||
Future<int> updatePerson(Person person) => _wrap('updatePerson', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.update(
|
||||
'people',
|
||||
person.toJson(),
|
||||
where: 'id = ?',
|
||||
whereArgs: [person.id],
|
||||
);
|
||||
});
|
||||
|
||||
// 仅更新封面偏移量
|
||||
Future<void> updateCoverOffset(String personId, double offset) => _wrap('updateCoverOffset', () async {
|
||||
final db = await _dbHelper.database;
|
||||
await db.update('people', {'cover_offset': offset}, where: 'id = ?', whereArgs: [personId]);
|
||||
});
|
||||
|
||||
// 软删除人物
|
||||
Future<int> deletePerson(String id) => _wrap('deletePerson', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.update(
|
||||
'people',
|
||||
{'is_deleted': 1, 'updated_at': DateTime.now().toIso8601String()},
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
});
|
||||
|
||||
// 恢复已删除的人物
|
||||
Future<int> restorePerson(String id) => _wrap('restorePerson', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.update(
|
||||
'people',
|
||||
{'is_deleted': 0, 'updated_at': DateTime.now().toIso8601String()},
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
});
|
||||
|
||||
// 获取已删除的人物
|
||||
Future<List<Person>> getDeletedPeople() => _wrap('getDeletedPeople', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final maps = await db.query(
|
||||
'people',
|
||||
where: 'is_deleted = ?',
|
||||
whereArgs: [1],
|
||||
orderBy: 'updated_at DESC',
|
||||
);
|
||||
return maps.map((m) => Person.fromJson(m)).toList();
|
||||
});
|
||||
|
||||
// 彻底删除人物
|
||||
Future<int> permanentDeletePerson(String id) => _wrap('permanentDeletePerson', () async {
|
||||
final db = await _dbHelper.database;
|
||||
// 清理关联记录
|
||||
await db.delete('movie_people', where: 'person_id = ?', whereArgs: [id]);
|
||||
await db.delete('book_people', where: 'person_id = ?', whereArgs: [id]);
|
||||
await db.delete('game_people', where: 'person_id = ?', whereArgs: [id]);
|
||||
return await db.delete('people', where: 'id = ?', whereArgs: [id]);
|
||||
});
|
||||
|
||||
/// 合并同名人物:每个名字组保留信息最全的一条,迁移关联并去重,删除冗余。
|
||||
/// 返回被删除的冗余人物数量。
|
||||
Future<int> mergeDuplicatePeople() => _wrap('mergeDuplicatePeople', () async {
|
||||
final db = await _dbHelper.database;
|
||||
// 取所有人物(含已删除),按 name 分组
|
||||
final maps = await db.query('people', orderBy: 'updated_at DESC');
|
||||
final byName = <String, List<Map<String, Object?>>>{};
|
||||
for (final m in maps) {
|
||||
final name = (m['name'] as String?)?.trim() ?? '';
|
||||
if (name.isEmpty) continue;
|
||||
byName.putIfAbsent(name, () => []).add(m);
|
||||
}
|
||||
|
||||
int removed = 0;
|
||||
final now = DateTime.now().toUtc().toIso8601String();
|
||||
|
||||
for (final entry in byName.entries) {
|
||||
final group = entry.value;
|
||||
if (group.length < 2) continue;
|
||||
|
||||
// 选保留者:优先未删除的、updated_at 最新的;信息更全的优先
|
||||
group.sort((a, b) {
|
||||
final aDel = (a['is_deleted'] as int?) == 1;
|
||||
final bDel = (b['is_deleted'] as int?) == 1;
|
||||
if (aDel != bDel) return aDel ? 1 : -1; // 未删除的排前
|
||||
final aScore = _infoScore(a);
|
||||
final bScore = _infoScore(b);
|
||||
if (aScore != bScore) return bScore - aScore; // 信息多的排前
|
||||
return 0;
|
||||
});
|
||||
final keeperRow = group.first;
|
||||
final keeperId = keeperRow['id'] as String;
|
||||
final dupes = group.skip(1).toList();
|
||||
|
||||
// 合并字段到 keeper
|
||||
final merged = Map<String, Object?>.from(keeperRow);
|
||||
for (final dupe in dupes) {
|
||||
_mergeFieldsInto(merged, dupe);
|
||||
}
|
||||
merged['updated_at'] = now;
|
||||
await db.update('people', merged, where: 'id = ?', whereArgs: [keeperId]);
|
||||
|
||||
// 迁移关联并去重
|
||||
for (final dupe in dupes) {
|
||||
final dupeId = dupe['id'] as String;
|
||||
await _migrateRelations(db, dupeId, keeperId, 'movie_people', 'movie_id');
|
||||
await _migrateRelations(db, dupeId, keeperId, 'book_people', 'book_id');
|
||||
await _migrateRelations(db, dupeId, keeperId, 'game_people', 'game_id');
|
||||
// 删除冗余人物
|
||||
await db.delete('people', where: 'id = ?', whereArgs: [dupeId]);
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
return removed;
|
||||
});
|
||||
|
||||
// 信息完整度评分(非空字段越多分越高)
|
||||
int _infoScore(Map<String, Object?> row) {
|
||||
int score = 0;
|
||||
if ((row['photo_path'] as String?)?.isNotEmpty == true) score += 5;
|
||||
if ((row['summary'] as String?)?.isNotEmpty == true) score += 3;
|
||||
if ((row['gender'] as String?)?.isNotEmpty == true) score += 1;
|
||||
if ((row['birth_date'] as String?)?.isNotEmpty == true) score += 1;
|
||||
if ((row['birth_place'] as String?)?.isNotEmpty == true) score += 1;
|
||||
final alt = (row['alternate_names'] as String?) ?? '';
|
||||
if (alt.isNotEmpty && alt != '[]') score += 1;
|
||||
final occ = (row['occupation'] as String?) ?? '';
|
||||
if (occ.isNotEmpty && occ != '[]') score += 1;
|
||||
return score;
|
||||
}
|
||||
|
||||
// 把 dupe 的非空字段并进 merged(merged 已有的非空值不覆盖)
|
||||
void _mergeFieldsInto(Map<String, Object?> merged, Map<String, Object?> dupe) {
|
||||
void mergeField(String key) {
|
||||
final cur = merged[key];
|
||||
final curEmpty = cur == null || (cur is String && cur.isEmpty);
|
||||
final dup = dupe[key];
|
||||
final dupEmpty = dup == null || (dup is String && dup.isEmpty);
|
||||
if (curEmpty && !dupEmpty) merged[key] = dup;
|
||||
}
|
||||
mergeField('photo_path');
|
||||
mergeField('summary');
|
||||
mergeField('gender');
|
||||
mergeField('birth_date');
|
||||
mergeField('birth_place');
|
||||
|
||||
// 列表字段取并集
|
||||
merged['alternate_names'] = _unionListJson(merged['alternate_names'], dupe['alternate_names']);
|
||||
merged['occupation'] = _unionListJson(merged['occupation'], dupe['occupation']);
|
||||
}
|
||||
|
||||
String _unionListJson(Object? a, Object? b) {
|
||||
final set = <String>{};
|
||||
for (final raw in [a, b]) {
|
||||
if (raw == null) continue;
|
||||
final s = raw.toString();
|
||||
if (s.isEmpty || s == '[]') continue;
|
||||
try {
|
||||
final list = jsonDecode(s);
|
||||
if (list is List) {
|
||||
for (final e in list) {
|
||||
if (e != null) set.add(e.toString());
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
return jsonEncode(set.toList());
|
||||
}
|
||||
|
||||
// 把 fromPersonId 的关联迁移到 toPersonId,按 (work_id, role_type) 去重
|
||||
Future<void> _migrateRelations(
|
||||
Database db,
|
||||
String fromPersonId,
|
||||
String toPersonId,
|
||||
String table,
|
||||
String workIdCol,
|
||||
) async {
|
||||
// 取 dupe 的所有关联
|
||||
final dupeRows = await db.query(
|
||||
table,
|
||||
columns: [workIdCol, 'role_type'],
|
||||
where: 'person_id = ?',
|
||||
whereArgs: [fromPersonId],
|
||||
);
|
||||
// 取 keeper 已有的关联键集合
|
||||
final keeperRows = await db.query(
|
||||
table,
|
||||
columns: [workIdCol, 'role_type'],
|
||||
where: 'person_id = ?',
|
||||
whereArgs: [toPersonId],
|
||||
);
|
||||
final keeperKeys = <String>{};
|
||||
for (final r in keeperRows) {
|
||||
keeperKeys.add('${r[workIdCol]}|${r['role_type']}');
|
||||
}
|
||||
// 把不与 keeper 冲突的关联改指向 keeper
|
||||
for (final r in dupeRows) {
|
||||
final key = '${r[workIdCol]}|${r['role_type']}';
|
||||
if (keeperKeys.contains(key)) continue;
|
||||
await db.update(
|
||||
table,
|
||||
{'person_id': toPersonId},
|
||||
where: 'person_id = ? AND $workIdCol = ? AND role_type = ?',
|
||||
whereArgs: [fromPersonId, r[workIdCol], r['role_type']],
|
||||
);
|
||||
keeperKeys.add(key);
|
||||
}
|
||||
// 删除剩余的(与 keeper 冲突的)关联
|
||||
await db.delete(table, where: 'person_id = ?', whereArgs: [fromPersonId]);
|
||||
}
|
||||
|
||||
// 获取所有人物名称(去重,供选择器使用)
|
||||
Future<List<String>> getAllPersonNames() => _wrap('getAllPersonNames', () async {
|
||||
final people = await getAllPeople();
|
||||
final names = <String>{};
|
||||
for (final p in people) {
|
||||
names.add(p.name);
|
||||
names.addAll(p.alternateNames);
|
||||
}
|
||||
return names.toList()..sort();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user