Browse Source

✨ feat(tenant-management-ui): 实现RPC客户端架构

- 创建单例模式的租户客户端管理器,确保全局唯一的客户端实例
- 实现延迟初始化和客户端重置功能,优化资源使用
- 使用Hono的InferRequestType和InferResponseType确保API调用类型安全
- 更新测试mock和调用路径以匹配新的RPC客户端结构

📝 docs(story): 更新租户管理UI包开发文档

- 标记RPC客户端架构相关任务为已完成
- 添加RPC客户端架构实施经验小节,记录开发要点
- 更新变更日志和关键成就,反映RPC客户端实现
- 完善完成笔记列表,增加RPC客户端架构相关条目
yourname 1 month ago
parent
commit
a79d5ff8c6

+ 17 - 5
docs/stories/007.014.tenant-management-ui-package.story.md

@@ -77,11 +77,11 @@
   - [x] 运行TypeScript类型检查确保无错误
   - [x] 运行构建流程确保成功
 
-- [ ] 实现RPC客户端架构 (新增任务)
-  - [ ] 创建单例模式的租户客户端管理器 [参考: packages/user-management-ui/src/api/userClient.ts]
-  - [ ] 实现延迟初始化和客户端重置功能 [参考: packages/user-management-ui/src/api/userClient.ts:17-33]
-  - [ ] 使用Hono的InferRequestType和InferResponseType确保类型安全 [参考: packages/user-management-ui/src/components/UserManagement.tsx:26-29]
-  - [ ] 提供全局唯一的客户端实例管理 [参考: packages/user-management-ui/src/api/userClient.ts:4-15]
+- [x] 实现RPC客户端架构 (新增任务)
+  - [x] 创建单例模式的租户客户端管理器 [参考: packages/user-management-ui/src/api/userClient.ts]
+  - [x] 实现延迟初始化和客户端重置功能 [参考: packages/user-management-ui/src/api/userClient.ts:17-33]
+  - [x] 使用Hono的InferRequestType和InferResponseType确保类型安全 [参考: packages/user-management-ui/src/components/UserManagement.tsx:26-29]
+  - [x] 提供全局唯一的客户端实例管理 [参考: packages/user-management-ui/src/api/userClient.ts:4-15]
 
 ## 开发说明
 
@@ -94,11 +94,19 @@
 - **表单验证**: 确保租户相关字段的验证规则正确
 - **状态管理**: 使用TanStack Query管理租户数据状态
 
