00f37d7e19
- 新增 ReminderActionExecutor 处理取消/稍后提醒操作 - 新增 ReminderOutboxStore 本地存储待处理操作 - 重构 LocalNotificationService 支持聚合提醒和交互操作 - 新增 event_color_resolver 工具类统一颜色解析 - 新增 CalendarService.archiveEvent 归档方法 - 增强 ModelTracking 支持缓存命中、推理token和成本追踪 - 添加 qwen3.5-35b-a3b 模型配置 - 更新 AndroidManifest 全屏intent权限 - 补充相关单元测试和文档
203 lines
5.4 KiB
Dart
203 lines
5.4 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
import 'models/reminder_action.dart';
|
|
|
|
class ReminderOutboxItem {
|
|
final String opId;
|
|
final String eventId;
|
|
final ReminderAction action;
|
|
final String? targetStatus;
|
|
final DateTime occurredAt;
|
|
final int retryCount;
|
|
final DateTime? nextRetryAt;
|
|
final ReminderOutboxState state;
|
|
final String? lastError;
|
|
|
|
const ReminderOutboxItem({
|
|
required this.opId,
|
|
required this.eventId,
|
|
required this.action,
|
|
required this.occurredAt,
|
|
this.targetStatus,
|
|
this.retryCount = 0,
|
|
this.nextRetryAt,
|
|
this.state = ReminderOutboxState.pending,
|
|
this.lastError,
|
|
});
|
|
|
|
String get idempotencyBucket {
|
|
final bucket =
|
|
occurredAt.millisecondsSinceEpoch ~/
|
|
const Duration(minutes: 1).inMilliseconds;
|
|
return '$eventId|${action.value}|$bucket';
|
|
}
|
|
|
|
ReminderOutboxItem copyWith({
|
|
int? retryCount,
|
|
DateTime? nextRetryAt,
|
|
ReminderOutboxState? state,
|
|
String? lastError,
|
|
}) {
|
|
return ReminderOutboxItem(
|
|
opId: opId,
|
|
eventId: eventId,
|
|
action: action,
|
|
targetStatus: targetStatus,
|
|
occurredAt: occurredAt,
|
|
retryCount: retryCount ?? this.retryCount,
|
|
nextRetryAt: nextRetryAt ?? this.nextRetryAt,
|
|
state: state ?? this.state,
|
|
lastError: lastError ?? this.lastError,
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
'opId': opId,
|
|
'eventId': eventId,
|
|
'action': action.value,
|
|
'targetStatus': targetStatus,
|
|
'occurredAt': occurredAt.toIso8601String(),
|
|
'retryCount': retryCount,
|
|
'nextRetryAt': nextRetryAt?.toIso8601String(),
|
|
'state': state.value,
|
|
'lastError': lastError,
|
|
};
|
|
}
|
|
|
|
factory ReminderOutboxItem.fromJson(Map<String, dynamic> json) {
|
|
return ReminderOutboxItem(
|
|
opId: (json['opId'] as String?) ?? '',
|
|
eventId: (json['eventId'] as String?) ?? '',
|
|
action: ReminderAction.fromValue(
|
|
(json['action'] as String?) ?? 'timeout_30s',
|
|
),
|
|
targetStatus: json['targetStatus'] as String?,
|
|
occurredAt: DateTime.parse(json['occurredAt'] as String),
|
|
retryCount: (json['retryCount'] as int?) ?? 0,
|
|
nextRetryAt: json['nextRetryAt'] != null
|
|
? DateTime.parse(json['nextRetryAt'] as String)
|
|
: null,
|
|
state: ReminderOutboxState.fromValue(
|
|
(json['state'] as String?) ?? 'pending',
|
|
),
|
|
lastError: json['lastError'] as String?,
|
|
);
|
|
}
|
|
}
|
|
|
|
enum ReminderOutboxState {
|
|
pending('pending'),
|
|
done('done'),
|
|
dead('dead');
|
|
|
|
const ReminderOutboxState(this.value);
|
|
final String value;
|
|
|
|
static ReminderOutboxState fromValue(String raw) {
|
|
return ReminderOutboxState.values.firstWhere(
|
|
(item) => item.value == raw,
|
|
orElse: () => ReminderOutboxState.pending,
|
|
);
|
|
}
|
|
}
|
|
|
|
class ReminderOutboxStore {
|
|
static const String _key = 'calendar_reminder_outbox_v1';
|
|
final SharedPreferences _prefs;
|
|
|
|
ReminderOutboxStore(this._prefs);
|
|
|
|
Future<void> enqueue(ReminderOutboxItem item) async {
|
|
final current = await _readAll();
|
|
final duplicated = current.any(
|
|
(existing) =>
|
|
existing.state == ReminderOutboxState.pending &&
|
|
existing.idempotencyBucket == item.idempotencyBucket,
|
|
);
|
|
if (duplicated) {
|
|
return;
|
|
}
|
|
current.add(item);
|
|
await _writeAll(current);
|
|
}
|
|
|
|
Future<List<ReminderOutboxItem>> listPending() async {
|
|
final all = await _readAll();
|
|
final now = DateTime.now();
|
|
return all
|
|
.where((item) => item.state == ReminderOutboxState.pending)
|
|
.where(
|
|
(item) => item.nextRetryAt == null || !item.nextRetryAt!.isAfter(now),
|
|
)
|
|
.toList();
|
|
}
|
|
|
|
Future<void> markDone(String opId) async {
|
|
final all = await _readAll();
|
|
final updated = all
|
|
.map(
|
|
(item) => item.opId == opId
|
|
? item.copyWith(
|
|
state: ReminderOutboxState.done,
|
|
nextRetryAt: null,
|
|
)
|
|
: item,
|
|
)
|
|
.toList();
|
|
await _writeAll(updated);
|
|
}
|
|
|
|
Future<void> markRetry(String opId, String error) async {
|
|
final all = await _readAll();
|
|
final updated = all.map((item) {
|
|
if (item.opId != opId) {
|
|
return item;
|
|
}
|
|
final nextRetryCount = item.retryCount + 1;
|
|
if (nextRetryCount >= 8) {
|
|
return item.copyWith(
|
|
retryCount: nextRetryCount,
|
|
state: ReminderOutboxState.dead,
|
|
lastError: error,
|
|
nextRetryAt: null,
|
|
);
|
|
}
|
|
final delayMinutes = nextRetryCount == 1 ? 0 : 1 << (nextRetryCount - 1);
|
|
return item.copyWith(
|
|
retryCount: nextRetryCount,
|
|
lastError: error,
|
|
nextRetryAt: DateTime.now().add(Duration(minutes: delayMinutes)),
|
|
);
|
|
}).toList();
|
|
await _writeAll(updated);
|
|
}
|
|
|
|
Future<List<ReminderOutboxItem>> _readAll() async {
|
|
try {
|
|
final raw = _prefs.getString(_key);
|
|
if (raw == null || raw.isEmpty) {
|
|
return [];
|
|
}
|
|
final list = jsonDecode(raw) as List<dynamic>;
|
|
return list
|
|
.whereType<Map>()
|
|
.map(
|
|
(item) =>
|
|
ReminderOutboxItem.fromJson(Map<String, dynamic>.from(item)),
|
|
)
|
|
.toList();
|
|
} catch (_) {
|
|
await _prefs.remove(_key);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
Future<void> _writeAll(List<ReminderOutboxItem> items) async {
|
|
final raw = jsonEncode(items.map((item) => item.toJson()).toList());
|
|
await _prefs.setString(_key, raw);
|
|
}
|
|
}
|