gangvy 5 miesięcy temu
rodzic
commit
d1142da91e
5 zmienionych plików z 146 dodań i 89 usunięć
  1. 3 1
      angular.json
  2. 2 1
      src/app/app.html
  3. 18 0
      src/app/app.scss
  4. 75 83
      src/app/app.ts
  5. 48 4
      src/app/login-modal.component.ts

+ 3 - 1
angular.json

@@ -66,7 +66,9 @@
                   "maximumError": "32kB"
                 }
               ],
-              "outputHashing": "all"
+              "outputHashing": "all",
+              "baseHref": "/dev/apig-pay/",
+              "deployUrl": "/dev/apig-pay/"
             },
             "development": {
               "optimization": false,

+ 2 - 1
src/app/app.html

@@ -6,10 +6,11 @@
   <div class="logo">BRAIN<span>HACK</span></div>
   <div class="subtitle">API 服务充值</div>
   <div class="user-bar" *ngIf="loggedInUser">
-    <span class="user-badge">👤 已登录</span>
+    <span class="user-badge">👤 {{loggedInUser}}</span>
     <button class="token-copy-btn" (click)="copyToken()" *ngIf="sessionToken">
       {{tokenCopied ? '✅ 已复制' : '📋 复制Token'}}
     </button>
+    <button class="logout-btn" (click)="logout()">退出</button>
   </div>
 </div>
 

+ 18 - 0
src/app/app.scss

@@ -101,6 +101,9 @@
 }
 .page-header .user-bar {
   margin-left: auto;
+  display: flex;
+  align-items: center;
+  gap: 8px;
 }
 .page-header .user-badge {
   display: inline-flex;
@@ -129,6 +132,21 @@
   background: rgba(0,212,255,0.15);
   border-color: var(--neon);
 }
