Просмотр исходного кода

♻️ refactor(salary-custom): 重构 Zod 错误处理逻辑

- 从 shared-utils 导入并使用新的 `createZodErrorResponse` 工具函数
- 移除多个路由处理函数中重复的 Zod 错误响应内联代码
- 统一错误响应的结构和生成方式,提升代码复用性

✨ feat(shared-utils): 新增类型安全的 Zod 错误处理工具

- 新增 `ZodIssueSchema` 和 `ZodErrorSchema` 用于定义错误响应的数据结构
- 新增 `handleZodError` 函数,提供类型安全且全面的错误详情映射
- 新增 `createZodErrorResponse` 函数,为路由提供简化的错误响应生成
- 支持映射 Zod 错误对象中的多种可选字段,如 `expected`、`received`、`minimum` 等
yourname 2 недель назад
Родитель
Сommit
a80197675e

+ 7 - 127
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, ZodErrorSchema, parseWithAwait } from '@d8d/shared-utils';
+import { AppDataSource, ErrorSchema, ZodErrorSchema, parseWithAwait, createZodErrorResponse } from '@d8d/shared-utils';
 import { authMiddleware } from '@d8d/auth-module';
 import { AuthContext } from '@d8d/shared-types';
 import { SalaryService } from '../services/salary.service';
