Browse Source

✨ feat(system-config): add custom create, update and delete routes

- implement custom create route with OpenAPI specification and auth middleware
- implement custom update route with parameter validation and error handling
- implement custom delete route with ID parameter and success response
- integrate custom routes with main system config routes
- add integration tests for cache refresh and multi-tenant isolation
- set CRUD routes to read-only mode to use custom routes for write operations
yourname 1 month ago
parent
commit
c060976122

+ 69 - 0
packages/core-module-mt/system-config-module-mt/src/routes/custom/create-system-config.mt.ts

@@ -0,0 +1,69 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { authMiddleware } from '../../../../auth-module-mt/src/middleware/index.mt';
+import { AppDataSource, parseWithAwait } from '@d8d/shared-utils';
+import { AuthContext } from '@d8d/shared-types';
+import { SystemConfigServiceMt } from '../../services/system-config.service.mt';
+import { CreateSystemConfigSchema, SystemConfigSchema } from '../../schemas/system-config.schema.mt';
+
+const createSystemConfigRoute = createRoute({
+  method: 'post',
+  path: '/',
+  middleware: [authMiddleware],
+  request: {
+    body: {
+      content: {
+        'application/json': {
+          schema: CreateSystemConfigSchema
+        }
+      }
+    }
+  },
+  responses: {
+    201: {
+      description: '系统配置创建成功',
+      content: {
+        'application/json': {
+          schema: SystemConfigSchema
+        }
+      }
+    },
+    400: {
+      description: '请求参数错误'
+    },
+    500: {
+      description: '服务器内部错误'
+    }
+  }
+});
+
+const createSystemConfigRoutes = new OpenAPIHono<AuthContext>()
+  // 创建系统配置路由
+  .openapi(createSystemConfigRoute, async (c) => {
+    const data = c.req.valid('json');
+    const user = c.get('user');
+
+    try {
+      const systemConfigService = new SystemConfigServiceMt(AppDataSource);
+
+      // 使用setConfig方法,它会自动处理缓存刷新
+      const result = await systemConfigService.setConfig(
+        data.configKey,
+        data.configValue,
+        user.tenantId,
+        data.description || undefined
+      );
+
+      // 验证响应格式
+      const validatedResult = await parseWithAwait(SystemConfigSchema, result);
+
+      return c.json(validatedResult, 201);
+    } catch (error) {
+      console.error('创建系统配置失败:', error);
+      return c.json(
+        { error: error instanceof Error ? error.message : '创建系统配置失败' },
+        500
+      );
+    }
+  });
+
+export default createSystemConfigRoutes;

+ 68 - 0
packages/core-module-mt/system-config-module-mt/src/routes/custom/delete-system-config.mt.ts

@@ -0,0 +1,68 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from '@hono/zod-openapi';
+import { authMiddleware } from '../../../../auth-module-mt/src/middleware/index.mt';
+import { AppDataSource } from '@d8d/shared-utils';
+import { AuthContext } from '@d8d/shared-types';
+import { SystemConfigServiceMt } from '../../services/system-config.service.mt';
+
+const deleteSystemConfigRoute = createRoute({
+  method: 'delete',
+  path: '/{id}',
+  middleware: [authMiddleware],
+  request: {
+    params: z.object({
+      id: z.coerce.number<number>().openapi({
+        description: '系统配置ID',
+        example: 1
+      })
+    })
+  },
+  responses: {
+    200: {
+      description: '系统配置删除成功',
+      content: {
+        'application/json': {
+          schema: z.object({
+            success: z.boolean().openapi({
+              description: '删除操作是否成功',
+              example: true
+            })
+          })
+        }
+      }
+    },
+    404: {
+      description: '系统配置不存在'
+    },
+    500: {
+      description: '服务器内部错误'
+    }
+  }
+});
+
+const deleteSystemConfigRoutes = new OpenAPIHono<AuthContext>()
+  // 删除系统配置路由
+  .openapi(deleteSystemConfigRoute, async (c) => {
+    const { id } = c.req.valid('param');
+
+    try {
+      const systemConfigService = new SystemConfigServiceMt(AppDataSource);
+
+      // 使用delete方法,它会自动处理缓存刷新
+      const result = await systemConfigService.delete(id);
+
+      if (!result) {
+        return c.json({ error: '系统配置不存在' }, 404);
+      }
+
+      return c.json({ success: true }, 200);
+    } catch (error) {
+      console.error('删除系统配置失败:', error);
+      return c.json(
+        { error: error instanceof Error ? error.message : '删除系统配置失败' },
+        500
+      );
+    }
+  });
+
+export default deleteSystemConfigRoutes;

