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

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');
}
}
}