From 0310f2b88b9b74c864e0d5f23582e5ff6680faec Mon Sep 17 00:00:00 2001 From: wcgzorro <76474110+wcgzorro@users.noreply.github.com> Date: Wed, 10 Apr 2024 21:52:40 +0800 Subject: [PATCH] =?UTF-8?q?V1.0=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 3 + Dockerfile | 33 + MP_verify_uuVKNEzV7WrJ2xyt.txt | 1 + README.md | 41 + docker-compose.yml | 102 + next.config.js | 14 +- package.json | 8 +- prisma/addUser.ts | 26 + prisma/schema.prisma | 17 +- public/assets/svg/logo.svg | 1 + public/favicon.ico | Bin 9068 -> 15086 bytes src/app/api/posts/route.ts | 10 +- src/app/api/subreddit/post/vote/route.ts | 32 +- src/app/api/upload/backroute | 61 + src/app/api/upload/route.ts | 71 + src/app/api/uploadvideo/route.ts | 56 + src/app/api/username/route.ts | 6 +- src/app/api/weixinauth/route.ts | 60 + src/app/layout.tsx | 12 +- src/app/myfeed/page.tsx | 50 + src/app/page.tsx | 26 +- src/app/r/[slug]/layout.tsx | 27 +- src/app/r/[slug]/page.tsx | 7 +- src/app/r/[slug]/post/[postId]/page.tsx | 142 +- src/app/r/[slug]/submit/page.tsx | 9 +- src/app/r/create/page.tsx | 28 +- src/app/settings/page.tsx | 6 +- src/app/weixin-callback/page.tsx | 64 + src/components/CommentsSection.tsx | 4 +- src/components/CreateComment.tsx | 6 +- src/components/Editor.tsx | 78 +- src/components/MiniCreatePost.tsx | 2 +- src/components/Navbar.tsx | 16 +- src/components/Post.tsx | 53 +- src/components/SearchBar.tsx | 8 +- src/components/SignIn.tsx | 79 +- src/components/SubscribeLeaveToggle.tsx | 12 +- src/components/ToFeedButton.tsx | 2 +- src/components/UserAccountNav.tsx | 8 +- src/components/UserAuthForm.tsx | 135 +- src/components/UserNameForm.tsx | 11 +- src/components/comments/PostComment.tsx | 4 +- src/components/homepage/CustomFeed.tsx | 1 - src/components/homepage/GeneralFeed.tsx | 1 - src/components/post-vote/PostVoteClient.tsx | 2 +- .../renderers/CustomImageRenderer.tsx | 33 +- src/components/ui/Command.tsx | 6 +- src/hooks/use-custom-toasts.tsx | 6 +- src/lib/auth.ts | 169 +- src/lib/redis.ts | 24 +- src/lib/utils.ts | 29 +- src/lib/validators/post.ts | 6 +- src/lib/validators/subreddit.ts | 2 +- src/styles/globals.css | 15 + yarn.lock | 12278 ++++++++-------- 55 files changed, 7089 insertions(+), 6814 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 MP_verify_uuVKNEzV7WrJ2xyt.txt create mode 100644 docker-compose.yml create mode 100644 prisma/addUser.ts create mode 100644 public/assets/svg/logo.svg create mode 100644 src/app/api/upload/backroute create mode 100644 src/app/api/upload/route.ts create mode 100644 src/app/api/uploadvideo/route.ts create mode 100644 src/app/api/weixinauth/route.ts create mode 100644 src/app/myfeed/page.tsx create mode 100644 src/app/weixin-callback/page.tsx diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..80c3e025 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +node_modules/* +.next +.git \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..6857c609 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +# 使用官方 Node.js 为基础镜像 +FROM node:latest + +# 设置工作目录 +WORKDIR /app + +# 复制 package.json 和 package-lock.json(如果存在) +COPY package.json yarn.lock ./ + +# 设置淘宝镜像源 +RUN yarn config set registry https://registry.npmmirror.com + +# 设置 Prisma 引擎的国内镜像源 +ENV PRISMA_ENGINES_MIRROR="https://registry.npmmirror.com/-/binary/prisma" + +# 安装项目依赖 +RUN yarn install + +# 复制项目文件和文件夹到工作目录 +COPY . . +COPY .env ./.env + +# 预安装 Prisma CLI +RUN npx prisma generate + +# 构建 Next.js 项目 +RUN yarn build + +# 暴露 3000 端口 +EXPOSE 3000 + +# 启动 Next.js 服务器 +CMD ["yarn", "start"] \ No newline at end of file diff --git a/MP_verify_uuVKNEzV7WrJ2xyt.txt b/MP_verify_uuVKNEzV7WrJ2xyt.txt new file mode 100644 index 00000000..e40151a5 --- /dev/null +++ b/MP_verify_uuVKNEzV7WrJ2xyt.txt @@ -0,0 +1 @@ +uuVKNEzV7WrJ2xyt \ No newline at end of file diff --git a/README.md b/README.md index 35e85473..0ceae45d 100644 --- a/README.md +++ b/README.md @@ -79,3 +79,44 @@ and that's all you need to get started! ## License [MIT](https://choosealicense.com/licenses/mit/) + + + +## 阿里云部署 +- 将 docker-compose.yml 文件部署到服务器上并使用 Podman Compose 运行 +- 运行 Podman Compose:在项目目录中,运行以下命令以使用 Podman Compose 启动你的服务: +- podman-compose up +停止服务:podman-compose down +查看运行的容器:podman ps + +Redis 容器启动时的内存超额分配警告指的是 Linux 系统中 vm.overcommit_memory 设置的问题。这个设置控制着 Linux 内核如何管理内存分配,尤其是对于需要创建大内存快照(如 Redis 的持久化操作)的场景。当 vm.overcommit_memory 设置为 0(默认值),操作系统允许超额分配内存,但在内存不足时可能导致OOM(内存不足)杀手终止进程。设置为 1 时,内核允许超额承诺所有物理内存和交换空间,这对于 Redis 来说是推荐的设置,因为它可以减少因内存超分配而导致的失败。你可以通过执行 sysctl vm.overcommit_memory=1 来调整这个设置。 + +- 测试mysql连接,测试redis连接 +redis-cli -h 60.205.108.91 -p 6379 -a your_redis_password + + +- 保存 schema.prisma 文件之后,运行以下命令重新生成 Prisma Client: +npx prisma generate + +为了将你的项目中的数据表部署到数据库中,首先确保你的环境变量 DATABASE_URL 正确配置了数据库连接信息。然后,在项目目录中运行 +npx prisma db push 命令。 +这个命令会根据你的 schema.prisma 文件中定义的模型,创建或更新数据库中的表结构,使其与你的 Prisma 模型匹配。这是一个快速同步 Prisma 模型到数据库的方法,非常适用于开发环境。在执行之前,确保数据库服务已经运行,并且可以从你的服务器访问。 + + +# 构建Docker镜像 +docker build -t your-app-name . + +# 运行Docker容器 +docker run -p 3000:3000 your-app-name + + + \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..c070ad3a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,102 @@ +version: "3.8" + +services: + huiyuan: + #app服务使用 build: . 来构建 Dockerfile。 + build: . + #podman build -t huiyuan-app:latest . 根据Dockerfile创建镜像 + #指定镜像名称 + image: huiyuan:latest + ports: + - "3000:3000" + environment: + #确保 DATABASE_URL 和 REDIS_URL 环境变量使用服务名,例如 mysql 和 redis。 + - NEXTAUTH_SECRET:woaiwo + - DATABASE_URL=mysql://your_user:your_user_password@60.205.108.91:3306/your_database + - REDIS_URL=redis://:your_redis_password@60.205.108.91:6379 + - UPLOAD_SERVER=http://img.ipaintgarden.com/upload + - UPLOAD_SECRET=img-generated-api-key + #微信回调域名 + - NEXT_PUBLIC_SERVER_DOMAIN=http://hy.ipaintgarden.com + #绘园公众号 + - NEXT_PUBLIC_WEIXIN_APP_ID=wxf32187893c13aafb + - WEIXIN_APP_SECRET=333e179ffcb17113165d8624de32ad23 + #绘园开放平台 + - NEXT_PUBLIC_WX_OPEN_APP_ID=wxf5e5974b6aacfafb + - WX_OPEN_APP_SECRET=b14baebd47f83f11bebd7ba17a275af9 + # volumes: + # - /opt/prisma-engines:/opt/prisma-engines + #depends_on 表示 app 服务依赖于 mysql 和 redis 服务 + depends_on: + - mysql + - redis + + mysql: + image: mysql:latest + environment: + MYSQL_ROOT_PASSWORD: your_password # Replace with your desired password + MYSQL_DATABASE: your_database # Replace with your desired database name + MYSQL_USER: your_user # Replace with your desired user + MYSQL_PASSWORD: your_user_password # Replace with your desired user password + ports: + - "3306:3306" + volumes: + - mysql_data:/var/lib/mysql + + redis: + image: redis:latest + command: redis-server --requirepass your_redis_password # Replace with your desired Redis password + ports: + - "6379:6379" + volumes: + - redis_data:/data + +volumes: + mysql_data: + redis_data: + + #npx prisma generate + #打包上传后解压 + #unzip -o huiyuan.zip + + #您可以通过 docker-compose up --build 启动所有服务。这样就可以确保您的应用容器能够与数据库和缓存容器通信 + #容器启动后手动运行 + #进入到运行你的 Node.js 应用的 Docker 容器内部,然后运行 + #将schema.prisma 文件中定义的模型,创建到数据库中 + #npx prisma db push + + # 根据Dockerfile创建镜像 + # podman build -t huiyuan-app:latest . + # docker build -t huiyuan-app:latest . + # docker run -p 3000:3000 --name huiyuan-app-1 huiyuan-app + # podman-compose up + + # podman exec -it huiyuan_app_1 sh 进入容器中 + # npx prisma db push 数据库迁移或创建操作 + + + #运行 podman-compose down 命令会停止并删除由 podman-compose up 命令启动的所有容器 + #这个命令在你需要清理所有由 docker-compose.yml 文件定义的服务时非常有用,比如在开发结束后,或者想要重启所有服务时。它为你提供了一种快速将环境恢复到初始状态的方法。 + #如果你想在删除容器的同时自动清理相关的卷,可以在删除容器时使用 docker-compose down -v 命令,其中 -v 标志会删除与在 docker-compose.yml 文件中定义的服务相关联的所有卷。 + #请记住,删除卷是不可逆的操作,所以在执行删除操作之前,请确保你已经保存了所有需要的数据。 + +# podman images +# podman rmi 8ce071eea1df 019814493c7a 170a1e90f843 +# 修改docker-compose.yml文件后 +# podman-compose down +# podman-compose up --build 重新生成镜像和容器 + +# docker-compose up 是 Docker Compose 的一个命令,用于启动并运行整个应用。你提到的两种形式之间的区别在于是否附带了 --build 选项: + +# docker-compose up:这个命令会启动并运行 docker-compose.yml 文件中定义的所有服务。如果服务所依赖的镜像不存在,Docker Compose 会尝试从本地或远程镜像仓库拉取这些镜像。如果镜像已经存在,它不会尝试重新构建镜像,而是直接使用现有的镜像来启动容器。 + +# docker-compose up --build:这个命令除了执行 docker-compose up 的所有操作之外,还会强制构建(或重建)服务所依赖的镜像,即使这些镜像已经存在。这对于确保使用的是最新的代码和依赖非常有用,特别是在开发过程中,当你频繁更改应用代码或依赖时。在构建完成后,它会启动并运行服务。 + +# 简而言之,不带 --build 选项时,Docker Compose 会尝试使用现有镜像来启动服务,而不会尝试构建新的镜像。当使用 --build 选项时,Docker Compose 会先构建(或重建)镜像,然后再启动服务,这样可以确保你的容器运行的是最新版本的镜像。 + +# 在开发过程中,如果你对 Dockerfile 或服务的构建上下文(如项目文件)进行了更改,使用 docker-compose up --build 会很有帮助,因为它确保了你的更改会被包含在新构建的镜像中。如果你确定没有对服务的依赖或代码进行更改,或者你只是想快速启动服务,使用 docker-compose up 就足够了。 + + + + + diff --git a/next.config.js b/next.config.js index 39656ff8..1139297f 100644 --- a/next.config.js +++ b/next.config.js @@ -1,11 +1,21 @@ /** @type {import('next').NextConfig} */ const nextConfig = { images: { - domains: ['uploadthing.com', 'lh3.googleusercontent.com'], + domains: ['127.0.0.1','60.205.108.91','thirdwx.qlogo.cn','localhost','img.ipaintgarden.com','lh3.googleusercontent.com'], }, experimental: { appDir: true - } + }, + webpack: (config, options) => { + // 添加 SVG 处理规则 + config.module.rules.push({ + test: /\.svg$/, + use: ['@svgr/webpack'], + }); + + // 返回更新后的配置 + return config; + }, } module.exports = nextConfig diff --git a/package.json b/package.json index 0e1e41f3..e3af730f 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@upstash/redis": "^1.21.0", "autoprefixer": "10.4.14", "axios": "^1.4.0", + "bcryptjs": "^2.4.3", "class-variance-authority": "^0.6.0", "clsx": "^1.2.1", "cmdk": "^0.2.0", @@ -55,6 +56,7 @@ "react-dropzone": "^14.2.3", "react-hook-form": "^7.44.2", "react-textarea-autosize": "^8.4.1", + "redis": "^4.6.13", "server-only": "^0.0.1", "sharp": "^0.32.1", "tailwind-merge": "^1.12.0", @@ -65,10 +67,12 @@ "zod": "^3.21.4" }, "devDependencies": { + "@svgr/webpack": "^8.1.0", + "@types/editorjs__header": "^2.6.0", + "@types/lodash.debounce": "^4.0.7", "@types/node": "20.2.5", "@types/react": "18.2.7", "@types/react-dom": "18.2.4", - "@types/editorjs__header": "^2.6.0", - "@types/lodash.debounce": "^4.0.7" + "@types/uuid": "^9.0.8" } } diff --git a/prisma/addUser.ts b/prisma/addUser.ts new file mode 100644 index 00000000..cc818c5b --- /dev/null +++ b/prisma/addUser.ts @@ -0,0 +1,26 @@ +const { PrismaClient } = require('@prisma/client'); +const bcrypt = require('bcryptjs'); +const prisma = new PrismaClient(); +//ts-node --esm prisma/addUser.ts +async function createUser() { + try { + // 使用 bcrypt 哈希密码 + const hashedPassword = await bcrypt.hash('123456', 10); + + // 使用 Prisma 创建用户 + const user = await prisma.user.create({ + data: { + username: 'test', + password: hashedPassword, + // 如果有其他必填字段,请在这里添加 + // 例如: email: 'your-email@example.com' + }, + }); + + console.log('User created:', user); + } catch (error) { + console.error('Error creating user:', error); + } +} + +createUser(); \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 929afe43..afab41bb 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1,8 +1,22 @@ // This is your Prisma schema file, // learn more about it in the docs: https://pris.ly/d/prisma-schema +// npm install -g prisma 安装新的Prisma CLI:然后,你可以通过npx来调用本地安装的Prisma CLI,例如:npx prisma --help +// npx prisma generate +// npx prisma db push + +//设置国内镜像 +//export PRISMA_ENGINES_MIRROR="https://registry.npmmirror.com/-/binary/prisma" +//使用 nano 或 vi 等命令行文本编辑器来编辑这些文件 +//vi ~/.bashrc +//滚动到文件的底部,然后添加以下行 +//export PRISMA_ENGINES_MIRROR="https://registry.npmmirror.com/-/binary/prisma" +//如果您使用 vi,请按 Esc,输入 :wq,然后按 Enter 来保存并退出 +//为了让这些更改立即生效(而不需要注销并重新登录),在终端中运行以下命令 +//source ~/.bashrc generator client { provider = "prisma-client-js" + binaryTargets = ["native", "linux-arm64-openssl-3.0.x", "rhel-openssl-1.1.x", "debian-openssl-3.0.x"] } datasource db { @@ -46,6 +60,7 @@ model Session { model User { id String @id @default(cuid()) name String? + password String? email String? @unique emailVerified DateTime? createdSubreddits Subreddit[] @relation("CreatedBy") @@ -134,4 +149,4 @@ model CommentVote { type VoteType @@id([userId, commentId]) -} +} \ No newline at end of file diff --git a/public/assets/svg/logo.svg b/public/assets/svg/logo.svg new file mode 100644 index 00000000..a993997f --- /dev/null +++ b/public/assets/svg/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/favicon.ico b/public/favicon.ico index 45c9ee5a053469b77929d8f8d819f9e828dcd9d5..a4953a6ef874b382e27f47257240bc30c458b79f 100644 GIT binary patch literal 15086 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 B$8rDw literal 9068 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 diff --git a/src/app/api/posts/route.ts b/src/app/api/posts/route.ts index 87aa72af..7d4259b3 100644 --- a/src/app/api/posts/route.ts +++ b/src/app/api/posts/route.ts @@ -4,12 +4,16 @@ import { z } from 'zod' export async function GET(req: Request) { const url = new URL(req.url) - const session = await getAuthSession() + // 获取请求的 Referer 头部 + const referer = req.headers.get('Referer'); + const isMyFeedRequest = referer && new URL(referer).pathname === '/myfeed'; + // console.log("isMyFeedRequest",isMyFeedRequest); + let followedCommunitiesIds: string[] = [] - if (session) { + if (session && isMyFeedRequest) { const followedCommunities = await db.subscription.findMany({ where: { userId: session.user.id, @@ -43,7 +47,7 @@ export async function GET(req: Request) { name: subredditName, }, } - } else if (session) { + } else if (session && isMyFeedRequest) { whereClause = { subreddit: { id: { diff --git a/src/app/api/subreddit/post/vote/route.ts b/src/app/api/subreddit/post/vote/route.ts index 40efac15..4a74d7f0 100644 --- a/src/app/api/subreddit/post/vote/route.ts +++ b/src/app/api/subreddit/post/vote/route.ts @@ -5,7 +5,7 @@ import { PostVoteValidator } from '@/lib/validators/vote' import { CachedPost } from '@/types/redis' import { z } from 'zod' -const CACHE_AFTER_UPVOTES = 1 +const CACHE_AFTER_UPVOTES = 0 export async function PATCH(req: Request) { try { @@ -61,16 +61,16 @@ export async function PATCH(req: Request) { }, 0) if (votesAmt >= CACHE_AFTER_UPVOTES) { - const cachePayload: CachedPost = { - authorUsername: post.author.username ?? '', + const cachePayload = { + authorUsername: post.author.name ?? '', content: JSON.stringify(post.content), id: post.id, title: post.title, - currentVote: null, - createdAt: post.createdAt, + currentVote: "null", + createdAt: post.createdAt.toISOString(), } - await redis.hset(`post:${postId}`, cachePayload) // Store the post data as a hash + await redis.hSet(`post:${postId}`, cachePayload) // Store the post data as a hash } return new Response('OK') @@ -97,16 +97,16 @@ export async function PATCH(req: Request) { }, 0) if (votesAmt >= CACHE_AFTER_UPVOTES) { - const cachePayload: CachedPost = { - authorUsername: post.author.username ?? '', + const cachePayload = { + authorUsername: post.author.name ?? '', content: JSON.stringify(post.content), id: post.id, title: post.title, - currentVote: voteType, - createdAt: post.createdAt, + currentVote: voteType ? voteType : 'null', + createdAt: post.createdAt.toISOString(), } - await redis.hset(`post:${postId}`, cachePayload) // Store the post data as a hash + await redis.hSet(`post:${postId}`, cachePayload) // Store the post data as a hash } return new Response('OK') @@ -129,16 +129,16 @@ export async function PATCH(req: Request) { }, 0) if (votesAmt >= CACHE_AFTER_UPVOTES) { - const cachePayload: CachedPost = { - authorUsername: post.author.username ?? '', + const cachePayload = { + authorUsername: post.author.name ?? '', content: JSON.stringify(post.content), id: post.id, title: post.title, - currentVote: voteType, - createdAt: post.createdAt, + currentVote: voteType ? voteType : 'null', + createdAt: post.createdAt.toISOString(), } - await redis.hset(`post:${postId}`, cachePayload) // Store the post data as a hash + await redis.hSet(`post:${postId}`, cachePayload) // Store the post data as a hash } return new Response('OK') diff --git a/src/app/api/upload/backroute b/src/app/api/upload/backroute new file mode 100644 index 00000000..91b032dd --- /dev/null +++ b/src/app/api/upload/backroute @@ -0,0 +1,61 @@ +import { getAuthSession } from '@/lib/auth' +import { writeFile } from "fs/promises"; +import { NextResponse } from "next/server"; +import { v4 as uuidv4 } from 'uuid'; // 引入uuid +import { existsSync, mkdirSync } from 'fs'; + +export async function POST(request: Request) { + + const session = await getAuthSession() + if (!session?.user) { + return new Response('Unauthorized', { status: 401 }) + } + + const formData = await request.formData(); + // 获取上传文件 + const file = formData.get("file") as File; + // 定义文件保存的目录和访问路径 + const currentYear = new Date().getFullYear(); + const uploadDir = `./public/uploads/${currentYear}/`; + const accessPath = `/uploads/${currentYear}/`; + + if (!file) { + return new NextResponse(JSON.stringify({ error: 'No file uploaded' }), { + status: 400, + headers: { "Content-Type": "application/json" }, + }); + } + + // 检查文件大小 + const maxFileSize = 4 * 1024 * 1024; // 4MB + if (file.size > maxFileSize) { + return new NextResponse(JSON.stringify({ error: 'File is too large' }), { + status: 400, + headers: { "Content-Type": "application/json" }, + }); + } + + // // 使用uuid生成唯一文件名 + // const fileExtension = file.name.split('.').pop(); // 获取文件扩展名 + // const fileName = uuidv4() + '.' + fileExtension; // 生成唯一文件名 + // // 将文件保存到服务器的文件系统中 + // const fileBuffer = await file.arrayBuffer(); + // // 生成文件保存路径 + // const filePath = uploadDir + fileName; + // // 生成访问URL + // const fileUrl = request.headers.get("origin") + accessPath + fileName; + + // // 检查上传目录是否存在,如果不存在则创建 + // if (!existsSync(uploadDir)) { + // mkdirSync(uploadDir, { recursive: true }); // 使用 recursive 选项创建嵌套目录 + // } + // await writeFile(filePath, Buffer.from(fileBuffer)); + + // return new NextResponse(JSON.stringify({ url: fileUrl }), { + // status: 200, + // headers: { + // "Content-Type": "application/json" + // } + // }); +} + diff --git a/src/app/api/upload/route.ts b/src/app/api/upload/route.ts new file mode 100644 index 00000000..e75ea441 --- /dev/null +++ b/src/app/api/upload/route.ts @@ -0,0 +1,71 @@ +import { getAuthSession } from '@/lib/auth' +// import { writeFile } from "fs/promises"; +import { NextResponse } from "next/server"; +// import { v4 as uuidv4 } from 'uuid'; // 引入uuid +// import { existsSync, mkdirSync } from 'fs'; +import axios from 'axios' + +export async function POST(request: Request) { + + const session = await getAuthSession() + if (!session?.user) { + return new Response('Unauthorized', { status: 401 }) + } + + const formData = await request.formData(); + // 获取上传文件 + const file = formData.get("file") as File; + + if (!file) { + return new NextResponse(JSON.stringify({ error: 'No file uploaded' }), { + status: 400, + headers: { "Content-Type": "application/json" }, + }); + } + + console.log("文件大小:",file.size / 1024 / 1024) + // 检查文件大小 + const maxFileSize = 4 * 1024 * 1024; // 4MB + if (file.size > maxFileSize) { + return new NextResponse(JSON.stringify({ error: 'File is too large' }), { + status: 400, + headers: { "Content-Type": "application/json" }, + }); + } + + //上传到图片服务器 .env中配置http://localhost:8080/upload + const uploadFormData = new FormData(); + uploadFormData.append('file', file); + + try { + // 替换这里的URL为你的图片服务器上传接口地址 + if (!process.env.UPLOAD_SERVER) { + throw new Error('UPLOAD_SERVER environment variable is not defined.'); + } + const response = await axios.post(process.env.UPLOAD_SERVER, uploadFormData, { + headers: { + 'Content-Type': 'multipart/form-data', + 'X-API-KEY': process.env.UPLOAD_SECRET, // 如果你的图片服务器需要API密钥 + }, + }); + + // 处理响应 + const { data } = response; + if (data && data.url) { + // 如果图片服务器返回的有图片的URL + return new Response(JSON.stringify({ url: data.url }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } else { + throw new Error('Failed to upload image.'); + } + } catch (error) { + // console.error(error); + return new Response(JSON.stringify({ error: 'Failed to upload image' }), { + status: 500, + headers: { "Content-Type": "application/json" }, + }); + } +} + diff --git a/src/app/api/uploadvideo/route.ts b/src/app/api/uploadvideo/route.ts new file mode 100644 index 00000000..8801d679 --- /dev/null +++ b/src/app/api/uploadvideo/route.ts @@ -0,0 +1,56 @@ +import { getAuthSession } from '@/lib/auth'; +import { writeFile } from 'fs/promises'; +import { NextResponse } from 'next/server'; +import { v4 as uuidv4 } from 'uuid'; +import { existsSync, mkdirSync } from 'fs'; + +export async function POST(request: Request) { + const session = await getAuthSession(); + if (!session?.user) { + return new Response('Unauthorized', { status: 401 }); + } + + const formData = await request.formData(); + // 获取上传的文件 + const file = formData.get('file') as File; + // 定义文件保存的目录和访问路径 + const currentYear = new Date().getFullYear(); + const uploadDir = `./public/uploadvideos/${currentYear}/videos/`; + const accessPath = `/uploadvideos/${currentYear}/videos/`; + + if (!file) { + return new NextResponse(JSON.stringify({ error: 'No file uploaded' }), { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }); + } + + // 检查文件大小(例如,设置为 100MB) + const maxFileSize = 100 * 1024 * 1024; // 100MB + if (file.size > maxFileSize) { + return new NextResponse(JSON.stringify({ error: 'File is too large' }), { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }); + } + + // 使用 uuid 生成唯一文件名 + const fileExtension = file.name.split('.').pop(); // 获取文件扩展名 + const fileName = uuidv4() + '.' + fileExtension; // 生成唯一文件名 + const fileBuffer = await file.arrayBuffer(); + const filePath = uploadDir + fileName; + const fileUrl = request.headers.get('origin') + accessPath + fileName; + + // 检查上传目录是否存在,如果不存在则创建 + if (!existsSync(uploadDir)) { + mkdirSync(uploadDir, { recursive: true }); + } + await writeFile(filePath, Buffer.from(fileBuffer)); + + return new NextResponse(JSON.stringify({ url: fileUrl }), { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + }); +} diff --git a/src/app/api/username/route.ts b/src/app/api/username/route.ts index 701d7cdc..c75ef11a 100644 --- a/src/app/api/username/route.ts +++ b/src/app/api/username/route.ts @@ -17,12 +17,12 @@ export async function PATCH(req: Request) { // check if username is taken const username = await db.user.findFirst({ where: { - username: name, + name: name, }, }) if (username) { - return new Response('Username is taken', { status: 409 }) + return new Response('用户名已被占用', { status: 409 }) } // update username @@ -31,7 +31,7 @@ export async function PATCH(req: Request) { id: session.user.id, }, data: { - username: name, + name: name, }, }) diff --git a/src/app/api/weixinauth/route.ts b/src/app/api/weixinauth/route.ts new file mode 100644 index 00000000..b3a9a9f7 --- /dev/null +++ b/src/app/api/weixinauth/route.ts @@ -0,0 +1,60 @@ +// src/app/weixinauth/route.ts + +import { signIn } from "next-auth/react"; + +export async function GET(req: Request) { + const url = new URL(req.url); + const code = url.searchParams.get('code'); + const state = url.searchParams.get('state'); + + if (!code) return new Response('Code is required', { status: 400 }); + + let APP_ID,APP_SECRET; + if(state === "open"){ + APP_ID = process.env.NEXT_PUBLIC_WX_OPEN_APP_ID; // 从环境变量中获取 + APP_SECRET = process.env.WX_OPEN_APP_SECRET; // 从环境变量中获取 + }else{ + APP_ID = process.env.NEXT_PUBLIC_WEIXIN_APP_ID; // 从环境变量中获取 + APP_SECRET = process.env.WEIXIN_APP_SECRET; // 从环境变量中获取 + } + console.log("code =",code); + console.log(APP_ID); + console.log(APP_SECRET); + if (!APP_ID || !APP_SECRET) { + return new Response('Missing WEIXIN_APP_ID or WEIXIN_APP_SECRET in environment variables', { status: 500 }); + } + + const accessTokenUrl = `https://api.weixin.qq.com/sns/oauth2/access_token?appid=${APP_ID}&secret=${APP_SECRET}&code=${code}&grant_type=authorization_code`; + console.log(accessTokenUrl); + try { + const accessTokenResponse = await fetch(accessTokenUrl); + const accessTokenData = await accessTokenResponse.json(); + console.log("accessTokenData"); + console.log(accessTokenData); + if (accessTokenData.errcode) { + return new Response(JSON.stringify(accessTokenData), { status: 500 }); + } + + // 获取用户信息 + const userInfoUrl = `https://api.weixin.qq.com/sns/userinfo?access_token=${accessTokenData.access_token}&openid=${accessTokenData.openid}&lang=zh_CN`; + + const userInfoResponse = await fetch(userInfoUrl); + const userInfo = await userInfoResponse.json(); + + if (userInfo.errcode) { + return new Response(JSON.stringify(userInfo), { status: 500 }); + } + + // 在userInfo对象中添加access_token和refresh_token + userInfo.access_token = accessTokenData.access_token; + userInfo.refresh_token = accessTokenData.refresh_token; + + console.log("获得微信用户信息和令牌"); + console.log(userInfo); + return new Response(JSON.stringify(userInfo)); + + } catch (error) { + console.error('Weixin Authentication Error:', error); + return new Response('Internal Server Error', { status: 500 }); + } +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index e876175e..44ea10ca 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,16 +1,16 @@ import Navbar from '@/components/Navbar' import { cn } from '@/lib/utils' -import { Inter } from 'next/font/google' +// import { Inter } from 'next/font/google' import Providers from '@/components/Providers' import { Toaster } from '@/components/ui/Toaster' import '@/styles/globals.css' -const inter = Inter({ subsets: ['latin'] }) +// const inter = Inter({ subsets: ['latin'] }) export const metadata = { - title: 'Breadit', - description: 'A Reddit clone built with Next.js and TypeScript.', + title: '绘画爱好者社区', + description: '绘园|绘画爱好者社区|分享你的绘画.', } export default function RootLayout({ @@ -25,7 +25,7 @@ export default function RootLayout({ lang='en' className={cn( 'bg-white text-slate-900 antialiased light', - inter.className + // inter.className )}> @@ -33,7 +33,7 @@ export default function RootLayout({ {authModal} -
+
{children}
diff --git a/src/app/myfeed/page.tsx b/src/app/myfeed/page.tsx new file mode 100644 index 00000000..49f4dc84 --- /dev/null +++ b/src/app/myfeed/page.tsx @@ -0,0 +1,50 @@ +import { redirect } from 'next/navigation' +import CustomFeed from '@/components/homepage/CustomFeed' +import { buttonVariants } from '@/components/ui/Button' +import { Home as HomeIcon } from 'lucide-react' +import Link from 'next/link' +import { authOptions, getAuthSession } from '@/lib/auth' + +export default async function MyFeed() { + const session = await getAuthSession() + if (!session?.user) { + redirect(authOptions?.pages?.signIn || '/login') + } + const PostCustomFeed = await CustomFeed(); + return ( + <> +

我的关注

+
+ {/* @ ts-expect-error server component */} + {/* {session ? : } */} + {PostCustomFeed} + + + {/* subreddit info */} +
+
+

+ + 主页 +

+
+
+
+

+ 创建属于你的版块 +

+
+ + + 创建版块 + +
+
+
+ + ) +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 1f388356..37ba5dd6 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,4 +1,4 @@ -import CustomFeed from '@/components/homepage/CustomFeed' +// import CustomFeed from '@/components/homepage/CustomFeed' import GeneralFeed from '@/components/homepage/GeneralFeed' import { buttonVariants } from '@/components/ui/Button' import { getAuthSession } from '@/lib/auth' @@ -10,27 +10,31 @@ export const fetchCache = 'force-no-store' export default async function Home() { const session = await getAuthSession() - + const PostFeed = await GeneralFeed(); return ( <> -

Your feed

+

+ 开始分享你的绘画 +

+ {/*

分享你的绘画

*/}
- {/* @ts-expect-error server component */} - {session ? : } + {/* @ ts-expect-error server component */} + {/* {session ? : } */} + {PostFeed} + {/* subreddit info */} -
+ { session &&

- Home + 主页

- Your personal Breadit frontpage. Come here to check in with your - favorite communities. + 创建属于你的版块

@@ -39,10 +43,10 @@ export default async function Home() { className: 'w-full mt-4 mb-6', })} href={`/r/create`}> - Create Community + 创建版块
-
+
}
) diff --git a/src/app/r/[slug]/layout.tsx b/src/app/r/[slug]/layout.tsx index 824b5faf..b122cee2 100644 --- a/src/app/r/[slug]/layout.tsx +++ b/src/app/r/[slug]/layout.tsx @@ -10,7 +10,7 @@ import { notFound } from 'next/navigation' import { ReactNode } from 'react' export const metadata: Metadata = { - title: 'Breadit', + title: '绘园', description: 'A Reddit clone built with Next.js and TypeScript.', } @@ -22,9 +22,10 @@ const Layout = async ({ params: { slug: string } }) => { const session = await getAuthSession() + const decodedSlug = decodeURIComponent(slug); const subreddit = await db.subreddit.findFirst({ - where: { name: slug }, + where: { name: decodedSlug }, include: { posts: { include: { @@ -40,7 +41,7 @@ const Layout = async ({ : await db.subscription.findFirst({ where: { subreddit: { - name: slug, + name: decodedSlug, }, user: { id: session.user.id, @@ -55,13 +56,13 @@ const Layout = async ({ const memberCount = await db.subscription.count({ where: { subreddit: { - name: slug, + name: decodedSlug, }, }, }) return ( -
+
@@ -71,26 +72,26 @@ const Layout = async ({ {/* info sidebar */}
-

About r/{subreddit.name}

+

版块/{subreddit.name}

-
Created
+
创建于
-
Members
+
已关注
-
{memberCount}
+
{memberCount}人
{subreddit.creatorId === session?.user?.id ? (
-
You created this community
+
你创造了这个版块
) : null} @@ -106,8 +107,8 @@ const Layout = async ({ variant: 'outline', className: 'w-full mb-6', })} - href={`r/${slug}/submit`}> - Create Post + href={`r/${decodedSlug}/submit`}> + 发布帖子
diff --git a/src/app/r/[slug]/page.tsx b/src/app/r/[slug]/page.tsx index f823b64f..f82c07ac 100644 --- a/src/app/r/[slug]/page.tsx +++ b/src/app/r/[slug]/page.tsx @@ -13,11 +13,11 @@ interface PageProps { const page = async ({ params }: PageProps) => { const { slug } = params - + const decodedSlug = decodeURIComponent(slug); const session = await getAuthSession() const subreddit = await db.subreddit.findFirst({ - where: { name: slug }, + where: { name: decodedSlug }, include: { posts: { include: { @@ -38,8 +38,9 @@ const page = async ({ params }: PageProps) => { return ( <> + {/* 版块/ */}

- r/{subreddit.name} + {subreddit.name}

diff --git a/src/app/r/[slug]/post/[postId]/page.tsx b/src/app/r/[slug]/post/[postId]/page.tsx index 484fab15..8de5f5bb 100644 --- a/src/app/r/[slug]/post/[postId]/page.tsx +++ b/src/app/r/[slug]/post/[postId]/page.tsx @@ -1,31 +1,47 @@ -import CommentsSection from '@/components/CommentsSection' -import EditorOutput from '@/components/EditorOutput' -import PostVoteServer from '@/components/post-vote/PostVoteServer' -import { buttonVariants } from '@/components/ui/Button' -import { db } from '@/lib/db' -import { redis } from '@/lib/redis' -import { formatTimeToNow } from '@/lib/utils' -import { CachedPost } from '@/types/redis' -import { Post, User, Vote } from '@prisma/client' -import { ArrowBigDown, ArrowBigUp, Loader2 } from 'lucide-react' -import { notFound } from 'next/navigation' -import { Suspense } from 'react' +import CommentsSection from "@/components/CommentsSection"; +import EditorOutput from "@/components/EditorOutput"; +import PostVoteServer from "@/components/post-vote/PostVoteServer"; +import { buttonVariants } from "@/components/ui/Button"; +import { db } from "@/lib/db"; +import { redis } from "@/lib/redis"; +import { formatTimeToNow } from "@/lib/utils"; +import { CachedPost } from "@/types/redis"; +import { Post, User, Vote } from "@prisma/client"; +import { ArrowBigDown, ArrowBigUp, Loader2 } from "lucide-react"; +import { notFound } from "next/navigation"; +import { Suspense } from "react"; interface SubRedditPostPageProps { params: { - postId: string - } + postId: string; + }; } -export const dynamic = 'force-dynamic' -export const fetchCache = 'force-no-store' +export const dynamic = "force-dynamic"; +export const fetchCache = "force-no-store"; const SubRedditPostPage = async ({ params }: SubRedditPostPageProps) => { - const cachedPost = (await redis.hgetall( - `post:${params.postId}` - )) as CachedPost - - let post: (Post & { votes: Vote[]; author: User }) | null = null + let cachedPost: CachedPost | null = null; + console.log("params", params) + try { + const result = await redis.hGetAll(`post:${params.postId}`); + console.log("result") + console.log(result) + if (Object.keys(result).length) { + // Ensure the result is of the expected type, or convert it as necessary + cachedPost = result as unknown as CachedPost; + console.log("cachedPost"); + console.log(cachedPost); + } else { + // Handle the case where the hash does not exist or is empty + console.log(`Post with ID ${params.postId} not found in cache.`); + } + } catch (error) { + console.error(`Error fetching post from Redis: ${error}`); + } + let post: (Post & { votes: Vote[]; author: User }) | null = null; + console.log(post); + console.log(!cachedPost); if (!cachedPost) { post = await db.post.findFirst({ @@ -36,18 +52,42 @@ const SubRedditPostPage = async ({ params }: SubRedditPostPageProps) => { votes: true, author: true, }, - }) + }); } + console.log("2"); - if (!post && !cachedPost) return notFound() + const postId = post?.id ?? cachedPost?.id; + console.log(postId); + if (!postId) return notFound(); return (
-
+
+ + +
+

+ 发表于 + {(post?.createdAt ?? cachedPost?.createdAt) && + formatTimeToNow( + new Date((post?.createdAt ?? cachedPost?.createdAt)!) + )}{" "} + /{" "}{post?.author.name ?? cachedPost?.authorUsername} +

+

+ {post?.title ?? cachedPost?.title} +

+ + + {/*
comment here
*/} +
+
+ +
}> {/* @ts-expect-error server component */} { return await db.post.findUnique({ where: { @@ -56,53 +96,43 @@ const SubRedditPostPage = async ({ params }: SubRedditPostPageProps) => { include: { votes: true, }, - }) + }); }} /> - -
-

- Posted by u/{post?.author.username ?? cachedPost.authorUsername}{' '} - {formatTimeToNow(new Date(post?.createdAt ?? cachedPost.createdAt))} -

-

- {post?.title ?? cachedPost.title} -

- - - - }> - {/* @ts-expect-error Server Component */} - - -
+ + + } + > + {/* @ts-expect-error Server Component */} + +
- ) -} + ); +}; function PostVoteShell() { return ( -
+
{/* upvote */} -
- +
+
{/* score */} -
- +
+
{/* downvote */} -
- +
+
- ) + ); } -export default SubRedditPostPage +export default SubRedditPostPage; \ No newline at end of file diff --git a/src/app/r/[slug]/submit/page.tsx b/src/app/r/[slug]/submit/page.tsx index 9acde787..783a3314 100644 --- a/src/app/r/[slug]/submit/page.tsx +++ b/src/app/r/[slug]/submit/page.tsx @@ -10,9 +10,10 @@ interface pageProps { } const page = async ({ params }: pageProps) => { + const decodedSlug = decodeURIComponent(params.slug); const subreddit = await db.subreddit.findFirst({ where: { - name: params.slug, + name: decodedSlug, }, }) @@ -24,10 +25,10 @@ const page = async ({ params }: pageProps) => {

- Create Post + 创建帖子

- in r/{params.slug} + 版块/{decodedSlug}

@@ -37,7 +38,7 @@ const page = async ({ params }: pageProps) => {
diff --git a/src/app/r/create/page.tsx b/src/app/r/create/page.tsx index 1b6c42aa..5a5c3f71 100644 --- a/src/app/r/create/page.tsx +++ b/src/app/r/create/page.tsx @@ -28,16 +28,16 @@ const Page = () => { if (err instanceof AxiosError) { if (err.response?.status === 409) { return toast({ - title: 'Subreddit already exists.', - description: 'Please choose a different name.', + title: '版块名已存在.', + description: '请重新输入一个版块名称.', variant: 'destructive', }) } if (err.response?.status === 422) { return toast({ - title: 'Invalid subreddit name.', - description: 'Please choose a name between 3 and 21 letters.', + title: '版块名错误.', + description: '版块名长度需要在1到21个字符之间.', variant: 'destructive', }) } @@ -48,8 +48,8 @@ const Page = () => { } toast({ - title: 'There was an error.', - description: 'Could not create subreddit.', + title: '出错了.', + description: '不能创版块.', variant: 'destructive', }) }, @@ -62,24 +62,24 @@ const Page = () => {
-

Create a Community

+

创建新版块


-

Name

+

请填写版块名称

- Community names including capitalization cannot be changed. + 名称不能为空

-

- r/ +

+ 版块/

setInput(e.target.value)} - className='pl-6' + className='pl-10' />
@@ -89,13 +89,13 @@ const Page = () => { disabled={isLoading} variant='subtle' onClick={() => router.back()}> - Cancel + 取消
diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index f9f3160d..57be2423 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -16,15 +16,15 @@ export default async function SettingsPage() { } return ( -
+
-

Settings

+

设置

diff --git a/src/app/weixin-callback/page.tsx b/src/app/weixin-callback/page.tsx new file mode 100644 index 00000000..17733c5c --- /dev/null +++ b/src/app/weixin-callback/page.tsx @@ -0,0 +1,64 @@ +'use client' + +import { signIn } from 'next-auth/react'; +import { FC,useEffect } from 'react' + +const page : FC = () => { + + // eslint-disable-next-line react-hooks/rules-of-hooks + useEffect(() => { + // 在这里处理微信回调的逻辑 + // 解析查询参数,执行身份验证等 + const queryParams = new URLSearchParams(window.location.search); + const code = queryParams.get('code'); + const state = queryParams.get('state'); + + // 示例:输出获取到的 code 和 state 参数 + console.log('code:', code); + console.log('state:', state); + + + if (!code) { + console.error('code is missing in environment variables.'); + return; + } + + // 调用你的自定义 API + fetch(`/api/weixinauth?code=${code}&state=${state}`) + .then(response => response.json()) + .then(data => { + // 假设返回的数据中包含了必要的用户信息 + console.log(JSON.stringify(data)); + + // 检查 data 是否包含必要字段 + if (data && data.openid && data.nickname) { + // 使用 signIn 完成登录 + signIn('weixin', { + redirect: true, + ...data + }); + } else { + console.error('Required data is missing'); + } + + }) + .catch(error => { + console.error('Error while signing in:', error); + }); + + + + + }, []); + + + return ( +
+

绘画是心灵的事务,不是手的事务

+

+

登录中,请稍后...

+
+ ); +}; + +export default page; \ No newline at end of file diff --git a/src/components/CommentsSection.tsx b/src/components/CommentsSection.tsx index 557f5670..a8c41345 100644 --- a/src/components/CommentsSection.tsx +++ b/src/components/CommentsSection.tsx @@ -42,8 +42,8 @@ const CommentsSection = async ({ postId }: CommentsSectionProps) => { }) return ( -
-
+
+
diff --git a/src/components/CreateComment.tsx b/src/components/CreateComment.tsx index f8c94cde..ab763da1 100644 --- a/src/components/CreateComment.tsx +++ b/src/components/CreateComment.tsx @@ -54,14 +54,14 @@ const CreateComment: FC = ({ postId, replyToId }) => { return (
- +