439 lines
15 KiB
Markdown
439 lines
15 KiB
Markdown
|
|
# 前端导航解耦与统一缓存重构 Implementation Plan
|
|||
|
|
|
|||
|
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|||
|
|
|
|||
|
|
**Goal:** 完成 Home/Calendar/Todo 的解耦切换与统一缓存改造,实现本地优先显示、后台静默刷新、写后精准失效,并修复 Dock 回主页语义。
|
|||
|
|
|
|||
|
|
**Architecture:** 路由层采用 `StatefulShellRoute.indexedStack` 维持主分支保活;数据层新增 `core/cache` 统一缓存模块(memory + persistent + hybrid);业务层通过 repository 接入缓存策略,页面仅负责发意图和渲染状态。写操作触发精准失效,读取遵循 soft/hard TTL + minimum refresh interval。
|
|||
|
|
|
|||
|
|
**Tech Stack:** Flutter, go_router, get_it, shared_preferences, flutter_test, mocktail
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Task 1: 建立统一缓存核心模型与策略
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `apps/lib/core/cache/cache_entry.dart`
|
|||
|
|
- Create: `apps/lib/core/cache/cache_key.dart`
|
|||
|
|
- Create: `apps/lib/core/cache/cache_policy.dart`
|
|||
|
|
- Test: `apps/test/core/cache/cache_policy_test.dart`
|
|||
|
|
|
|||
|
|
**Step 1: Write the failing test**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
import 'package:flutter_test/flutter_test.dart';
|
|||
|
|
import 'package:social_app/core/cache/cache_policy.dart';
|
|||
|
|
|
|||
|
|
void main() {
|
|||
|
|
test('soft expired should allow stale read with background refresh', () {
|
|||
|
|
final now = DateTime(2026, 3, 20, 12);
|
|||
|
|
final policy = CachePolicy(
|
|||
|
|
softTtl: const Duration(minutes: 2),
|
|||
|
|
hardTtl: const Duration(minutes: 30),
|
|||
|
|
minRefreshInterval: const Duration(minutes: 1),
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
final fetchedAt = now.subtract(const Duration(minutes: 3));
|
|||
|
|
final decision = policy.evaluate(now: now, fetchedAt: fetchedAt);
|
|||
|
|
expect(decision.canUseCached, true);
|
|||
|
|
expect(decision.shouldRefreshInBackground, true);
|
|||
|
|
expect(decision.mustBlockForNetwork, false);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 2: Run test to verify it fails**
|
|||
|
|
|
|||
|
|
Run: `cd apps && flutter test test/core/cache/cache_policy_test.dart`
|
|||
|
|
Expected: FAIL with missing cache policy symbols.
|
|||
|
|
|
|||
|
|
**Step 3: Write minimal implementation**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
class CacheDecision {
|
|||
|
|
final bool canUseCached;
|
|||
|
|
final bool shouldRefreshInBackground;
|
|||
|
|
final bool mustBlockForNetwork;
|
|||
|
|
const CacheDecision({
|
|||
|
|
required this.canUseCached,
|
|||
|
|
required this.shouldRefreshInBackground,
|
|||
|
|
required this.mustBlockForNetwork,
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 4: Run test to verify it passes**
|
|||
|
|
|
|||
|
|
Run: `cd apps && flutter test test/core/cache/cache_policy_test.dart`
|
|||
|
|
Expected: PASS.
|
|||
|
|
|
|||
|
|
**Step 5: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add apps/lib/core/cache/cache_entry.dart apps/lib/core/cache/cache_key.dart apps/lib/core/cache/cache_policy.dart apps/test/core/cache/cache_policy_test.dart
|
|||
|
|
git commit -m "feat: add unified cache policy primitives"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Task 2: 实现 memory/persistent/hybrid cache store(含 singleflight)
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `apps/lib/core/cache/cache_store.dart`
|
|||
|
|
- Create: `apps/lib/core/cache/memory_cache_store.dart`
|
|||
|
|
- Create: `apps/lib/core/cache/persistent_cache_store.dart`
|
|||
|
|
- Create: `apps/lib/core/cache/hybrid_cache_store.dart`
|
|||
|
|
- Test: `apps/test/core/cache/hybrid_cache_store_test.dart`
|
|||
|
|
|
|||
|
|
**Step 1: Write the failing test**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
test('same key concurrent load should execute loader once', () async {
|
|||
|
|
var calls = 0;
|
|||
|
|
final store = HybridCacheStore(...);
|
|||
|
|
Future<String> loader() async {
|
|||
|
|
calls += 1;
|
|||
|
|
return 'ok';
|
|||
|
|
}
|
|||
|
|
await Future.wait([
|
|||
|
|
store.getOrLoad<String>('k', loader: loader),
|
|||
|
|
store.getOrLoad<String>('k', loader: loader),
|
|||
|
|
]);
|
|||
|
|
expect(calls, 1);
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 2: Run test to verify it fails**
|
|||
|
|
|
|||
|
|
Run: `cd apps && flutter test test/core/cache/hybrid_cache_store_test.dart`
|
|||
|
|
Expected: FAIL with missing HybridCacheStore.
|
|||
|
|
|
|||
|
|
**Step 3: Write minimal implementation**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
final Map<String, Future<dynamic>> _inflight = {};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 4: Run test to verify it passes**
|
|||
|
|
|
|||
|
|
Run: `cd apps && flutter test test/core/cache/hybrid_cache_store_test.dart`
|
|||
|
|
Expected: PASS.
|
|||
|
|
|
|||
|
|
**Step 5: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add apps/lib/core/cache/cache_store.dart apps/lib/core/cache/memory_cache_store.dart apps/lib/core/cache/persistent_cache_store.dart apps/lib/core/cache/hybrid_cache_store.dart apps/test/core/cache/hybrid_cache_store_test.dart
|
|||
|
|
git commit -m "feat: implement hybrid cache store with singleflight"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Task 3: 接入 DI 与统一失效器
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `apps/lib/core/cache/cache_invalidator.dart`
|
|||
|
|
- Modify: `apps/lib/core/di/injection.dart`
|
|||
|
|
- Test: `apps/test/core/cache/cache_invalidator_test.dart`
|
|||
|
|
|
|||
|
|
**Step 1: Write the failing test**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
test('invalidate calendar day should also invalidate month key', () {
|
|||
|
|
final inv = CacheInvalidator(...);
|
|||
|
|
inv.invalidateCalendarDay(DateTime(2026, 3, 20));
|
|||
|
|
expect(inv.wasInvalidated('calendar:day:2026-03-20'), true);
|
|||
|
|
expect(inv.wasInvalidated('calendar:month:2026-03'), true);
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 2: Run test to verify it fails**
|
|||
|
|
|
|||
|
|
Run: `cd apps && flutter test test/core/cache/cache_invalidator_test.dart`
|
|||
|
|
Expected: FAIL.
|
|||
|
|
|
|||
|
|
**Step 3: Write minimal implementation**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
class CacheInvalidator {
|
|||
|
|
void invalidateCalendarDay(DateTime date) { /* invalidate day + month */ }
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 4: Run test to verify it passes**
|
|||
|
|
|
|||
|
|
Run: `cd apps && flutter test test/core/cache/cache_invalidator_test.dart`
|
|||
|
|
Expected: PASS.
|
|||
|
|
|
|||
|
|
**Step 5: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add apps/lib/core/cache/cache_invalidator.dart apps/lib/core/di/injection.dart apps/test/core/cache/cache_invalidator_test.dart
|
|||
|
|
git commit -m "refactor: wire unified cache and invalidator in di"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Task 4: 合并个人信息缓存(替换 SettingsUserCache)
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `apps/lib/features/settings/data/services/settings_user_cache.dart`
|
|||
|
|
- Create: `apps/lib/features/settings/data/services/user_profile_cache_repository.dart`
|
|||
|
|
- Modify: `apps/lib/features/settings/ui/screens/settings_screen.dart`
|
|||
|
|
- Test: `apps/test/features/settings/data/services/settings_user_cache_test.dart`
|
|||
|
|
- Create: `apps/test/features/settings/data/services/user_profile_cache_repository_test.dart`
|
|||
|
|
|
|||
|
|
**Step 1: Write the failing test**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
test('repository should return persistent cache first then refresh in background', () async {
|
|||
|
|
// Arrange cached profile in persistent store
|
|||
|
|
// Assert immediate cached result + refresh called once
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 2: Run test to verify it fails**
|
|||
|
|
|
|||
|
|
Run: `cd apps && flutter test test/features/settings/data/services/user_profile_cache_repository_test.dart`
|
|||
|
|
Expected: FAIL.
|
|||
|
|
|
|||
|
|
**Step 3: Write minimal implementation**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
class UserProfileCacheRepository {
|
|||
|
|
Future<UserResponse> getProfile({bool forceRefresh = false}) async { ... }
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 4: Run tests to verify they pass**
|
|||
|
|
|
|||
|
|
Run: `cd apps && flutter test test/features/settings/data/services/settings_user_cache_test.dart test/features/settings/data/services/user_profile_cache_repository_test.dart`
|
|||
|
|
Expected: PASS.
|
|||
|
|
|
|||
|
|
**Step 5: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add apps/lib/features/settings/data/services/settings_user_cache.dart apps/lib/features/settings/data/services/user_profile_cache_repository.dart apps/lib/features/settings/ui/screens/settings_screen.dart apps/test/features/settings/data/services/settings_user_cache_test.dart apps/test/features/settings/data/services/user_profile_cache_repository_test.dart
|
|||
|
|
git commit -m "refactor: merge profile cache into unified cache repository"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Task 5: 路由改造为 StatefulShellRoute + Dock 切换分支
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `apps/lib/core/router/app_router.dart`
|
|||
|
|
- Modify: `apps/lib/core/router/app_routes.dart`
|
|||
|
|
- Modify: `apps/lib/features/calendar/ui/widgets/bottom_dock.dart`
|
|||
|
|
- Modify: `apps/lib/features/home/ui/navigation/home_return_policy.dart`
|
|||
|
|
- Test: `apps/test/core/router/app_routes_test.dart`
|
|||
|
|
- Modify: `apps/test/features/home/ui/navigation/home_return_policy_test.dart`
|
|||
|
|
|
|||
|
|
**Step 1: Write the failing test**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
test('dock home action should always resolve to goHome', () {
|
|||
|
|
final action = resolveHomeReturnAction(canPop: true, isAuthEntry: false);
|
|||
|
|
expect(action, HomeReturnAction.goHomeForDock);
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 2: Run test to verify it fails**
|
|||
|
|
|
|||
|
|
Run: `cd apps && flutter test test/features/home/ui/navigation/home_return_policy_test.dart`
|
|||
|
|
Expected: FAIL with old behavior.
|
|||
|
|
|
|||
|
|
**Step 3: Write minimal implementation**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
enum HomeReturnAction { pop, goHome, goHomeForDock }
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 4: Run tests to verify they pass**
|
|||
|
|
|
|||
|
|
Run: `cd apps && flutter test test/core/router/app_routes_test.dart test/features/home/ui/navigation/home_return_policy_test.dart`
|
|||
|
|
Expected: PASS.
|
|||
|
|
|
|||
|
|
**Step 5: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add apps/lib/core/router/app_router.dart apps/lib/core/router/app_routes.dart apps/lib/features/calendar/ui/widgets/bottom_dock.dart apps/lib/features/home/ui/navigation/home_return_policy.dart apps/test/core/router/app_routes_test.dart apps/test/features/home/ui/navigation/home_return_policy_test.dart
|
|||
|
|
git commit -m "feat: switch main navigation to stateful shell tabs"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Task 6: Calendar repository 化并移除路由监听刷新
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `apps/lib/features/calendar/data/services/calendar_repository.dart`
|
|||
|
|
- Modify: `apps/lib/features/calendar/ui/screens/calendar_dayweek_screen.dart`
|
|||
|
|
- Modify: `apps/lib/features/calendar/ui/screens/calendar_month_screen.dart`
|
|||
|
|
- Modify: `apps/lib/features/calendar/ui/calendar_state_manager.dart`
|
|||
|
|
- Create: `apps/test/features/calendar/data/services/calendar_repository_test.dart`
|
|||
|
|
|
|||
|
|
**Step 1: Write the failing test**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
test('getDayEvents returns cache immediately and refreshes in background', () async {
|
|||
|
|
// Arrange cache day key
|
|||
|
|
// Assert cached list emitted before network completion
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 2: Run test to verify it fails**
|
|||
|
|
|
|||
|
|
Run: `cd apps && flutter test test/features/calendar/data/services/calendar_repository_test.dart`
|
|||
|
|
Expected: FAIL.
|
|||
|
|
|
|||
|
|
**Step 3: Write minimal implementation**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
class CalendarRepository {
|
|||
|
|
Future<List<ScheduleItemModel>> getDayEvents(DateTime date, {bool forceRefresh = false}) async { ... }
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 4: Run tests to verify they pass**
|
|||
|
|
|
|||
|
|
Run: `cd apps && flutter test test/features/calendar/data/services/calendar_repository_test.dart`
|
|||
|
|
Expected: PASS.
|
|||
|
|
|
|||
|
|
**Step 5: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add apps/lib/features/calendar/data/services/calendar_repository.dart apps/lib/features/calendar/ui/screens/calendar_dayweek_screen.dart apps/lib/features/calendar/ui/screens/calendar_month_screen.dart apps/lib/features/calendar/ui/calendar_state_manager.dart apps/test/features/calendar/data/services/calendar_repository_test.dart
|
|||
|
|
git commit -m "refactor: decouple calendar screens from route-driven reload"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Task 7: Todo repository 化与写后精准失效
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `apps/lib/features/todo/data/todo_repository.dart`
|
|||
|
|
- Modify: `apps/lib/features/todo/ui/screens/todo_quadrants_screen.dart`
|
|||
|
|
- Modify: `apps/lib/features/todo/data/todo_api.dart`
|
|||
|
|
- Create: `apps/test/features/todo/todo_repository_test.dart`
|
|||
|
|
- Modify: `apps/test/features/todo/quadrant_drag_test.dart`
|
|||
|
|
|
|||
|
|
**Step 1: Write the failing test**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
test('complete todo should optimistically update and invalidate pending list key', () async {
|
|||
|
|
// assert local list updated first, invalidator called
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 2: Run test to verify it fails**
|
|||
|
|
|
|||
|
|
Run: `cd apps && flutter test test/features/todo/todo_repository_test.dart`
|
|||
|
|
Expected: FAIL.
|
|||
|
|
|
|||
|
|
**Step 3: Write minimal implementation**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
class TodoRepository {
|
|||
|
|
Future<void> completeTodo(String id) async { ... }
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 4: Run tests to verify they pass**
|
|||
|
|
|
|||
|
|
Run: `cd apps && flutter test test/features/todo/todo_repository_test.dart test/features/todo/quadrant_drag_test.dart`
|
|||
|
|
Expected: PASS.
|
|||
|
|
|
|||
|
|
**Step 5: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add apps/lib/features/todo/data/todo_repository.dart apps/lib/features/todo/ui/screens/todo_quadrants_screen.dart apps/lib/features/todo/data/todo_api.dart apps/test/features/todo/todo_repository_test.dart apps/test/features/todo/quadrant_drag_test.dart
|
|||
|
|
git commit -m "feat: add todo cache repository and precise invalidation"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Task 8: App 生命周期与网络恢复刷新策略
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `apps/lib/core/cache/cache_refresh_coordinator.dart`
|
|||
|
|
- Modify: `apps/lib/main.dart`
|
|||
|
|
- Create: `apps/test/core/cache/cache_refresh_coordinator_test.dart`
|
|||
|
|
|
|||
|
|
**Step 1: Write the failing test**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
test('resume should trigger refresh only when min interval elapsed', () {
|
|||
|
|
// Arrange last refreshed timestamp
|
|||
|
|
// Assert callback invocation count
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 2: Run test to verify it fails**
|
|||
|
|
|
|||
|
|
Run: `cd apps && flutter test test/core/cache/cache_refresh_coordinator_test.dart`
|
|||
|
|
Expected: FAIL.
|
|||
|
|
|
|||
|
|
**Step 3: Write minimal implementation**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
class CacheRefreshCoordinator with WidgetsBindingObserver { ... }
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 4: Run test to verify it passes**
|
|||
|
|
|
|||
|
|
Run: `cd apps && flutter test test/core/cache/cache_refresh_coordinator_test.dart`
|
|||
|
|
Expected: PASS.
|
|||
|
|
|
|||
|
|
**Step 5: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add apps/lib/core/cache/cache_refresh_coordinator.dart apps/lib/main.dart apps/test/core/cache/cache_refresh_coordinator_test.dart
|
|||
|
|
git commit -m "feat: add app lifecycle refresh coordinator"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Task 9: 全量验证与文档同步
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `docs/protocols/*`(仅当路由/数据契约文档需更新时)
|
|||
|
|
- Modify: `docs/plans/2026-03-20-navigation-cache-decoupling-design.md`(回填最终参数)
|
|||
|
|
|
|||
|
|
**Step 1: Run focused tests**
|
|||
|
|
|
|||
|
|
Run:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
cd apps && flutter test test/core/cache test/features/settings/data/services/settings_user_cache_test.dart test/features/calendar test/features/todo test/features/home/ui/navigation/home_return_policy_test.dart test/core/router/app_routes_test.dart
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Expected: PASS.
|
|||
|
|
|
|||
|
|
**Step 2: Run app-level verification**
|
|||
|
|
|
|||
|
|
Run: `cd apps && flutter test`
|
|||
|
|
Expected: PASS.
|
|||
|
|
|
|||
|
|
**Step 3: Static checks**
|
|||
|
|
|
|||
|
|
Run: `cd apps && flutter analyze`
|
|||
|
|
Expected: No errors.
|
|||
|
|
|
|||
|
|
**Step 4: Manual verification checklist**
|
|||
|
|
|
|||
|
|
1. 冷启动先显示缓存,随后静默更新。
|
|||
|
|
2. Home/Calendar/Todo 来回切换不重建主页面。
|
|||
|
|
3. 日/月切换不触发无必要请求。
|
|||
|
|
4. Dock Home 始终回主页。
|
|||
|
|
5. 写后数据可见一致,失败可回滚提示。
|
|||
|
|
|
|||
|
|
**Step 5: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add docs/plans/2026-03-20-navigation-cache-decoupling-design.md docs/protocols
|
|||
|
|
git commit -m "docs: finalize navigation decoupling and unified cache rollout"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 实施顺序约束
|
|||
|
|
|
|||
|
|
1. 必须先完成 Task 1-3 再改业务页面(否则会出现重复实现)。
|
|||
|
|
2. Task 5(路由壳层)与 Task 6/7(业务接入)要分开提交,便于回滚。
|
|||
|
|
3. 每个 Task 的测试必须在本 Task 完成后立即执行,避免堆积回归。
|
|||
|
|
4. 不允许在未通过 focused tests 的情况下进入全量验证。
|
|||
|
|
|
|||
|
|
## 回滚策略
|
|||
|
|
|
|||
|
|
1. 若导航回归:回滚 Task 5 提交,保留缓存模块提交。
|
|||
|
|
2. 若缓存一致性异常:优先回滚 Task 6/7 的 repository 接入提交。
|
|||
|
|
3. 若生命周期刷新过于频繁:关闭 Task 8 coordinator 挂载,保留手动刷新兜底。
|
|||
|
|
|
|||
|
|
## Done 定义
|
|||
|
|
|
|||
|
|
1. 所有测试与 analyze 通过。
|
|||
|
|
2. 主页按钮行为稳定,无“返回上一页”误行为。
|
|||
|
|
3. 切换页面请求数明显下降,写后一致性符合设计预期。
|
|||
|
|
4. 统一缓存已接管用户信息、日历、待办三域。
|