From 9048a5a38997ec51d02a237d7a127949ff3b8bf9 Mon Sep 17 00:00:00 2001 From: zty Date: Tue, 7 Jul 2026 03:22:29 -0400 Subject: [PATCH] feat(forum): public read endpoints (boards/threads/detail) --- .../iapip-svr/src/controllers/forum.ts | 77 +++++++++++++++++++ .../源码/服务端/iapip-svr/src/index.ts | 71 +++++++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 空气质量预测/源码/服务端/iapip-svr/src/controllers/forum.ts create mode 100644 空气质量预测/源码/服务端/iapip-svr/src/index.ts diff --git a/空气质量预测/源码/服务端/iapip-svr/src/controllers/forum.ts b/空气质量预测/源码/服务端/iapip-svr/src/controllers/forum.ts new file mode 100644 index 0000000..7c6f951 --- /dev/null +++ b/空气质量预测/源码/服务端/iapip-svr/src/controllers/forum.ts @@ -0,0 +1,77 @@ +import Router from 'koa-router' +import validate from '../middlewares/validator' +import { db } from '..' +import { fail } from '../common/utils' +import { ERR_CODE, FORUM_BOARDS } from '../common/constants' + +const forumRouter = new Router() +forumRouter.prefix('/api/forum') + +// 板块列表 + 帖数(公开) +forumRouter.get('/boards', async ctx => { + const grouped = await db.forumThread.groupBy({ + by: ['board'], + where: { deleted: 0 }, + _count: { _all: true } + }) + const countMap: Record = {} + grouped.forEach(g => { countMap[g.board] = g._count._all }) + ctx.body = FORUM_BOARDS.map(board => ({ board, count: countMap[board] ?? 0 })) +}) + +// 帖子列表(公开,分页,按最新) +forumRouter.get( + '/threads', + validate({ + query: { + type: 'object', + properties: { + board: { type: 'string' }, + page: { type: 'number' }, + size: { type: 'number' } + } + } + }), + async ctx => { + const q = ctx.state.query as { board?: string, page?: number, size?: number } + const page = q.page && q.page > 0 ? q.page : 1 + const size = q.size && q.size > 0 ? q.size : 20 + const where: { deleted: number, board?: string } = { deleted: 0 } + if (q.board && FORUM_BOARDS.includes(q.board)) where.board = q.board + const [list, total] = await Promise.all([ + db.forumThread.findMany({ + where, + orderBy: { created_at: 'desc' }, + skip: (page - 1) * size, + take: size, + select: { + id: true, board: true, title: true, author_name: true, + like_count: true, reply_count: true, created_at: true + } + }), + db.forumThread.count({ where }) + ]) + ctx.body = { list, total } + } +) + +// 帖子详情 + 回帖(公开) +forumRouter.get('/threads/:id', async ctx => { + const id = Number(ctx.params.id) + if (!Number.isInteger(id)) { + ctx.throw(422, fail(ERR_CODE.INVALID_REQ_DATA, '参数错误')) + return + } + const thread = await db.forumThread.findFirst({ where: { id, deleted: 0 } }) + if (!thread) { + ctx.throw(422, fail(ERR_CODE.UNKNOWN_ERR, '帖子不存在')) + return + } + const replies = await db.forumReply.findMany({ + where: { thread_id: id, deleted: 0 }, + orderBy: { floor: 'asc' } + }) + ctx.body = { thread, replies } +}) + +export const forumRoutes = forumRouter.routes() diff --git a/空气质量预测/源码/服务端/iapip-svr/src/index.ts b/空气质量预测/源码/服务端/iapip-svr/src/index.ts new file mode 100644 index 0000000..d000d4f --- /dev/null +++ b/空气质量预测/源码/服务端/iapip-svr/src/index.ts @@ -0,0 +1,71 @@ +import Koa from 'koa' +import logger, { initLogger } from './common/logger' +import errorHandler from './middlewares/error-handler' +import queryTypeParser from './middlewares/query-typeparser' +import { testRoutes } from './controllers/_test' +import { adminRoutes } from './controllers/admin' +import { userRoutes } from './controllers/user' +import { createServer } from 'http' +import cors from '@koa/cors' +import { PrismaClient } from '@prisma/client' +import { materialRoutes } from './controllers/material' +import { configRoutes } from './controllers/config' +import { templateRoutes } from './controllers/template' +import { projectRoutes } from './controllers/project' +import { predictRoutes } from './controllers/predict' +import { auditRoutes } from './controllers/audit' +import { forumRoutes } from './controllers/forum' +import koaBody from 'koa-body' +import path from 'path' +import { scheduleJob } from 'node-schedule' +// import { Server } from 'socket.io' + +const app = new Koa() +const port = process.env.IAPIP_SVR_PORT ?? 6060 +const server = createServer(app.callback()) + +const isDev = app.env == 'development' + +initLogger(app.env) + +app.use(errorHandler) +if (isDev) app.use(cors({ credentials: true })) +app.use(queryTypeParser()) +app.use(koaBody({ + multipart: true, + formidable: { + keepExtensions: true, + uploadDir: path.join(__dirname, '/', '../../') + } +})) +app.use(testRoutes) +app.use(userRoutes) +app.use(adminRoutes) +app.use(configRoutes) +app.use(predictRoutes) +app.use(projectRoutes) +app.use(templateRoutes) +app.use(materialRoutes) +app.use(auditRoutes) +app.use(forumRoutes) + +// export const ws = new Server(server, { cors: isDev ? { credentials: true } : undefined }) +// ws.use((_, next) => next(new Error())) + +export const db = new PrismaClient({ log: isDev ? ['query', 'info', 'warn', 'error'] : ['error'] }) +process.on('SIGINT', async () => { + await db.$disconnect() + process.exit() +}) + +scheduleJob('0 1 * * *', async () => { + try { + const deleteRows = await db.$executeRaw`DELETE FROM project WHERE status = 0 AND updated_at < adddate(current_date(), -30)` + logger.info(`Deleted ${deleteRows} expired projects.`) + } catch (err) { + logger.error('Schedule Task Error', '->', err) + } +}) + +server.listen(port) +logger.info(`Server started at ${port}.`) \ No newline at end of file