Selaa lähdekoodia

feat: wire frontend to backend via proxy + auth interceptor; fix query DTO type coercion

17846405080 1 kuukausi sitten
vanhempi
commit
89355a1b9a

+ 3 - 23
campus_health_app/backend/src/dto/weight-record.dto.ts

@@ -9,6 +9,7 @@ import {
   IsArray,
   Matches
 } from 'class-validator';
+import { Type } from 'class-transformer';
 import { MeasurementCondition } from '../entities/WeightRecord';
 
 export class CreateWeightRecordDto {
@@ -100,36 +101,15 @@ export class WeightRecordQueryDto {
   tags?: string[];
 
   @IsOptional()
+  @Type(() => Number)
   @IsNumber({}, { message: '页码必须是数字' })
   @Min(1, { message: '页码不能小于1' })
   page?: number;
 
   @IsOptional()
+  @Type(() => Number)
   @IsNumber({}, { message: '每页数量必须是数字' })
   @Min(1, { message: '每页数量不能小于1' })
   @Max(100, { message: '每页数量不能大于100' })
   limit?: number;
 }
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-

+ 3 - 0
campus_health_app/frontend/campus-health-app/angular.json

@@ -57,6 +57,9 @@
         },
         "serve": {
           "builder": "@angular/build:dev-server",
+          "options": {
+            "proxyConfig": "proxy.conf.json"
+          },
           "configurations": {
             "production": {
               "buildTarget": "campus-health-app:build:production"

+ 8 - 0
campus_health_app/frontend/campus-health-app/proxy.conf.json

@@ -0,0 +1,8 @@
+{
+  "/api": {
+    "target": "http://localhost:3000",
+    "secure": false,
+    "changeOrigin": true,
+    "logLevel": "debug"
+  }
+}

+ 3 - 2
campus_health_app/frontend/campus-health-app/src/app/app.config.ts

@@ -1,16 +1,17 @@
 import { ApplicationConfig, importProvidersFrom } from '@angular/core';
 import { provideRouter } from '@angular/router';
-import { provideHttpClient } from '@angular/common/http';
+import { provideHttpClient, withInterceptors } from '@angular/common/http';
 import { provideAnimations } from '@angular/platform-browser/animations';
 import { NgxEchartsModule } from 'ngx-echarts';
 
 import { routes } from './app.routes';
 import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
+import { authInterceptor } from './core/interceptors/auth.interceptor';
 
 export const appConfig: ApplicationConfig = {
   providers: [
     provideRouter(routes),
-    provideHttpClient(),
+    provideHttpClient(withInterceptors([authInterceptor])),
     provideAnimations(),
     provideClientHydration(withEventReplay()),
     importProvidersFrom(

+ 14 - 0
campus_health_app/frontend/campus-health-app/src/app/core/interceptors/auth.interceptor.ts

@@ -0,0 +1,14 @@
+import { HttpInterceptorFn } from '@angular/common/http';
+
+/**
+ * 临时认证拦截器
+ * 目前后端用 x-user-id header 识别用户(TODO: 换 JWT)
+ */
+export const authInterceptor: HttpInterceptorFn = (req, next) => {
+  const cloned = req.clone({
+    setHeaders: {
+      'x-user-id': 'synthetic-user-001'
+    }
+  });
+  return next(cloned);
+};

+ 21 - 18
campus_health_app/frontend/campus-health-app/src/app/services/weight-data.service.ts

@@ -26,6 +26,8 @@ export class WeightDataService {
   constructor(private http: HttpClient) {
     this.initializeData();
     this.startAutoSync();
+    // 启动时立刻从后端拉一次真实数据
+    setTimeout(() => this.syncFromBackend(), 100);
   }
 
   /**
@@ -306,31 +308,32 @@ export class WeightDataService {
    * 从后端同步数据
    */
   private syncFromBackend(): void {
-    // 获取记录
-    this.http.get<WeightRecord[]>(`${this.API_BASE}/records`)
-      .pipe(catchError(() => []))
-      .subscribe(backendRecords => {
+    // 获取记录(后端返回 { success, data: { records, total, page, limit } })
+    this.http.get<{ success: boolean; data: { records: WeightRecord[]; total: number } }>(
+      `${this.API_BASE}/records?limit=100&page=1`
+    )
+      .pipe(catchError(() => of({ success: false, data: { records: [], total: 0 } })))
+      .subscribe(resp => {
+        const backendRecords = resp?.data?.records ?? [];
+        if (backendRecords.length === 0) return;
+
+        // 后端数据优先:直接覆盖本地记录
         const localData = this.getLocalData();
-        
-        // 合并本地和后端数据(后端数据优先)
-        const mergedRecords = this.mergeRecords(localData.records, backendRecords as WeightRecord[]);
-        
-        localData.records = mergedRecords;
+        localData.records = backendRecords;
         localData.lastSyncTime = Date.now();
-        
         this.saveLocalData(localData);
-        this.recordsSubject.next(mergedRecords);
+        this.recordsSubject.next(backendRecords);
       });
-    
+
     // 获取目标
-    this.http.get<WeightGoal>(`${this.API_BASE}/goal`)
-      .pipe(catchError(() => of(null as unknown as WeightGoal)))
-      .subscribe(backendGoal => {
-        if (backendGoal) {
+    this.http.get<{ success: boolean; data: WeightGoal | null }>(`${this.API_BASE}/goal`)
+      .pipe(catchError(() => of({ success: false, data: null })))
+      .subscribe(resp => {
+        if (resp?.data) {
           const localData = this.getLocalData();
-          localData.goal = backendGoal as WeightGoal;
+          localData.goal = resp.data;
           this.saveLocalData(localData);
-          this.goalSubject.next(backendGoal as WeightGoal);
+          this.goalSubject.next(resp.data);
         }
       });
   }