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

feat: integrate English reading into mobile H5

彭峰 1 месяц назад
Родитель
Сommit
31ea45aa91

+ 3 - 4
projects/xiaoshu-mobile/src/app/app.routes.ts

@@ -25,16 +25,15 @@ const components: Record<PageKind, ComponentLoader> = {
   studyRecords: () => import('./features/study-records/study-records-page.component').then((module) => module.StudyRecordsPageComponent),
   team: () => import('./features/team/team-page.component').then((module) => module.TeamPageComponent),
   merchant: () => import('./features/merchant/merchant-page.component').then((module) => module.MerchantPageComponent),
+  readingSetup: () => import('./features/english-reading/reading-setup.component').then((module) => module.ReadingSetupComponent),
   webview: () => import('./features/webview/webview-page.component').then((module) => module.WebviewPageComponent),
   support: () => import('./features/support/support-page.component').then((module) => module.SupportPageComponent),
   supportFeedback: () => import('./features/support/support-feedback-page.component').then((module) => module.SupportFeedbackPageComponent),
 };
 
-const readingSetup: ComponentLoader = () => import('./features/english-reading/reading-setup.component').then((module) => module.ReadingSetupComponent);
-
 const migratedRoutes: Routes = PAGE_CATALOG.map((meta): Route => ({
   path: meta.path,
-  loadComponent: meta.path === 'pages/iframe/index' ? readingSetup : components[meta.kind],
+  loadComponent: components[meta.kind],
   canActivate: meta.auth ? [authGuard] : undefined,
   data: { meta },
 }));
@@ -53,7 +52,7 @@ export const routes: Routes = [
       },
       {
         path: 'words/go-reading',
-        loadComponent: readingSetup,
+        loadComponent: components.readingSetup,
         canActivate: [authGuard],
         data: { meta: { path: 'words/go-reading', title: 'AI 阅读设置', domain: 'learn', kind: 'learning', hideHeader: true, hideTabbar: true } },
       },

+ 1 - 7
projects/xiaoshu-mobile/src/app/core/external-reading.spec.ts

