Просмотр исходного кода

Stop repeated anti-forget calendar refreshes

彭峰 1 неделя назад
Родитель
Сommit
ae9c15e6c6

+ 30 - 0
projects/xiaoshu-mobile/src/app/core/home-learning-modules.integration.spec.ts

@@ -298,6 +298,36 @@ describe('home learning modules', () => {
     });
   });
 
+  it('loads the review calendar once per login and refreshes only on an explicit request', async () => {
+    const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
+    const originalPost = api.post.bind(api);
+    const post = spyOn(api, 'post').and.callFake((action, params) => originalPost(action, params));
+    const count = (action: string) => post.calls.allArgs().filter(([name]) => name === action).length;
+    const harness = await RouterTestingHarness.create();
+
+    await harness.navigateByUrl('/pages/stu/stu_calendar');
+    harness.fixture.detectChanges();
+    expect(count('app_review_page')).toBe(1);
+    expect(count('app_live_versions')).toBe(0);
+
+    await harness.navigateByUrl('/pages/member/starshop');
+    await harness.navigateByUrl('/pages/stu/stu_calendar');
+    harness.fixture.detectChanges();
+    expect(count('app_review_page')).toBe(1);
+
+    const calendar = harness.fixture.debugElement.query(By.directive(LearningPageComponent)).componentInstance as LearningPageComponent;
+    calendar.retryCalendar();
+    harness.fixture.detectChanges();
+    expect(count('app_review_page')).toBe(2);
+
+    await harness.navigateByUrl('/pages/member/starshop');
+    session.token.set('next-login-token');
+    await harness.navigateByUrl('/pages/stu/stu_calendar');
+    harness.fixture.detectChanges();
+    expect(count('app_review_page')).toBe(3);
+    session.token.set('test-token');
+  });
+
   it('silently reveals pending and completed words opened from student learning content', async () => {
     const play = spyOn(TestBed.inject(PlatformService), 'play');
     const harness = await RouterTestingHarness.create();

+ 29 - 15
projects/xiaoshu-mobile/src/app/features/learning/learning-page.component.ts

@@ -44,7 +44,6 @@ import { canStartTeacherLedLearning, isTeacherLedLearningPath } from '../../core
 import { isLearningStage, learningReturnUrl } from '../../core/learning-navigation';
 import { PlatformService } from '../../core/platform.service';
 import { SessionService } from '../../core/session.service';
-import { LiveDataRefreshService } from '../../core/live-data-refresh.service';
 import { nextReviewWordOrder } from '../../core/review-word-order';
 
 type SourceItem = Record<string, unknown>;
@@ -109,6 +108,7 @@ const WORD_CATALOG_CATEGORIES = [
   changeDetection: ChangeDetectionStrategy.OnPush,
 })
 export class LearningPageComponent implements OnInit, OnDestroy {
+  private static readonly reviewCacheByApi = new WeakMap<ApiService, { identity: string; pages: Map<string, ReviewPage> }>();
   readonly meta = inject(ActivatedRoute).snapshot.data['meta'] as PageMeta;
   readonly nodes = signal<SourceItem[]>([]);
   readonly catalogCategories = WORD_CATALOG_CATEGORIES;
@@ -278,14 +278,12 @@ export class LearningPageComponent implements OnInit, OnDestroy {
   private readonly api = inject(ApiService);
   private readonly session = inject(SessionService);
   private readonly platform = inject(PlatformService);
-  private readonly liveData = inject(LiveDataRefreshService);
   private readonly wordPreviewPageSize = 30;
   private examTimer: ReturnType<typeof setInterval> | null = null;
   private antiStartedAt = Date.now();
   private calendarRequest?: Subscription;
   private choiceOptionsRequest?: Subscription;
   private choiceOptionsVersion = 0;
-  private stopLiveRefresh?: () => void;
 
   ngOnInit(): void {
     if (isTeacherLedLearningPath(this.meta.path) && !this.isReadOnlyWordView && !this.isReadOnlySelectionPage && !this.canStartPointReading && !this.isNewWordSelfStudyStage) {
@@ -298,20 +296,12 @@ export class LearningPageComponent implements OnInit, OnDestroy {
       return;
     }
     this.load();
-    if (this.isCalendarPage) {
-      this.stopLiveRefresh = this.liveData.watch({
-        scopes: ['review', 'account'],
-        studentId: () => Number(this.memoryStudentId() || this.session.user()?.userId) || undefined,
-        onRefresh: () => this.loadCalendar(),
-      });
-    }
   }
 
   ngOnDestroy(): void {
     this.calendarRequestId++;this.calendarRequest?.unsubscribe();this.selectionRequest?.unsubscribe();
     this.choiceOptionsVersion++;
     this.choiceOptionsRequest?.unsubscribe();
-    this.stopLiveRefresh?.();
     this.stopExamTimer();
     this.platform.stopAudio();
   }
@@ -953,7 +943,10 @@ export class LearningPageComponent implements OnInit, OnDestroy {
       content: JSON.stringify({ ModelID: 60, nodeId: 388, status: 99, GeneralID: this.query('gid'), UpDateTime: now }),
       addon: JSON.stringify({ fxzt: 1, wcsj: now }),
     }).pipe(finalize(() => this.submitting.set(false))).subscribe({
-      next: () => void this.router.navigateByUrl('/pages/stu/stu_calendar'),
+      next: () => {
+        this.reviewCache().clear();
+        void this.router.navigateByUrl('/pages/stu/stu_calendar');
+      },
       error: (error: ApiError) => this.platform.toast(error.message, 'error'),
     });
   }
@@ -1777,18 +1770,39 @@ export class LearningPageComponent implements OnInit, OnDestroy {
   }
 
   turnReviewPage(delta:number):void{this.reviewListPage.update(p=>Math.max(1,Math.min(this.reviewPageData()?.page.pageCount||1,p+delta)));this.loadCalendar();}
-  retryCalendar(): void { this.loadCalendar(); }
+  retryCalendar(): void { this.loadCalendar(false, true); }
 
-  private loadCalendar(initial = false): void {
+  private reviewCache(): Map<string, ReviewPage> {
+    const identity = `${this.session.token()}|${this.session.user()?.userId ?? ''}`;
+    let cache = LearningPageComponent.reviewCacheByApi.get(this.api);
+    if (!cache || cache.identity !== identity) {
+      cache = { identity, pages: new Map() };
+      LearningPageComponent.reviewCacheByApi.set(this.api, cache);
+    }
+    return cache.pages;
+  }
+
+  private loadCalendar(initial = false, force = false): void {
     const requestId=++this.calendarRequestId;
     this.calendarRequest?.unsubscribe();
     this.calendarError.set('');
+    const date = this.memorySelectedDate() || this.dateOnly();
+    const key = `${this.memoryStudentId()}|${date}|${this.reviewListPage()}`;
+    const cache = this.reviewCache();
+    const cached = force ? undefined : cache.get(key);
+    if (cached) {
+      this.reviewPageData.set(cached);this.calendarItems.set(cached.items);
+      if(!this.memorySelectedDate())this.memorySelectedDate.set(date);
+      this.loading.set(false);this.calendarLoading.set(false);
+      return;
+    }
     if (initial) this.loading.set(true); else this.calendarLoading.set(true);
-    this.calendarRequest = this.api.post<ReviewPage>('app_review_page',{uid:this.session.user()?.userId,studentId:this.memoryStudentId()||undefined,date:this.memorySelectedDate()||this.dateOnly(),page:this.reviewListPage(),pageSize:20}).pipe(finalize(()=>{
+    this.calendarRequest = this.api.post<ReviewPage>('app_review_page',{uid:this.session.user()?.userId,studentId:this.memoryStudentId()||undefined,date,page:this.reviewListPage(),pageSize:20}).pipe(finalize(()=>{
       if(requestId!==this.calendarRequestId)return;
       if(initial)this.loading.set(false);else this.calendarLoading.set(false);
     })).subscribe({next:({result})=>{
       if(requestId!==this.calendarRequestId)return;
+      cache.set(key,result);
       this.reviewPageData.set(result);this.calendarItems.set(result.items);
       if(!this.memorySelectedDate())this.memorySelectedDate.set(this.dateOnly());
     },error:(e:ApiError)=>{