فهرست منبع

✨ feat(salary-routes): 增强参数验证错误响应

- 导入 ZodErrorSchema 以提供更详细的参数验证错误信息
- 在所有路由的 400 错误响应中将 ErrorSchema 替换为 ZodErrorSchema
- 在错误处理中为 ZodError 添加详细的错误信息映射,包括错误代码、路径、消息及可选验证详情
- 为重复记录错误和其他业务错误添加统一的错误响应结构,包含空错误数组

🗑️ chore(tests): 删除旧的集成测试文件

- 移除 salary.integration.test.simple.ts 文件,该文件包含过时的薪资管理 API 集成测试
yourname 2 هفته پیش
والد
کامیت
fc366f95f0

+ 123 - 17
allin-packages/salary-module/src/routes/salary-custom.routes.ts

@@ -1,6 +1,6 @@
 import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
 import { z } from '@hono/zod-openapi';
-import { AppDataSource, ErrorSchema, parseWithAwait } from '@d8d/shared-utils';
+import { AppDataSource, ErrorSchema, ZodErrorSchema, parseWithAwait } from '@d8d/shared-utils';
 import { authMiddleware } from '@d8d/auth-module';
 import { AuthContext } from '@d8d/shared-types';
 import { SalaryService } from '../services/salary.service';