@@ -256,27 +256,7 @@ const app = new OpenAPIHono<AuthContext>()
       return c.json(await parseWithAwait(SalaryLevelSchema, result), 200);
     } catch (error) {
       if (error instanceof z.ZodError) {
-        return c.json({
-          code: 400,
-          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);
+        return c.json(createZodErrorResponse(error), 400);
       }
 
       // 处理区域重复错误
@@ -325,27 +305,7 @@ const app = new OpenAPIHono<AuthContext>()
       return c.json(validatedResult, 200);
     } catch (error) {
       if (error instanceof z.ZodError) {
-        return c.json({
-          code: 400,
-          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);
+        return c.json(createZodErrorResponse(error), 400);
       }
 
       // 处理区域重复错误
@@ -400,27 +360,7 @@ const app = new OpenAPIHono<AuthContext>()
       return c.json({ success }, 200);
     } catch (error) {
       if (error instanceof z.ZodError) {
-        return c.json({
-          code: 400,
-          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);
+        return c.json(createZodErrorResponse(error), 400);
       }
 
       return c.json({
@@ -451,27 +391,7 @@ const app = new OpenAPIHono<AuthContext>()
       return c.json({ data: validatedData, total: result.total }, 200);
     } catch (error) {
       if (error instanceof z.ZodError) {
-        return c.json({
-          code: 400,
-          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);
+        return c.json(createZodErrorResponse(error), 400);
       }
 
       return c.json({
@@ -496,27 +416,7 @@ const app = new OpenAPIHono<AuthContext>()
       return c.json(validatedResult, 200);
     } catch (error) {
       if (error instanceof z.ZodError) {
-        return c.json({
-          code: 400,
-          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);
+        return c.json(createZodErrorResponse(error), 400);
       }
 
       return c.json({
@@ -541,27 +441,7 @@ const app = new OpenAPIHono<AuthContext>()
       return c.json(validatedResult, 200);
     } catch (error) {
       if (error instanceof z.ZodError) {
-        return c.json({
-          code: 400,
-          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);
+        return c.json(createZodErrorResponse(error), 400);
       }
 
       if (error instanceof Error && error.message.includes('该省份城市的薪资记录不存在')) {

+ 194 - 0
packages/shared-utils/src/utils/errorHandler.ts

@@ -10,6 +10,200 @@ export const ErrorSchema = z.object({
   }),
 })
 
+// Zod错误详情schema - 类型安全版本
+export const ZodIssueSchema = z.object({
+  code: z.string().openapi({
+    example: 'invalid_type',
+  }),
+  expected: z.string().optional().openapi({
+    example: 'number',
+  }),
+  received: z.string().optional().openapi({
+    example: 'string',
+  }),
+  path: z.array(z.union([z.string(), z.number(), z.null()])).openapi({
+    example: ['basicSalary'],
+  }),
+  message: z.string().openapi({
+    example: '基本工资必须大于0',
+  }),
+  origin: z.string().optional().openapi({
+    example: 'value',
+  }),
+  minimum: z.number().optional().openapi({
+    example: 0,
+  }),
+  maximum: z.number().optional().openapi({
+    example: 100,
+  }),
+  inclusive: z.boolean().optional().openapi({
+    example: false,
+  }),
+  // 添加更多可能的字段
+  input: z.any().optional().openapi({
+    description: '原始输入数据(当reportInput为true时)',
+  }),
+  fatal: z.boolean().optional().openapi({
+    example: false,
+  }),
+  type: z.string().optional().openapi({
+    example: 'string',
+  }),
+  exact: z.boolean().optional().openapi({
+    example: false,
+  }),
+  validation: z.string().optional().openapi({
+    example: 'email',
+  }),
+  keys: z.array(z.string()).optional().openapi({
+    example: ['extraField'],
+  }),
+  options: z.array(z.union([z.string(), z.number()])).optional().openapi({
+    example: ['option1', 'option2'],
+  }),
+  multipleOf: z.number().optional().openapi({
+    example: 5,
+  }),
+  unionErrors: z.array(z.any()).optional().openapi({
+    description: '联合类型错误详情',
+  }),
+  argumentsError: z.any().optional().openapi({
+    description: '参数错误详情',
+  }),
+  returnTypeError: z.any().optional().openapi({
+    description: '返回类型错误详情',
+  }),
+})
+
+// Zod错误响应schema
+export const ZodErrorSchema = z.object({
+  code: z.number().openapi({
+    example: 400,
+  }),
+  message: z.string().openapi({
+    example: '参数错误',
+  }),
+  errors: z.array(ZodIssueSchema).openapi({
+    description: 'Zod验证错误详情',
+  }),
+})
+
+// 类型安全的Zod错误处理函数
+export function handleZodError(error: z.ZodError): z.infer<typeof ZodErrorSchema> {
+  const issues = error.issues.map(issue => {
+    // 基础字段 - 过滤掉symbol类型的path,转换null为字符串
+    const mappedIssue: z.infer<typeof ZodIssueSchema> = {
+      code: issue.code,
+      path: issue.path.map(p => {
+        if (typeof p === 'string' || typeof p === 'number') {
+          return p
+        }
+        // 将symbol和null转换为字符串
+        return String(p)
+      }),
+      message: issue.message,
+    }
+
+    // 根据错误类型添加特定字段
+    if ('expected' in issue && issue.expected !== undefined) {
+      mappedIssue.expected = String(issue.expected)
+    }
+
+    if ('received' in issue && issue.received !== undefined) {
+      mappedIssue.received = String(issue.received)
+    }
+
+    if ('origin' in issue && issue.origin !== undefined) {
+      mappedIssue.origin = String(issue.origin)
+    }
+
+    if ('minimum' in issue && issue.minimum !== undefined) {
+      mappedIssue.minimum = Number(issue.minimum)
+    }
+
+    if ('maximum' in issue && issue.maximum !== undefined) {
+      mappedIssue.maximum = Number(issue.maximum)
+    }
+
+    if ('inclusive' in issue && issue.inclusive !== undefined) {
+      mappedIssue.inclusive = Boolean(issue.inclusive)
+    }
+
+    if ('fatal' in issue && issue.fatal !== undefined) {
+      mappedIssue.fatal = Boolean(issue.fatal)
+    }
+
+    if ('type' in issue && issue.type !== undefined) {
+      mappedIssue.type = String(issue.type)
+    }
+
+    if ('exact' in issue && issue.exact !== undefined) {
+      mappedIssue.exact = Boolean(issue.exact)
+    }
+
+    if ('validation' in issue && issue.validation !== undefined) {
+      mappedIssue.validation = String(issue.validation)
+    }
+
+    if ('keys' in issue && issue.keys !== undefined && Array.isArray(issue.keys)) {
+      mappedIssue.keys = issue.keys.map(key => String(key))
+    }
+
+    if ('options' in issue && issue.options !== undefined && Array.isArray(issue.options)) {
+      mappedIssue.options = issue.options.map(opt =>
+        typeof opt === 'string' || typeof opt === 'number' ? opt : String(opt)
+      )
+    }
+
+    if ('multipleOf' in issue && issue.multipleOf !== undefined) {
+      mappedIssue.multipleOf = Number(issue.multipleOf)
+    }
+
+    if ('input' in issue && issue.input !== undefined) {
+      mappedIssue.input = issue.input
+    }
+
+    if ('unionErrors' in issue && issue.unionErrors !== undefined && Array.isArray(issue.unionErrors)) {
+      mappedIssue.unionErrors = issue.unionErrors
+    }
+
+    if ('argumentsError' in issue && issue.argumentsError !== undefined) {
+      mappedIssue.argumentsError = issue.argumentsError
+    }
+
+    if ('returnTypeError' in issue && issue.returnTypeError !== undefined) {
+      mappedIssue.returnTypeError = issue.returnTypeError
+    }
+
+    return mappedIssue
+  })
+
+  return {
+    code: 400,
+    message: '参数错误',
+    errors: issues,
+  }
+}
+
+// 简化的Zod错误处理函数(用于路由)
+export function createZodErrorResponse(error: z.ZodError) {
+  return {
+    code: 400,
+    message: '参数错误',
+    errors: error.issues.map(issue => ({
+      code: issue.code,
+      path: issue.path,
+      message: issue.message,
+      // 可选字段 - 根据实际需要添加
+      ...('expected' in issue && issue.expected !== undefined && { expected: String(issue.expected) }),
+      ...('received' in issue && issue.received !== undefined && { received: String(issue.received) }),
+      ...('minimum' in issue && issue.minimum !== undefined && { minimum: Number(issue.minimum) }),
+      ...('maximum' in issue && issue.maximum !== undefined && { maximum: Number(issue.maximum) }),
+      ...('inclusive' in issue && issue.inclusive !== undefined && { inclusive: Boolean(issue.inclusive) }),
+    }))
+  }
+}
+
 export const errorHandler = async (err: Error, c: Context) => {
   return c.json(
     {