feat: 支持批量上传关卡

This commit is contained in:
richarjiang
2026-05-01 08:44:56 +08:00
parent f3f27def2b
commit 66a9ee2950
27 changed files with 5262 additions and 515 deletions

View File

@@ -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..NN 含当前行)。不传表示不移动位置。
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 } : {}),
},
})