+.page-header .logout-btn {
+  padding: 4px 12px;
+  font-size: 12px;
+  font-weight: 600;
+  color: var(--danger);
+  background: rgba(255,77,106,0.08);
+  border: 1px solid rgba(255,77,106,0.2);
+  border-radius: 20px;
+  cursor: pointer;
+  transition: all 0.2s;
+}
+.page-header .logout-btn:hover {
+  background: rgba(255,77,106,0.15);
+  border-color: var(--danger);
+}
 
 /* ─── Main Container ─── */
 .main-container {

+ 75 - 83
src/app/app.ts

@@ -114,14 +114,14 @@ export class App implements OnInit {
     // 2. URL 没有 user → 检查 localStorage 缓存
     const cachedUserId = localStorage.getItem('apig_user_id');
     const cachedToken = localStorage.getItem('apig_session_token');
+    const cachedName = localStorage.getItem('apig_display_name');
     if (cachedUserId && cachedToken) {
       console.log('[AUTH] 从 localStorage 恢复用户:', cachedUserId);
       this.userId = cachedUserId;
-      this.loggedInUser = cachedUserId;
+      this.loggedInUser = cachedName || cachedUserId;
       this.sessionToken = cachedToken;
-      // 后台验证 token 是否还有效
-      this.validateCachedToken(cachedToken, cachedUserId);
-      this.loadApig();
+      // 先验证 token,验证通过再加载数据
+      this.validateAndLoad(cachedToken, cachedUserId);
       return;
     }
 
@@ -130,7 +130,7 @@ export class App implements OnInit {
     this.needLogin = true;
   }
 
-  async validateCachedToken(token: string, userId: string): Promise<void> {
+  async validateAndLoad(token: string, userId: string): Promise<void> {
     try {
       const resp = await fetch(`${API_BASE}/parse/users/me`, {
         headers: {
@@ -143,17 +143,37 @@ export class App implements OnInit {
         console.warn('[AUTH] 缓存 token 已失效,需要重新登录');
         localStorage.removeItem('apig_user_id');
         localStorage.removeItem('apig_session_token');
+        localStorage.removeItem('apig_display_name');
         this.needLogin = true;
+        this.cdr.detectChanges();
+        return;
+      }
+      // token 有效 — 更新 displayName(可能 localStorage 里是旧的)
+      const serverName = data.nickname || data.username || data.mobilePhoneNumber || userId;
+      if (serverName && serverName !== this.loggedInUser) {
+        this.loggedInUser = serverName;
+        localStorage.setItem('apig_display_name', serverName);
+      }
+      // userId 可能变化(极端情况:session 对应不同用户)
+      if (data.objectId !== userId) {
+        console.warn('[AUTH] session 对应用户不匹配,纠正:', userId, '→', data.objectId);
+        this.userId = data.objectId;
+        localStorage.setItem('apig_user_id', data.objectId);
       }
+      console.log('[AUTH] token 验证通过, user:', data.objectId, data.username || '');
+      this.cdr.detectChanges();
+      this.loadApig();
     } catch (e) {
       console.warn('[AUTH] 验证 token 失败:', e);
+      // 网络错误时仍然尝试加载(可能离线缓存能用)
+      this.loadApig();
     }
   }
 
-  onLoginSuccess(event: { userId: string; sessionToken: string }): void {
-    console.log('[AUTH] 登录成功, userId:', event.userId);
+  onLoginSuccess(event: { userId: string; sessionToken: string; displayName: string }): void {
+    console.log('[AUTH] 登录成功, userId:', event.userId, 'display:', event.displayName);
     this.userId = event.userId;
-    this.loggedInUser = event.userId;
+    this.loggedInUser = event.displayName || event.userId;
     this.sessionToken = event.sessionToken;
     this.needLogin = false;
     this.errorMsg = '';
@@ -161,6 +181,23 @@ export class App implements OnInit {
     this.loadApig();
   }
 
+  logout(): void {
+    console.log('[AUTH] 退出登录');
+    localStorage.removeItem('apig_user_id');
+    localStorage.removeItem('apig_session_token');
+    localStorage.removeItem('apig_display_name');
+    this.userId = '';
+    this.loggedInUser = '';
+    this.sessionToken = '';
+    this.authId = '';
+    this.apig = null;
+    this.orders = [];
+    this.errorMsg = '';
+    this.showSuccess = false;
+    this.needLogin = true;
+    this.cdr.detectChanges();
+  }
+
   // ─── Load available APIG list (when no apigId) ───
   async loadApigList(): Promise<void> {
     this.apigListLoading = true;
@@ -214,6 +251,7 @@ export class App implements OnInit {
         const resolved = await this.resolveAuthId(this.userId, this.apigId);
         if (!resolved) {
           this.errorMsg = '无法获取或创建计费账套,请联系管理员。';
+          this.cdr.detectChanges();
           return;
         }
         this.authId = resolved;
@@ -254,14 +292,19 @@ export class App implements OnInit {
       }
 
       this.errorMsg = '无法加载接口信息。请确认参数正确。';
+      this.cdr.detectChanges();
     } catch (e: any) {
       this.errorMsg = '网络错误: ' + e.message;
+      this.cdr.detectChanges();
     }
   }
 
   // ─── 通过 user+apigid 查询或创建 APIGAuth ───
   async resolveAuthId(user: string, apig: string): Promise<string | null> {
-    // 1. 查询已有 APIGAuth(按 user 指针)
+    const authHeaders: any = { 'X-Parse-Application-Id': APP_ID };
+    if (this.sessionToken) authHeaders['X-Parse-Session-Token'] = this.sessionToken;
+
+    // 1. 精确查询:按 user + api 指针
     try {
       const query: any = {
         where: JSON.stringify({
@@ -271,96 +314,45 @@ export class App implements OnInit {
         limit: '1'
       };
       const resp = await fetch(API_BASE + '/parse/classes/APIGAuth?' + new URLSearchParams(query), {
-        headers: { 'X-Parse-Application-Id': APP_ID }
+        headers: authHeaders
       });
       const data = await resp.json();
       if (data.results?.length > 0) {
-        console.log('[resolveAuthId] 找到已有 APIGAuth:', data.results[0].objectId);
+        console.log('[resolveAuthId] 找到已有 APIGAuth:', data.results[0].objectId, 'count:', data.results[0].count);
         return data.results[0].objectId;
       }
     } catch (e: any) {
       console.warn('[resolveAuthId] 查询失败:', e.message);
     }
 
-    // 2. 回退:按 api 查询所有记录(兼容旧 company / 不同 user 的记录)
-    //    合并所有记录的 count 和 used 到一条主记录,绑定当前 user,删除多余记录
+    // 2. 未找到 → 为当前用户创建新 APIGAuth(余额=0,需充值)
     try {
-      const fallbackQuery: any = {
-        where: JSON.stringify({
-          api: { __type: 'Pointer', className: 'APIG', objectId: apig }
-        }),
-        order: '-count',
-        limit: '20'
-      };
-      const fbResp = await fetch(API_BASE + '/parse/classes/APIGAuth?' + new URLSearchParams(fallbackQuery), {
-        headers: { 'X-Parse-Application-Id': APP_ID }
-      });
-      const fbData = await fbResp.json();
-      const records = fbData.results || [];
-
-      if (records.length > 0) {
-        // 汇总所有记录的 count 和 used
-        let totalCount = 0;
-        let totalUsed = 0;
-        for (const r of records) {
-          totalCount += (r.count || 0);
-          totalUsed += (r.used || 0);
-        }
-
-        // 选第一条(count 最高)作为主记录
-        const primary = records[0];
-        console.log(`[resolveAuthId] 找到 ${records.length} 条旧记录,合并余额: count=${totalCount}, used=${totalUsed}`);
-
-        // 更新主记录:绑定当前 user + 合并余额
-        try {
-          await fetch(API_BASE + '/parse/classes/APIGAuth/' + primary.objectId, {
-            method: 'PUT',
-            headers: { 'X-Parse-Application-Id': APP_ID, 'Content-Type': 'application/json' },
-            body: JSON.stringify({
-              user: { __type: 'Pointer', className: '_User', objectId: user },
-              count: totalCount,
-              used: totalUsed
-            })
-          });
-          console.log('[resolveAuthId] 主记录已更新:', primary.objectId);
-        } catch (e: any) {
-          console.warn('[resolveAuthId] 更新主记录失败:', e.message);
-        }
+      console.log('[resolveAuthId] 未找到该用户的 APIGAuth,创建新记录(余额=0)...');
 
-        // 删除多余记录
-        for (let i = 1; i < records.length; i++) {
-          try {
-            await fetch(API_BASE + '/parse/classes/APIGAuth/' + records[i].objectId, {
-              method: 'DELETE',
-              headers: { 'X-Parse-Application-Id': APP_ID }
-            });
-            console.log('[resolveAuthId] 删除重复记录:', records[i].objectId);
-          } catch (e: any) {
-            console.warn('[resolveAuthId] 删除失败:', records[i].objectId, e.message);
-          }
+      // 获取用户的 company 指针(ensureCompany 已在登录时创建)
+      let companyPointer: any = undefined;
+      try {
+        const meResp = await fetch(API_BASE + '/parse/users/me', { headers: authHeaders });
+        const meData = await meResp.json();
+        if (meData.company?.objectId) {
+          companyPointer = { __type: 'Pointer', className: 'Company', objectId: meData.company.objectId };
         }
-
-        return primary.objectId;
+      } catch (e: any) {
+        console.warn('[resolveAuthId] 获取 company 失败:', e.message);
       }
-    } catch (e: any) {
-      console.warn('[resolveAuthId] 回退查询失败:', e.message);
-    }
 
-    // 3. 创建新 APIGAuth
-    try {
-      console.log('[resolveAuthId] 未找到任何 APIGAuth,创建新记录...');
+      const body: any = {
+        api: { __type: 'Pointer', className: 'APIG', objectId: apig },
+        user: { __type: 'Pointer', className: '_User', objectId: user },
+        count: 0,
+        used: 0
+      };
+      if (companyPointer) body.company = companyPointer;
+
       const createResp = await fetch(API_BASE + '/parse/classes/APIGAuth', {
         method: 'POST',
-        headers: {
-          'X-Parse-Application-Id': APP_ID,
-          'Content-Type': 'application/json'
-        },
-        body: JSON.stringify({
-          api: { __type: 'Pointer', className: 'APIG', objectId: apig },
-          user: { __type: 'Pointer', className: '_User', objectId: user },
-          count: 0,
-          used: 0
-        })
+        headers: { ...authHeaders, 'Content-Type': 'application/json' },
+        body: JSON.stringify(body)
       });
       const created = await createResp.json();
       if (created.objectId) {

+ 48 - 4
src/app/login-modal.component.ts

@@ -237,7 +237,7 @@ const APP_ID = 'ncloudmaster';
 })
 export class LoginModalComponent {
   @Input() visible = false;
-  @Output() loginSuccess = new EventEmitter<{ userId: string; sessionToken: string }>();
+  @Output() loginSuccess = new EventEmitter<{ userId: string; sessionToken: string; displayName: string }>();
   phone = '';
   code = '';
   errorMsg = '';
@@ -333,12 +333,17 @@ export class LoginModalComponent {
         throw new Error('登录成功但无法获取用户信息');
       }
 
-      // 3. 缓存登录态
+      // 3. 自动补全 Company(后端 voc-ecom/forward 强制要求)
+      await this.ensureCompany(userData, sessionToken);
+
+      // 4. 缓存登录态
+      const displayName = this.phone ? this.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') : (userData.username || userData.objectId);
       localStorage.setItem('apig_session_token', sessionToken);
       localStorage.setItem('apig_user_id', userData.objectId);
+      localStorage.setItem('apig_display_name', displayName);
 
-      console.log('[Login] userId:', userData.objectId);
-      this.loginSuccess.emit({ userId: userData.objectId, sessionToken });
+      console.log('[Login] userId:', userData.objectId, 'display:', displayName);
+      this.loginSuccess.emit({ userId: userData.objectId, sessionToken, displayName });
       this.hide();
     } catch (e: any) {
       console.error('[Login] 登录失败:', e);
@@ -348,6 +353,45 @@ export class LoginModalComponent {
     }
   }
 
+  // ─── 自动创建 Company(后端 forward 接口强制要求 user.company 指针)───
+  async ensureCompany(userData: any, sessionToken: string): Promise<void> {
+    if (userData.company && userData.company.objectId) {
+      console.log('[Login] Company 已存在:', userData.company.objectId);
+      return;
+    }
+    console.log('[Login] 用户无 Company,自动创建...');
+    try {
+      // 创建 Company
+      const companyName = (userData.username || 'user') + '_company';
+      const createResp = await fetch(`${API_HOST}/parse/classes/Company`, {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json', 'X-Parse-Application-Id': APP_ID, 'X-Parse-Session-Token': sessionToken },
+        body: JSON.stringify({ name: companyName })
+      });
+      const created = await createResp.json();
+      if (!created.objectId) {
+        console.warn('[Login] Company 创建失败:', created);
+        return;
+      }
+      console.log('[Login] Company 创建成功:', created.objectId);
+
+      // 绑定到用户
+      const bindResp = await fetch(`${API_HOST}/parse/users/${userData.objectId}`, {
+        method: 'PUT',
+        headers: { 'Content-Type': 'application/json', 'X-Parse-Application-Id': APP_ID, 'X-Parse-Session-Token': sessionToken },
+        body: JSON.stringify({ company: { __type: 'Pointer', className: 'Company', objectId: created.objectId } })
+      });
+      const bindData = await bindResp.json();
+      if (bindResp.ok) {
+        console.log('[Login] 用户已绑定 Company');
+      } else {
+        console.warn('[Login] 用户绑定 Company 失败:', bindData);
+      }
+    } catch (e: any) {
+      console.warn('[Login] ensureCompany 异常:', e.message);
+    }
+  }
+
   // 检查是否已登录(从 localStorage 恢复)
   async checkExistingLogin(): Promise<{ userId: string; sessionToken: string } | null> {
     try {