283 lines
7.8 KiB
TypeScript
283 lines
7.8 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
|
||
}
|
||
|
||
function parseRiddleId(raw: unknown): string | null {
|
||
if (raw === undefined || raw === null || raw === '') return null
|
||
if (typeof raw !== 'string') {
|
||
throw new Error('riddleId 必须是字符串')
|
||
}
|
||
|
||
const riddleId = raw.trim()
|
||
if (!riddleId) return null
|
||
if (riddleId.length > 128) {
|
||
throw new Error('riddleId 长度不能超过 128')
|
||
}
|
||
return riddleId
|
||
}
|
||
|
||
function optionalString(raw: unknown): string | null {
|
||
return raw ? String(raw) : null
|
||
}
|
||
|
||
function buildLevelData(body: Record<string, unknown>) {
|
||
return {
|
||
image1Url: body.image1Url as string,
|
||
image1Description: optionalString(body.image1Description),
|
||
image2Url: body.image2Url as string,
|
||
image2Description: optionalString(body.image2Description),
|
||
answer: body.answer as string,
|
||
punchline: optionalString(body.punchline),
|
||
hint1: optionalString(body.hint1),
|
||
hint2: optionalString(body.hint2),
|
||
hint3: optionalString(body.hint3),
|
||
}
|
||
}
|
||
|
||
// 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, image2Url, answer } = body
|
||
|
||
if (!image1Url || !image2Url || !answer) {
|
||
return NextResponse.json(
|
||
{ error: 'image1Url, image2Url and answer are required' },
|
||
{ status: 400 }
|
||
)
|
||
}
|
||
|
||
let position: number | undefined
|
||
let riddleId: string | null
|
||
try {
|
||
position = parsePosition(body.position)
|
||
riddleId = parseRiddleId(body.riddleId)
|
||
} catch (e) {
|
||
return NextResponse.json(
|
||
{ error: e instanceof Error ? e.message : 'invalid level payload' },
|
||
{ status: 400 }
|
||
)
|
||
}
|
||
|
||
const data = buildLevelData(body)
|
||
|
||
if (riddleId) {
|
||
const existing = await prisma.level.findUnique({
|
||
where: { riddleId },
|
||
})
|
||
|
||
if (existing) {
|
||
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, existing.id)
|
||
}
|
||
|
||
const level = await prisma.level.update({
|
||
where: { id: existing.id },
|
||
data: {
|
||
...data,
|
||
...(newSortKey ? { sortKey: newSortKey } : {}),
|
||
},
|
||
})
|
||
|
||
return NextResponse.json(level)
|
||
}
|
||
}
|
||
|
||
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(),
|
||
riddleId,
|
||
...data,
|
||
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 } = 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 data = buildLevelData(body)
|
||
|
||
const level = await prisma.level.update({
|
||
where: { id },
|
||
data: {
|
||
...data,
|
||
...(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 }
|
||
)
|
||
}
|
||
}
|