實作 Excel 匯出匯入題目管理
This commit is contained in:
269
app/api/questions/import/route.ts
Normal file
269
app/api/questions/import/route.ts
Normal file
@@ -0,0 +1,269 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import * as XLSX from "xlsx"
|
||||
import {
|
||||
createLogicQuestion,
|
||||
updateLogicQuestion,
|
||||
getAllLogicQuestions,
|
||||
clearLogicQuestions
|
||||
} from "@/lib/database/models/logic_question"
|
||||
import {
|
||||
createCreativeQuestion,
|
||||
updateCreativeQuestion,
|
||||
getAllCreativeQuestions,
|
||||
clearCreativeQuestions
|
||||
} from "@/lib/database/models/creative_question"
|
||||
|
||||
// 定義解析結果介面
|
||||
interface ImportResult {
|
||||
success: boolean
|
||||
message: string
|
||||
data?: any[]
|
||||
errors?: string[]
|
||||
}
|
||||
|
||||
// 解析邏輯題目
|
||||
function parseLogicQuestions(data: any[][]): ImportResult {
|
||||
const errors: string[] = []
|
||||
const questions: any[] = []
|
||||
|
||||
// 跳過標題行
|
||||
for (let i = 1; i < data.length; i++) {
|
||||
const row = data[i]
|
||||
if (!row || row.length < 7) continue
|
||||
|
||||
try {
|
||||
const question = {
|
||||
id: Number.parseInt(row[0]) || i,
|
||||
question: row[1]?.toString() || "",
|
||||
option_a: row[2]?.toString() || "",
|
||||
option_b: row[3]?.toString() || "",
|
||||
option_c: row[4]?.toString() || "",
|
||||
option_d: row[5]?.toString() || "",
|
||||
option_e: row[6]?.toString() || undefined,
|
||||
correct_answer: row[7]?.toString() || "",
|
||||
explanation: row[8]?.toString() || "",
|
||||
}
|
||||
|
||||
// 驗證必填欄位
|
||||
if (
|
||||
!question.question ||
|
||||
!question.option_a ||
|
||||
!question.option_b ||
|
||||
!question.option_c ||
|
||||
!question.option_d ||
|
||||
!question.correct_answer
|
||||
) {
|
||||
errors.push(`第 ${i + 1} 行:缺少必填欄位`)
|
||||
continue
|
||||
}
|
||||
|
||||
// 驗證正確答案格式
|
||||
const validAnswers = question.option_e ? ["A", "B", "C", "D", "E"] : ["A", "B", "C", "D"]
|
||||
if (!validAnswers.includes(question.correct_answer.toUpperCase())) {
|
||||
errors.push(`第 ${i + 1} 行:正確答案必須是 ${validAnswers.join("、")}`)
|
||||
continue
|
||||
}
|
||||
|
||||
questions.push(question)
|
||||
} catch (error) {
|
||||
errors.push(`第 ${i + 1} 行:資料格式錯誤`)
|
||||
}
|
||||
}
|
||||
|
||||
if (questions.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
message: "沒有找到有效的題目資料",
|
||||
errors,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `成功解析 ${questions.length} 道題目`,
|
||||
data: questions,
|
||||
errors: errors.length > 0 ? errors : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
// 解析創意題目
|
||||
function parseCreativeQuestions(data: any[][]): ImportResult {
|
||||
const errors: string[] = []
|
||||
const questions: any[] = []
|
||||
|
||||
// 跳過標題行
|
||||
for (let i = 1; i < data.length; i++) {
|
||||
const row = data[i]
|
||||
if (!row || row.length < 4) continue
|
||||
|
||||
try {
|
||||
const question = {
|
||||
id: Number.parseInt(row[0]) || i,
|
||||
statement: row[1]?.toString() || "",
|
||||
category: (row[2]?.toString().toLowerCase() as any) || "innovation",
|
||||
is_reverse: row[3]?.toString().toLowerCase() === "是" || row[3]?.toString().toLowerCase() === "true",
|
||||
}
|
||||
|
||||
// 驗證必填欄位
|
||||
if (!question.statement) {
|
||||
errors.push(`第 ${i + 1} 行:缺少陳述內容`)
|
||||
continue
|
||||
}
|
||||
|
||||
// 驗證類別
|
||||
const validCategories = ["innovation", "imagination", "flexibility", "originality"]
|
||||
if (!validCategories.includes(question.category)) {
|
||||
errors.push(`第 ${i + 1} 行:類別必須是 innovation、imagination、flexibility 或 originality`)
|
||||
continue
|
||||
}
|
||||
|
||||
questions.push(question)
|
||||
} catch (error) {
|
||||
errors.push(`第 ${i + 1} 行:資料格式錯誤`)
|
||||
}
|
||||
}
|
||||
|
||||
if (questions.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
message: "沒有找到有效的題目資料",
|
||||
errors,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `成功解析 ${questions.length} 道題目`,
|
||||
data: questions,
|
||||
errors: errors.length > 0 ? errors : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const formData = await request.formData()
|
||||
const file = formData.get("file") as File
|
||||
const type = formData.get("type") as "logic" | "creative"
|
||||
|
||||
if (!file || !type) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "缺少檔案或題目類型" },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
console.log(`開始處理 ${type} 題目匯入,檔案大小: ${file.size} bytes`)
|
||||
|
||||
// 讀取檔案內容
|
||||
const arrayBuffer = await file.arrayBuffer()
|
||||
console.log(`檔案讀取完成,大小: ${arrayBuffer.byteLength} bytes`)
|
||||
|
||||
// 根據檔案類型處理
|
||||
let jsonData
|
||||
if (file.name.endsWith('.csv')) {
|
||||
// 處理 CSV 檔案
|
||||
const text = new TextDecoder('utf-8').decode(arrayBuffer)
|
||||
const lines = text.split('\n').filter(line => line.trim())
|
||||
jsonData = lines.map(line => {
|
||||
// 簡單的 CSV 解析(處理引號包圍的欄位)
|
||||
const result = []
|
||||
let current = ''
|
||||
let inQuotes = false
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
const char = line[i]
|
||||
if (char === '"') {
|
||||
inQuotes = !inQuotes
|
||||
} else if (char === ',' && !inQuotes) {
|
||||
result.push(current.trim())
|
||||
current = ''
|
||||
} else {
|
||||
current += char
|
||||
}
|
||||
}
|
||||
result.push(current.trim())
|
||||
return result
|
||||
})
|
||||
} else {
|
||||
// 處理 Excel 檔案
|
||||
const workbook = XLSX.read(arrayBuffer, { type: "array" })
|
||||
const sheetName = workbook.SheetNames[0]
|
||||
const worksheet = workbook.Sheets[sheetName]
|
||||
jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 })
|
||||
}
|
||||
|
||||
console.log(`資料解析完成,共 ${jsonData.length} 行`)
|
||||
|
||||
// 解析資料
|
||||
let result
|
||||
if (type === "logic") {
|
||||
result = parseLogicQuestions(jsonData as any[][])
|
||||
} else {
|
||||
result = parseCreativeQuestions(jsonData as any[][])
|
||||
}
|
||||
|
||||
console.log(`解析結果: ${result.success ? '成功' : '失敗'}, ${result.message}`)
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: result.message,
|
||||
errors: result.errors
|
||||
})
|
||||
}
|
||||
|
||||
if (type === "logic") {
|
||||
const questions = result.data as any[]
|
||||
|
||||
// 清空現有邏輯題目
|
||||
await clearLogicQuestions()
|
||||
|
||||
// 插入新題目
|
||||
for (const question of questions) {
|
||||
await createLogicQuestion({
|
||||
question: question.question,
|
||||
option_a: question.option_a,
|
||||
option_b: question.option_b,
|
||||
option_c: question.option_c,
|
||||
option_d: question.option_d,
|
||||
option_e: question.option_e || null,
|
||||
correct_answer: question.correct_answer,
|
||||
explanation: question.explanation
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `成功匯入 ${questions.length} 道邏輯思維題目`,
|
||||
count: questions.length
|
||||
})
|
||||
|
||||
} else {
|
||||
const questions = result.data as any[]
|
||||
|
||||
// 清空現有創意題目
|
||||
await clearCreativeQuestions()
|
||||
|
||||
// 插入新題目
|
||||
for (const question of questions) {
|
||||
await createCreativeQuestion({
|
||||
statement: question.statement,
|
||||
category: question.category,
|
||||
is_reverse: question.is_reverse
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `成功匯入 ${questions.length} 道創意能力題目`,
|
||||
count: questions.length
|
||||
})
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("匯入題目失敗:", error)
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "匯入失敗,請檢查檔案格式" },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user