@@ -33,7 +33,7 @@ const createSalaryRoute = createRoute({
     },
     400: {
       description: '参数错误或区域重复',
-      content: { 'application/json': { schema: ErrorSchema } }
+      content: { 'application/json': { schema: ZodErrorSchema } }
     },
     401: {
       description: '认证失败',
@@ -74,7 +74,7 @@ const updateSalaryRoute = createRoute({
     },
     400: {
       description: '参数错误或区域重复',
-      content: { 'application/json': { schema: ErrorSchema } }
+      content: { 'application/json': { schema: ZodErrorSchema } }
     },
     401: {
       description: '认证失败',
@@ -118,7 +118,7 @@ const deleteSalaryRoute = createRoute({
     },
     400: {
       description: '参数错误',
-      content: { 'application/json': { schema: ErrorSchema } }
+      content: { 'application/json': { schema: ZodErrorSchema } }
     },
     401: {
       description: '认证失败',
@@ -157,7 +157,7 @@ const getAllSalariesRoute = createRoute({
     },
     400: {
       description: '参数错误',
-      content: { 'application/json': { schema: ErrorSchema } }
+      content: { 'application/json': { schema: ZodErrorSchema } }
     },
     401: {
       description: '认证失败',
@@ -193,7 +193,7 @@ const getSalaryByIdRoute = createRoute({
     },
     400: {
       description: '参数错误',
-      content: { 'application/json': { schema: ErrorSchema } }
+      content: { 'application/json': { schema: ZodErrorSchema } }
     },
     401: {
       description: '认证失败',
@@ -227,7 +227,7 @@ const getSalaryByProvinceCityRoute = createRoute({
     },
     400: {
       description: '参数错误',
-      content: { 'application/json': { schema: ErrorSchema } }
+      content: { 'application/json': { schema: ZodErrorSchema } }
     },
     401: {
       description: '认证失败',
@@ -258,7 +258,24 @@ const app = new OpenAPIHono<AuthContext>()
       if (error instanceof z.ZodError) {
         return c.json({
           code: 400,
-          message: '参数错误'
+          message: '参数错误',
+          errors: error.issues.map(issue => {
+            const mappedIssue: any = {
+              code: issue.code,
+              path: issue.path,
+              message: issue.message
+            };
+
+            // 可选字段,使用类型断言
+            if ('expected' in issue) mappedIssue.expected = issue.expected as string;
+            if ('received' in issue) mappedIssue.received = issue.received as string;
+            if ('origin' in issue) mappedIssue.origin = issue.origin as string;
+            if ('minimum' in issue) mappedIssue.minimum = issue.minimum as number;
+            if ('maximum' in issue) mappedIssue.maximum = issue.maximum as number;
+            if ('inclusive' in issue) mappedIssue.inclusive = issue.inclusive as boolean;
+
+            return mappedIssue;
+          })
         }, 400);
       }
 
@@ -266,7 +283,8 @@ const app = new OpenAPIHono<AuthContext>()
       if (error instanceof Error && error.message.includes('该省份城市的薪资记录已存在')) {
         return c.json({
           code: 400,
-          message: '该省份城市的薪资记录已存在'
+          message: '该省份城市的薪资记录已存在',
+          errors: []
         }, 400);
       }
 
@@ -279,7 +297,8 @@ const app = new OpenAPIHono<AuthContext>()
       )) {
         return c.json({
           code: 400,
-          message: error.message
+          message: error.message,
+          errors: []
         }, 400);
       }
 
@@ -308,7 +327,24 @@ const app = new OpenAPIHono<AuthContext>()
       if (error instanceof z.ZodError) {
         return c.json({
           code: 400,
-          message: '参数错误'
+          message: '参数错误',
+          errors: error.issues.map(issue => {
+            const mappedIssue: any = {
+              code: issue.code,
+              path: issue.path,
+              message: issue.message
+            };
+
+            // 可选字段,使用类型断言
+            if ('expected' in issue) mappedIssue.expected = issue.expected as string;
+            if ('received' in issue) mappedIssue.received = issue.received as string;
+            if ('origin' in issue) mappedIssue.origin = issue.origin as string;
+            if ('minimum' in issue) mappedIssue.minimum = issue.minimum as number;
+            if ('maximum' in issue) mappedIssue.maximum = issue.maximum as number;
+            if ('inclusive' in issue) mappedIssue.inclusive = issue.inclusive as boolean;
+
+            return mappedIssue;
+          })
         }, 400);
       }
 
@@ -316,7 +352,8 @@ const app = new OpenAPIHono<AuthContext>()
       if (error instanceof Error && error.message.includes('该省份城市的薪资记录已存在')) {
         return c.json({
           code: 400,
-          message: '该省份城市的薪资记录已存在'
+          message: '该省份城市的薪资记录已存在',
+          errors: []
         }, 400);
       }
 
@@ -329,7 +366,8 @@ const app = new OpenAPIHono<AuthContext>()
       )) {
         return c.json({
           code: 400,
-          message: error.message
+          message: error.message,
+          errors: []
         }, 400);
       }
 
@@ -364,7 +402,24 @@ const app = new OpenAPIHono<AuthContext>()
       if (error instanceof z.ZodError) {
         return c.json({
           code: 400,
-          message: '参数错误'
+          message: '参数错误',
+          errors: error.issues.map(issue => {
+            const mappedIssue: any = {
+              code: issue.code,
+              path: issue.path,
+              message: issue.message
+            };
+
+            // 可选字段,使用类型断言
+            if ('expected' in issue) mappedIssue.expected = issue.expected as string;
+            if ('received' in issue) mappedIssue.received = issue.received as string;
+            if ('origin' in issue) mappedIssue.origin = issue.origin as string;
+            if ('minimum' in issue) mappedIssue.minimum = issue.minimum as number;
+            if ('maximum' in issue) mappedIssue.maximum = issue.maximum as number;
+            if ('inclusive' in issue) mappedIssue.inclusive = issue.inclusive as boolean;
+
+            return mappedIssue;
+          })
         }, 400);
       }
 
@@ -398,7 +453,24 @@ const app = new OpenAPIHono<AuthContext>()
       if (error instanceof z.ZodError) {
         return c.json({
           code: 400,
-          message: '参数错误'
+          message: '参数错误',
+          errors: error.issues.map(issue => {
+            const mappedIssue: any = {
+              code: issue.code,
+              path: issue.path,
+              message: issue.message
+            };
+
+            // 可选字段,使用类型断言
+            if ('expected' in issue) mappedIssue.expected = issue.expected as string;
+            if ('received' in issue) mappedIssue.received = issue.received as string;
+            if ('origin' in issue) mappedIssue.origin = issue.origin as string;
+            if ('minimum' in issue) mappedIssue.minimum = issue.minimum as number;
+            if ('maximum' in issue) mappedIssue.maximum = issue.maximum as number;
+            if ('inclusive' in issue) mappedIssue.inclusive = issue.inclusive as boolean;
+
+            return mappedIssue;
+          })
         }, 400);
       }
 
@@ -426,7 +498,24 @@ const app = new OpenAPIHono<AuthContext>()
       if (error instanceof z.ZodError) {
         return c.json({
           code: 400,
-          message: '参数错误'
+          message: '参数错误',
+          errors: error.issues.map(issue => {
+            const mappedIssue: any = {
+              code: issue.code,
+              path: issue.path,
+              message: issue.message
+            };
+
+            // 可选字段,使用类型断言
+            if ('expected' in issue) mappedIssue.expected = issue.expected as string;
+            if ('received' in issue) mappedIssue.received = issue.received as string;
+            if ('origin' in issue) mappedIssue.origin = issue.origin as string;
+            if ('minimum' in issue) mappedIssue.minimum = issue.minimum as number;
+            if ('maximum' in issue) mappedIssue.maximum = issue.maximum as number;
+            if ('inclusive' in issue) mappedIssue.inclusive = issue.inclusive as boolean;
+
+            return mappedIssue;
+          })
         }, 400);
       }
 
@@ -454,7 +543,24 @@ const app = new OpenAPIHono<AuthContext>()
       if (error instanceof z.ZodError) {
         return c.json({
           code: 400,
-          message: '参数错误'
+          message: '参数错误',
+          errors: error.issues.map(issue => {
+            const mappedIssue: any = {
+              code: issue.code,
+              path: issue.path,
+              message: issue.message
+            };
+
+            // 可选字段,使用类型断言
+            if ('expected' in issue) mappedIssue.expected = issue.expected as string;
+            if ('received' in issue) mappedIssue.received = issue.received as string;
+            if ('origin' in issue) mappedIssue.origin = issue.origin as string;
+            if ('minimum' in issue) mappedIssue.minimum = issue.minimum as number;
+            if ('maximum' in issue) mappedIssue.maximum = issue.maximum as number;
+            if ('inclusive' in issue) mappedIssue.inclusive = issue.inclusive as boolean;
+
+            return mappedIssue;
+          })
         }, 400);
       }
 

+ 0 - 115
allin-packages/salary-module/tests/integration/salary.integration.test.simple.ts

@@ -1,115 +0,0 @@
-import { describe, it, expect, beforeEach } from 'vitest';
-import { testClient } from 'hono/testing';
-import { IntegrationTestDatabase, setupIntegrationDatabaseHooksWithEntities } from '@d8d/shared-test-util';
-import { JWTUtil } from '@d8d/shared-utils';
-import { UserEntity, Role } from '@d8d/user-module';
-import { File } from '@d8d/file-module';
-import { AreaEntity } from '@d8d/geo-areas';
-import salaryRoutes from '../../src/routes/salary.routes';
-import { SalaryLevel } from '../../src/entities/salary-level.entity';
-
-// 设置集成测试钩子
-setupIntegrationDatabaseHooksWithEntities([UserEntity, File, Role, AreaEntity, SalaryLevel])
-
-describe('薪资管理API集成测试(简化版)', () => {
-  let client: ReturnType<typeof testClient<typeof salaryRoutes>>;
-  let testToken: string;
-  let testUser: UserEntity;
-  let testProvince: AreaEntity;
-  let testCity: AreaEntity;
-
-  beforeEach(async () => {
-    // 创建测试客户端
-    client = testClient(salaryRoutes);
-
-    // 获取数据源
-    const dataSource = await IntegrationTestDatabase.getDataSource();
-
-    // 创建测试用户
-    const userRepository = dataSource.getRepository(UserEntity);
-    testUser = userRepository.create({
-      username: `test_user_${Date.now()}`,
-      password: 'test_password',
-      nickname: '测试用户',
-      registrationSource: 'web'
-    });
-    await userRepository.save(testUser);
-
-    // 生成测试用户的token
-    testToken = JWTUtil.generateToken({
-      id: testUser.id,
-      username: testUser.username,
-      roles: [{name:'user'}]
-    });
-
-    // 创建测试区域数据
-    const areaRepository = dataSource.getRepository(AreaEntity);
-
-    // 创建省份
-    testProvince = areaRepository.create({
-      code: '110000',
-      name: '北京市',
-      level: 1,
-      parentId: null
-    });
-    await areaRepository.save(testProvince);
-
-    // 创建城市
-    testCity = areaRepository.create({
-      code: '110100',
-      name: '北京市',
-      level: 2,
-      parentId: testProvince.id
-    });
-    await areaRepository.save(testCity);
-  });
-
-  describe('基本功能测试', () => {
-    it('应该成功创建薪资水平', async () => {
-      const createData = {
-        provinceId: testProvince.id,
-        cityId: testCity.id,
-        basicSalary: 5000.00
-      };
-
-      const response = await client.create.$post({
-        json: createData
-      }, {
-        headers: {
-          'Authorization': `Bearer ${testToken}`
-        }
-      });
-
-      expect(response.status).toBe(200);
-      const result = await response.json();
-
-      // 检查返回结果是否有错误码
-      if ('code' in result) {
-        // 如果有错误码,测试失败
-        expect(result.code).not.toBe(400);
-        expect(result.code).not.toBe(401);
-      } else {
-        // 成功情况
-        expect(result).toHaveProperty('id');
-        expect(result.provinceId).toBe(testProvince.id);
-        expect(result.cityId).toBe(testCity.id);
-        expect(result.basicSalary).toBe(5000.00);
-      }
-    });
-
-    it('应该要求认证', async () => {
-      const createData = {
-        provinceId: testProvince.id,
-        cityId: testCity.id,
-        basicSalary: 5000.00
-      };
-
-      const response = await client.create.$post({
-        json: createData
-        // 不提供Authorization header
-      });
-
-      expect(response.status).toBe(401);
-    });
-  });
-});