- Add errors lib for standardized error handling - Add question-content lib for question content processing - Update action-utils, ai/provider-config, auth-guard, permissions, types/permissions - Update UI sheet component - Update proxy middleware
172 lines
4.8 KiB
TypeScript
172 lines
4.8 KiB
TypeScript
/**
|
|
* 共享错误处理工具:统一 Server Action 的错误响应与客户端 Action 调用模式。
|
|
*
|
|
* 设计目标:
|
|
* 1. 避免将内部错误消息(如 SQL 错误、堆栈信息)直接暴露给客户端
|
|
* 2. 统一 ActionState<T> 的失败结构
|
|
* 3. 为客户端调用 Server Action 提供 try/catch/finally 包装,防止 UI 永久卡 loading
|
|
*/
|
|
|
|
import type { ActionState } from "@/shared/types/action-state"
|
|
import { PermissionDeniedError } from "@/shared/lib/errors"
|
|
|
|
/**
|
|
* 已知的业务错误类型,消息可以安全返回给客户端。
|
|
* 其他 Error 一律视为系统错误,返回通用消息。
|
|
*/
|
|
export class BusinessError extends Error {
|
|
constructor(
|
|
message: string,
|
|
public readonly code?: string
|
|
) {
|
|
super(message)
|
|
this.name = "BusinessError"
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 资源不存在错误。消息可安全返回客户端。
|
|
*/
|
|
export class NotFoundError extends BusinessError {
|
|
constructor(resource: string) {
|
|
super(`${resource} 不存在`, "not_found")
|
|
this.name = "NotFoundError"
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 输入校验错误。消息可安全返回客户端。
|
|
*/
|
|
export class ValidationError extends BusinessError {
|
|
constructor(message: string) {
|
|
super(message, "validation_error")
|
|
this.name = "ValidationError"
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 统一的 Server Action 错误处理器。
|
|
*
|
|
* - PermissionDeniedError:返回权限不足消息(可安全暴露)
|
|
* - BusinessError / NotFoundError / ValidationError:返回其 message(可安全暴露)
|
|
* - 其他 Error:返回通用消息,原始错误通过 console.error 记录到服务端日志
|
|
*
|
|
* @returns ActionState<never> 的失败分支
|
|
*/
|
|
export function handleActionError(e: unknown): ActionState<never> {
|
|
// 权限错误:消息已由 PermissionDeniedError 构造为用户友好文案
|
|
if (e instanceof PermissionDeniedError) {
|
|
return { success: false, message: e.message }
|
|
}
|
|
|
|
// 业务错误:消息可安全暴露
|
|
if (e instanceof BusinessError) {
|
|
return { success: false, message: e.message }
|
|
}
|
|
|
|
// 未知错误:不暴露内部细节,仅记录服务端日志
|
|
if (e instanceof Error) {
|
|
console.error("[ActionError]", e.name, e.message, e.stack)
|
|
return { success: false, message: "操作失败,请稍后重试" }
|
|
}
|
|
|
|
console.error("[ActionError] Unknown error:", e)
|
|
return { success: false, message: "操作失败,请稍后重试" }
|
|
}
|
|
|
|
/**
|
|
* 安全地调用 Server Action,自动处理 try/catch/finally。
|
|
*
|
|
* 用于客户端组件中调用 Server Action,确保:
|
|
* 1. 网络错误或 Action 抛出异常时,catch 块执行 onError 回调
|
|
* 2. 无论成功失败,finally 块执行 onFinally 回调(用于重置 loading 状态)
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* const [isSubmitting, setIsSubmitting] = useState(false)
|
|
* const result = await safeActionCall(
|
|
* () => createGradeRecordAction(null, formData),
|
|
* {
|
|
* onError: () => toast.error("保存失败"),
|
|
* onFinally: () => setIsSubmitting(false),
|
|
* }
|
|
* )
|
|
* if (result?.success) { toast.success("保存成功") }
|
|
* ```
|
|
*/
|
|
export async function safeActionCall<T>(
|
|
action: () => Promise<ActionState<T>>,
|
|
options?: {
|
|
onError?: (error: unknown) => void
|
|
onFinally?: () => void
|
|
}
|
|
): Promise<ActionState<T> | null> {
|
|
try {
|
|
return await action()
|
|
} catch (e) {
|
|
// Action 抛出异常(非返回 failure),如网络错误、序列化错误等
|
|
options?.onError?.(e)
|
|
console.error("[SafeActionCall]", e)
|
|
return null
|
|
} finally {
|
|
options?.onFinally?.()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 安全解析 JSON 字符串,失败时抛出 ValidationError。
|
|
*
|
|
* 用于 Server Action 中包装 JSON.parse,避免 SyntaxError 被外层 catch
|
|
* 捕获后暴露解析细节给客户端。
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* const records = safeJsonParse(recordsJson, "成绩数据格式无效")
|
|
* ```
|
|
*/
|
|
export function safeJsonParse<T>(json: string, errorMessage: string): T {
|
|
try {
|
|
return JSON.parse(json) as T
|
|
} catch {
|
|
throw new ValidationError(errorMessage)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 校验日期字符串是否有效,无效则抛出 ValidationError。
|
|
*
|
|
* @returns 解析后的 Date 对象
|
|
*/
|
|
export function safeParseDate(value: string, fieldName: string): Date {
|
|
const d = new Date(value)
|
|
if (Number.isNaN(d.getTime())) {
|
|
throw new ValidationError(`${fieldName} 格式无效`)
|
|
}
|
|
return d
|
|
}
|
|
|
|
/**
|
|
* 校验数字字符串,无效则抛出 ValidationError。
|
|
*
|
|
* @returns 解析后的 number
|
|
*/
|
|
export function safeParseNumber(value: string, fieldName: string): number {
|
|
const n = Number(value)
|
|
if (!Number.isFinite(n)) {
|
|
throw new ValidationError(`${fieldName} 必须是有效数字`)
|
|
}
|
|
return n
|
|
}
|
|
|
|
/**
|
|
* 转义 SQL LIKE 通配符(% 和 _),防止用户输入干扰模糊查询。
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* const needle = `%${escapeLikePattern(q)}%`
|
|
* ```
|
|
*/
|
|
export function escapeLikePattern(input: string): string {
|
|
return input.replace(/[%_\\]/g, "\\$&")
|
|
}
|