diff --git a/.gitignore b/.gitignore
index 1b7a73a..cc10aef 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,8 +14,8 @@
/coverage
# next.js
-/.next/
-/out/
+.next/
+out/
# production
/build
diff --git a/app/[locale]/course/[moduleIndex]/[lessonIndex]/page.tsx b/app/[locale]/course/[moduleIndex]/[lessonIndex]/page.tsx
index dc1c297..85c71f8 100644
--- a/app/[locale]/course/[moduleIndex]/[lessonIndex]/page.tsx
+++ b/app/[locale]/course/[moduleIndex]/[lessonIndex]/page.tsx
@@ -6,7 +6,7 @@ import { Link, useTranslation } from "@/i18n/navigation";
import { VideoPlayer } from "@/app/components/course/VideoPlayer";
import { LessonMarkdown } from "./LessonMarkdown";
import { getLesson } from "@/config/lessons";
-import { canWatchLesson } from "@/app/lib/course-payment";
+import { canWatchLesson, isFreeTrial } from "@/app/lib/course-payment";
import { useAuthAndVip } from "@/app/lib/useAuthAndVip";
import { getStoredToken, getStoredUser } from "@/app/lib/pocketbase";
import { setLastWatched, markCompleted } from "@/app/lib/learning";
@@ -31,7 +31,6 @@ export default function LessonPage() {
const lesson = getLesson(moduleIndex, lessonIndex);
const unlocked = canWatchLesson(moduleIndex, lessonIndex, vip);
- const isFreeTrial = moduleIndex === 0 && lessonIndex < 2;
useEffect(() => {
if (lesson && unlocked) setLastWatched(moduleIndex, lessonIndex, lesson.name);
@@ -146,7 +145,7 @@ export default function LessonPage() {
{lesson.name}
- {isFreeTrial && (
+ {isFreeTrial(moduleIndex, lessonIndex) && (
试看
diff --git a/app/components/course/CurriculumSection.tsx b/app/components/course/CurriculumSection.tsx
index a7eed9e..054cc10 100644
--- a/app/components/course/CurriculumSection.tsx
+++ b/app/components/course/CurriculumSection.tsx
@@ -4,7 +4,7 @@ import { useState, useEffect } from "react";
import { Link } from "@/i18n/navigation";
import { Section } from "./Section";
import { CURRICULUM_CONFIG } from "@/config/course";
-import { canWatchLesson } from "@/app/lib/course-payment";
+import { canWatchLesson, isFreeTrial } from "@/app/lib/course-payment";
import { useAuthAndVip } from "@/app/lib/useAuthAndVip";
import { isBookmarked, toggleBookmark, getBookmarks, getCompletedLessons } from "@/app/lib/learning";
@@ -99,7 +99,6 @@ export function CurriculumSection() {
{isOpen && (
{c.lessons.map((l) => {
- const free = l.moduleIndex === 0 && l.lessonIndex < 2;
const unlocked = canWatchLesson(l.moduleIndex, l.lessonIndex, vip);
const key = `${l.moduleIndex}-${l.lessonIndex}`;
const bookmarked = isBookmarked(l.moduleIndex, l.lessonIndex);
@@ -126,7 +125,7 @@ export function CurriculumSection() {
{l.name}
- {free && (
+ {isFreeTrial(l.moduleIndex, l.lessonIndex) && (
试看
diff --git a/app/lib/course-payment.ts b/app/lib/course-payment.ts
index 0173a74..93b57ec 100644
--- a/app/lib/course-payment.ts
+++ b/app/lib/course-payment.ts
@@ -3,9 +3,8 @@
* 与 join 报名支付分离,课程解锁用 VIP 状态
*/
-/** 前 2 章为试看,无需支付 */
-export function isFreeTrial(moduleIndex: number, lessonIndex: number): boolean {
- if (moduleIndex === 0 && lessonIndex < 2) return true;
+/** 前 2 章试看(暂时关闭,全部需 VIP 解锁) */
+export function isFreeTrial(_moduleIndex: number, _lessonIndex: number): boolean {
return false;
}
diff --git a/config/course.ts b/config/course.ts
index ac927a5..44c86d8 100644
--- a/config/course.ts
+++ b/config/course.ts
@@ -185,7 +185,7 @@ export const FAQ_CONFIG = {
title: "常见问题",
items: [
{ q: "课程可以永久回看吗?", a: "可以,报名后永久有效,随时回看。" },
- { q: "适合零基础吗?", a: "适合。课程从入门到进阶,前 2 节可免费试看。" },
+ { q: "适合零基础吗?", a: "适合。课程从入门到进阶;视频试看暂时关闭,报名后可解锁全部章节。" },
{ q: "有社群答疑吗?", a: "有,报名后加入专属社群,导师和同学一起交流。" },
{ q: "支持退款吗?", a: "7 天内不满意可申请全额退款。" },
],
diff --git a/next.config.ts b/next.config.ts
index 20e4381..f9987d4 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -1,8 +1,22 @@
import type { NextConfig } from "next";
+/** 由 scripts/build-safe.cjs 设置;在 VPS 上也可手动 export 后执行 pnpm build */
+const lowMemBuild = process.env.NEXT_BUILD_LOW_MEM === "1";
+
const nextConfig: NextConfig = {
experimental: {
serverActions: { bodySizeLimit: "25mb" },
+ ...(lowMemBuild
+ ? {
+ // 限制并行与预渲染并发,降低峰值内存(Next 16 默认 Turbopack 构建)
+ cpus: 1,
+ turbopackMemoryLimit: 1536 * 1024 * 1024,
+ staticGenerationMaxConcurrency: 1,
+ webpackMemoryOptimizations: true,
+ parallelServerCompiles: false,
+ parallelServerBuildTraces: false,
+ }
+ : {}),
},
async headers() {
const noCacheHeaders = [
diff --git a/package.json b/package.json
index b37f5cb..4691871 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,7 @@
"private": true,
"scripts": {
"dev": "next dev -p 3000 -H 0.0.0.0",
- "build": "next build",
+ "build": "node ./scripts/build-safe.cjs",
"start": "next start",
"lint": "eslint"
},
diff --git a/scripts/build-safe.cjs b/scripts/build-safe.cjs
new file mode 100644
index 0000000..59737e5
--- /dev/null
+++ b/scripts/build-safe.cjs
@@ -0,0 +1,30 @@
+/**
+ * 低端 VPS / 小内存环境:限制 Node 堆、开启 Next 低内存构建选项。
+ * 用法:pnpm build(默认经本脚本)
+ * 可选环境变量:
+ * NEXT_BUILD_HEAP_MB — Node --max-old-space-size(默认 1536)
+ */
+const { spawnSync } = require("child_process");
+const path = require("path");
+
+const heapMb = process.env.NEXT_BUILD_HEAP_MB || "1536";
+const nextBin = path.join(
+ path.dirname(require.resolve("next/package.json")),
+ "dist",
+ "bin",
+ "next"
+);
+
+const result = spawnSync(
+ process.execPath,
+ ["--max-old-space-size=" + heapMb, nextBin, "build"],
+ {
+ stdio: "inherit",
+ env: {
+ ...process.env,
+ NEXT_BUILD_LOW_MEM: "1",
+ },
+ }
+);
+
+process.exit(result.status ?? 1);