/** * 共享错误处理工具:统一 Server Action 的错误响应与客户端 Action 调用模式。 * * 设计目标: * 1. 避免将内部错误消息(如 SQL 错误、堆栈信息)直接暴露给客户端 * 2. 统一 ActionState 的失败结构 * 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 的失败分支 */ export function handleActionError(e: unknown): ActionState { // 权限错误:消息已由 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( action: () => Promise>, options?: { onError?: (error: unknown) => void onFinally?: () => void } ): Promise | 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(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, "\\$&") }