chore: 初始化脚手架 — sunny_mochi 企业级 Flutter 模板

Core 基础设施:Flavor 三包体系、Drift+SQLCipher 加密数据库
7 拦截器 Dio 网络栈、CrashReporter 崩溃日志、Sealed Failure 错误体系
5 色板主题系统、Auth 认证骨架(Clean Architecture)、Dev Panel、Mock Adapter

通过验证:flutter analyze(0 errors)、flutter test(全绿)、staging APK 74.8MB
This commit is contained in:
SkyJourney
2026-05-14 12:51:05 +08:00
commit 61017f1c39
204 changed files with 15386 additions and 0 deletions
+38
View File
@@ -0,0 +1,38 @@
// ignore_for_file: one_member_abstracts — Strategy 接口,多种实现(ServerWins/LastWriteWins/Custom
/// 冲突解决策略:服务端响应与本地版本不一致时如何决策。
///
/// v1.2 默认策略:`LastWriteWinsResolver`(按 serverUpdatedAt 比较时间戳)。
/// 业务场景需要乐观锁 / 三方合并的,单独实现 `ConflictResolver` 子类。
abstract class ConflictResolver<T> {
/// 返回解决后的最终值(由调用方写回 DB)。
T resolve({required T local, required T server});
}
/// 默认实现:服务端权威 — 本地直接被服务端覆盖。
/// 适合健康数据采集这种"服务端是 source-of-truth"的场景。
class ServerWinsResolver<T> implements ConflictResolver<T> {
const ServerWinsResolver();
@override
T resolve({required T local, required T server}) => server;
}
/// last-write-wins by timestamp。
/// 调用方传入 timestamp 比较函数(avoid 包装 generic)。
class LastWriteWinsResolver<T> implements ConflictResolver<T> {
const LastWriteWinsResolver({
required this.localTimestamp,
required this.serverTimestamp,
});
final DateTime Function(T value) localTimestamp;
final DateTime Function(T value) serverTimestamp;
@override
T resolve({required T local, required T server}) {
return serverTimestamp(server).isAfter(localTimestamp(local))
? server
: local;
}
}
+103
View File
@@ -0,0 +1,103 @@
import 'dart:async';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:sunny_mochi/core/observability/talker_setup.dart';
import 'package:sunny_mochi/core/sync/sync_status.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'sync_service.g.dart';
/// 单条 dirty 记录的同步执行器。各业务模块实现此接口注册到 SyncService。
abstract class SyncTask {
/// 唯一标识(用于日志和去重)
String get key;
/// 执行同步:成功返回 true,失败返回 false(保留 pending 等下轮)。
/// 冲突时由实现方写入 conflictPayload 并返回 false。
Future<bool> execute();
}
/// Offline-First 后台同步调度器。
///
/// - 监听 [Connectivity] 网络状态变化,恢复联网时触发一轮同步
/// - 业务层通过 [enqueue] 注册 SyncTask
/// - 串行执行(避免并发污染服务端)
///
/// 详见 docs/flutter-architecture-design.md §五.21。
class SyncService {
SyncService({
required Connectivity connectivity,
}) : _connectivity = connectivity;
final Connectivity _connectivity;
final List<SyncTask> _queue = [];
StreamSubscription<List<ConnectivityResult>>? _connSub;
bool _running = false;
/// 启动后台监听。在 main.dart 完成 ProviderScope 初始化后调用。
///
/// **R-ROB-1 / 2026-05-10**:幂等 — 重复调用会先 cancel 旧 subscription
/// 防止 hot reload / Widget 重建时累积订阅造成 _drain 多次触发。
Future<void> start() async {
await _connSub?.cancel();
_connSub = _connectivity.onConnectivityChanged.listen(_onConnectivity);
final initial = await _connectivity.checkConnectivity();
if (_isOnline(initial)) {
unawaited(_drain());
}
}
Future<void> dispose() async {
await _connSub?.cancel();
_connSub = null;
}
/// 业务层注册一个 dirty 任务(同步后由任务自身从队列移除)。
void enqueue(SyncTask task) {
_queue.add(task);
appTalker.verbose('[Sync] enqueue ${task.key} (queue=${_queue.length})');
unawaited(_drain());
}
Future<void> _drain() async {
if (_running || _queue.isEmpty) return;
_running = true;
try {
while (_queue.isNotEmpty) {
final task = _queue.removeAt(0);
try {
final ok = await task.execute();
appTalker.info(
'[Sync] ${task.key} ${ok ? "" : "✗ (status=${SyncStatus.conflict.name} 待解决)"}',
);
} on Object catch (e, st) {
appTalker.warning('[Sync] ${task.key} 异常:$e', e, st);
// 失败任务不重新入队(保持 dirty 在 DB 等下轮启动)
}
}
} finally {
_running = false;
}
}
void _onConnectivity(List<ConnectivityResult> results) {
if (_isOnline(results)) {
appTalker.info('[Sync] 检测到联网,触发 drain');
unawaited(_drain());
}
}
bool _isOnline(List<ConnectivityResult> results) =>
results.any((r) => r != ConnectivityResult.none);
}
@Riverpod(keepAlive: true)
Connectivity connectivity(Ref ref) => Connectivity();
@Riverpod(keepAlive: true)
SyncService syncService(Ref ref) {
final svc = SyncService(connectivity: ref.watch(connectivityProvider));
ref.onDispose(() => unawaited(svc.dispose()));
return svc;
}
+92
View File
@@ -0,0 +1,92 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'sync_service.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(connectivity)
final connectivityProvider = ConnectivityProvider._();
final class ConnectivityProvider
extends $FunctionalProvider<Connectivity, Connectivity, Connectivity>
with $Provider<Connectivity> {
ConnectivityProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'connectivityProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$connectivityHash();
@$internal
@override
$ProviderElement<Connectivity> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
Connectivity create(Ref ref) {
return connectivity(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(Connectivity value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<Connectivity>(value),
);
}
}
String _$connectivityHash() => r'e66720f09edf1a8b09e450e1eaedd51da9443f0e';
@ProviderFor(syncService)
final syncServiceProvider = SyncServiceProvider._();
final class SyncServiceProvider
extends $FunctionalProvider<SyncService, SyncService, SyncService>
with $Provider<SyncService> {
SyncServiceProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'syncServiceProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$syncServiceHash();
@$internal
@override
$ProviderElement<SyncService> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
SyncService create(Ref ref) {
return syncService(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(SyncService value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<SyncService>(value),
);
}
}
String _$syncServiceHash() => r'6a8c53aab166422156d95e8cd712592195946e99';
+14
View File
@@ -0,0 +1,14 @@
/// 单条业务记录的同步状态机。Drift 表用 SyncColumns mixin 4 字段约定记录此状态。
enum SyncStatus {
/// 本地新增/修改,待同步到服务端
pending,
/// 正在同步中(避免并发重复发送)
syncing,
/// 已成功同步到服务端
synced,
/// 服务端拒绝/冲突,需要人工或策略解决
conflict,
}