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 sync() async { if (_running) return; _running = true; try { await _sync(); } catch (e, st) { Logger.w(_tag, '列表同步失败(沿用本地缓存): $e\n$st'); } finally { _running = false; } } static Future _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 = {}; final taskList = >[]; 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 = { '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()) { Get.find().startTask(taskList); } Logger.i(_tag, '列表已对齐:服务端 ${records.length} 条,进行中 ${taskList.length} 条'); } /// 删掉「本地有、服务端没有」的记录:连本地音频、上传队列、轮询任务一起清。 static Future _removeLocalOrphans(Set 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()) { Get.find().removeUpload(id, isDeleteTask: true); } if (Get.isRegistered()) { Get.find().removeLocalTask(id); } Logger.i(_tag, '服务端已不存在,删除本地记录 id=$id'); } } }