+**RPC客户端架构实施经验**
+- **单例模式实现**: 使用私有构造函数和静态实例确保全局唯一的客户端管理器
+- **延迟初始化**: 客户端在首次使用时创建,避免不必要的资源消耗
+- **类型安全集成**: 使用Hono的`InferRequestType`和`InferResponseType`确保API调用的类型安全
+- **路径结构适配**: 通用CRUD路由使用`index.$get()`、`index.$post()`等路径结构,不同于简单的mock客户端
+- **测试兼容性**: 更新测试mock和调用路径以匹配新的RPC客户端结构
+
 **最佳实践** [Source: docs/prd/epic-007-multi-tenant-package-replication.md#最佳实践]
 - **组件复用**: 基于现有用户管理界面进行复制和修改
 - **依赖管理**: 正确配置包间依赖关系
 - **测试配置**: 配置独立的测试环境
 - **类型处理**: 确保TypeScript类型正确导出
+- **RPC客户端模式**: 采用单例模式管理API客户端,提供延迟初始化和重置功能
 
 ### 数据模型
 **租户实体结构** [Source: packages/tenant-module-mt/src/schemas/tenant.schema.ts]
@@ -177,6 +185,7 @@
 |------|------|------|------|
 | 2025-11-15 | 1.0 | 初始故事创建 | Bob (Scrum Master) |
 | 2025-11-15 | 1.0 | 故事开发完成,租户管理界面包成功创建 | James (Developer) |
+| 2025-11-16 | 1.1 | 实现RPC客户端架构,提供单例模式、延迟初始化和类型安全 | James (Developer) |
 
 ## 开发代理记录
 
@@ -185,6 +194,7 @@
 
 ### Debug Log References
 - 2025-11-15: 基于史诗007需求创建租户管理界面独立包故事
+- 2025-11-16: 实现RPC客户端架构,包括单例模式、延迟初始化、类型安全和测试兼容性
 
 ### Completion Notes List
 - ✅ 已完成: 创建租户管理界面包基础结构
@@ -195,12 +205,14 @@
 - ✅ 已完成: 配置workspace包依赖复用机制
 - ✅ 已完成: 实现单元测试和集成测试
 - ✅ 已完成: 验证现有功能无回归
+- ✅ 已完成: 实现RPC客户端架构(单例模式、延迟初始化、类型安全)
 
 ### 关键成就
 - 基于现有用户管理界面实现租户管理功能
 - 依赖共享UI组件包提供一致的用户体验
 - 依赖租户模块包提供完整的API客户端和类型定义
 - 提供完整的租户CRUD和配置管理功能
+- 实现RPC客户端架构,提供单例模式、延迟初始化和类型安全
 - 100%测试覆盖率,所有18个测试通过
 - 成功构建,生成完整的dist输出
 

+ 42 - 9
packages/tenant-management-ui/src/api/tenantClient.ts

@@ -1,11 +1,44 @@
-// Mock tenant client for testing purposes
-// This will be replaced with actual Hono client when server routes are available
+import { tenantRoutes } from '@d8d/tenant-module-mt';
+import { rpcClient } from '@d8d/shared-ui-components/utils/hc';
 
-export const tenantClient = {
-  $get: () => Promise.resolve({ status: 200, json: () => Promise.resolve({ data: [], pagination: { total: 0, page: 1, pageSize: 10 } }) }),
-  $post: () => Promise.resolve({ status: 201, json: () => Promise.resolve({}) }),
-  ':id': {
-    $put: () => Promise.resolve({ status: 200, json: () => Promise.resolve({}) }),
-    $delete: () => Promise.resolve({ status: 204 })
+class TenantClientManager {
+  private static instance: TenantClientManager;
+  private client: ReturnType<typeof rpcClient<typeof tenantRoutes>> | null = null;
+
+  private constructor() {}
+
+  public static getInstance(): TenantClientManager {
+    if (!TenantClientManager.instance) {
+      TenantClientManager.instance = new TenantClientManager();
+    }
+    return TenantClientManager.instance;
+  }
+
+  // 初始化客户端
+  public init(baseUrl: string = '/'): ReturnType<typeof rpcClient<typeof tenantRoutes>> {
+    return this.client = rpcClient<typeof tenantRoutes>(baseUrl);
+  }
+
+  // 获取客户端实例
+  public get(): ReturnType<typeof rpcClient<typeof tenantRoutes>> {
+    if (!this.client) {
+      return this.init()
+    }
+    return this.client;
   }
-};
+
+  // 重置客户端(用于测试或重新初始化)
+  public reset(): void {
+    this.client = null;
+  }
+}
+
+// 导出单例实例
+const tenantClientManager = TenantClientManager.getInstance();
+
+// 导出默认客户端实例(延迟初始化)
+export const tenantClient = tenantClientManager.get()
+
+export {
+  tenantClientManager
+}

+ 1 - 1
packages/tenant-management-ui/src/components/TenantConfigPage.tsx

@@ -10,7 +10,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
 import { Switch } from '@d8d/shared-ui-components/components/ui/switch';
 import { useForm } from 'react-hook-form';
 import { toast } from 'sonner';
-import { tenantClient } from '@/client/api';
+import { tenantClient } from '@/api/tenantClient';
 import { useTenantConfig } from '@/hooks/useTenantConfig';
 
 interface TenantConfigFormData {

+ 1 - 1
packages/tenant-management-ui/src/components/TenantForm.tsx

@@ -4,7 +4,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
 import { CreateTenantDto, UpdateTenantDto } from '@d8d/tenant-module-mt/schemas';
 import { Button, Input, Textarea, Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, Switch } from '@d8d/shared-ui-components';
 import type { InferResponseType } from 'hono/client';
-import type { tenantClient } from '@/client/api';
+import type { tenantClient } from '@/api/tenantClient';
 
 type TenantResponse = InferResponseType<typeof tenantClient.index.$get, 200>['data'][0];
 

+ 1 - 1
packages/tenant-management-ui/src/components/TenantsPage.tsx

@@ -2,7 +2,7 @@ import React, { useState, useMemo, useCallback } from 'react';
 import { useQuery } from '@tanstack/react-query';
 import { format } from 'date-fns';
 import { Plus, Search, Edit, Trash2, Filter, X } from 'lucide-react';
-import { tenantClient } from '@/client/api';
+import { tenantClient } from '@/api/tenantClient';
 import type { InferRequestType, InferResponseType } from 'hono/client';
 import { Button, Input, Card, CardContent, CardDescription, CardHeader, CardTitle, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Badge, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, Skeleton, Switch, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Popover, PopoverContent, PopoverTrigger, Calendar } from '@d8d/shared-ui-components';
 import { DataTablePagination } from '@/components/DataTablePagination';

+ 1 - 1
packages/tenant-management-ui/src/hooks/useTenantConfig.ts

@@ -1,5 +1,5 @@
 import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
-import { tenantClient } from '@/client/api';
+import { tenantClient } from '@/api/tenantClient';
 import { toast } from 'sonner';
 
 export function useTenantConfig(tenantId: number) {

+ 9 - 7
packages/tenant-management-ui/src/hooks/useTenants.test.tsx

@@ -7,8 +7,10 @@ import { tenantClient } from '../api/tenantClient';
 // Mock the API client
 vi.mock('../api/tenantClient', () => ({
   tenantClient: {
-    $get: vi.fn(),
-    $post: vi.fn(),
+    index: {
+      $get: vi.fn(),
+      $post: vi.fn()
+    },
     ':id': {
       $put: vi.fn(),
       $delete: vi.fn()
@@ -45,7 +47,7 @@ describe('useTenants', () => {
       pagination: { total: 2, page: 1, pageSize: 10 }
     };
 
-    (tenantClient.$get as any).mockResolvedValue({
+    (tenantClient.index.$get as any).mockResolvedValue({
       status: 200,
       json: async () => mockResponse
     });
@@ -57,7 +59,7 @@ describe('useTenants', () => {
     await waitFor(() => expect(result.current.isLoading).toBe(false));
 
     expect(result.current.data).toEqual(mockResponse);
-    expect(tenantClient.$get).toHaveBeenCalledWith({
+    expect(tenantClient.index.$get).toHaveBeenCalledWith({
       query: {
         page: 1,
         pageSize: 10,
@@ -68,7 +70,7 @@ describe('useTenants', () => {
   });
 
   it('should handle fetch error', async () => {
-    (tenantClient.$get as any).mockResolvedValue({
+    (tenantClient.index.$get as any).mockResolvedValue({
       status: 500,
       json: async () => ({ error: 'Internal Server Error' })
     });
@@ -86,7 +88,7 @@ describe('useTenants', () => {
       pagination: { total: 0, page: 2, pageSize: 20 }
     };
 
-    (tenantClient.$get as any).mockResolvedValue({
+    (tenantClient.index.$get as any).mockResolvedValue({
       status: 200,
       json: async () => mockResponse
     });
@@ -105,7 +107,7 @@ describe('useTenants', () => {
 
     await waitFor(() => expect(result.current.isLoading).toBe(false));
 
-    expect(tenantClient.$get).toHaveBeenCalledWith({
+    expect(tenantClient.index.$get).toHaveBeenCalledWith({
       query: {
         page: 2,
         pageSize: 20,

+ 3 - 3
packages/tenant-management-ui/src/hooks/useTenants.ts

@@ -22,7 +22,7 @@ export function useTenants(options: UseTenantsOptions = {}) {
   const query = useQuery({
     queryKey: ['tenants', page, pageSize, keyword, filters],
     queryFn: async () => {
-      const res = await tenantClient.$get({
+      const res = await tenantClient.index.$get({
         query: {
           page,
           pageSize,
@@ -40,8 +40,8 @@ export function useTenants(options: UseTenantsOptions = {}) {
   });
 
   const createMutation = useMutation({
-    mutationFn: async (data: Parameters<typeof tenantClient.$post>[0]['json']) => {
-      const res = await tenantClient.$post({ json: data });
+    mutationFn: async (data: Parameters<typeof tenantClient.index.$post>[0]['json']) => {
+      const res = await tenantClient.index.$post({ json: data });
       if (res.status !== 201) {
         throw new Error('创建租户失败');
       }