251 lines
6.7 KiB
TypeScript
251 lines
6.7 KiB
TypeScript
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..N(N 含当前行)。不传表示不移动位置。
|
||
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 }
|
||
)
|
||
}
|
||
}
|