feat: 支持批量上传关卡
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
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
|
||||
@@ -14,10 +15,20 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const levels = await prisma.level.findMany({
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
// 注意:不能交给 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)
|
||||
@@ -28,7 +39,25 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 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({
|
||||
@@ -40,7 +69,17 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { image1Url, image1Description, image2Url, image2Description, answer, punchline, hint1, hint2, hint3 } = body
|
||||
const {
|
||||
image1Url,
|
||||
image1Description,
|
||||
image2Url,
|
||||
image2Description,
|
||||
answer,
|
||||
punchline,
|
||||
hint1,
|
||||
hint2,
|
||||
hint3,
|
||||
} = body
|
||||
|
||||
if (!image1Url || !image2Url || !answer) {
|
||||
return NextResponse.json(
|
||||
@@ -49,12 +88,29 @@ export async function POST(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
// Get max sort order
|
||||
const maxSortOrder = await prisma.level.aggregate({
|
||||
_max: { sortOrder: true },
|
||||
})
|
||||
let position: number | undefined
|
||||
try {
|
||||
position = parsePosition(body.position)
|
||||
} catch (e) {
|
||||
return NextResponse.json(
|
||||
{ error: e instanceof Error ? e.message : 'invalid position' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const sortOrder = (maxSortOrder._max.sortOrder || 0) + 1
|
||||
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: {
|
||||
@@ -68,7 +124,7 @@ export async function POST(request: NextRequest) {
|
||||
hint1: hint1 || null,
|
||||
hint2: hint2 || null,
|
||||
hint3: hint3 || null,
|
||||
sortOrder,
|
||||
sortKey,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -83,6 +139,7 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
// PUT /api/levels - Update a level
|
||||
// body.position 可选:1..N(N 含当前行)。不传表示不移动位置。
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth.api.getSession({
|
||||
@@ -94,12 +151,46 @@ export async function PUT(request: NextRequest) {
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { id, image1Url, image1Description, image2Url, image2Description, answer, punchline, hint1, hint2, hint3 } = body
|
||||
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: {
|
||||
@@ -112,6 +203,7 @@ export async function PUT(request: NextRequest) {
|
||||
hint1: hint1 || null,
|
||||
hint2: hint2 || null,
|
||||
hint3: hint3 || null,
|
||||
...(newSortKey ? { sortKey: newSortKey } : {}),
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user