You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
133 lines
5.6 KiB
133 lines
5.6 KiB
import 'dart:io';
|
|
|
|
import 'package:get/get.dart';
|
|
|
|
import '../../data/services/db/database_helper.dart';
|
|
import '../../data/services/meeting/meeting_task_service.dart';
|
|
import '../../data/services/meeting/meeting_upload_service.dart';
|
|
import '../../data/services/network/api.dart';
|
|
import 'logger.dart';
|
|
|
|
/// 语音纪要列表与服务端对齐。
|
|
///
|
|
/// 模型:**服务端是唯一真源,本地 sqlite 只是缓存。**
|
|
/// - `echomeet_getallrecords` 只回骨架(title/rtype/state/seconds/audiourl/creationtime),
|
|
/// 每条 ~250 字节,上千条也是一次拉完,所以没有分页、没有增量、没有软删除;
|
|
/// - 转写(`meetingspeaker`) 与总结(`summary/overview/personnel`) **不在这里写**,
|
|
/// 详情页进页面时 `echomeet_getrecord` 拉整条再落库(见 MeetingDetailsController);
|
|
/// - 本地有、服务端没有的记录 = 在别的设备上删了,这里跟着删。
|
|
/// 本地行必然带服务端 id(归档是先 `addrecord` 拿到 id 才 `insertMeeting`),不会误删归档中的。
|
|
///
|
|
/// 原先这个类还带一套「离线操作队列 → echomeet_putrecords」的上行逻辑,服务端那个接口整个
|
|
/// 注释掉了、客户端上传段也注释掉了,是死代码,2026-09-18 一并删除。
|
|
class Synchrodata {
|
|
static const String _tag = 'Synchrodata';
|
|
static bool _running = false;
|
|
|
|
/// 拉一次骨架并对齐本地库。并发调用直接返回;失败只记日志(列表照用本地缓存)。
|
|
static Future<void> sync() async {
|
|
if (_running) return;
|
|
_running = true;
|
|
try {
|
|
await _sync();
|
|
} catch (e, st) {
|
|
Logger.w(_tag, '列表同步失败(沿用本地缓存): $e\n$st');
|
|
} finally {
|
|
_running = false;
|
|
}
|
|
}
|
|
|
|
static Future<void> _sync() async {
|
|
final result = await Api.getOperationAllRecord();
|
|
// ⚠️ 业务错误时 AuthInterceptor 把 data 置 null 后 resolve(不抛),必须判空——
|
|
// 把 null 当「服务端没有记录」会把本地整个清空。
|
|
if (result == null) {
|
|
Logger.w(_tag, '列表接口返回业务错误,本轮不对齐');
|
|
return;
|
|
}
|
|
final List records = result['records'] ?? [];
|
|
final serverIds = <int>{};
|
|
final taskList = <Map<String, Object?>>[];
|
|
|
|
final db = await DatabaseHelper().database;
|
|
await db.transaction((txn) async {
|
|
for (final element in records) {
|
|
final int id = element['id'];
|
|
final int state = element['state'] ?? 0;
|
|
serverIds.add(id);
|
|
if (state > 0 && state < 5) {
|
|
taskList.add({'id': id, 'tasktype': state});
|
|
}
|
|
final meeting = <String, Object?>{
|
|
'id': id,
|
|
'title': element['title'] ?? '',
|
|
'type': element['rtype'] ?? '',
|
|
'seconds': element['seconds'] ?? 0,
|
|
'audiourl': element['audiourl'] ?? '',
|
|
'tasktype': state,
|
|
// 服务端秒,本地毫秒
|
|
'creationtime': (element['creationtime'] ?? 0) * 1000,
|
|
};
|
|
final exists = await txn.query('meeting',
|
|
columns: ['id'], where: 'id = ?', whereArgs: [id], limit: 1);
|
|
if (exists.isNotEmpty) {
|
|
// 不碰 filepath(本机录音的路径只有本机知道)
|
|
await txn.update('meeting', meeting, where: 'id = ?', whereArgs: [id]);
|
|
// 状态在两张表各存一份,以 meeting 表为准,details 跟着走(详情页读的是这份)
|
|
await txn.update('meetingdetails', {'tasktype': state},
|
|
where: 'meetingid = ?', whereArgs: [id]);
|
|
} else {
|
|
await txn.insert('meeting', {...meeting, 'filepath': ''});
|
|
await txn.insert('meetingdetails', {
|
|
'meetingid': id,
|
|
'address': element['address'] ?? '',
|
|
'personnel': element['personnel'] ?? '',
|
|
'taskid': element['taskid'] ?? '',
|
|
'tasktype': state,
|
|
'overview': '',
|
|
'summary': '',
|
|
'mindmap': '',
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
await _removeLocalOrphans(serverIds);
|
|
|
|
if (taskList.isNotEmpty && Get.isRegistered<MeetingTaskService>()) {
|
|
Get.find<MeetingTaskService>().startTask(taskList);
|
|
}
|
|
Logger.i(_tag, '列表已对齐:服务端 ${records.length} 条,进行中 ${taskList.length} 条');
|
|
}
|
|
|
|
/// 删掉「本地有、服务端没有」的记录:连本地音频、上传队列、轮询任务一起清。
|
|
static Future<void> _removeLocalOrphans(Set<int> serverIds) async {
|
|
final db = await DatabaseHelper().database;
|
|
final local = await db.query('meeting', columns: ['id', 'filepath']);
|
|
final orphans = local.where((r) => !serverIds.contains(r['id'] as int));
|
|
for (final row in orphans) {
|
|
final int id = row['id'] as int;
|
|
final String path = (row['filepath'] as String?) ?? '';
|
|
await db.transaction((txn) async {
|
|
await txn.delete('meeting', where: 'id = ?', whereArgs: [id]);
|
|
await txn.delete('meetingdetails', where: 'meetingid = ?', whereArgs: [id]);
|
|
await txn.delete('meetingspeaker', where: 'meetingid = ?', whereArgs: [id]);
|
|
});
|
|
if (path.isNotEmpty) {
|
|
try {
|
|
final f = File(path);
|
|
if (await f.exists()) await f.delete();
|
|
} catch (e) {
|
|
Logger.w(_tag, '删除孤儿录音文件失败 $path: $e');
|
|
}
|
|
}
|
|
if (Get.isRegistered<MeetingUploadService>()) {
|
|
Get.find<MeetingUploadService>().removeUpload(id, isDeleteTask: true);
|
|
}
|
|
if (Get.isRegistered<MeetingTaskService>()) {
|
|
Get.find<MeetingTaskService>().removeLocalTask(id);
|
|
}
|
|
Logger.i(_tag, '服务端已不存在,删除本地记录 id=$id');
|
|
}
|
|
}
|
|
}
|
|
|