diff --git a/frontend/admin-web/src/app/providers.tsx b/frontend/admin-web/src/app/providers.tsx
index 9e1d7b2..4319cef 100644
--- a/frontend/admin-web/src/app/providers.tsx
+++ b/frontend/admin-web/src/app/providers.tsx
@@ -1,8 +1,9 @@
'use client';
import React, { useState } from 'react';
+import { Provider as ReduxProvider } from 'react-redux';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
-import { AuthProvider } from '@/lib/auth-context';
+import { store } from '@/store';
import { DeployGuard } from '@/lib/deploy-guard';
export function Providers({ children }: { children: React.ReactNode }) {
@@ -20,11 +21,11 @@ export function Providers({ children }: { children: React.ReactNode }) {
);
return (
-
-
+
+
{children}
-
-
+
+
);
}
diff --git a/frontend/admin-web/src/domain/entities/index.ts b/frontend/admin-web/src/domain/entities/index.ts
new file mode 100644
index 0000000..a0592fe
--- /dev/null
+++ b/frontend/admin-web/src/domain/entities/index.ts
@@ -0,0 +1,77 @@
+// ============================================================
+// Domain Entities — 业务实体定义
+// Clean Architecture: Domain 层不依赖任何外部框架
+// ============================================================
+
+// ── Auth ────────────────────────────────────────────────────
+export interface AdminUser {
+ id: string;
+ email: string;
+ name: string;
+ role: string;
+}
+
+export interface AuthTokens {
+ accessToken: string;
+ refreshToken: string;
+ expiresIn: number;
+}
+
+// ── User ────────────────────────────────────────────────────
+export type KycStatus = 'UNSUBMITTED' | 'PENDING' | 'APPROVED' | 'REJECTED';
+
+export interface User {
+ id: string;
+ phone?: string;
+ email?: string;
+ name?: string;
+ kycStatus: KycStatus;
+ isActive: boolean;
+ createdAt: string;
+ updatedAt: string;
+}
+
+// ── Issuer ──────────────────────────────────────────────────
+export type IssuerStatus = 'PENDING' | 'APPROVED' | 'REJECTED' | 'SUSPENDED';
+
+export interface Issuer {
+ id: string;
+ companyName: string;
+ contactEmail: string;
+ status: IssuerStatus;
+ createdAt: string;
+}
+
+// ── AppVersion ──────────────────────────────────────────────
+export type AppPlatform = 'ANDROID' | 'IOS';
+export type AppType = 'GENEX_MOBILE' | 'ADMIN_APP';
+
+export interface AppVersion {
+ id: string;
+ appType: AppType;
+ platform: AppPlatform;
+ versionName: string;
+ versionCode: number;
+ minOsVersion?: string;
+ fileSize?: number;
+ downloadUrl?: string;
+ changelog?: string;
+ isForceUpdate: boolean;
+ isEnabled: boolean;
+ releasedAt?: string;
+ createdAt: string;
+}
+
+// ── Common ──────────────────────────────────────────────────
+export interface PaginatedResult {
+ items: T[];
+ total: number;
+ page: number;
+ pageSize: number;
+}
+
+export interface ApiResponse {
+ code: number;
+ data: T;
+ message: string;
+}
diff --git a/frontend/admin-web/src/domain/repositories/auth.repository.interface.ts b/frontend/admin-web/src/domain/repositories/auth.repository.interface.ts
new file mode 100644
index 0000000..babc84c
--- /dev/null
+++ b/frontend/admin-web/src/domain/repositories/auth.repository.interface.ts
@@ -0,0 +1,6 @@
+import type { AdminUser, AuthTokens } from '../entities';
+
+export interface IAuthRepository {
+ login(identifier: string, password: string): Promise<{ user: AdminUser; tokens: AuthTokens }>;
+ logout(): Promise;
+}
diff --git a/frontend/admin-web/src/domain/repositories/user.repository.interface.ts b/frontend/admin-web/src/domain/repositories/user.repository.interface.ts
new file mode 100644
index 0000000..f62f249
--- /dev/null
+++ b/frontend/admin-web/src/domain/repositories/user.repository.interface.ts
@@ -0,0 +1,15 @@
+import type { User, PaginatedResult } from '../entities';
+
+export interface UserListFilters {
+ search?: string;
+ kycStatus?: string;
+ page?: number;
+ pageSize?: number;
+}
+
+export interface IUserRepository {
+ findMany(filters: UserListFilters): Promise>;
+ findById(id: string): Promise;
+ updateKycStatus(id: string, status: string): Promise;
+ toggleActive(id: string, isActive: boolean): Promise;
+}
diff --git a/frontend/admin-web/src/infrastructure/http/http.client.ts b/frontend/admin-web/src/infrastructure/http/http.client.ts
new file mode 100644
index 0000000..0674315
--- /dev/null
+++ b/frontend/admin-web/src/infrastructure/http/http.client.ts
@@ -0,0 +1,80 @@
+// ============================================================
+// Infrastructure — HTTP Client
+// 替代旧版 src/lib/api-client.ts,作为所有 Repository 的底层实现
+// 从 Zustand auth store 读取 token(而非直接操作 localStorage)
+// ============================================================
+
+import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
+
+const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'https://api.gogenex.com';
+
+class HttpClient {
+ private client: AxiosInstance;
+
+ constructor() {
+ this.client = axios.create({
+ baseURL: API_BASE_URL,
+ timeout: 30000,
+ headers: { 'Content-Type': 'application/json' },
+ });
+
+ // Request 拦截器:注入 Bearer token
+ this.client.interceptors.request.use((config) => {
+ if (typeof window !== 'undefined') {
+ // 从 Zustand persisted storage 读取 token(避免直接耦合 store 模块导致循环依赖)
+ try {
+ const raw = localStorage.getItem('gcx-admin-auth');
+ if (raw) {
+ const { state } = JSON.parse(raw);
+ if (state?.token) {
+ config.headers.Authorization = `Bearer ${state.token}`;
+ }
+ }
+ } catch {
+ // ignore parse errors
+ }
+ }
+ return config;
+ });
+
+ // Response 拦截器:401 → 清空 auth store 并跳转登录
+ this.client.interceptors.response.use(
+ (response) => response,
+ (error) => {
+ if (error.response?.status === 401 && typeof window !== 'undefined') {
+ // 清空 Zustand persisted auth
+ localStorage.removeItem('gcx-admin-auth');
+ window.location.href = '/login';
+ }
+ return Promise.reject(error);
+ },
+ );
+ }
+
+ async get(url: string, config?: AxiosRequestConfig): Promise {
+ const response: AxiosResponse = await this.client.get(url, config);
+ return response.data?.data ?? response.data;
+ }
+
+ async post(url: string, data?: unknown, config?: AxiosRequestConfig): Promise {
+ const response: AxiosResponse = await this.client.post(url, data, config);
+ return response.data?.data ?? response.data;
+ }
+
+ async put(url: string, data?: unknown, config?: AxiosRequestConfig): Promise {
+ const response: AxiosResponse = await this.client.put(url, data, config);
+ return response.data?.data ?? response.data;
+ }
+
+ async patch(url: string, data?: unknown, config?: AxiosRequestConfig): Promise {
+ const response: AxiosResponse = await this.client.patch(url, data, config);
+ return response.data?.data ?? response.data;
+ }
+
+ async delete(url: string, config?: AxiosRequestConfig): Promise {
+ const response: AxiosResponse = await this.client.delete(url, config);
+ return response.data?.data ?? response.data;
+ }
+}
+
+export const httpClient = new HttpClient();
diff --git a/frontend/admin-web/src/infrastructure/repositories/auth.repository.ts b/frontend/admin-web/src/infrastructure/repositories/auth.repository.ts
new file mode 100644
index 0000000..dd4c9ad
--- /dev/null
+++ b/frontend/admin-web/src/infrastructure/repositories/auth.repository.ts
@@ -0,0 +1,18 @@
+import type { IAuthRepository } from '@/domain/repositories/auth.repository.interface';
+import type { AdminUser, AuthTokens } from '@/domain/entities';
+import { httpClient } from '../http/http.client';
+
+class AuthRepository implements IAuthRepository {
+ async login(
+ identifier: string,
+ password: string,
+ ): Promise<{ user: AdminUser; tokens: AuthTokens }> {
+ return httpClient.post('/api/v1/auth/login', { identifier, password });
+ }
+
+ async logout(): Promise {
+ await httpClient.post('/api/v1/auth/logout').catch(() => {});
+ }
+}
+
+export const authRepository = new AuthRepository();
diff --git a/frontend/admin-web/src/infrastructure/repositories/user.repository.ts b/frontend/admin-web/src/infrastructure/repositories/user.repository.ts
new file mode 100644
index 0000000..270c718
--- /dev/null
+++ b/frontend/admin-web/src/infrastructure/repositories/user.repository.ts
@@ -0,0 +1,23 @@
+import type { IUserRepository, UserListFilters } from '@/domain/repositories/user.repository.interface';
+import type { User, PaginatedResult } from '@/domain/entities';
+import { httpClient } from '../http/http.client';
+
+class UserRepository implements IUserRepository {
+ async findMany(filters: UserListFilters): Promise> {
+ return httpClient.get('/api/v1/admin/users', { params: filters });
+ }
+
+ async findById(id: string): Promise {
+ return httpClient.get(`/api/v1/admin/users/${id}`);
+ }
+
+ async updateKycStatus(id: string, status: string): Promise {
+ return httpClient.patch(`/api/v1/admin/users/${id}/kyc`, { status });
+ }
+
+ async toggleActive(id: string, isActive: boolean): Promise {
+ return httpClient.patch(`/api/v1/admin/users/${id}/status`, { isActive });
+ }
+}
+
+export const userRepository = new UserRepository();
diff --git a/frontend/admin-web/src/layouts/AdminLayout.tsx b/frontend/admin-web/src/layouts/AdminLayout.tsx
index bd7148e..5c4962d 100644
--- a/frontend/admin-web/src/layouts/AdminLayout.tsx
+++ b/frontend/admin-web/src/layouts/AdminLayout.tsx
@@ -1,10 +1,11 @@
'use client';
-import React, { useState, useEffect } from 'react';
+import React, { useEffect } from 'react';
import { usePathname, useRouter } from 'next/navigation';
import Link from 'next/link';
import { t } from '@/i18n/locales';
-import { useAuth } from '@/lib/auth-context';
+import { useAuth } from '@/store/zustand/auth.store';
+import { useUIStore } from '@/store/zustand/ui.store';
/**
* D. Web管理前端 - 主布局
@@ -98,7 +99,8 @@ export const AdminLayout: React.FC<{ children: React.ReactNode }> = ({ children
const pathname = usePathname();
const router = useRouter();
const { isAuthenticated, isLoading, user, logout } = useAuth();
- const [collapsed, setCollapsed] = useState(false);
+ // Zustand UI store:sidebar 折叠状态持久化(替代本地 useState)
+ const { sidebarCollapsed: collapsed, toggleSidebar } = useUIStore();
// Derive activeKey from current pathname
// ⚠️ 必须在所有 useState/useEffect 之前计算,因为 expandedKeys 的初始值依赖它
@@ -265,7 +267,7 @@ export const AdminLayout: React.FC<{ children: React.ReactNode }> = ({ children
{/* Collapse toggle */}