feat(forum): public read endpoints (boards/threads/detail)

This commit is contained in:
zty 2026-07-07 03:22:29 -04:00
parent 8cfa20d3aa
commit 9048a5a389
2 changed files with 148 additions and 0 deletions

View File

@ -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<string, number> = {}
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()

View File

@ -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}.`)