@@ -1,4 +1,4 @@
-import { extractReadingWords, readNcloudSessionToken } from './external-reading';
+import { extractReadingWords } from './external-reading';
 import { normalizeReadingParams } from '../features/english-reading/reading.utils';
 
 describe('external reading migration', () => {
@@ -8,12 +8,6 @@ describe('external reading migration', () => {
     expect(extractReadingWords('not-json')).toEqual([]);
   });
 
-  it('reads the original NCloud cache key without failing on invalid data', () => {
-    const storage = { getItem: () => JSON.stringify({ sessionToken: 'session-123' }) };
-    expect(readNcloudSessionToken(storage)).toBe('session-123');
-    expect(readNcloudSessionToken({ getItem: () => '{' })).toBe('');
-  });
-
   it('accepts packed legacy params in the internal reading route', () => {
     const value = JSON.stringify({ wordList: ['one'], token: 'token' });
     expect(normalizeReadingParams({ params: value }).wordList).toEqual(['one']);

+ 0 - 9
projects/xiaoshu-mobile/src/app/core/external-reading.ts

@@ -11,12 +11,3 @@ export function extractReadingWords(value: unknown): string[] {
     .map((entry) => entry && typeof entry === 'object' ? String((entry as Record<string, unknown>)['Title'] ?? '').trim() : '')
     .filter(Boolean);
 }
-
-export function readNcloudSessionToken(storage: Pick<Storage, 'getItem'> = localStorage): string {
-  try {
-    const cached = JSON.parse(storage.getItem('NCloud/dev/User') || '{}') as { sessionToken?: unknown };
-    return typeof cached.sessionToken === 'string' ? cached.sessionToken : '';
-  } catch {
-    return '';
-  }
-}

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

@@ -327,9 +327,9 @@ describe('home learning modules', () => {
     aiReadingCta.click();
     await harness.fixture.whenStable();
     const aiReadingRouter = TestBed.inject(Router);
-    expect(aiReadingRouter.url).toContain('/pages/iframe/index');
+    expect(aiReadingRouter.url).toContain('/words/go-reading');
     expect(aiReadingRouter.parseUrl(aiReadingRouter.url).queryParams['returnUrl']).toBe('/pages/his/his');
-    expect(JSON.parse(aiReadingRouter.parseUrl(aiReadingRouter.url).queryParams['params'])['GeneralID']).toBe(201);
+    expect(aiReadingRouter.parseUrl(aiReadingRouter.url).queryParams['GeneralID']).toBe('201');
 
     await harness.navigateByUrl('/pages/his/his');
     const currentLearningCard = harness.routeNativeElement?.querySelector('.current-learning-card') as HTMLElement;
@@ -376,7 +376,7 @@ describe('home learning modules', () => {
     (harness.routeNativeElement?.querySelector('.reading-button') as HTMLButtonElement).click();
     await harness.fixture.whenStable();
     const router = TestBed.inject(Router);
-    expect(router.url).toContain('/pages/iframe/index');
+    expect(router.url).toContain('/words/go-reading');
     expect(router.parseUrl(router.url).queryParams['returnUrl']).toBe('/pages/his/his');
     expect(harness.routeNativeElement?.querySelector('.back-button')).not.toBeNull();
     (harness.routeNativeElement?.querySelector('.back-button') as HTMLButtonElement).click();

+ 1 - 0
projects/xiaoshu-mobile/src/app/core/models.ts

@@ -18,6 +18,7 @@ export type PageKind =
   | 'studyRecords'
   | 'team'
   | 'merchant'
+  | 'readingSetup'
   | 'webview'
   | 'support'
   | 'supportFeedback';

+ 1 - 1
projects/xiaoshu-mobile/src/app/core/page-catalog.ts

@@ -48,7 +48,7 @@ export const PAGE_CATALOG: PageMeta[] = [
   { path: 'pages/game/downletter', title: '字母雨', domain: 'game', kind: 'game', hideTabbar: true },
   { path: 'pages/his/his_words', title: '学习单词列表', domain: 'history', kind: 'learning', auth: true },
   { path: 'pages/his/exam', title: '词汇测试', domain: 'history', kind: 'learning', auth: true },
-  { path: 'pages/iframe/index', title: 'AI 阅读设置', domain: 'learn', kind: 'webview', hideHeader: true, hideTabbar: true },
+  { path: 'pages/iframe/index', title: 'AI 阅读设置', domain: 'learn', kind: 'readingSetup', auth: true, hideHeader: true, hideTabbar: true },
   { path: 'pages/game/llk', title: '单词连连看', domain: 'game', kind: 'game', auth: true, hideTabbar: true },
   { path: 'pages/his/well', title: '专项训练', domain: 'history', kind: 'resource', auth: true, api: { action: 'content_list', params: { nid: 3 } }, onLoadActions: [{ action: 'node_get', params: { id: 3 } }], itemRoute: '/pages/home/enjoy_item' },
   { path: 'pages/help/help', title: '帮助中心', domain: 'system', kind: 'resource', api: { action: 'content_list', params: { nid: 363 } }, onLoadActions: [{ action: 'node_get' }], itemRoute: '/pages/home/enjoy_item' },

+ 2 - 2
projects/xiaoshu-mobile/src/app/features/english-reading/english-reading.service.ts

@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
 import { Subscription } from 'rxjs';
 import Mustache from 'mustache';
 import { FmodeAiService } from './fmode-ai.service';
-import { NcloudReadingService } from './ncloud-reading.service';
+import { ReadingCloudService } from './reading-cloud.service';
 import {
   ReadingArticle, ReadingHistoryEntry, ReadingLog, ReadingOption, ReadingProgress,
   ReadingQuestion, ReadingRouteParams,
@@ -22,7 +22,7 @@ interface GeneratedQuestion {
 @Injectable({ providedIn: 'root' })
 export class EnglishReadingService {
   constructor(
-    private readonly cloud: NcloudReadingService,
+    private readonly cloud: ReadingCloudService,
     private readonly ai: FmodeAiService,
   ) {}
 

+ 0 - 101
projects/xiaoshu-mobile/src/app/features/english-reading/ncloud-reading.service.ts

@@ -1,101 +0,0 @@
-import { HttpClient, HttpHeaders } from '@angular/common/http';
-import { Injectable } from '@angular/core';
-import { firstValueFrom } from 'rxjs';
-import { API_CONFIG } from '../../core/app.constants';
-import { AppUser } from '../../core/models';
-import { SessionService } from '../../core/session.service';
-import { NcloudPointer, NcloudRecord, NcloudUser } from './reading.models';
-
-const USER_CACHE_KEY = 'NCloud/dev/User';
-interface CloudEnvelope<T> { retcode: number; retmsg?: string; result: T }
-
-@Injectable({ providedIn: 'root' })
-export class NcloudReadingService {
-  private currentUser: NcloudUser | null = this.readCachedUser();
-
-  constructor(private readonly http: HttpClient, private readonly sessions: SessionService) {}
-
-  get user(): NcloudUser | null { return this.currentUser; }
-
-  get userPointer(): NcloudPointer {
-    if (!this.currentUser?.objectId) throw new Error('NCloud 用户尚未登录');
-    return this.pointer('_User', this.currentUser.objectId);
-  }
-
-  adoptSessionToken(token: string): void {
-    if (token) this.currentUser = { ...(this.currentUser ?? { objectId: '' }), sessionToken: token };
-  }
-
-  async ensureUser(appUser: AppUser | null, tokenHint = '', mobileHint = ''): Promise<NcloudUser> {
-    void mobileHint;
-    const token = tokenHint || this.sessions.token();
-    if (token) this.adoptSessionToken(token);
-    const objectId = String(appUser?.['objectId'] || this.currentUser?.objectId || '').trim();
-    if (token.startsWith('r:') && objectId) {
-      return this.persistUser({
-        ...(this.currentUser ?? { objectId }),
-        objectId,
-        sessionToken: token,
-        username: String(appUser?.userName || this.currentUser?.username || ''),
-        nickname: String(appUser?.honeyName || this.currentUser?.nickname || ''),
-        mobile: String(appUser?.mobile || this.currentUser?.mobile || ''),
-      });
-    }
-    throw new Error('阅读学习账户需要新的登录会话,请退出后重新登录');
-  }
-
-  async updateUser(patch: Partial<NcloudUser>): Promise<NcloudUser> {
-    if (!this.currentUser?.objectId) throw new Error('NCloud 用户尚未登录');
-    const updated = await this.call<NcloudUser>('reading/user/update', { patch });
-    return this.persistUser({ ...this.currentUser, ...updated, ...patch });
-  }
-
-  async query<T extends NcloudRecord>(className: string, where: Record<string, unknown>, options: {
-    order?: string; limit?: number; skip?: number;
-  } = {}): Promise<T[]> {
-    return this.call<T[]>('reading/query', { className, where, ...options });
-  }
-
-  async get<T extends NcloudRecord>(className: string, objectId: string): Promise<T> {
-    return this.call<T>('reading/get', { className, objectId });
-  }
-
-  async create<T extends NcloudRecord>(className: string, value: Omit<T, 'objectId'>): Promise<T> {
-    return this.call<T>('reading/create', { className, value });
-  }
-
-  async update<T extends NcloudRecord>(className: string, value: T, patch: Partial<T>): Promise<T> {
-    const response = await this.call<Partial<T>>('reading/update', { className, objectId: value.objectId, patch });
-    return { ...value, ...patch, ...response };
-  }
-
-  pointer(className: string, objectId: string): NcloudPointer {
-    return { __type: 'Pointer', className, objectId };
-  }
-
-  private async call<T>(action: string, params: Record<string, unknown> = {}): Promise<T> {
-    const token = String(this.currentUser?.sessionToken || this.sessions.token() || '').trim();
-    if (!token.startsWith('r:')) throw new Error('阅读学习账户需要新的登录会话,请退出后重新登录');
-    const response = await firstValueFrom(this.http.post<CloudEnvelope<T>>(API_CONFIG.cloudFunctionUrl, {
-      token,
-      params: { action, ...params },
-    }, {
-      headers: new HttpHeaders({ 'X-Parse-Application-Id': API_CONFIG.parseAppId, 'Content-Type': 'application/json' }),
-    }));
-    if (response.retcode === -1) throw new Error(response.retmsg || '阅读云函数请求失败');
-    return response.result;
-  }
-
-  private persistUser(user: NcloudUser): NcloudUser {
-    this.currentUser = user;
-    localStorage.setItem(USER_CACHE_KEY, JSON.stringify(user));
-    return user;
-  }
-
-  private readCachedUser(): NcloudUser | null {
-    try {
-      const user = JSON.parse(localStorage.getItem(USER_CACHE_KEY) || 'null') as NcloudUser | null;
-      return user?.sessionToken ? user : null;
-    } catch { return null; }
-  }
-}

+ 22 - 10
projects/xiaoshu-mobile/src/app/features/english-reading/ncloud-reading.service.spec.ts → projects/xiaoshu-mobile/src/app/features/english-reading/reading-cloud.service.spec.ts

@@ -3,37 +3,49 @@ import { HttpTestingController, provideHttpClientTesting } from '@angular/common
 import { TestBed } from '@angular/core/testing';
 import { API_CONFIG } from '../../core/app.constants';
 import { SessionService } from '../../core/session.service';
-import { NcloudReadingService } from './ncloud-reading.service';
+import { ReadingCloudService } from './reading-cloud.service';
 
-describe('NcloudReadingService', () => {
-  let service: NcloudReadingService;
+describe('ReadingCloudService', () => {
+  let service: ReadingCloudService;
   let http: HttpTestingController;
+  let token: string;
 
   beforeEach(() => {
     localStorage.removeItem('NCloud/dev/User');
+    localStorage.removeItem('xiaoshu.reading.user');
+    localStorage.setItem('NCloud/dev/User', JSON.stringify({ objectId: 'legacy-user', sessionToken: 'r:legacy-token' }));
+    token = '';
     TestBed.configureTestingModule({ providers: [
       provideHttpClient(),
       provideHttpClientTesting(),
-      { provide: SessionService, useValue: { token: () => '' } },
+      { provide: SessionService, useValue: { token: () => token } },
     ] });
-    service = TestBed.inject(NcloudReadingService);
+    service = TestBed.inject(ReadingCloudService);
     http = TestBed.inject(HttpTestingController);
   });
 
   afterEach(() => {
     http.verify();
     localStorage.removeItem('NCloud/dev/User');
+    localStorage.removeItem('xiaoshu.reading.user');
   });
 
-  it('adopts the migrated Parse session without creating a second deterministic account', async () => {
-    const result = service.ensureUser({ userId: 7, objectId: 'user-1', mobile: '13800000000', honeyName: '小树' }, 'r:token-1');
+  it('uses the current H5 login session to load the migrated reading profile', async () => {
+    token = 'r:token-1';
+    const result = service.ensureUser({ userId: 7, objectId: 'user-1', mobile: '13800000000', honeyName: '小树' });
+    const request = http.expectOne(API_CONFIG.cloudFunctionUrl);
+    expect(request.request.body).toEqual({ token: 'r:token-1', params: { action: 'reading/me' } });
+    request.flush({ retcode: 0, result: { objectId: 'user-1', username: 'member-7', nickname: '小树' } });
     expect((await result).objectId).toBe('user-1');
-    expect(JSON.parse(localStorage.getItem('NCloud/dev/User') || '{}').sessionToken).toBe('r:token-1');
-    http.expectNone(API_CONFIG.cloudFunctionUrl);
+    expect(JSON.parse(localStorage.getItem('xiaoshu.reading.user') || '{}').sessionToken).toBe('r:token-1');
+    expect(localStorage.getItem('NCloud/dev/User')).toBeNull();
   });
 
   it('sends filters and the active session token only to the reading cloud gateway', async () => {
-    await service.ensureUser({ userId: 7, objectId: 'user-1', mobile: '13800000000' }, 'r:token-1');
+    token = 'r:token-1';
+    const profile = service.ensureUser({ userId: 7, objectId: 'user-1', mobile: '13800000000' });
+    http.expectOne(API_CONFIG.cloudFunctionUrl).flush({ retcode: 0, result: { objectId: 'user-1', username: 'member-7' } });
+    await profile;
     const query = service.query('SurveyItem', { type: 'reading', isDeleted: { '$ne': true } }, { order: '-createdAt', limit: 3 });
     const request = http.expectOne(API_CONFIG.cloudFunctionUrl);
     expect(request.request.body).toEqual({

+ 98 - 0
projects/xiaoshu-mobile/src/app/features/english-reading/reading-cloud.service.ts

@@ -0,0 +1,98 @@
+import { HttpClient, HttpHeaders } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { firstValueFrom } from 'rxjs';
+import { API_CONFIG } from '../../core/app.constants';
+import { AppUser } from '../../core/models';
+import { SessionService } from '../../core/session.service';
+import { ReadingPointer, ReadingRecord, ReadingUser } from './reading.models';
+
+const USER_CACHE_KEY = 'xiaoshu.reading.user';
+const LEGACY_USER_CACHE_KEY = 'NCloud/dev/User';
+interface CloudEnvelope<T> { retcode: number; retmsg?: string; result: T }
+
+@Injectable({ providedIn: 'root' })
+export class ReadingCloudService {
+  private currentUser: ReadingUser | null;
+
+  constructor(private readonly http: HttpClient, private readonly sessions: SessionService) {
+    this.currentUser = this.readCachedUser();
+    localStorage.removeItem(LEGACY_USER_CACHE_KEY);
+  }
+
+  get user(): ReadingUser | null { return this.currentUser; }
+
+  get userPointer(): ReadingPointer {
+    if (!this.currentUser?.objectId) throw new Error('NCloud 用户尚未登录');
+    return this.pointer('_User', this.currentUser.objectId);
+  }
+
+  async ensureUser(appUser: AppUser | null, mobileHint = ''): Promise<ReadingUser> {
+    const token = String(this.sessions.token() || '').trim();
+    if (!token.startsWith('r:')) throw new Error('阅读学习需要登录,请退出后重新登录');
+    if (this.currentUser?.sessionToken === token && this.currentUser.objectId) return this.currentUser;
+    const profile = await this.call<ReadingUser>('reading/me');
+    return this.persistUser({
+      ...profile,
+      sessionToken: token,
+      username: profile.username || String(appUser?.userName || ''),
+      nickname: profile.nickname || String(appUser?.honeyName || ''),
+      mobile: profile.mobile || String(appUser?.mobile || mobileHint || ''),
+    });
+  }
+
+  async updateUser(patch: Partial<ReadingUser>): Promise<ReadingUser> {
+    if (!this.currentUser?.objectId) throw new Error('NCloud 用户尚未登录');
+    const updated = await this.call<ReadingUser>('reading/user/update', { patch });
+    return this.persistUser({ ...this.currentUser, ...updated, ...patch });
+  }
+
+  async query<T extends ReadingRecord>(className: string, where: Record<string, unknown>, options: {
+    order?: string; limit?: number; skip?: number;
+  } = {}): Promise<T[]> {
+    return this.call<T[]>('reading/query', { className, where, ...options });
+  }
+
+  async get<T extends ReadingRecord>(className: string, objectId: string): Promise<T> {
+    return this.call<T>('reading/get', { className, objectId });
+  }
+
+  async create<T extends ReadingRecord>(className: string, value: Omit<T, 'objectId'>): Promise<T> {
+    return this.call<T>('reading/create', { className, value });
+  }
+
+  async update<T extends ReadingRecord>(className: string, value: T, patch: Partial<T>): Promise<T> {
+    const response = await this.call<Partial<T>>('reading/update', { className, objectId: value.objectId, patch });
+    return { ...value, ...patch, ...response };
+  }
+
+  pointer(className: string, objectId: string): ReadingPointer {
+    return { __type: 'Pointer', className, objectId };
+  }
+
+  private async call<T>(action: string, params: Record<string, unknown> = {}): Promise<T> {
+    const token = String(this.sessions.token() || '').trim();
+    if (!token.startsWith('r:')) throw new Error('阅读学习账户需要新的登录会话,请退出后重新登录');
+    const response = await firstValueFrom(this.http.post<CloudEnvelope<T>>(API_CONFIG.cloudFunctionUrl, {
+      token,
+      params: { action, ...params },
+    }, {
+      headers: new HttpHeaders({ 'X-Parse-Application-Id': API_CONFIG.parseAppId, 'Content-Type': 'application/json' }),
+    }));
+    if (response.retcode === -1) throw new Error(response.retmsg || '阅读云函数请求失败');
+    return response.result;
+  }
+
+  private persistUser(user: ReadingUser): ReadingUser {
+    this.currentUser = user;
+    localStorage.setItem(USER_CACHE_KEY, JSON.stringify(user));
+    return user;
+  }
+
+  private readCachedUser(): ReadingUser | null {
+    try {
+      const user = JSON.parse(localStorage.getItem(USER_CACHE_KEY) || 'null') as ReadingUser | null;
+      const token = String(this.sessions.token() || '').trim();
+      return user?.sessionToken === token && token.startsWith('r:') ? user : null;
+    } catch { return null; }
+  }
+}

+ 3 - 3
projects/xiaoshu-mobile/src/app/features/english-reading/reading-exercise.component.ts

@@ -5,7 +5,7 @@ import { BookOpen, CheckCircle2, ChevronRight, CirclePlus, FileDown, History, Lo
 import { PlatformService } from '../../core/platform.service';
 import { SessionService } from '../../core/session.service';
 import { EnglishReadingService } from './english-reading.service';
-import { NcloudReadingService } from './ncloud-reading.service';
+import { ReadingCloudService } from './reading-cloud.service';
 import { ReadingArticle, ReadingHistoryEntry, ReadingProgress, ReadingQuestion, ReadingRouteParams } from './reading.models';
 import { highlightWords, normalizeReadingParams, shuffle } from './reading.utils';
 
@@ -63,7 +63,7 @@ export class ReadingExerciseComponent implements OnInit {
   private readonly route = inject(ActivatedRoute);
   private readonly router = inject(Router);
   private readonly session = inject(SessionService);
-  private readonly cloud = inject(NcloudReadingService);
+  private readonly cloud = inject(ReadingCloudService);
   private readonly reading = inject(EnglishReadingService);
   private readonly platform = inject(PlatformService);
 
@@ -186,7 +186,7 @@ export class ReadingExerciseComponent implements OnInit {
     this.submitted.set(false);
     this.loadingTip.set(this.tips[Math.floor(Math.random() * this.tips.length)]);
     try {
-      await this.cloud.ensureUser(this.session.student() ?? this.session.user(), this.params.token, this.params.phone);
+      await this.cloud.ensureUser(this.session.student() ?? this.session.user(), this.params.phone);
       await this.refreshHistoryCount();
       let article = await this.withTimeout(
         this.reading.createArticle({ ...this.params, renew: false }, (value) => this.updateArticleProgress(value)),

+ 1 - 2
projects/xiaoshu-mobile/src/app/features/english-reading/reading-home.component.ts

@@ -17,10 +17,9 @@ export class ReadingHomeComponent {
 
   async open(feature: 'go-reading' | 'source' | 'companion'): Promise<void> {
     if (feature === 'go-reading') {
-      await this.router.navigate(['/words/go-reading'], { queryParams: { t: Date.now() } });
+      await this.router.navigate(['/words/go-reading'], { queryParams: { returnUrl: '/english', t: Date.now() } });
       return;
     }
     this.comingSoon.set(true);
   }
 }
-

+ 2 - 2
projects/xiaoshu-mobile/src/app/features/english-reading/reading-pdf.component.ts

@@ -5,7 +5,7 @@ import html2pdf from 'html2pdf.js';
 import { ArrowLeft, Download, LoaderCircle, LucideAngularModule } from 'lucide-angular';
 import { PlatformService } from '../../core/platform.service';
 import { SessionService } from '../../core/session.service';
-import { NcloudReadingService } from './ncloud-reading.service';
+import { ReadingCloudService } from './reading-cloud.service';
 import { ReadingArticle, ReadingQuestion } from './reading.models';
 import { highlightWords } from './reading.utils';
 
@@ -29,7 +29,7 @@ export class ReadingPdfComponent implements OnInit {
   readonly error = signal('');
 
   private readonly sid = inject(ActivatedRoute).snapshot.paramMap.get('sid') ?? '';
-  private readonly cloud = inject(NcloudReadingService);
+  private readonly cloud = inject(ReadingCloudService);
   private readonly session = inject(SessionService);
   private readonly platform = inject(PlatformService);
 

+ 4 - 9
projects/xiaoshu-mobile/src/app/features/english-reading/reading-setup.component.ts

@@ -6,7 +6,7 @@ import { ArrowLeft, BookOpen, ChevronRight, Clock3, History, LoaderCircle, Lucid
 import { PlatformService } from '../../core/platform.service';
 import { SessionService } from '../../core/session.service';
 import { EnglishReadingService } from './english-reading.service';
-import { NcloudReadingService } from './ncloud-reading.service';
+import { ReadingCloudService } from './reading-cloud.service';
 import { ReadingHistoryEntry, ReadingRouteParams } from './reading.models';
 import { normalizeReadingParams, READING_LEVELS, READING_THEMES, READING_TYPES, splitWords } from './reading.utils';
 
@@ -37,14 +37,13 @@ export class ReadingSetupComponent implements OnInit {
   vocabulary = '';
   type = '随机';
   theme = '随机';
-  private tokenHint = '';
   private historyPageIndex = 0;
   private readonly historyPageSize = 10;
 
   private readonly route = inject(ActivatedRoute);
   private readonly router = inject(Router);
   private readonly session = inject(SessionService);
-  readonly cloud = inject(NcloudReadingService);
+  readonly cloud = inject(ReadingCloudService);
   private readonly reading = inject(EnglishReadingService);
   private readonly platform = inject(PlatformService);
   readonly splitWords = splitWords;
@@ -65,8 +64,6 @@ export class ReadingSetupComponent implements OnInit {
     this.vocabulary = params.wordList.join(', ');
     this.type = params.type || '随机';
     this.theme = params.theme || '随机';
-    this.tokenHint = params.token ?? '';
-    if (this.tokenHint) this.cloud.adoptSessionToken(this.tokenHint);
     this.level = params.level || this.level;
   }
 
@@ -74,8 +71,7 @@ export class ReadingSetupComponent implements OnInit {
 
   goBack(): void {
     const requestedUrl = this.route.snapshot.queryParamMap.get('returnUrl');
-    const sourcePath = String(this.route.snapshot.data['meta']?.path ?? '');
-    const fallbackUrl = sourcePath === 'pages/iframe/index' ? '/pages/his/his' : '/english';
+    const fallbackUrl = '/pages/his/his';
     const returnUrl = requestedUrl?.startsWith('/') && !requestedUrl.startsWith('//') ? requestedUrl : fallbackUrl;
     void this.router.navigateByUrl(returnUrl, { replaceUrl: true });
   }
@@ -166,8 +162,7 @@ export class ReadingSetupComponent implements OnInit {
 
   private async ensureUser(): Promise<void> {
     const appUser = this.session.student() ?? this.session.user();
-    const user = await this.cloud.ensureUser(appUser, this.tokenHint, this.mobile);
-    this.tokenHint = user.sessionToken;
+    await this.cloud.ensureUser(appUser, this.mobile);
   }
 
   private random<T>(values: readonly T[]): T { return values[Math.floor(Math.random() * values.length)]; }

+ 9 - 10
projects/xiaoshu-mobile/src/app/features/english-reading/reading.models.ts

@@ -1,11 +1,11 @@
-export interface NcloudRecord {
+export interface ReadingRecord {
   objectId: string;
   createdAt?: string;
   updatedAt?: string;
   [key: string]: unknown;
 }
 
-export interface NcloudUser extends NcloudRecord {
+export interface ReadingUser extends ReadingRecord {
   sessionToken: string;
   username?: string;
   mobile?: string;
@@ -13,7 +13,7 @@ export interface NcloudUser extends NcloudRecord {
   level?: string;
 }
 
-export interface NcloudPointer {
+export interface ReadingPointer {
   __type: 'Pointer';
   className: string;
   objectId: string;
@@ -28,7 +28,7 @@ export interface ReadingCreateParams {
   theme: string;
 }
 
-export interface ReadingArticle extends NcloudRecord {
+export interface ReadingArticle extends ReadingRecord {
   type: 'reading';
   title: string;
   content: string;
@@ -36,7 +36,7 @@ export interface ReadingArticle extends NcloudRecord {
     tpl: string;
     params: ReadingCreateParams;
   };
-  user?: NcloudPointer;
+  user?: ReadingPointer;
 }
 
 export interface ReadingOption {
@@ -45,9 +45,9 @@ export interface ReadingOption {
   answer?: string;
 }
 
-export interface ReadingQuestion extends NcloudRecord {
+export interface ReadingQuestion extends ReadingRecord {
   type: 'reading-item';
-  parent: NcloudPointer;
+  parent: ReadingPointer;
   index: number;
   title: string;
   difficulty?: string;
@@ -59,8 +59,8 @@ export interface ReadingQuestion extends NcloudRecord {
   };
 }
 
-export interface ReadingLog extends NcloudRecord {
-  surveyItem: NcloudPointer;
+export interface ReadingLog extends ReadingRecord {
+  surveyItem: ReadingPointer;
   answer: Record<string, string>;
   right: number;
   wrong: number;
@@ -93,4 +93,3 @@ export interface ReadingProgress {
   message: string;
   content?: string;
 }
-

+ 4 - 6
projects/xiaoshu-mobile/src/app/features/study-records/study-records-page.component.ts

@@ -5,7 +5,7 @@ import { ActivatedRoute, Router, RouterLink } from '@angular/router';
 import { AlertCircle, ArrowRight, BookOpen, CalendarDays, CheckCircle2, ChevronLeft, ChevronRight, Clock3, LoaderCircle, LucideAngularModule, RotateCcw, Sparkles } from 'lucide-angular';
 import { catchError, finalize, forkJoin, of } from 'rxjs';
 import { ApiError, ApiService } from '../../core/api.service';
-import { extractReadingWords, readNcloudSessionToken } from '../../core/external-reading';
+import { extractReadingWords } from '../../core/external-reading';
 import { canStartTeacherLedLearning } from '../../core/learning-access';
 import { PageMeta } from '../../core/models';
 import { SessionService } from '../../core/session.service';
@@ -261,14 +261,12 @@ export class StudyRecordsPageComponent implements OnInit {
     const wordList = extractReadingWords(item['xxqs']);
     if (!wordList.length) return;
     const params = {
-      wordList,
-      token: readNcloudSessionToken(),
+      wordList: wordList.join(','),
       GeneralID: item['GeneralID'],
+      returnUrl: this.router.url,
       t: Date.now(),
     };
-    void this.router.navigate(['/pages/iframe/index'], {
-      queryParams: { params: JSON.stringify(params), returnUrl: this.router.url },
-    });
+    void this.router.navigate(['/words/go-reading'], { queryParams: params });
   }
 
   title(item: Item): string {