+ 79 - 0
packages/core-module-mt/system-config-module-mt/src/routes/custom/update-system-config.mt.ts

@@ -0,0 +1,79 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from '@hono/zod-openapi';
+import { authMiddleware } from '../../../../auth-module-mt/src/middleware/index.mt';
+import { AppDataSource, parseWithAwait } from '@d8d/shared-utils';
+import { AuthContext } from '@d8d/shared-types';
+import { SystemConfigServiceMt } from '../../services/system-config.service.mt';
+import { UpdateSystemConfigSchema, SystemConfigSchema } from '../../schemas/system-config.schema.mt';
+
+const updateSystemConfigRoute = createRoute({
+  method: 'put',
+  path: '/{id}',
+  middleware: [authMiddleware],
+  request: {
+    params: z.object({
+      id: z.coerce.number<number>().openapi({
+        description: '系统配置ID',
+        example: 1
+      })
+    }),
+    body: {
+      content: {
+        'application/json': {
+          schema: UpdateSystemConfigSchema
+        }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: '系统配置更新成功',
+      content: {
+        'application/json': {
+          schema: SystemConfigSchema
+        }
+      }
+    },
+    400: {
+      description: '请求参数错误'
+    },
+    404: {
+      description: '系统配置不存在'
+    },
+    500: {
+      description: '服务器内部错误'
+    }
+  }
+});
+
+const updateSystemConfigRoutes = new OpenAPIHono<AuthContext>()
+  // 更新系统配置路由
+  .openapi(updateSystemConfigRoute, async (c) => {
+    const { id } = c.req.valid('param');
+    const data = c.req.valid('json');
+    const user = c.get('user');
+
+    try {
+      const systemConfigService = new SystemConfigServiceMt(AppDataSource);
+
+      // 使用update方法,它会自动处理缓存刷新
+      const result = await systemConfigService.update(id, data);
+
+      if (!result) {
+        return c.json({ error: '系统配置不存在' }, 404);
+      }
+
+      // 验证响应格式
+      const validatedResult = await parseWithAwait(SystemConfigSchema, result);
+
+      return c.json(validatedResult, 200);
+    } catch (error) {
+      console.error('更新系统配置失败:', error);
+      return c.json(
+        { error: error instanceof Error ? error.message : '更新系统配置失败' },
+        500
+      );
+    }
+  });
+
+export default updateSystemConfigRoutes;

+ 8 - 1
packages/core-module-mt/system-config-module-mt/src/routes/system-config.routes.mt.ts

@@ -3,6 +3,9 @@ import { createCrudRoutes } from '@d8d/shared-crud';
 import { SystemConfigMt } from '../entities/system-config.entity.mt';
 import { SystemConfigSchema, CreateSystemConfigSchema, UpdateSystemConfigSchema } from '../schemas/system-config.schema.mt';
 import { authMiddleware } from '../../../auth-module-mt/src/middleware/index.mt';
+import createSystemConfigRoutes from './custom/create-system-config.mt';
+import updateSystemConfigRoutes from './custom/update-system-config.mt';
+import deleteSystemConfigRoutes from './custom/delete-system-config.mt';
 
 const systemConfigCrudRoutes = createCrudRoutes({
   entity: SystemConfigMt,
@@ -12,6 +15,7 @@ const systemConfigCrudRoutes = createCrudRoutes({
   listSchema: SystemConfigSchema,
   searchFields: ['configKey', 'description'],
   middleware: [authMiddleware],
+  readOnly: true, // 设为只读,创建、更新、删除操作通过自定义路由处理
   tenantOptions: {
     enabled: true,
     tenantIdField: 'tenantId',
@@ -19,8 +23,11 @@ const systemConfigCrudRoutes = createCrudRoutes({
   }
 });
 
-// 创建路由实例
+// 创建路由实例 - 聚合自定义路由和CRUD路由
 const systemConfigRoutesMt = new OpenAPIHono()
+  .route('/', createSystemConfigRoutes)
+  .route('/', updateSystemConfigRoutes)
+  .route('/', deleteSystemConfigRoutes)
   .route('/', systemConfigCrudRoutes);
 
 export { systemConfigRoutesMt };

+ 221 - 0
packages/core-module-mt/system-config-module-mt/tests/integration/system-config.routes.integration.test.ts

@@ -12,6 +12,7 @@ import { FileMt } from '@d8d/core-module-mt/file-module-mt';
 import { TestDataFactory } from '../utils/integration-test-db';
 import { AuthService } from '@d8d/core-module-mt/auth-module-mt';
 import { UserServiceMt } from '@d8d/core-module-mt/user-module-mt';
+import { redisUtil } from '@d8d/shared-utils';
 
 // 设置集成测试钩子
 setupIntegrationDatabaseHooksWithEntities([SystemConfigMt, UserEntityMt, RoleMt, FileMt])
@@ -316,4 +317,224 @@ describe('系统配置路由API集成测试 (使用hono/testing)', () => {
       expect(response.status).toBe(400);
     });
   });
+
+  describe('自定义路由缓存刷新验证', () => {
+    beforeEach(async () => {
+      // 清除测试缓存
+      await redisUtil.deleteSystemConfig(testUser.tenantId, 'app.login.enabled');
+      await redisUtil.deleteSystemConfig(testUser.tenantId, 'app.payment.enabled');
+    });
+
+    it('应该通过自定义创建路由刷新缓存', async () => {
+      const configData = {
+        configKey: 'app.login.enabled',
+        configValue: 'true',
+        description: '控制小程序登录功能是否开启'
+      };
+
+      // 验证缓存初始为空
+      const initialCache = await redisUtil.getSystemConfig(testUser.tenantId, configData.configKey);
+      expect(initialCache).toBeNull();
+
+      // 通过自定义路由创建配置
+      const response = await client.index.$post({
+        json: configData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      expect(response.status).toBe(201);
+      const result = await response.json() as any;
+      expect(result.configKey).toBe(configData.configKey);
+      expect(result.configValue).toBe(configData.configValue);
+
+      // 验证缓存已被刷新(为空,因为setConfig会删除缓存)
+      const afterCreateCache = await redisUtil.getSystemConfig(testUser.tenantId, configData.configKey);
+      expect(afterCreateCache).toBeNull();
+    });
+
+    it('应该通过自定义更新路由刷新缓存', async () => {
+      // 先创建配置
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      if (!dataSource) throw new Error('Database not initialized');
+
+      const systemConfigService = new SystemConfigServiceMt(dataSource);
+      const config = await systemConfigService.create({
+        configKey: 'app.payment.enabled',
+        configValue: 'true',
+        description: '控制支付功能是否开启',
+        tenantId: testUser.tenantId
+      } as any);
+
+      // 设置缓存
+      await redisUtil.setSystemConfig(testUser.tenantId, config.configKey, 'cached-value', 3600);
+      const initialCache = await redisUtil.getSystemConfig(testUser.tenantId, config.configKey);
+      expect(initialCache).toBe('cached-value');
+
+      // 通过自定义路由更新配置
+      const updateData = {
+        configValue: 'false',
+        description: '支付功能已关闭'
+      };
+
+      const response = await client[':id'].$put({
+        param: { id: config.id },
+        json: updateData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      expect(response.status).toBe(200);
+      const result = await response.json() as any;
+      expect(result.configValue).toBe(updateData.configValue);
+
+      // 验证缓存已被刷新
+      const afterUpdateCache = await redisUtil.getSystemConfig(testUser.tenantId, config.configKey);
+      expect(afterUpdateCache).toBeNull();
+    });
+
+    it('应该通过自定义删除路由刷新缓存', async () => {
+      // 先创建配置
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      if (!dataSource) throw new Error('Database not initialized');
+
+      const systemConfigService = new SystemConfigServiceMt(dataSource);
+      const config = await systemConfigService.create({
+        configKey: 'app.analytics.enabled',
+        configValue: 'true',
+        description: '控制分析功能是否开启',
+        tenantId: testUser.tenantId
+      } as any);
+
+      // 设置缓存
+      await redisUtil.setSystemConfig(testUser.tenantId, config.configKey, 'cached-value', 3600);
+      const initialCache = await redisUtil.getSystemConfig(testUser.tenantId, config.configKey);
+      expect(initialCache).toBe('cached-value');
+
+      // 通过自定义路由删除配置
+      const response = await client[':id'].$delete({
+        param: { id: config.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      expect(response.status).toBe(200);
+
+      // 验证缓存已被刷新
+      const afterDeleteCache = await redisUtil.getSystemConfig(testUser.tenantId, config.configKey);
+      expect(afterDeleteCache).toBeNull();
+    });
+  });
+
+  describe('自定义路由多租户隔离验证', () => {
+    let tenant1User: any;
+    let tenant2User: any;
+    let tenant1Token: string;
+    let tenant2Token: string;
+
+    beforeEach(async () => {
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      if (!dataSource) throw new Error('Database not initialized');
+
+      // 创建租户1的用户
+      tenant1User = await TestDataFactory.createTestUser(dataSource, {
+        username: 'tenant1_customroutes',
+        password: 'TestPassword123!',
+        email: 'tenant1_customroutes@example.com',
+        tenantId: 1
+      });
+
+      // 创建租户2的用户
+      tenant2User = await TestDataFactory.createTestUser(dataSource, {
+        username: 'tenant2_customroutes',
+        password: 'TestPassword123!',
+        email: 'tenant2_customroutes@example.com',
+        tenantId: 2
+      });
+
+      // 生成租户用户的token
+      tenant1Token = authService.generateToken(tenant1User);
+      tenant2Token = authService.generateToken(tenant2User);
+
+      // 清除测试缓存
+      await redisUtil.deleteSystemConfig(1, 'app.shared.config');
+      await redisUtil.deleteSystemConfig(2, 'app.shared.config');
+    });
+
+    it('应该确保多租户缓存隔离', async () => {
+      const configData = {
+        configKey: 'app.shared.config',
+        configValue: 'tenant1-value',
+        description: '共享配置'
+      };
+
+      // 租户1创建配置
+      const response1 = await client.index.$post({
+        json: configData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${tenant1Token}`
+        }
+      });
+
+      expect(response1.status).toBe(201);
+
+      // 租户2创建配置
+      const response2 = await client.index.$post({
+        json: { ...configData, configValue: 'tenant2-value' }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${tenant2Token}`
+        }
+      });
+
+      expect(response2.status).toBe(201);
+
+      // 验证租户1的缓存操作不影响租户2
+      const tenant1Cache = await redisUtil.getSystemConfig(1, configData.configKey);
+      const tenant2Cache = await redisUtil.getSystemConfig(2, configData.configKey);
+
+      // 两个租户的缓存都应该是空的,因为setConfig会删除缓存
+      expect(tenant1Cache).toBeNull();
+      expect(tenant2Cache).toBeNull();
+    });
+
+    it('应该验证租户ID正确提取', async () => {
+      const configData = {
+        configKey: 'app.tenant.specific',
+        configValue: 'tenant-specific-value',
+        description: '租户特定配置'
+      };
+
+      // 租户1创建配置
+      const response = await client.index.$post({
+        json: configData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${tenant1Token}`
+        }
+      });
+
+      expect(response.status).toBe(201);
+      const result = await response.json() as any;
+
+      // 验证配置属于正确的租户
+      expect(result.tenantId).toBe(1);
+
+      // 验证租户2无法访问租户1的配置
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      if (!dataSource) throw new Error('Database not initialized');
+
+      const systemConfigService = new SystemConfigServiceMt(dataSource);
+      const tenant2Configs = await systemConfigService.getAllConfigs(2);
+      const tenant2HasConfig = tenant2Configs.some(config => config.configKey === configData.configKey);
+      expect(tenant2HasConfig).toBe(false);
+    });
+  });
 });