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 45c9ee5a..a4953a6e 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ 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 (
- +