feat: 系统优化 - ESLint、Tailwind、前端健壮性、后端工程化、运维可观测性

- 前端: ESLint+Prettier配置、Tailwind v4配置、ErrorBoundary、全局AuthLoader优化、ReactQuery分层
- 后端: MinIO凭证移除、Docker统一为govai品牌、zerolog日志封装、错误码枚举、文件上传校验、单元测试(13项全通过)
- 运维: 健康检查增强(PG/Redis ping)、Prometheus指标(/metrics端点)、多租户tenant包、RateLimit nil防御
- 移动: citation_prompt.txt → internal/assets/
This commit is contained in:
selfrelease
2026-06-23 10:48:22 +08:00
parent 91f4fac23c
commit 65dc805eb5
28 changed files with 1414 additions and 174 deletions
-48
View File
@@ -1,48 +0,0 @@
"use client";
import { useEffect } from "react";
import { AlertCircle, RotateCcw, Home } from "lucide-react";
import { Button } from "@/components/ui/button";
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
console.error("[GlobalError]", error);
}, [error]);
return (
<div className="flex min-h-[60vh] flex-col items-center justify-center px-4">
<div className="flex flex-col items-center text-center max-w-md">
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-destructive/10 mb-6">
<AlertCircle className="h-8 w-8 text-destructive" />
</div>
<h2 className="text-xl font-semibold mb-2"></h2>
<p className="text-sm text-muted-foreground mb-6">
</p>
{error.digest && (
<p className="text-xs text-muted-foreground/60 mb-4 font-mono">
{error.digest}
</p>
)}
<div className="flex gap-3">
<Button variant="outline" onClick={reset} className="gap-2">
<RotateCcw className="h-4 w-4" />
</Button>
<a href="/store">
<Button className="gap-2">
<Home className="h-4 w-4" />
</Button>
</a>
</div>
</div>
</div>
);
}
+48
View File
@@ -0,0 +1,48 @@
"use client";
import { AlertCircle, RefreshCw, Home } from "lucide-react";
import { Button } from "@/components/ui/button";
import Link from "next/link";
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<html lang="zh-CN" suppressHydrationWarning>
<body>
<div className="flex min-h-screen flex-col items-center justify-center px-4">
<div className="flex flex-col items-center text-center max-w-md">
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-destructive/10 mb-6">
<AlertCircle className="h-8 w-8 text-destructive" />
</div>
<h2 className="text-xl font-semibold mb-2"></h2>
<p className="text-sm text-muted-foreground mb-6">
</p>
{error.digest && (
<p className="text-xs text-muted-foreground/60 mb-4 font-mono">
{error.digest}
</p>
)}
<div className="flex gap-3">
<Button variant="outline" onClick={reset} className="gap-2">
<RefreshCw className="h-4 w-4" />
</Button>
<Link href="/store">
<Button className="gap-2">
<Home className="h-4 w-4" />
</Button>
</Link>
</div>
</div>
</div>
</body>
</html>
);
}
+5 -6
View File
@@ -1,9 +1,9 @@
import type { Metadata } from "next";
import { Providers } from "@/components/providers";
import { Toaster } from "@/components/ui/sonner";
import { ErrorBoundary } from "@/components/error-boundary";
import "./globals.css";
// 使用系统字体,避免构建时联网下载 Google 字体(内网/离线环境)
const geistSans = {
variable: "--font-geist-sans",
};
@@ -23,12 +23,11 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html
lang="zh-CN"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
>
<html lang="zh-CN" className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}>
<body className="min-h-full flex flex-col">
<Providers>{children}</Providers>
<ErrorBoundary>
<Providers>{children}</Providers>
</ErrorBoundary>
<Toaster position="top-center" richColors />
</body>
</html>
@@ -0,0 +1,71 @@
"use client";
import { Component, type ReactNode } from "react";
import { AlertTriangle, RefreshCw, Home } from "lucide-react";
import { Button } from "@/components/ui/button";
import Link from "next/link";
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
errorId?: string;
}
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): State {
const id = Math.random().toString(36).slice(2, 10);
console.error(`[ErrorBoundary:${id}]`, error);
return { hasError: true, error, errorId: id };
}
reset = () => this.setState({ hasError: false, error: undefined, errorId: undefined });
render() {
if (this.state.hasError) {
if (this.props.fallback) return <>{this.props.fallback}</>;
return (
<div className="flex min-h-[60vh] flex-col items-center justify-center px-4">
<div className="flex flex-col items-center text-center max-w-md">
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-destructive/10 mb-6">
<AlertTriangle className="h-8 w-8 text-destructive" />
</div>
<h2 className="text-xl font-semibold mb-2"></h2>
<p className="text-sm text-muted-foreground mb-6">
</p>
{this.state.errorId && (
<p className="text-xs text-muted-foreground/60 mb-4 font-mono">
{this.state.errorId}
</p>
)}
<div className="flex gap-3">
<Button variant="outline" onClick={this.reset} className="gap-2">
<RefreshCw className="h-4 w-4" />
</Button>
<Link href="/store">
<Button variant="secondary" className="gap-2">
<Home className="h-4 w-4" />
</Button>
</Link>
</div>
</div>
</div>
);
}
return this.props.children;
}
}
+40 -16
View File
@@ -2,16 +2,42 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useRef, useEffect } from "react";
import { useRouter, usePathname } from "next/navigation";
import { useAuthStore } from "@/stores/auth";
import { TooltipProvider } from "@/components/ui/tooltip";
// 公开路由(无需鉴权)
const PUBLIC_PATHS = ["/login", "/register"];
function AuthLoader({ children }: { children: React.ReactNode }) {
const fetchUser = useAuthStore((s) => s.fetchUser);
const isLoading = useAuthStore((s) => s.isLoading);
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
const router = useRouter();
const pathname = usePathname();
const isPublic = PUBLIC_PATHS.some((p) => pathname === p || pathname.startsWith(p + "/"));
useEffect(() => {
fetchUser();
}, [fetchUser]);
if (!isPublic) {
fetchUser();
}
}, [fetchUser, isPublic]);
// 公开路由:直接渲染,不阻塞
if (isPublic) {
return <>{children}</>;
}
// 鉴权路由:未登录则跳转
if (!isLoading && !isAuthenticated) {
router.push("/login");
return (
<div className="flex h-screen items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
</div>
);
}
if (isLoading) {
return (
@@ -24,23 +50,21 @@ function AuthLoader({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}
const createQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000,
gcTime: 10 * 60 * 1000,
refetchOnWindowFocus: false,
retry: 1,
refetchOnMount: false,
},
},
});
export function Providers({ children }: { children: React.ReactNode }) {
const queryClientRef = useRef<QueryClient>(null);
if (!queryClientRef.current) {
queryClientRef.current = createQueryClient();
queryClientRef.current = new QueryClient({
defaultOptions: {
queries: {
// 高频变更数据(审计、用户列表)
staleTime: 30 * 1000,
gcTime: 5 * 60 * 1000,
refetchOnWindowFocus: false,
retry: 1,
refetchOnMount: "always",
},
},
});
}
return (