Compare commits
9
Commits
8205ae709c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
647e48cc84 | ||
|
|
781b9b6c3a | ||
|
|
f4ab32ebd1 | ||
|
|
205e68594e | ||
|
|
391c64532e | ||
|
|
3a69724a7d | ||
|
|
638144506b | ||
|
|
9f08275fc5 | ||
|
|
157d330110 |
@@ -1,15 +0,0 @@
|
|||||||
# Environment variables declared in this file are NOT automatically loaded by Prisma.
|
|
||||||
# Please add `import "dotenv/config";` to your `prisma.config.ts` file, or use the Prisma CLI with Bun
|
|
||||||
# to load environment variables from .env files: https://pris.ly/prisma-config-env-vars.
|
|
||||||
|
|
||||||
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
|
|
||||||
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
|
|
||||||
|
|
||||||
DATABASE_URL="mysql://mohe001:Mohe168899%23@rm-0jlbgr2rv6dj3t6jngo.mysql.rds.aliyuncs.com:3306/zhuiguang_ai?charset=utf8mb4"
|
|
||||||
|
|
||||||
NEXTAUTH_URL="http://localhost:8301"
|
|
||||||
NEXTAUTH_SECRET="zhuiguang-ai-dev-secret"
|
|
||||||
|
|
||||||
GITHUB_TOKEN=""
|
|
||||||
|
|
||||||
DEEPSEEK_API_KEY="sk-74c0b4f4eb5647398e35ecd064d1829a"
|
|
||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
# WARNING: .env 已被加入 .gitignore,请勿提交真实密钥
|
# WARNING: .env 已被加入 .gitignore,请勿提交真实密钥
|
||||||
|
|
||||||
# 数据库连接 (必填)
|
# 数据库连接 (必填)
|
||||||
DATABASE_URL="mysql://user:password@host:3306/zhuiguang_ai?charset=utf8mb4"
|
DATABASE_URL="mariadb://user:password@host:3306/zhuiguang_ai?charset=utf8mb4"
|
||||||
|
|
||||||
# NextAuth 配置 (必填)
|
# NextAuth 配置 (必填)
|
||||||
NEXTAUTH_URL="http://localhost:8301"
|
NEXTAUTH_URL="http://localhost:8301"
|
||||||
|
|||||||
+2
-3
@@ -1,6 +1,5 @@
|
|||||||
// This file was generated by Prisma, and assumes you have installed the following:
|
// 运行时由 Dockerfile build-arg 注入 DATABASE_URL
|
||||||
// npm install --save-dev prisma dotenv
|
// 本地开发时手动 export DATABASE_URL 或用 .env 加载(next.config.mjs 处理)
|
||||||
import "dotenv/config";
|
|
||||||
import { defineConfig } from "prisma/config";
|
import { defineConfig } from "prisma/config";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
|||||||
@@ -23,16 +23,7 @@ interface Props {
|
|||||||
searchParams: Promise<{ page?: string }>;
|
searchParams: Promise<{ page?: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateStaticParams() {
|
export const dynamic = "force-dynamic";
|
||||||
const categories = await prisma.category.findMany({
|
|
||||||
select: { slug: true },
|
|
||||||
orderBy: { sortOrder: "asc" },
|
|
||||||
});
|
|
||||||
return categories.map((c) => ({ slug: c.slug }));
|
|
||||||
}
|
|
||||||
|
|
||||||
export const dynamicParams = true;
|
|
||||||
export const revalidate = 3600; // 分类页1小时 ISR
|
|
||||||
|
|
||||||
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||||
const { slug } = await params;
|
const { slug } = await params;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import dynamic from "next/dynamic";
|
import nextDynamic from "next/dynamic";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -11,15 +11,15 @@ import { CompareButton } from "@/components/common/CompareButton";
|
|||||||
import { BreadcrumbJsonLd } from "@/components/seo/JsonLd";
|
import { BreadcrumbJsonLd } from "@/components/seo/JsonLd";
|
||||||
import PersonalizedRecommend from "@/components/common/PersonalizedRecommend";
|
import PersonalizedRecommend from "@/components/common/PersonalizedRecommend";
|
||||||
|
|
||||||
const SkillReviewCard = dynamic(
|
const SkillReviewCard = nextDynamic(
|
||||||
() => import("@/components/common/SkillReviewCard"),
|
() => import("@/components/common/SkillReviewCard"),
|
||||||
{ ssr: false }
|
{ ssr: false }
|
||||||
);
|
);
|
||||||
const ShareButton = dynamic(() => import("@/components/common/ShareButton"), { ssr: false });
|
const ShareButton = nextDynamic(() => import("@/components/common/ShareButton"), { ssr: false });
|
||||||
const SkillInstallButton = dynamic(() => import("@/components/common/SkillInstallButton").then((mod) => ({ default: mod.SkillInstallButton })), { ssr: false });
|
const SkillInstallButton = nextDynamic(() => import("@/components/common/SkillInstallButton").then((mod) => ({ default: mod.SkillInstallButton })), { ssr: false });
|
||||||
const ReviewSection = dynamic(() => import("@/components/common/ReviewSection").then((mod) => ({ default: mod.ReviewSection })), { ssr: false });
|
const ReviewSection = nextDynamic(() => import("@/components/common/ReviewSection").then((mod) => ({ default: mod.ReviewSection })), { ssr: false });
|
||||||
const SimilarSkills = dynamic(() => import("@/components/common/SimilarSkills"), { ssr: false });
|
const SimilarSkills = nextDynamic(() => import("@/components/common/SimilarSkills"), { ssr: false });
|
||||||
const FeedbackButton = dynamic(() => import("@/components/common/FeedbackButton"), { ssr: false });
|
const FeedbackButton = nextDynamic(() => import("@/components/common/FeedbackButton"), { ssr: false });
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
params: { slug: string };
|
params: { slug: string };
|
||||||
@@ -64,18 +64,7 @@ function formatRelativeTime(date: Date): string {
|
|||||||
return `${Math.floor(days / 365)}年前`;
|
return `${Math.floor(days / 365)}年前`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateStaticParams() {
|
export const dynamic = "force-dynamic";
|
||||||
const skills = await prisma.skill.findMany({
|
|
||||||
where: { status: "published" },
|
|
||||||
select: { slug: true },
|
|
||||||
orderBy: { viewCount: "desc" },
|
|
||||||
take: 100,
|
|
||||||
});
|
|
||||||
return skills.map((s) => ({ slug: s.slug }));
|
|
||||||
}
|
|
||||||
|
|
||||||
export const dynamicParams = true;
|
|
||||||
export const revalidate = 3600; // 技能详情页1小时 ISR,减少 RDS 压力
|
|
||||||
|
|
||||||
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||||
const skill = await prisma.skill.findUnique({
|
const skill = await prisma.skill.findUnique({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import dynamic from "next/dynamic";
|
import nextDynamic from "next/dynamic";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { authOptions } from "@/lib/auth";
|
import { authOptions } from "@/lib/auth";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -14,13 +14,13 @@ import { PRICING_LABEL, PRICING_COLOR } from "@/lib/constants";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
const ShareButton = dynamic(() => import("@/components/common/ShareButton"), { ssr: false });
|
const ShareButton = nextDynamic(() => import("@/components/common/ShareButton"), { ssr: false });
|
||||||
const ReviewSection = dynamic(() => import("@/components/common/ReviewSection").then((mod) => ({ default: mod.ReviewSection })), { ssr: false });
|
const ReviewSection = nextDynamic(() => import("@/components/common/ReviewSection").then((mod) => ({ default: mod.ReviewSection })), { ssr: false });
|
||||||
const SimilarTools = dynamic(() => import("@/components/common/SimilarTools"), { ssr: false });
|
const SimilarTools = nextDynamic(() => import("@/components/common/SimilarTools"), { ssr: false });
|
||||||
const FeedbackButton = dynamic(() => import("@/components/common/FeedbackButton"), { ssr: false });
|
const FeedbackButton = nextDynamic(() => import("@/components/common/FeedbackButton"), { ssr: false });
|
||||||
const ScreenshotGallery = dynamic(() => import("@/components/common/ScreenshotGallery"), { ssr: false });
|
const ScreenshotGallery = nextDynamic(() => import("@/components/common/ScreenshotGallery"), { ssr: false });
|
||||||
const ToolLivePreview = dynamic(() => import("@/components/common/ToolLivePreview"), { ssr: false });
|
const ToolLivePreview = nextDynamic(() => import("@/components/common/ToolLivePreview"), { ssr: false });
|
||||||
const ApiPlayground = dynamic(() => import("@/components/common/ApiPlayground"), { ssr: false });
|
const ApiPlayground = nextDynamic(() => import("@/components/common/ApiPlayground"), { ssr: false });
|
||||||
|
|
||||||
const VERIFIED_TOOLS_KEY = "verified_tools";
|
const VERIFIED_TOOLS_KEY = "verified_tools";
|
||||||
|
|
||||||
@@ -58,18 +58,7 @@ interface PageProps {
|
|||||||
params: { slug: string };
|
params: { slug: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateStaticParams() {
|
export const dynamic = "force-dynamic";
|
||||||
const tools = await prisma.tool.findMany({
|
|
||||||
where: { status: "published" },
|
|
||||||
select: { slug: true },
|
|
||||||
orderBy: { viewCount: "desc" },
|
|
||||||
take: 100,
|
|
||||||
});
|
|
||||||
return tools.map((t) => ({ slug: t.slug }));
|
|
||||||
}
|
|
||||||
|
|
||||||
export const dynamicParams = true;
|
|
||||||
export const revalidate = 3600; // 工具详情页1小时 ISR,减少 RDS 压力
|
|
||||||
|
|
||||||
export async function generateMetadata({ params }: PageProps) {
|
export async function generateMetadata({ params }: PageProps) {
|
||||||
const tool = await prisma.tool.findUnique({
|
const tool = await prisma.tool.findUnique({
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import { notFound } from "next/navigation";
|
|||||||
import { getLevelInfo } from "@/lib/level-config";
|
import { getLevelInfo } from "@/lib/level-config";
|
||||||
import UserPublicProfile from "./UserPublicProfile";
|
import UserPublicProfile from "./UserPublicProfile";
|
||||||
|
|
||||||
export const revalidate = 3600;
|
export const dynamic = "force-dynamic";
|
||||||
export const dynamicParams = true;
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
params: { id: string };
|
params: { id: string };
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ function checkLoginRateLimit(email: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const authOptions = {
|
export const authOptions = {
|
||||||
|
trustHost: true,
|
||||||
providers: [
|
providers: [
|
||||||
CredentialsProvider({
|
CredentialsProvider({
|
||||||
name: "credentials",
|
name: "credentials",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ function getRedis() {
|
|||||||
_redis = new Redis({
|
_redis = new Redis({
|
||||||
host: process.env.REDIS_HOST || "127.0.0.1",
|
host: process.env.REDIS_HOST || "127.0.0.1",
|
||||||
port: parseInt(process.env.REDIS_PORT || "6379"),
|
port: parseInt(process.env.REDIS_PORT || "6379"),
|
||||||
|
username: process.env.REDIS_USERNAME || undefined,
|
||||||
password: process.env.REDIS_PASSWORD || undefined,
|
password: process.env.REDIS_PASSWORD || undefined,
|
||||||
db: parseInt(process.env.REDIS_CACHE_DB || "0"), // API 缓存专用 DB
|
db: parseInt(process.env.REDIS_CACHE_DB || "0"), // API 缓存专用 DB
|
||||||
lazyConnect: true,
|
lazyConnect: true,
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@ function createPrismaClient() {
|
|||||||
if (!process.env.DATABASE_URL) {
|
if (!process.env.DATABASE_URL) {
|
||||||
throw new Error("DATABASE_URL 环境变量未设置,无法连接数据库");
|
throw new Error("DATABASE_URL 环境变量未设置,无法连接数据库");
|
||||||
}
|
}
|
||||||
const base = process.env.DATABASE_URL;
|
const base = process.env.DATABASE_URL.replace("mysql://", "mariadb://");
|
||||||
const sep = base.includes("?") ? "&" : "?";
|
const sep = base.includes("?") ? "&" : "?";
|
||||||
const connectionString = `${base}${sep}connection_limit=20&pool_timeout=30`;
|
const connectionString = `${base}${sep}connection_limit=20&pool_timeout=30`;
|
||||||
const adapter = new PrismaMariaDb(connectionString);
|
const adapter = new PrismaMariaDb(connectionString);
|
||||||
|
|||||||
+7
-1
@@ -5,6 +5,12 @@ export default withAuth(
|
|||||||
function middleware(req) {
|
function middleware(req) {
|
||||||
const token = req.nextauth.token;
|
const token = req.nextauth.token;
|
||||||
const path = req.nextUrl.pathname;
|
const path = req.nextUrl.pathname;
|
||||||
|
const host = req.headers.get("host") || "";
|
||||||
|
|
||||||
|
// ai.zhuig.com 根路径 → 重定向到 /admin
|
||||||
|
if (host.startsWith("ai.zhuig.com") && path === "/") {
|
||||||
|
return NextResponse.redirect(new URL("/admin", req.url));
|
||||||
|
}
|
||||||
|
|
||||||
// 行业情报(/community)暂不对外开放:需登录后访问
|
// 行业情报(/community)暂不对外开放:需登录后访问
|
||||||
if (path.startsWith("/community")) {
|
if (path.startsWith("/community")) {
|
||||||
@@ -77,5 +83,5 @@ export default withAuth(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: ["/admin/:path*", "/api/user/:path*", "/api/admin/:path*", "/community/:path*", "/api/community/:path*"],
|
matcher: ["/", "/admin/:path*", "/api/user/:path*", "/api/admin/:path*", "/community/:path*", "/api/community/:path*"],
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user