Files
MemeStudio/app/api/levels/route.ts
2026-05-01 08:44:56 +08:00

251 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { auth } from '@/lib/auth'
import { compareSortKey, keyAfterLast, keyForPosition } from '@/lib/sort-key'
import { v4 as uuidv4 } from 'uuid'
// GET /api/levels - Get all levels
export async function GET(request: NextRequest) {
try {
const session = await auth.api.getSession({
headers: request.headers,
})
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// 注意:不能交给 MySQL 排序。默认 collation 大小写不敏感,
// 会把 fractional-indexing 的 Z 系列 key 错排到 z 系列之后。
// 拉全表到应用层按字节序排。
const rows = await prisma.level.findMany()
rows.sort((a, b) => {
const c = compareSortKey(a.sortKey, b.sortKey)
if (c !== 0) return c
if (a.sortOrder !== b.sortOrder) return a.sortOrder - b.sortOrder
return a.createdAt.getTime() - b.createdAt.getTime()
})
// 响应层回填连续整数 sortOrder保持前端 / 小程序对这个字段的既有依赖
const levels = rows.map((r, i) => ({ ...r, sortOrder: i }))
return NextResponse.json(levels)
} catch (error) {
console.error('Error fetching levels:', error)
return NextResponse.json(
{ error: 'Failed to fetch levels' },
{ status: 500 }
)
}
}
/**
* 解析 position
* - undefined / null → 返回 undefined交给调用方走"默认行为"
* - 数字字符串 → 解析成整数
* - 其它 → 抛错
*
* 合法范围的校验由调用方根据当前上下文(创建 / 编辑)判断。
*/
function parsePosition(raw: unknown): number | undefined {
if (raw === undefined || raw === null || raw === '') return undefined
const n = typeof raw === 'number' ? raw : Number(raw)
if (!Number.isInteger(n) || n < 1) {
throw new Error('position 必须是 >= 1 的整数')
}
return n
}
// POST /api/levels - Create a new level
// body.position 可选1..N+1不传则追加末尾
export async function POST(request: NextRequest) {
try {
const session = await auth.api.getSession({
headers: request.headers,
})
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const {
image1Url,
image1Description,
image2Url,
image2Description,
answer,
punchline,
hint1,
hint2,
hint3,
} = body
if (!image1Url || !image2Url || !answer) {
return NextResponse.json(
{ error: 'image1Url, image2Url and answer are required' },
{ status: 400 }
)
}
let position: number | undefined
try {
position = parsePosition(body.position)
} catch (e) {
return NextResponse.json(
{ error: e instanceof Error ? e.message : 'invalid position' },
{ status: 400 }
)
}
let sortKey: string
if (position === undefined) {
sortKey = await keyAfterLast()
} else {
const total = await prisma.level.count()
if (position > total + 1) {
return NextResponse.json(
{ error: `position 超出范围,合法区间 [1, ${total + 1}]` },
{ status: 400 }
)
}
sortKey = await keyForPosition(position)
}
const level = await prisma.level.create({
data: {
id: uuidv4(),
image1Url,
image1Description: image1Description || null,
image2Url,
image2Description: image2Description || null,
answer,
punchline: punchline || null,
hint1: hint1 || null,
hint2: hint2 || null,
hint3: hint3 || null,
sortKey,
},
})
return NextResponse.json(level, { status: 201 })
} catch (error) {
console.error('Error creating level:', error)
return NextResponse.json(
{ error: 'Failed to create level' },
{ status: 500 }
)
}
}
// PUT /api/levels - Update a level
// body.position 可选1..NN 含当前行)。不传表示不移动位置。
export async function PUT(request: NextRequest) {
try {
const session = await auth.api.getSession({
headers: request.headers,
})
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const {
id,
image1Url,
image1Description,
image2Url,
image2Description,
answer,
punchline,
hint1,
hint2,
hint3,
} = body
if (!id) {
return NextResponse.json({ error: 'id is required' }, { status: 400 })
}
let position: number | undefined
try {
position = parsePosition(body.position)
} catch (e) {
return NextResponse.json(
{ error: e instanceof Error ? e.message : 'invalid position' },
{ status: 400 }
)
}
// 只有在 position 确实变化时才重算 sortKey避免无谓写入
let newSortKey: string | undefined
if (position !== undefined) {
const total = await prisma.level.count()
if (position > total) {
return NextResponse.json(
{ error: `position 超出范围,合法区间 [1, ${total}]` },
{ status: 400 }
)
}
newSortKey = await keyForPosition(position, id)
}
const level = await prisma.level.update({
where: { id },
data: {
image1Url,
image1Description: image1Description || null,
image2Url,
image2Description: image2Description || null,
answer,
punchline: punchline || null,
hint1: hint1 || null,
hint2: hint2 || null,
hint3: hint3 || null,
...(newSortKey ? { sortKey: newSortKey } : {}),
},
})
return NextResponse.json(level)
} catch (error) {
console.error('Error updating level:', error)
return NextResponse.json(
{ error: 'Failed to update level' },
{ status: 500 }
)
}
}
// DELETE /api/levels - Delete a level
export async function DELETE(request: NextRequest) {
try {
const session = await auth.api.getSession({
headers: request.headers,
})
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { searchParams } = new URL(request.url)
const id = searchParams.get('id')
if (!id) {
return NextResponse.json({ error: 'id is required' }, { status: 400 })
}
await prisma.level.delete({
where: { id },
})
return NextResponse.json({ success: true })
} catch (error) {
console.error('Error deleting level:', error)
return NextResponse.json(
{ error: 'Failed to delete level' },
{ status: 500 }
)
}
}