diff --git a/README.md b/README.md index 5111f40..1deb52e 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ QQ:964979747 想了解社区效果,请参考 [ZeroCat](https://ourworld.wuyuan.dev)。 ## 安装 - +![使用Nodejs开发](.public/Node.js.png) 这个项目使用 [node](http://nodejs.org) , [npm](https://npmjs.com), [docker](https://docker.com),请确保你本地已经安装了祂们 ```sh @@ -106,4 +106,5 @@ ZeroCat 的项目 遵循 [Contributor Covenant](http://contributor-covenant.org/ 感谢:https://gitee.com/scratch-cn/lite
此项目声明了 MIT 协议 + ![社区(Github图床)](https://github.com/ZeroCatDev/ZeroCat/assets/88357633/d6f4a6ba-daa1-45c8-88f7-4b20d9edbb22) diff --git a/package.json b/package.json index c08223a..322fa29 100644 --- a/package.json +++ b/package.json @@ -4,12 +4,13 @@ "scripts": { "test": "echo \"No test specified\" && exit 0", "start": "node .bin/www", - "prisma": "prisma db pull && prisma generate", + "prisma": "prisma generate", + "prisma:pull": "prisma db pull && prisma generate", "dev": "cross-env NODE_ENV=development nodemon node .bin/www" }, "dependencies": { "@aws-sdk/client-s3": "^3.693.0", - "@prisma/client": "^5.22.0", + "@prisma/client": "^6.0.1", "axios": "^1.7.3", "body-parser": "^1.20.1", "compression": "^1.7.5", @@ -36,6 +37,6 @@ }, "devDependencies": { "cross-env": "^7.0.3", - "prisma": "^5.22.0" + "prisma": "^6.0.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 005658c..496e560 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: specifier: ^3.693.0 version: 3.693.0 '@prisma/client': - specifier: ^5.22.0 - version: 5.22.0(prisma@5.22.0) + specifier: ^6.0.1 + version: 6.0.1(prisma@6.0.1) axios: specifier: ^1.7.3 version: 1.7.7 @@ -88,8 +88,8 @@ importers: specifier: ^7.0.3 version: 7.0.3 prisma: - specifier: ^5.22.0 - version: 5.22.0 + specifier: ^6.0.1 + version: 6.0.1 packages: @@ -267,29 +267,29 @@ packages: resolution: {integrity: sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==} engines: {node: ^14.21.3 || >=16} - '@prisma/client@5.22.0': - resolution: {integrity: sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==} - engines: {node: '>=16.13'} + '@prisma/client@6.0.1': + resolution: {integrity: sha512-60w7kL6bUxz7M6Gs/V+OWMhwy94FshpngVmOY05TmGD0Lhk+Ac0ZgtjlL6Wll9TD4G03t4Sq1wZekNVy+Xdlbg==} + engines: {node: '>=18.18'} peerDependencies: prisma: '*' peerDependenciesMeta: prisma: optional: true - '@prisma/debug@5.22.0': - resolution: {integrity: sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==} + '@prisma/debug@6.0.1': + resolution: {integrity: sha512-jQylgSOf7ibTVxqBacnAlVGvek6fQxJIYCQOeX2KexsfypNzXjJQSS2o5s+Mjj2Np93iSOQUaw6TvPj8syhG4w==} - '@prisma/engines-version@5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2': - resolution: {integrity: sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==} + '@prisma/engines-version@5.23.0-27.5dbef10bdbfb579e07d35cc85fb1518d357cb99e': + resolution: {integrity: sha512-JmIds0Q2/vsOmnuTJYxY4LE+sajqjYKhLtdOT6y4imojqv5d/aeVEfbBGC74t8Be1uSp0OP8lxIj2OqoKbLsfQ==} - '@prisma/engines@5.22.0': - resolution: {integrity: sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==} + '@prisma/engines@6.0.1': + resolution: {integrity: sha512-4hxzI+YQIR2uuDyVsDooFZGu5AtixbvM2psp+iayDZ4hRrAHo/YwgA17N23UWq7G6gRu18NvuNMb48qjP3DPQw==} - '@prisma/fetch-engine@5.22.0': - resolution: {integrity: sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==} + '@prisma/fetch-engine@6.0.1': + resolution: {integrity: sha512-T36bWFVGeGYYSyYOj9d+O9G3sBC+pAyMC+jc45iSL63/Haq1GrYjQPgPMxrEj9m739taXrupoysRedQ+VyvM/Q==} - '@prisma/get-platform@5.22.0': - resolution: {integrity: sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==} + '@prisma/get-platform@6.0.1': + resolution: {integrity: sha512-zspC9vlxAqx4E6epMPMLLBMED2VD8axDe8sPnquZ8GOsn6tiacWK0oxrGK4UAHYzYUVuMVUApJbdXB2dFpLhvg==} '@smithy/abort-controller@3.1.8': resolution: {integrity: sha512-+3DOBcUn5/rVjlxGvUPKc416SExarAQ+Qe0bqk30YSUjbepwpS7QN0cyKUSifvLJhdMZ0WPzPP5ymut0oonrpQ==} @@ -1121,9 +1121,9 @@ packages: phpass@0.1.1: resolution: {integrity: sha512-ZGkvcdZbIVN6k94p87LavU+GJbVJ8u1BKUkG/qRhdKi8v8YQONT8Ad1DaeXTeEjHBQbMK0+eiI03/M3PbNHB5g==} - prisma@5.22.0: - resolution: {integrity: sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==} - engines: {node: '>=16.13'} + prisma@6.0.1: + resolution: {integrity: sha512-CaMNFHkf+DDq8zq3X/JJsQ4Koy7dyWwwtOKibkT/Am9j/tDxcfbg7+lB1Dzhx18G/+RQCMgjPYB61bhRqteNBQ==} + engines: {node: '>=18.18'} hasBin: true process-nextick-args@2.0.1: @@ -1868,30 +1868,30 @@ snapshots: '@noble/hashes@1.5.0': {} - '@prisma/client@5.22.0(prisma@5.22.0)': + '@prisma/client@6.0.1(prisma@6.0.1)': optionalDependencies: - prisma: 5.22.0 + prisma: 6.0.1 - '@prisma/debug@5.22.0': {} + '@prisma/debug@6.0.1': {} - '@prisma/engines-version@5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2': {} + '@prisma/engines-version@5.23.0-27.5dbef10bdbfb579e07d35cc85fb1518d357cb99e': {} - '@prisma/engines@5.22.0': + '@prisma/engines@6.0.1': dependencies: - '@prisma/debug': 5.22.0 - '@prisma/engines-version': 5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2 - '@prisma/fetch-engine': 5.22.0 - '@prisma/get-platform': 5.22.0 + '@prisma/debug': 6.0.1 + '@prisma/engines-version': 5.23.0-27.5dbef10bdbfb579e07d35cc85fb1518d357cb99e + '@prisma/fetch-engine': 6.0.1 + '@prisma/get-platform': 6.0.1 - '@prisma/fetch-engine@5.22.0': + '@prisma/fetch-engine@6.0.1': dependencies: - '@prisma/debug': 5.22.0 - '@prisma/engines-version': 5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2 - '@prisma/get-platform': 5.22.0 + '@prisma/debug': 6.0.1 + '@prisma/engines-version': 5.23.0-27.5dbef10bdbfb579e07d35cc85fb1518d357cb99e + '@prisma/get-platform': 6.0.1 - '@prisma/get-platform@5.22.0': + '@prisma/get-platform@6.0.1': dependencies: - '@prisma/debug': 5.22.0 + '@prisma/debug': 6.0.1 '@smithy/abort-controller@3.1.8': dependencies: @@ -2880,9 +2880,9 @@ snapshots: phpass@0.1.1: {} - prisma@5.22.0: + prisma@6.0.1: dependencies: - '@prisma/engines': 5.22.0 + '@prisma/engines': 6.0.1 optionalDependencies: fsevents: 2.3.3 diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 2184bfa..12b50a5 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -115,23 +115,25 @@ model ow_counter { /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments model ow_projects { - id Int @id @default(autoincrement()) @db.UnsignedInt - type String? @default("text") @db.VarChar(32) - licence String? @db.VarChar(32) + id Int @id @default(autoincrement()) @db.UnsignedInt + type String? @default("text") @db.VarChar(32) + licence String? @db.VarChar(32) authorid Int - teacherid Int? @default(0) @db.UnsignedInt - state String? @default("private") @db.VarChar(32) - view_count Int? @default(0) @db.UnsignedInt - like_count Int? @default(0) - favo_count Int? @default(0) - time DateTime? @default(now()) @db.Timestamp(0) - title String? @default("Scratch新项目") @db.VarChar(1000) - description String? @default("OurWorld上的Scratch项目") @db.VarChar(1000) - source String? @db.MediumText - history Boolean @default(true) - devenv Boolean @default(true) - devsource String @db.MediumText - tags String @default("") @db.VarChar(100) + teacherid Int? @default(0) @db.UnsignedInt + state String? @default("private") @db.VarChar(32) + view_count Int? @default(0) @db.UnsignedInt + like_count Int? @default(0) + favo_count Int? @default(0) + time DateTime? @default(now()) @db.Timestamp(0) + title String? @default("Scratch新项目") @db.VarChar(1000) + description String? @default("OurWorld上的Scratch项目") @db.VarChar(1000) + source String? @db.MediumText + history Boolean @default(true) + devenv Boolean @default(true) + devsource String @db.MediumText + // 移除原来的 tags 字符串字段 + // 添加与 tags 表的关联 + tags ow_projects_tags[] } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments @@ -364,6 +366,17 @@ model magiclinktoken { expiresAt DateTime @db.DateTime(0) } +model ow_projects_tags { + id Int @id @default(autoincrement()) + name String @db.VarChar(45) + projectid Int + created_at DateTime? @default(now()) @db.Timestamp(0) + // 添加与 projects 表的关联 + project ow_projects @relation(fields: [projectid], references: [id]) + + @@index([projectid]) +} + /// The underlying view does not contain a valid unique identifier and can therefore currently not be handled by Prisma Client. /// This view or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments view ow_projects_view { diff --git a/public/Node.js.png b/public/Node.js.png new file mode 100644 index 0000000..a61e377 Binary files /dev/null and b/public/Node.js.png differ diff --git a/server/lib/method/projects.js b/server/lib/method/projects.js index c3dcf16..24320b5 100644 --- a/server/lib/method/projects.js +++ b/server/lib/method/projects.js @@ -1,54 +1,45 @@ const logger = require("../logger.js"); -//prisma client const { PrismaClient } = require("@prisma/client"); const prisma = new PrismaClient(); - const crypto = require("crypto"); - const { getUsersByList } = require("./users.js"); -const { log } = require("console"); +/** + * Get projects by list of IDs + * @param {Array} list - List of project IDs + * @param {number} userid - User ID + * @returns {Promise>} + */ async function getProjectsByList(list, userid) { - var select = { - id: true, - type: true, - licence: true, - authorid: true, - state: true, - view_count: true, - time: true, - title: true, - description: true, - tags: true, - }; - - // 获取每个项目的详细信息 - var projects = await prisma.ow_projects.findMany({ - where: { - id: { in: list.map((item) => parseInt(item)) }, - }, - select: select, + const select = projectSelectionFields(); + const projectIds = list.map(Number); + const projects = await prisma.ow_projects.findMany({ + where: { id: { in: projectIds } }, + select, }); - - //logger.debug(projects); - projects = projects.filter((project) => { - return !(project.state == "private" && project.authorid != userid); - }); - //logger.debug(projects); - - return projects; + return projects.filter( + (project) => !(project.state === "private" && project.authorid !== userid) + ); } + +/** + * Get projects and users by list of IDs + * @param {Array} list - List of project IDs + * @param {number} userid - User ID + * @returns {Promise<{ projects: Array, users: Array}>} + */ async function getProjectsAndUsersByProjectsList(list, userid) { - var projects = await getProjectsByList(list, userid); - var userslist = [...new Set(projects.map((project) => project.authorid))]; - var users = await getUsersByList(userslist); - return { projects: projects, users: users }; + const projects = await getProjectsByList(list, userid); + const userslist = [...new Set(projects.map((project) => project.authorid))]; + const users = await getUsersByList(userslist); + return { projects, users }; } -//(async () => {logger.debug(await getProjectsAndUsersByProjectsList([3, 126, 130, 131, 129], 2));})(); - -//logger.debug(await getProjectsAndUsersByProjectsList([3, 126, 130, 131, 129], 2)) -// 工具函数:提取项目数据 +/** + * Extract project data from body + * @param {Object} body - Request body + * @returns {Object} - Extracted project data + */ function extractProjectData(body) { const fields = [ "type", @@ -57,7 +48,6 @@ function extractProjectData(body) { "title", "description", "history", - "tags", ]; return fields.reduce( (acc, field) => (body[field] ? { ...acc, [field]: body[field] } : acc), @@ -65,39 +55,71 @@ function extractProjectData(body) { ); } -// 工具函数:判断是否为有效 JSON -function isJson(str) { - try { - JSON.stringify(str); // 如果能成功解析,说明是合法的 JSON - return true; - } catch (error) { - logger.debug(error); - return false; // 如果解析失败,说明不是 JSON - } +/** + * Extract project tags from string or array + * @param {string | Array} tags - Project tags + * @returns {Array} - Extracted project tags + */ +const extractProjectTags = (tags) => + // 如果某项为空,则删除 + Array.isArray(tags) + ? tags.map(String).filter((tag) => tag) + : tags + .split(",") + .map((tag) => tag.trim()) + .filter((tag) => tag); +/** + * Handle tags change and save + * @param {number} projectId - Project ID + * @param {string | Array} tags - Project tags + * @returns {Promise} + */ +async function handleTagsChange(projectId, tags) { + const existingTags = await prisma.ow_projects_tags.findMany({ + where: { projectid: projectId }, + select: { id: true, name: true }, + }); + tags = extractProjectTags(tags); + + const tagNames = new Set(tags); + await Promise.all( + existingTags.map(async (tag) => { + if (!tagNames.has(tag.name)) { + await prisma.ow_projects_tags.delete({ where: { id: tag.id } }); + } else { + tagNames.delete(tag.name); + } + }) + ); + + await Promise.all( + [...tagNames].map(async (name) => { + await prisma.ow_projects_tags.create({ + data: { projectid: projectId, name }, + }); + }) + ); } -// 工具函数:设置项目文件 +/** + * Set project file + * @param {string | Object} source - Project source + * @returns {Promise} - SHA256 of the source + */ function setProjectFile(source) { - //logger.debug(source); - var sourcedata=''; - if (isJson(source)) { - sourcedata = JSON.stringify(source); - }else { - sourcedata = source; - } - logger.debug(typeof sourcedata); - + const sourcedata = isJson(source) ? JSON.stringify(source) : source; const sha256 = crypto.createHash("sha256").update(sourcedata).digest("hex"); - logger.debug("sha256:", sha256); - - //logger.debug(sourcedata); prisma.ow_projects_file - .create({ data: { sha256: sha256, source: sourcedata } }) + .create({ data: { sha256, source: sourcedata } }) .catch(logger.error); return sha256; } -// 工具函数:获取项目文件 +/** + * Get project file + * @param {string} sha256 - SHA256 of the source + * @returns {Promise<{ source: string }>} - Project source + */ async function getProjectFile(sha256) { return prisma.ow_projects_file.findFirst({ where: { sha256 }, @@ -105,13 +127,22 @@ async function getProjectFile(sha256) { }); } -// 工具函数:处理错误响应 +/** + * Handle error response + * @param {Response} res - Response object + * @param {Error} err - Error object + * @param {string} msg - Error message + * @returns {void} + */ function handleError(res, err, msg) { logger.error(err); res.status(500).send({ status: "0", msg, error: err }); } -// 工具函数:选择项目信息字段 +/** + * Select project information fields + * @returns {Object} - Selected fields + */ function projectSelectionFields() { return { id: true, @@ -128,7 +159,10 @@ function projectSelectionFields() { }; } -// 工具函数:选择作者信息字段 +/** + * Select author information fields + * @returns {Object} - Selected fields + */ function authorSelectionFields() { return { id: true, @@ -141,6 +175,17 @@ function authorSelectionFields() { }; } +// 工具函数:判断是否为有效 JSON +function isJson(str) { + try { + JSON.stringify(str); + return true; + } catch (error) { + logger.debug(error); + return false; + } +} + module.exports = { getProjectsByList, getUsersByList, @@ -149,7 +194,8 @@ module.exports = { isJson, setProjectFile, getProjectFile, - handleError, projectSelectionFields, authorSelectionFields, + extractProjectTags, + handleTagsChange, }; diff --git a/server/lib/method/users.js b/server/lib/method/users.js index 7a949cd..cb56cdd 100644 --- a/server/lib/method/users.js +++ b/server/lib/method/users.js @@ -22,7 +22,7 @@ async function getUsersByList(list) { select: select, }); - //logger.debug(users); + logger.debug(users); return users; } diff --git a/server/router_project.js b/server/router_project.js index 823507b..2df3694 100644 --- a/server/router_project.js +++ b/server/router_project.js @@ -6,16 +6,12 @@ const DB = require("./lib/database.js"); const I = require("./lib/global.js"); const default_project = require("./lib/default_project.js"); const { - getProjectsByList, - getUsersByList, - getProjectsAndUsersByProjectsList, extractProjectData, - isJson, setProjectFile, getProjectFile, - handleError, projectSelectionFields, authorSelectionFields, + handleTagsChange, } = require("./lib/method/projects.js"); const { Logger } = require("winston"); // 中间件,确保所有请求均经过该处理 @@ -94,7 +90,7 @@ router.put("/:id/source", async (req, res, next) => { } //logger.debug(req.body); var reqbody = req.body - logger.debug('1111111111111'); + //logger.debug('1111111111111'); logger.debug(typeof reqbody); const sha256 = setProjectFile(reqbody); const projectId = Number(req.params.id); @@ -132,6 +128,11 @@ router.put("/:id", async (req, res, next) => { where: { id: Number(req.params.id), authorid: Number(res.locals.userid) }, data: updatedData, }); + // 处理标签 + if(req.body.tags){ + await handleTagsChange(Number(req.params.id), req.body.tags); + } + res.status(200).send({ status: "1", msg: "保存成功" }); } catch (err) { logger.error("Error updating project information:", err); @@ -204,7 +205,16 @@ router.get("/:id", async (req, res, next) => { where: { id: Number(project.authorid) }, select: authorSelectionFields(), }); + + const tags = await I.prisma.ow_projects_tags.findMany({ + where: { projectid: Number(req.params.id) }, + select: { name: true, id: true ,created_at: true}, + }); + project.author = author; + project.tags = tags + logger.debug(tags); + logger.debug(project); res.status(200).send(project); } catch (err) { logger.error("Error fetching project information:", err); diff --git a/server/router_scratch.js b/server/router_scratch.js index c33fc00..a4cfa34 100644 --- a/server/router_scratch.js +++ b/server/router_scratch.js @@ -12,16 +12,7 @@ var I = require("./lib/global.js"); const { log, error } = require("console"); const { needlogin } = require("./middleware/auth.js"); const { - getProjectsByList, - getUsersByList, - getProjectsAndUsersByProjectsList, - extractProjectData, - isJson, - setProjectFile, getProjectFile, - handleError, - projectSelectionFields, - authorSelectionFields, } = require("./lib/method/projects.js"); router.all("*", function (req, res, next) { next(); diff --git a/server/router_search.js b/server/router_search.js index e13de3e..4a28690 100644 --- a/server/router_search.js +++ b/server/router_search.js @@ -5,7 +5,9 @@ const express = require("express"); const router = express.Router(); const I = require("./lib/global.js"); // 功能函数集 const DB = require("./lib/database.js"); // 数据库 - +const { + getUsersByList +} = require("./lib/method/projects.js"); // 搜索:Scratch项目列表:数据(只搜索标题) router.get("/", async (req, res, next) => { try { @@ -42,17 +44,30 @@ router.get("/", async (req, res, next) => { const orderBy = orderbyMap[orderbyField] || "time"; const order = orderDirectionMap[orderDirection] || "desc"; - // 搜索条件 + // 构建基本搜索条件 const searchinfo = { - title: { contains: title }, + title: title ? { contains: title } : undefined, source: source ? { contains: source } : undefined, - description: { contains: description }, - type: { contains: type }, + description: description ? { contains: description } : undefined, + type: type ? { contains: type } : undefined, state: { in: state }, - tags: { contains: tags }, authorid: userid ? { equals: Number(userid) } : undefined, }; + // 添加标签搜索条件 + if (tags) { + searchinfo['tags'] = { + some: { + name: { contains: tags } + } + }; + } + + // 查询项目总数 + const totalCount = await I.prisma.ow_projects.count({ + where: searchinfo, + }); + // 查询项目结果 const projectresult = await I.prisma.ow_projects.findMany({ where: searchinfo, @@ -66,34 +81,25 @@ router.get("/", async (req, res, next) => { description: true, view_count: true, time: true, - tags: true, + tags: { + select: { + name: true, + id: true + } + } }, skip: (Number(curr) - 1) * Number(limit), take: Number(limit), }); - logger.debug(searchinfo); - // 统计项目总数 - const projectcount = await I.prisma.ow_projects.count({ - where: searchinfo, - }); // 获取作者信息 const authorIds = [...new Set(projectresult.map((item) => item.authorid))]; - const userresult = await I.prisma.ow_users.findMany({ - where: { id: { in: authorIds } }, - select: { - id: true, - username: true, - display_name: true, - motto: true, - images: true, - }, - }); + const userresult = await getUsersByList(authorIds); res.status(200).send({ projects: projectresult, users: userresult, - totalCount: [{ totalCount: projectcount }], + totalCount: [{ totalCount }], }); } catch (error) { next(error);