chore: 批量代码优化与功能迭代更新

本次提交包含大量代码优化、功能新增与服务端配置更新:
1. 修复分析报告统计数据,调整CMake策略设置
2. 优化APP权限配置、编辑器与聊天界面组件
3. 更新依赖库版本与pubspec配置
4. 新增文件传输服务端、信令服务器相关配置与脚本
5. 完善用户注销功能与数据库迁移脚本
6. 优化多处动画效果、代码风格与日志输出
7. 新增多种调试与部署脚本,修复已知BUG
This commit is contained in:
Developer
2026-05-12 06:28:04 +08:00
parent 72f64f9ca9
commit 283950ea07
245 changed files with 50255 additions and 6160 deletions

View File

@@ -1,14 +1,15 @@
/// ============================================================
/// 闲言APP — Drift 数据库定义
/// 创建时间: 2026-04-20
/// 更新时间: 2026-05-08
/// 更新时间: 2026-05-12
/// 作用: 本地 SQLite 数据库表结构定义 (Drift)
/// 上次更新: 新增传输表TransferDevices/TransferRecords/PairingRecords/TransferMessages (v12)
/// 上次更新: v13 新增6张表(云暂存/统计/剪贴板/画布/语音) + ALTER传输表断点续传/回执字段
/// ============================================================
import 'package:drift/drift.dart';
import 'package:hive/hive.dart';
import 'package:intl/intl.dart';
import 'package:xianyan/core/utils/logger.dart';
import 'database_connection/native.dart'
if (dart.library.html) 'database_connection/web.dart';
@@ -401,6 +402,11 @@ class TransferMsgRecords extends Table {
TextColumn get transferStatus => text().nullable()();
TextColumn get deviceAlias => text().nullable()();
TextColumn get deviceEmoji => text().nullable()();
TextColumn get deliveryStatus => text().nullable()();
DateTimeColumn get deliveredAt => dateTime().nullable()();
DateTimeColumn get readAt => dateTime().nullable()();
IntColumn get voiceDuration => integer().nullable()();
TextColumn get voiceWaveform => text().nullable()();
DateTimeColumn get timestamp => dateTime()();
DateTimeColumn get createdAt => dateTime()();
@@ -408,6 +414,145 @@ class TransferMsgRecords extends Table {
Set<Column> get primaryKey => {id};
}
// ============================================================
// 云暂存记录表 — 文件传输助手
// ============================================================
class CloudCacheRecords extends Table {
TextColumn get id => text()();
TextColumn get fileName => text()();
IntColumn get fileSize => integer().withDefault(const Constant(0))();
TextColumn get mimeType =>
text().withDefault(const Constant('application/octet-stream'))();
TextColumn get localPath => text().nullable()();
TextColumn get cloudUrl => text().nullable()();
TextColumn get encryptionKey => text().nullable()();
TextColumn get iv => text().nullable()();
TextColumn get uploadStatus =>
text().withDefault(const Constant('pending'))();
TextColumn get downloadStatus => text().withDefault(const Constant('none'))();
DateTimeColumn get expiresAt => dateTime().nullable()();
DateTimeColumn get uploadedAt => dateTime().nullable()();
DateTimeColumn get downloadedAt => dateTime().nullable()();
TextColumn get ownerId => text().withDefault(const Constant(''))();
DateTimeColumn get createdAt => dateTime()();
DateTimeColumn get updatedAt => dateTime()();
@override
Set<Column> get primaryKey => {id};
}
// ============================================================
// 传输统计表 — 文件传输助手
// ============================================================
class TransferStatsRecords extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get date => text()();
IntColumn get totalSentBytes => integer().withDefault(const Constant(0))();
IntColumn get totalReceivedBytes =>
integer().withDefault(const Constant(0))();
IntColumn get fileCount => integer().withDefault(const Constant(0))();
RealColumn get avgSpeed => real().withDefault(const Constant(0.0))();
TextColumn get transportType => text().withDefault(const Constant('all'))();
DateTimeColumn get createdAt => dateTime()();
}
// ============================================================
// 剪贴板记录表 — 文件传输助手
// ============================================================
class ClipboardRecords extends Table {
TextColumn get id => text()();
TextColumn get content => text()();
TextColumn get contentType => text().withDefault(const Constant('text'))();
TextColumn get sourceDevice => text().withDefault(const Constant(''))();
TextColumn get deviceId => text().withDefault(const Constant(''))();
BoolColumn get isPinned => boolean().withDefault(const Constant(false))();
DateTimeColumn get createdAt => dateTime()();
@override
Set<Column> get primaryKey => {id};
}
// ============================================================
// 协作画布文档表 — 文件传输助手
// ============================================================
class CanvasDocuments extends Table {
TextColumn get id => text()();
TextColumn get name => text()();
TextColumn get ownerId => text().withDefault(const Constant(''))();
IntColumn get width => integer().withDefault(const Constant(1920))();
IntColumn get height => integer().withDefault(const Constant(1080))();
TextColumn get backgroundJson => text().withDefault(const Constant('{}'))();
DateTimeColumn get createdAt => dateTime()();
DateTimeColumn get updatedAt => dateTime()();
@override
Set<Column> get primaryKey => {id};
}
// ============================================================
// 协作画布笔画表 — 文件传输助手
// ============================================================
class CanvasStrokes extends Table {
TextColumn get id => text()();
TextColumn get documentId => text()();
TextColumn get userId => text()();
TextColumn get pointsJson => text()();
TextColumn get color => text().withDefault(const Constant('#000000'))();
RealColumn get strokeWidth => real().withDefault(const Constant(2.0))();
TextColumn get toolType => text().withDefault(const Constant('pen'))();
DateTimeColumn get createdAt => dateTime()();
@override
Set<Column> get primaryKey => {id};
}
// ============================================================
// 语音消息表 — 文件传输助手
// ============================================================
class VoiceMessages extends Table {
TextColumn get id => text()();
TextColumn get messageId => text()();
TextColumn get sessionId => text()();
TextColumn get filePath => text()();
IntColumn get duration => integer().withDefault(const Constant(0))();
TextColumn get waveformJson => text().withDefault(const Constant('[]'))();
BoolColumn get isRemote => boolean().withDefault(const Constant(false))();
DateTimeColumn get createdAt => dateTime()();
@override
Set<Column> get primaryKey => {id};
}
// ============================================================
// 迁移版本表 — 记录已执行的迁移步骤
// ============================================================
class SchemaMigrations extends Table {
IntColumn get version => integer()();
DateTimeColumn get executedAt => dateTime()();
@override
Set<Column> get primaryKey => {version};
}
// ============================================================
// 迁移错误日志表 — 记录迁移失败的详细信息
// ============================================================
class MigrationErrors extends Table {
IntColumn get id => integer().autoIncrement()();
IntColumn get version => integer()();
TextColumn get errorMessage => text()();
TextColumn get errorStack => text()();
DateTimeColumn get failedAt => dateTime()();
}
// ============================================================
// 数据库实例
// ============================================================
@@ -433,6 +578,14 @@ class TransferMsgRecords extends Table {
TransferRecords,
PairingRecords,
TransferMsgRecords,
CloudCacheRecords,
TransferStatsRecords,
ClipboardRecords,
CanvasDocuments,
CanvasStrokes,
VoiceMessages,
SchemaMigrations,
MigrationErrors,
],
)
class AppDatabase extends _$AppDatabase {
@@ -442,86 +595,304 @@ class AppDatabase extends _$AppDatabase {
static AppDatabase get instance => _instance;
@override
int get schemaVersion => 12;
int get schemaVersion => 13;
Future<bool> _isMigrationExecuted(int version) async {
final result = await (select(
schemaMigrations,
)..where((t) => t.version.equals(version))).getSingleOrNull();
return result != null;
}
Future<void> _markMigrationExecuted(int version) async {
await into(schemaMigrations).insert(
SchemaMigrationsCompanion(
version: Value(version),
executedAt: Value(DateTime.now()),
),
);
}
Future<void> _runMigration(
int version,
Future<void> Function() migration,
) async {
if (await _isMigrationExecuted(version)) {
return;
}
try {
await migration();
await _markMigrationExecuted(version);
} catch (e, stack) {
await customStatement(
'INSERT OR REPLACE INTO migration_errors (version, error_message, error_stack, failed_at) VALUES (?, ?, ?, ?)',
[
version,
e.toString(),
stack.toString(),
DateTime.now().toIso8601String(),
],
);
rethrow;
}
}
Future<void> _safeAddColumn(
String table,
String column,
String definition,
) async {
final result = await customSelect(
'SELECT COUNT(*) AS cnt FROM pragma_table_info(?) WHERE name = ?',
variables: [Variable.withString(table), Variable.withString(column)],
).getSingle();
final exists = result.read<int>('cnt') > 0;
if (!exists) {
await customStatement('ALTER TABLE $table ADD COLUMN $definition');
Log.i('DB Migration: Added column $column to $table');
} else {
Log.i('DB Migration: Column $column already exists in $table, skipping');
}
}
Future<void> _migrateToV2(Migrator m) async {
await m.createTable(toolUsageStats);
}
Future<void> _migrateToV3(Migrator m) async {
await m.createTable(feedCache);
await m.createTable(offlineActionQueue);
}
Future<void> _migrateToV4(Migrator m) async {
await m.createTable(hanziCaches);
}
Future<void> _migrateToV5(Migrator m) async {
await m.addColumn(sentences, sentences.feedType);
await m.addColumn(sentences, sentences.feedName);
await m.addColumn(sentences, sentences.feedIcon);
await m.addColumn(sentences, sentences.views);
}
Future<void> _migrateToV6(Migrator m) async {
await m.createTable(shareHistories);
}
Future<void> _migrateToV7(Migrator m) async {
await m.createTable(learningPlans);
await m.createTable(learningRecords);
}
Future<void> _migrateToV8(Migrator m) async {
await _safeAddColumn(
'sentences',
'isLiked',
'isLiked INTEGER NOT NULL DEFAULT 0',
);
}
Future<void> _migrateToV9(Migrator m) async {
await m.createTable(chatConversations);
await m.createTable(chatMsgRecords);
await m.createTable(chatAttachments);
await m.createTable(ipLocationCaches);
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_chat_msg_records_conv ON chat_msg_records (conversation_id, timestamp DESC)',
);
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_chat_msg_records_conv_deleted ON chat_msg_records (conversation_id, is_deleted)',
);
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_chat_attachments_msg ON chat_attachments (message_id)',
);
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_chat_attachments_conv ON chat_attachments (conversation_id)',
);
}
Future<void> _migrateToV10(Migrator m) async {
await _safeAddColumn(
'chat_msg_records',
'reply_to_id',
'reply_to_id TEXT DEFAULT NULL',
);
await _safeAddColumn(
'chat_msg_records',
'rich_content',
'rich_content TEXT NOT NULL DEFAULT \'\'',
);
await _safeAddColumn(
'chat_msg_records',
'ip_text',
'ip_text TEXT NOT NULL DEFAULT \'\'',
);
await _safeAddColumn(
'chat_msg_records',
'ip_detail_json',
'ip_detail_json TEXT NOT NULL DEFAULT \'\'',
);
}
Future<void> _migrateToV11(Migrator m) async {
await _safeAddColumn(
'chat_conversations',
'is_hidden',
'is_hidden INTEGER NOT NULL DEFAULT 0',
);
}
Future<void> _migrateToV12(Migrator m) async {
await m.createTable(transferDeviceRecords);
await m.createTable(transferRecords);
await m.createTable(pairingRecords);
await m.createTable(transferMsgRecords);
}
Future<void> _migrateToV13(Migrator m) async {
await m.createTable(cloudCacheRecords);
await m.createTable(transferStatsRecords);
await m.createTable(clipboardRecords);
await m.createTable(canvasDocuments);
await m.createTable(canvasStrokes);
await m.createTable(voiceMessages);
await _safeAddColumn(
'transfer_records',
'file_id',
'file_id TEXT DEFAULT NULL',
);
await _safeAddColumn(
'transfer_records',
'chunk_size',
'chunk_size INTEGER NOT NULL DEFAULT 65536',
);
await _safeAddColumn(
'transfer_records',
'total_chunks',
'total_chunks INTEGER DEFAULT NULL',
);
await _safeAddColumn(
'transfer_records',
'received_chunks',
'received_chunks TEXT DEFAULT NULL',
);
await _safeAddColumn(
'transfer_records',
'retry_count',
'retry_count INTEGER NOT NULL DEFAULT 0',
);
await _safeAddColumn(
'transfer_records',
'is_resumable',
'is_resumable INTEGER NOT NULL DEFAULT 0',
);
await _safeAddColumn(
'transfer_records',
'paused_at',
'paused_at TEXT DEFAULT NULL',
);
await _safeAddColumn(
'transfer_msg_records',
'delivery_status',
'delivery_status TEXT DEFAULT NULL',
);
await _safeAddColumn(
'transfer_msg_records',
'delivered_at',
'delivered_at TEXT DEFAULT NULL',
);
await _safeAddColumn(
'transfer_msg_records',
'read_at',
'read_at TEXT DEFAULT NULL',
);
await _safeAddColumn(
'transfer_msg_records',
'voice_duration',
'voice_duration INTEGER DEFAULT NULL',
);
await _safeAddColumn(
'transfer_msg_records',
'voice_waveform',
'voice_waveform TEXT DEFAULT NULL',
);
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_cloud_cache_records_owner ON cloud_cache_records (owner_id)',
);
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_transfer_stats_records_date ON transfer_stats_records (date)',
);
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_clipboard_records_created ON clipboard_records (created_at DESC)',
);
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_canvas_strokes_document ON canvas_strokes (document_id)',
);
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_voice_messages_session ON voice_messages (session_id)',
);
}
@override
MigrationStrategy get migration => MigrationStrategy(
onCreate: (m) => m.createAll(),
onUpgrade: (m, from, to) async {
if (from < 2) {
await m.createTable(toolUsageStats);
}
if (from < 3) {
await m.createTable(feedCache);
await m.createTable(offlineActionQueue);
}
if (from < 4) {
await m.createTable(hanziCaches);
}
if (from < 5) {
await m.addColumn(sentences, sentences.feedType);
await m.addColumn(sentences, sentences.feedName);
await m.addColumn(sentences, sentences.feedIcon);
await m.addColumn(sentences, sentences.views);
}
if (from < 6) {
await m.createTable(shareHistories);
}
if (from < 7) {
await m.createTable(learningPlans);
await m.createTable(learningRecords);
}
if (from < 8) {
await customStatement(
'ALTER TABLE sentences ADD COLUMN isLiked INTEGER NOT NULL DEFAULT 0',
);
}
if (from < 9) {
await m.createTable(chatConversations);
await m.createTable(chatMsgRecords);
await m.createTable(chatAttachments);
await m.createTable(ipLocationCaches);
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_chat_msg_records_conv ON chat_msg_records (conversation_id, timestamp DESC)',
);
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_chat_msg_records_conv_deleted ON chat_msg_records (conversation_id, is_deleted)',
);
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_chat_attachments_msg ON chat_attachments (message_id)',
);
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_chat_attachments_conv ON chat_attachments (conversation_id)',
);
}
if (from < 10) {
await customStatement(
'ALTER TABLE chat_msg_records ADD COLUMN reply_to_id TEXT DEFAULT NULL',
);
await customStatement(
'ALTER TABLE chat_msg_records ADD COLUMN rich_content TEXT NOT NULL DEFAULT \'\'',
);
await customStatement(
'ALTER TABLE chat_msg_records ADD COLUMN ip_text TEXT NOT NULL DEFAULT \'\'',
);
await customStatement(
'ALTER TABLE chat_msg_records ADD COLUMN ip_detail_json TEXT NOT NULL DEFAULT \'\'',
);
}
if (from < 11) {
await customStatement(
'ALTER TABLE chat_conversations ADD COLUMN is_hidden INTEGER NOT NULL DEFAULT 0',
);
}
if (from < 12) {
await m.createTable(transferDeviceRecords);
await m.createTable(transferRecords);
await m.createTable(pairingRecords);
await m.createTable(transferMsgRecords);
onCreate: (m) async {
await m.createAll();
for (int v = 2; v <= schemaVersion; v++) {
await _markMigrationExecuted(v);
}
},
onUpgrade: (m, from, to) async {
bool migrationTableExists = false;
try {
await customStatement('SELECT 1 FROM schema_migrations LIMIT 1');
migrationTableExists = true;
} catch (_) {
migrationTableExists = false;
}
if (!migrationTableExists) {
await m.createTable(schemaMigrations);
await m.createTable(migrationErrors);
for (int v = 2; v <= from; v++) {
await _markMigrationExecuted(v);
}
}
await _runMigration(2, () => _migrateToV2(m));
await _runMigration(3, () => _migrateToV3(m));
await _runMigration(4, () => _migrateToV4(m));
await _runMigration(5, () => _migrateToV5(m));
await _runMigration(6, () => _migrateToV6(m));
await _runMigration(7, () => _migrateToV7(m));
await _runMigration(8, () => _migrateToV8(m));
await _runMigration(9, () => _migrateToV9(m));
await _runMigration(10, () => _migrateToV10(m));
await _runMigration(11, () => _migrateToV11(m));
await _runMigration(12, () => _migrateToV12(m));
await _runMigration(13, () => _migrateToV13(m));
},
);
Future<MigrationStatus> getMigrationStatus() async {
final executedMigrations = await (select(
schemaMigrations,
)..orderBy([(t) => OrderingTerm.asc(t.version)])).get();
final failedMigrations = await (select(
migrationErrors,
)..orderBy([(t) => OrderingTerm.desc(t.failedAt)])).get();
return MigrationStatus(
currentVersion: schemaVersion,
executedVersions: executedMigrations.map((e) => e.version).toSet(),
failedMigrations: failedMigrations,
);
}
// ---- 句子 CRUD ----
// ---- 分享历史 CRUD ----
@@ -1456,3 +1827,20 @@ class HistorySentenceWithTime {
final Sentence sentence;
final DateTime readAt;
}
class MigrationStatus {
const MigrationStatus({
required this.currentVersion,
required this.executedVersions,
required this.failedMigrations,
});
final int currentVersion;
final Set<int> executedVersions;
final List<MigrationError> failedMigrations;
bool get hasFailedMigrations => failedMigrations.isNotEmpty;
bool get isFullyMigrated =>
executedVersions.contains(currentVersion) && !hasFailedMigrations;
}

File diff suppressed because it is too large Load Diff