新增評分項目設定、資料庫整合
This commit is contained in:
166
app/api/criteria-templates/[id]/route.ts
Normal file
166
app/api/criteria-templates/[id]/route.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { CriteriaTemplateService, CriteriaItemService } from '@/lib/services/database';
|
||||
|
||||
// GET - 獲取特定評分標準模板
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const templateId = parseInt(params.id);
|
||||
|
||||
if (isNaN(templateId)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '無效的模板 ID' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const template = await CriteriaTemplateService.findWithItems(templateId);
|
||||
|
||||
if (!template) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '找不到指定的評分標準模板' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: template
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('獲取評分標準模板失敗:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '獲取評分標準模板失敗' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// PUT - 更新評分標準模板
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const templateId = parseInt(params.id);
|
||||
|
||||
if (isNaN(templateId)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '無效的模板 ID' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { name, description, items } = body;
|
||||
|
||||
if (!name || !items || !Array.isArray(items)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '請提供模板名稱和評分項目' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 驗證權重總和
|
||||
const totalWeight = items.reduce((sum: number, item: any) => sum + (item.weight || 0), 0);
|
||||
if (Math.abs(totalWeight - 100) > 0.01) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '權重總和必須等於 100%' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 驗證所有項目都有名稱
|
||||
if (items.some((item: any) => !item.name?.trim())) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '所有評分項目都必須有名稱' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 使用事務更新模板和項目
|
||||
const { transaction } = await import('@/lib/database');
|
||||
|
||||
await transaction(async (connection) => {
|
||||
// 更新模板
|
||||
await connection.execute(
|
||||
'UPDATE criteria_templates SET name = ?, description = ?, total_weight = ?, updated_at = NOW() WHERE id = ?',
|
||||
[name, description || '', totalWeight, templateId]
|
||||
);
|
||||
|
||||
// 刪除舊的評分項目
|
||||
await connection.execute(
|
||||
'DELETE FROM criteria_items WHERE template_id = ?',
|
||||
[templateId]
|
||||
);
|
||||
|
||||
// 創建新的評分項目
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
await connection.execute(
|
||||
'INSERT INTO criteria_items (template_id, name, description, weight, max_score, sort_order) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
[
|
||||
templateId,
|
||||
item.name,
|
||||
item.description || '',
|
||||
item.weight,
|
||||
item.maxScore || 10,
|
||||
i + 1
|
||||
]
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '評分標準模板更新成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('更新評分標準模板失敗:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '更新評分標準模板失敗' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE - 刪除評分標準模板
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const templateId = parseInt(params.id);
|
||||
|
||||
if (isNaN(templateId)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '無效的模板 ID' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 檢查是否為預設模板
|
||||
const template = await CriteriaTemplateService.findById(templateId);
|
||||
if (template?.is_default) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '無法刪除預設模板' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
await CriteriaTemplateService.delete(templateId);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '評分標準模板刪除成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('刪除評分標準模板失敗:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '刪除評分標準模板失敗' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
29
app/api/criteria-templates/default/route.ts
Normal file
29
app/api/criteria-templates/default/route.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { CriteriaTemplateService } from '@/lib/services/database';
|
||||
|
||||
// GET - 獲取預設評分標準模板
|
||||
export async function GET() {
|
||||
try {
|
||||
const template = await CriteriaTemplateService.findDefault();
|
||||
|
||||
if (!template) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '找不到預設評分標準模板' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
const templateWithItems = await CriteriaTemplateService.findWithItems(template.id);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: templateWithItems
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('獲取預設評分標準模板失敗:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '獲取預設評分標準模板失敗' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
126
app/api/criteria-templates/route.ts
Normal file
126
app/api/criteria-templates/route.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { CriteriaTemplateService, CriteriaItemService } from '@/lib/services/database';
|
||||
|
||||
// GET - 獲取所有評分標準模板
|
||||
export async function GET() {
|
||||
try {
|
||||
// 這裡暫時使用 user_id = 1,實際應用中應該從 session 獲取
|
||||
const templates = await CriteriaTemplateService.findByUserId(1);
|
||||
|
||||
// 獲取每個模板的評分項目
|
||||
const templatesWithItems = await Promise.all(
|
||||
templates.map(async (template) => {
|
||||
const items = await CriteriaItemService.findByTemplateId(template.id);
|
||||
return { ...template, items };
|
||||
})
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: templatesWithItems
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('獲取評分標準模板失敗:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '獲取評分標準模板失敗' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// POST - 創建或更新評分標準模板(單一模板模式)
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { name, description, items } = body;
|
||||
|
||||
if (!name || !items || !Array.isArray(items)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '請提供模板名稱和評分項目' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 驗證權重總和
|
||||
const totalWeight = items.reduce((sum: number, item: any) => sum + (Number(item.weight) || 0), 0);
|
||||
if (Math.abs(totalWeight - 100) > 0.01) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '權重總和必須等於 100%' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 驗證所有項目都有名稱
|
||||
if (items.some((item: any) => !item.name?.trim())) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '所有評分項目都必須有名稱' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 使用事務創建或更新模板
|
||||
const { transaction } = await import('@/lib/database');
|
||||
|
||||
const result = await transaction(async (connection) => {
|
||||
// 檢查是否已存在模板
|
||||
const [existingTemplates] = await connection.execute(
|
||||
'SELECT id FROM criteria_templates WHERE user_id = ? LIMIT 1',
|
||||
[1]
|
||||
);
|
||||
|
||||
let templateId;
|
||||
|
||||
if (existingTemplates.length > 0) {
|
||||
// 更新現有模板
|
||||
templateId = existingTemplates[0].id;
|
||||
await connection.execute(
|
||||
'UPDATE criteria_templates SET name = ?, description = ?, total_weight = ?, updated_at = NOW() WHERE id = ?',
|
||||
[name, description || '', totalWeight, templateId]
|
||||
);
|
||||
|
||||
// 刪除舊的評分項目
|
||||
await connection.execute(
|
||||
'DELETE FROM criteria_items WHERE template_id = ?',
|
||||
[templateId]
|
||||
);
|
||||
} else {
|
||||
// 創建新模板
|
||||
const [templateResult] = await connection.execute(
|
||||
'INSERT INTO criteria_templates (user_id, name, description, is_default, is_public, total_weight) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
[1, name, description || '', 1, 1, totalWeight]
|
||||
);
|
||||
templateId = (templateResult as any).insertId;
|
||||
}
|
||||
|
||||
// 創建評分項目
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
await connection.execute(
|
||||
'INSERT INTO criteria_items (template_id, name, description, weight, max_score, sort_order) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
[
|
||||
templateId,
|
||||
item.name,
|
||||
item.description || '',
|
||||
Number(item.weight) || 0,
|
||||
Number(item.maxScore) || 10,
|
||||
i + 1
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
return templateId;
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: { id: result },
|
||||
message: '評分標準模板儲存成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('儲存評分標準模板失敗:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '儲存評分標準模板失敗' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useState, useEffect } from "react"
|
||||
import { Sidebar } from "@/components/sidebar"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
@@ -9,60 +9,93 @@ import { Label } from "@/components/ui/label"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Slider } from "@/components/ui/slider"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Plus, Trash2, Save, RotateCcw, FileText } from "lucide-react"
|
||||
import { Plus, Trash2, Save, RotateCcw, FileText, Loader2 } from "lucide-react"
|
||||
import { useToast } from "@/hooks/use-toast"
|
||||
|
||||
interface CriteriaItem {
|
||||
id: string
|
||||
id: number | string
|
||||
name: string
|
||||
description: string
|
||||
weight: number
|
||||
maxScore: number
|
||||
}
|
||||
|
||||
const defaultCriteria: CriteriaItem[] = [
|
||||
{
|
||||
id: "1",
|
||||
name: "內容品質",
|
||||
description: "內容的準確性、完整性和專業度",
|
||||
weight: 25,
|
||||
maxScore: 10,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "視覺設計",
|
||||
description: "版面設計、色彩搭配和視覺效果",
|
||||
weight: 20,
|
||||
maxScore: 10,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "邏輯結構",
|
||||
description: "內容組織的邏輯性和條理性",
|
||||
weight: 20,
|
||||
maxScore: 10,
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "創新性",
|
||||
description: "創意思維和獨特觀點的展現",
|
||||
weight: 15,
|
||||
maxScore: 10,
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
name: "實用性",
|
||||
description: "內容的實際應用價值和可操作性",
|
||||
weight: 20,
|
||||
maxScore: 10,
|
||||
},
|
||||
]
|
||||
interface CriteriaTemplate {
|
||||
id: number
|
||||
name: string
|
||||
description: string
|
||||
total_weight: number
|
||||
items: CriteriaItem[]
|
||||
}
|
||||
|
||||
export default function CriteriaPage() {
|
||||
const [criteria, setCriteria] = useState<CriteriaItem[]>(defaultCriteria)
|
||||
const [templateName, setTemplateName] = useState("預設評分標準")
|
||||
const [criteria, setCriteria] = useState<CriteriaItem[]>([])
|
||||
const [templateName, setTemplateName] = useState("")
|
||||
const [templateDescription, setTemplateDescription] = useState("")
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [currentTemplate, setCurrentTemplate] = useState<CriteriaTemplate | null>(null)
|
||||
const { toast } = useToast()
|
||||
|
||||
// 載入評分標準
|
||||
useEffect(() => {
|
||||
loadTemplate()
|
||||
}, [])
|
||||
|
||||
const loadTemplate = async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const response = await fetch('/api/criteria-templates')
|
||||
const result = await response.json()
|
||||
|
||||
if (result.success && result.data && result.data.length > 0) {
|
||||
// 載入第一個模板(單一模板模式)
|
||||
const template = result.data[0]
|
||||
setCurrentTemplate(template)
|
||||
setTemplateName(template.name)
|
||||
setTemplateDescription(template.description || '')
|
||||
setCriteria(template.items || [])
|
||||
} else {
|
||||
// 如果沒有模板,載入預設模板
|
||||
await loadDefaultTemplate()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('載入評分標準失敗:', error)
|
||||
// 如果載入失敗,嘗試載入預設模板
|
||||
await loadDefaultTemplate()
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const loadDefaultTemplate = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/criteria-templates/default')
|
||||
const result = await response.json()
|
||||
|
||||
if (result.success && result.data) {
|
||||
const template = result.data
|
||||
setCurrentTemplate(template)
|
||||
setTemplateName(template.name)
|
||||
setTemplateDescription(template.description || '')
|
||||
setCriteria(template.items || [])
|
||||
} else {
|
||||
// 如果連預設模板都沒有,使用空模板
|
||||
setCurrentTemplate(null)
|
||||
setTemplateName('')
|
||||
setTemplateDescription('')
|
||||
setCriteria([])
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('載入預設評分標準失敗:', error)
|
||||
// 使用空模板
|
||||
setCurrentTemplate(null)
|
||||
setTemplateName('')
|
||||
setTemplateDescription('')
|
||||
setCriteria([])
|
||||
}
|
||||
}
|
||||
|
||||
const addCriteria = () => {
|
||||
const newCriteria: CriteriaItem = {
|
||||
id: Date.now().toString(),
|
||||
@@ -83,12 +116,12 @@ export default function CriteriaPage() {
|
||||
}
|
||||
|
||||
const updateWeight = (id: string, weight: number[]) => {
|
||||
updateCriteria(id, "weight", weight[0])
|
||||
updateCriteria(id, "weight", Number(weight[0]) || 0)
|
||||
}
|
||||
|
||||
const totalWeight = criteria.reduce((sum, item) => sum + item.weight, 0)
|
||||
const totalWeight = criteria.reduce((sum, item) => sum + (Number(item.weight) || 0), 0)
|
||||
|
||||
const saveCriteria = () => {
|
||||
const saveCriteria = async () => {
|
||||
if (totalWeight !== 100) {
|
||||
toast({
|
||||
title: "權重設定錯誤",
|
||||
@@ -107,20 +140,98 @@ export default function CriteriaPage() {
|
||||
return
|
||||
}
|
||||
|
||||
// 這裡會連接到後端 API 儲存評分標準
|
||||
if (!templateName.trim()) {
|
||||
toast({
|
||||
title: "設定不完整",
|
||||
description: "請填寫模板名稱",
|
||||
variant: "destructive",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSaving(true)
|
||||
|
||||
const templateData = {
|
||||
name: templateName.trim(),
|
||||
description: templateDescription.trim(),
|
||||
items: criteria.map(item => ({
|
||||
name: item.name.trim(),
|
||||
description: item.description.trim(),
|
||||
weight: item.weight,
|
||||
maxScore: item.maxScore
|
||||
}))
|
||||
}
|
||||
|
||||
// 單一模板模式,總是使用 POST 進行覆蓋
|
||||
const url = '/api/criteria-templates'
|
||||
const method = 'POST'
|
||||
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(templateData),
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (result.success) {
|
||||
toast({
|
||||
title: "儲存成功",
|
||||
description: "評分標準已成功儲存",
|
||||
})
|
||||
|
||||
// 重新載入資料
|
||||
await loadTemplate()
|
||||
} else {
|
||||
throw new Error(result.error || '儲存失敗')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('儲存評分標準失敗:', error)
|
||||
toast({
|
||||
title: "儲存失敗",
|
||||
description: error instanceof Error ? error.message : "無法儲存評分標準",
|
||||
variant: "destructive",
|
||||
})
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const resetToDefault = () => {
|
||||
setCriteria(defaultCriteria)
|
||||
setTemplateName("預設評分標準")
|
||||
const resetToDefault = async () => {
|
||||
try {
|
||||
await loadDefaultTemplate()
|
||||
toast({
|
||||
title: "已重置",
|
||||
description: "評分標準已重置為預設值",
|
||||
})
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "重置失敗",
|
||||
description: "無法重置評分標準",
|
||||
variant: "destructive",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Sidebar />
|
||||
<main className="md:ml-64 p-6">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="text-center">
|
||||
<Loader2 className="h-8 w-8 animate-spin mx-auto mb-4" />
|
||||
<p className="text-muted-foreground">載入評分標準中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -140,16 +251,29 @@ export default function CriteriaPage() {
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<FileText className="h-5 w-5" />
|
||||
標準模板名稱
|
||||
標準模板資訊
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="template-name" className="mb-2 block">模板名稱 *</Label>
|
||||
<Input
|
||||
id="template-name"
|
||||
value={templateName}
|
||||
onChange={(e) => setTemplateName(e.target.value)}
|
||||
placeholder="輸入評分標準名稱"
|
||||
className="max-w-md"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="template-description" className="mb-2 block">模板描述</Label>
|
||||
<Textarea
|
||||
id="template-description"
|
||||
value={templateDescription}
|
||||
onChange={(e) => setTemplateDescription(e.target.value)}
|
||||
placeholder="輸入模板描述(選填)"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -158,7 +282,9 @@ export default function CriteriaPage() {
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<span className="text-sm font-medium">權重總計</span>
|
||||
<Badge variant={totalWeight === 100 ? "default" : "destructive"}>{totalWeight}%</Badge>
|
||||
<Badge variant={totalWeight === 100 ? "default" : "destructive"}>
|
||||
{Number(totalWeight).toFixed(1)}%
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="w-full bg-muted rounded-full h-2">
|
||||
<div
|
||||
@@ -190,7 +316,7 @@ export default function CriteriaPage() {
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor={`name-${item.id}`}>項目名稱</Label>
|
||||
<Label htmlFor={`name-${item.id}`} className="mb-2 block">項目名稱</Label>
|
||||
<Input
|
||||
id={`name-${item.id}`}
|
||||
value={item.name}
|
||||
@@ -199,7 +325,7 @@ export default function CriteriaPage() {
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor={`maxScore-${item.id}`}>滿分</Label>
|
||||
<Label htmlFor={`maxScore-${item.id}`} className="mb-2 block">滿分</Label>
|
||||
<Input
|
||||
id={`maxScore-${item.id}`}
|
||||
type="number"
|
||||
@@ -212,7 +338,7 @@ export default function CriteriaPage() {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor={`description-${item.id}`}>評分說明</Label>
|
||||
<Label htmlFor={`description-${item.id}`} className="mb-2 block">評分說明</Label>
|
||||
<Textarea
|
||||
id={`description-${item.id}`}
|
||||
value={item.description}
|
||||
@@ -225,10 +351,10 @@ export default function CriteriaPage() {
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Label>權重比例</Label>
|
||||
<Badge variant="outline">{item.weight}%</Badge>
|
||||
<Badge variant="outline">{Number(item.weight).toFixed(1)}%</Badge>
|
||||
</div>
|
||||
<Slider
|
||||
value={[item.weight]}
|
||||
value={[Number(item.weight) || 0]}
|
||||
onValueChange={(value) => updateWeight(item.id, value)}
|
||||
max={100}
|
||||
min={0}
|
||||
@@ -253,11 +379,20 @@ export default function CriteriaPage() {
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<Button onClick={saveCriteria} className="flex-1">
|
||||
<Button onClick={saveCriteria} disabled={isSaving || isLoading} className="flex-1">
|
||||
{isSaving ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
儲存中...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
儲存評分標準
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button onClick={resetToDefault} variant="outline">
|
||||
<Button onClick={resetToDefault} variant="outline" disabled={isLoading || isSaving}>
|
||||
<RotateCcw className="h-4 w-4 mr-2" />
|
||||
重置為預設
|
||||
</Button>
|
||||
@@ -278,7 +413,7 @@ export default function CriteriaPage() {
|
||||
{item.description && <div className="text-sm text-muted-foreground mt-1">{item.description}</div>}
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="font-medium">權重: {item.weight}%</div>
|
||||
<div className="font-medium">權重: {Number(item.weight).toFixed(1)}%</div>
|
||||
<div className="text-sm text-muted-foreground">滿分: {item.maxScore}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-border flex h-9 w-full min-w-0 rounded-md border-2 bg-background px-3 py-1 text-base shadow-sm transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
|
||||
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
||||
className,
|
||||
|
@@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
'border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
'border-border placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border-2 bg-background px-3 py-2 text-base shadow-sm transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
78
database/README.md
Normal file
78
database/README.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# 資料庫配置說明
|
||||
|
||||
## 資料庫資訊
|
||||
- **主機**: mysql.theaken.com
|
||||
- **端口**: 33306
|
||||
- **資料庫名稱**: db_AI_scoring
|
||||
- **用戶名**: root
|
||||
- **密碼**: zh6161168
|
||||
|
||||
## 環境變數設定
|
||||
|
||||
請在專案根目錄建立 `.env.local` 檔案,並加入以下配置:
|
||||
|
||||
```env
|
||||
# 資料庫配置
|
||||
DB_HOST=mysql.theaken.com
|
||||
DB_PORT=33306
|
||||
DB_NAME=db_AI_scoring
|
||||
DB_USER=root
|
||||
DB_PASSWORD=zh6161168
|
||||
|
||||
# 應用程式配置
|
||||
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
||||
NEXTAUTH_URL=http://localhost:3000
|
||||
NEXTAUTH_SECRET=your-secret-key-here
|
||||
|
||||
# 文件上傳配置
|
||||
UPLOAD_DIR=./uploads
|
||||
MAX_FILE_SIZE=104857600
|
||||
ALLOWED_FILE_TYPES=ppt,pptx,pdf,mp4,avi,mov,wmv,flv,webm
|
||||
```
|
||||
|
||||
## 初始化資料庫
|
||||
|
||||
1. 安裝依賴:
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
2. 初始化資料庫:
|
||||
```bash
|
||||
pnpm run db:init
|
||||
```
|
||||
|
||||
3. 測試資料庫連接:
|
||||
```bash
|
||||
pnpm run db:test
|
||||
```
|
||||
|
||||
## 資料表結構
|
||||
|
||||
### 核心資料表
|
||||
- `users` - 用戶管理
|
||||
- `criteria_templates` - 評分標準模板
|
||||
- `criteria_items` - 評分項目明細
|
||||
- `projects` - 評審專案
|
||||
- `project_files` - 專案文件
|
||||
- `project_websites` - 專案網站連結
|
||||
- `evaluations` - 評審記錄
|
||||
- `evaluation_scores` - 評分結果明細
|
||||
- `evaluation_feedback` - AI 評語和建議
|
||||
- `system_settings` - 系統設定
|
||||
|
||||
### 關聯關係
|
||||
- 用戶 → 多個評分標準模板
|
||||
- 評分標準模板 → 多個評分項目
|
||||
- 用戶 → 多個專案
|
||||
- 專案 → 多個文件/網站連結
|
||||
- 專案 → 一個評審記錄
|
||||
- 評審記錄 → 多個評分結果
|
||||
- 評審記錄 → 多個評語建議
|
||||
|
||||
## 預設數據
|
||||
|
||||
系統會自動建立:
|
||||
- 預設評分標準模板
|
||||
- 5個預設評分項目(內容品質、視覺設計、邏輯結構、創新性、實用性)
|
||||
- 系統設定參數
|
233
database/schema.sql
Normal file
233
database/schema.sql
Normal file
@@ -0,0 +1,233 @@
|
||||
-- AI 評審系統資料庫架構
|
||||
-- 資料庫: db_AI_scoring
|
||||
|
||||
-- 建立資料庫 (如果不存在)
|
||||
CREATE DATABASE IF NOT EXISTS `db_AI_scoring`
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE `db_AI_scoring`;
|
||||
|
||||
-- 1. 用戶表
|
||||
CREATE TABLE `users` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`email` varchar(255) NOT NULL UNIQUE,
|
||||
`username` varchar(100) NOT NULL UNIQUE,
|
||||
`password_hash` varchar(255) NOT NULL,
|
||||
`full_name` varchar(255) DEFAULT NULL,
|
||||
`avatar_url` varchar(500) DEFAULT NULL,
|
||||
`role` enum('admin', 'user') DEFAULT 'user',
|
||||
`is_active` tinyint(1) DEFAULT 1,
|
||||
`email_verified_at` timestamp NULL DEFAULT NULL,
|
||||
`last_login_at` timestamp NULL DEFAULT NULL,
|
||||
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_email` (`email`),
|
||||
KEY `idx_username` (`username`),
|
||||
KEY `idx_created_at` (`created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 2. 用戶會話表
|
||||
CREATE TABLE `user_sessions` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`user_id` bigint(20) unsigned NOT NULL,
|
||||
`session_token` varchar(255) NOT NULL UNIQUE,
|
||||
`ip_address` varchar(45) DEFAULT NULL,
|
||||
`user_agent` text DEFAULT NULL,
|
||||
`expires_at` timestamp NOT NULL,
|
||||
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_session_token` (`session_token`),
|
||||
KEY `idx_expires_at` (`expires_at`),
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 3. 評分標準模板表
|
||||
CREATE TABLE `criteria_templates` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`user_id` bigint(20) unsigned NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`description` text DEFAULT NULL,
|
||||
`is_default` tinyint(1) DEFAULT 0,
|
||||
`is_public` tinyint(1) DEFAULT 0,
|
||||
`total_weight` decimal(5,2) DEFAULT 100.00,
|
||||
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_is_default` (`is_default`),
|
||||
KEY `idx_is_public` (`is_public`),
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 4. 評分項目表
|
||||
CREATE TABLE `criteria_items` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`template_id` bigint(20) unsigned NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`description` text DEFAULT NULL,
|
||||
`weight` decimal(5,2) NOT NULL,
|
||||
`max_score` decimal(5,2) DEFAULT 10.00,
|
||||
`sort_order` int(11) DEFAULT 0,
|
||||
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_template_id` (`template_id`),
|
||||
KEY `idx_sort_order` (`sort_order`),
|
||||
FOREIGN KEY (`template_id`) REFERENCES `criteria_templates`(`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 5. 評審專案表
|
||||
CREATE TABLE `projects` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`user_id` bigint(20) unsigned NOT NULL,
|
||||
`template_id` bigint(20) unsigned NOT NULL,
|
||||
`title` varchar(255) NOT NULL,
|
||||
`description` text DEFAULT NULL,
|
||||
`status` enum('draft', 'uploading', 'analyzing', 'completed', 'failed') DEFAULT 'draft',
|
||||
`analysis_started_at` timestamp NULL DEFAULT NULL,
|
||||
`analysis_completed_at` timestamp NULL DEFAULT NULL,
|
||||
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_template_id` (`template_id`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_created_at` (`created_at`),
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
|
||||
FOREIGN KEY (`template_id`) REFERENCES `criteria_templates`(`id`) ON DELETE RESTRICT
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 6. 專案文件表
|
||||
CREATE TABLE `project_files` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`project_id` bigint(20) unsigned NOT NULL,
|
||||
`original_name` varchar(255) NOT NULL,
|
||||
`file_name` varchar(255) NOT NULL,
|
||||
`file_path` varchar(500) NOT NULL,
|
||||
`file_size` bigint(20) unsigned NOT NULL,
|
||||
`file_type` varchar(100) NOT NULL,
|
||||
`mime_type` varchar(100) NOT NULL,
|
||||
`upload_status` enum('uploading', 'completed', 'failed') DEFAULT 'uploading',
|
||||
`upload_progress` decimal(5,2) DEFAULT 0.00,
|
||||
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_project_id` (`project_id`),
|
||||
KEY `idx_file_type` (`file_type`),
|
||||
KEY `idx_upload_status` (`upload_status`),
|
||||
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 7. 專案網站連結表
|
||||
CREATE TABLE `project_websites` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`project_id` bigint(20) unsigned NOT NULL,
|
||||
`url` varchar(500) NOT NULL,
|
||||
`title` varchar(255) DEFAULT NULL,
|
||||
`description` text DEFAULT NULL,
|
||||
`status` enum('pending', 'analyzing', 'completed', 'failed') DEFAULT 'pending',
|
||||
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_project_id` (`project_id`),
|
||||
KEY `idx_status` (`status`),
|
||||
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 8. 評審記錄表
|
||||
CREATE TABLE `evaluations` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`project_id` bigint(20) unsigned NOT NULL,
|
||||
`overall_score` decimal(5,2) DEFAULT NULL,
|
||||
`max_possible_score` decimal(5,2) DEFAULT 100.00,
|
||||
`grade` varchar(10) DEFAULT NULL,
|
||||
`analysis_duration` int(11) DEFAULT NULL COMMENT '分析耗時(秒)',
|
||||
`ai_model_version` varchar(50) DEFAULT NULL,
|
||||
`status` enum('pending', 'analyzing', 'completed', 'failed') DEFAULT 'pending',
|
||||
`error_message` text DEFAULT NULL,
|
||||
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_project_id` (`project_id`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_created_at` (`created_at`),
|
||||
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 9. 評分結果明細表
|
||||
CREATE TABLE `evaluation_scores` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`evaluation_id` bigint(20) unsigned NOT NULL,
|
||||
`criteria_item_id` bigint(20) unsigned NOT NULL,
|
||||
`score` decimal(5,2) NOT NULL,
|
||||
`max_score` decimal(5,2) NOT NULL,
|
||||
`weight` decimal(5,2) NOT NULL,
|
||||
`weighted_score` decimal(5,2) NOT NULL,
|
||||
`percentage` decimal(5,2) NOT NULL,
|
||||
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_evaluation_id` (`evaluation_id`),
|
||||
KEY `idx_criteria_item_id` (`criteria_item_id`),
|
||||
UNIQUE KEY `unique_evaluation_criteria` (`evaluation_id`, `criteria_item_id`),
|
||||
FOREIGN KEY (`evaluation_id`) REFERENCES `evaluations`(`id`) ON DELETE CASCADE,
|
||||
FOREIGN KEY (`criteria_item_id`) REFERENCES `criteria_items`(`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 10. AI 評語和建議表
|
||||
CREATE TABLE `evaluation_feedback` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`evaluation_id` bigint(20) unsigned NOT NULL,
|
||||
`criteria_item_id` bigint(20) unsigned DEFAULT NULL,
|
||||
`feedback_type` enum('overall', 'criteria', 'strength', 'improvement') NOT NULL,
|
||||
`content` text NOT NULL,
|
||||
`sort_order` int(11) DEFAULT 0,
|
||||
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_evaluation_id` (`evaluation_id`),
|
||||
KEY `idx_criteria_item_id` (`criteria_item_id`),
|
||||
KEY `idx_feedback_type` (`feedback_type`),
|
||||
FOREIGN KEY (`evaluation_id`) REFERENCES `evaluations`(`id`) ON DELETE CASCADE,
|
||||
FOREIGN KEY (`criteria_item_id`) REFERENCES `criteria_items`(`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 11. 系統設定表
|
||||
CREATE TABLE `system_settings` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`setting_key` varchar(100) NOT NULL UNIQUE,
|
||||
`setting_value` text DEFAULT NULL,
|
||||
`description` varchar(255) DEFAULT NULL,
|
||||
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_setting_key` (`setting_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 插入預設評分標準模板
|
||||
INSERT INTO `criteria_templates` (`user_id`, `name`, `description`, `is_default`, `is_public`, `total_weight`)
|
||||
VALUES (1, '預設評分標準', '系統預設的評分標準模板', 1, 1, 100.00);
|
||||
|
||||
-- 插入預設評分項目
|
||||
INSERT INTO `criteria_items` (`template_id`, `name`, `description`, `weight`, `max_score`, `sort_order`) VALUES
|
||||
(1, '內容品質', '內容的準確性、完整性和專業度', 25.00, 10.00, 1),
|
||||
(1, '視覺設計', '版面設計、色彩搭配和視覺效果', 20.00, 10.00, 2),
|
||||
(1, '邏輯結構', '內容組織的邏輯性和條理性', 20.00, 10.00, 3),
|
||||
(1, '創新性', '創意思維和獨特觀點的展現', 15.00, 10.00, 4),
|
||||
(1, '實用性', '內容的實際應用價值和可操作性', 20.00, 10.00, 5);
|
||||
|
||||
-- 插入系統設定
|
||||
INSERT INTO `system_settings` (`setting_key`, `setting_value`, `description`) VALUES
|
||||
('max_file_size', '104857600', '最大文件上傳大小(位元組)'),
|
||||
('allowed_file_types', 'ppt,pptx,pdf,mp4,avi,mov,wmv,flv,webm', '允許上傳的文件類型'),
|
||||
('ai_analysis_timeout', '300', 'AI 分析超時時間(秒)'),
|
||||
('max_concurrent_analyses', '5', '最大並發分析數量'),
|
||||
('default_grade_thresholds', '{"A":90,"B":80,"C":70,"D":60}', '預設等級閾值');
|
||||
|
||||
-- 建立索引優化查詢性能
|
||||
CREATE INDEX `idx_projects_user_status` ON `projects` (`user_id`, `status`);
|
||||
CREATE INDEX `idx_evaluations_project_status` ON `evaluations` (`project_id`, `status`);
|
||||
CREATE INDEX `idx_project_files_project_status` ON `project_files` (`project_id`, `upload_status`);
|
||||
CREATE INDEX `idx_criteria_items_template_order` ON `criteria_items` (`template_id`, `sort_order`);
|
78
lib/database.ts
Normal file
78
lib/database.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import mysql from 'mysql2/promise';
|
||||
|
||||
// 資料庫配置
|
||||
const dbConfig = {
|
||||
host: process.env.DB_HOST || 'mysql.theaken.com',
|
||||
port: parseInt(process.env.DB_PORT || '33306'),
|
||||
user: process.env.DB_USER || 'root',
|
||||
password: process.env.DB_PASSWORD || 'zh6161168',
|
||||
database: process.env.DB_NAME || 'db_AI_scoring',
|
||||
charset: 'utf8mb4',
|
||||
timezone: '+08:00',
|
||||
acquireTimeout: 60000,
|
||||
timeout: 60000,
|
||||
reconnect: true,
|
||||
multipleStatements: true,
|
||||
};
|
||||
|
||||
// 建立連接池
|
||||
const pool = mysql.createPool({
|
||||
...dbConfig,
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0,
|
||||
});
|
||||
|
||||
// 資料庫連接函數
|
||||
export async function getConnection() {
|
||||
try {
|
||||
const connection = await pool.getConnection();
|
||||
return connection;
|
||||
} catch (error) {
|
||||
console.error('資料庫連接失敗:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 執行查詢函數
|
||||
export async function query(sql: string, params?: any[]) {
|
||||
try {
|
||||
const [rows] = await pool.execute(sql, params);
|
||||
return rows;
|
||||
} catch (error) {
|
||||
console.error('查詢執行失敗:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 執行事務函數
|
||||
export async function transaction(callback: (connection: mysql.PoolConnection) => Promise<any>) {
|
||||
const connection = await pool.getConnection();
|
||||
try {
|
||||
await connection.beginTransaction();
|
||||
const result = await callback(connection);
|
||||
await connection.commit();
|
||||
return result;
|
||||
} catch (error) {
|
||||
await connection.rollback();
|
||||
throw error;
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
|
||||
// 測試資料庫連接
|
||||
export async function testConnection() {
|
||||
try {
|
||||
const connection = await getConnection();
|
||||
await connection.ping();
|
||||
connection.release();
|
||||
console.log('✅ 資料庫連接成功');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ 資料庫連接失敗:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default pool;
|
141
lib/models/index.ts
Normal file
141
lib/models/index.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
// 資料庫模型定義
|
||||
export interface User {
|
||||
id: number;
|
||||
email: string;
|
||||
username: string;
|
||||
password_hash: string;
|
||||
full_name?: string;
|
||||
avatar_url?: string;
|
||||
role: 'admin' | 'user';
|
||||
is_active: boolean;
|
||||
email_verified_at?: Date;
|
||||
last_login_at?: Date;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
}
|
||||
|
||||
export interface CriteriaTemplate {
|
||||
id: number;
|
||||
user_id: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
is_default: boolean;
|
||||
is_public: boolean;
|
||||
total_weight: number;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
}
|
||||
|
||||
export interface CriteriaItem {
|
||||
id: number;
|
||||
template_id: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
weight: number;
|
||||
max_score: number;
|
||||
sort_order: number;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
}
|
||||
|
||||
export interface Project {
|
||||
id: number;
|
||||
user_id: number;
|
||||
template_id: number;
|
||||
title: string;
|
||||
description?: string;
|
||||
status: 'draft' | 'uploading' | 'analyzing' | 'completed' | 'failed';
|
||||
analysis_started_at?: Date;
|
||||
analysis_completed_at?: Date;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
}
|
||||
|
||||
export interface ProjectFile {
|
||||
id: number;
|
||||
project_id: number;
|
||||
original_name: string;
|
||||
file_name: string;
|
||||
file_path: string;
|
||||
file_size: number;
|
||||
file_type: string;
|
||||
mime_type: string;
|
||||
upload_status: 'uploading' | 'completed' | 'failed';
|
||||
upload_progress: number;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
}
|
||||
|
||||
export interface ProjectWebsite {
|
||||
id: number;
|
||||
project_id: number;
|
||||
url: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
status: 'pending' | 'analyzing' | 'completed' | 'failed';
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
}
|
||||
|
||||
export interface Evaluation {
|
||||
id: number;
|
||||
project_id: number;
|
||||
overall_score?: number;
|
||||
max_possible_score: number;
|
||||
grade?: string;
|
||||
analysis_duration?: number;
|
||||
ai_model_version?: string;
|
||||
status: 'pending' | 'analyzing' | 'completed' | 'failed';
|
||||
error_message?: string;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
}
|
||||
|
||||
export interface EvaluationScore {
|
||||
id: number;
|
||||
evaluation_id: number;
|
||||
criteria_item_id: number;
|
||||
score: number;
|
||||
max_score: number;
|
||||
weight: number;
|
||||
weighted_score: number;
|
||||
percentage: number;
|
||||
created_at: Date;
|
||||
}
|
||||
|
||||
export interface EvaluationFeedback {
|
||||
id: number;
|
||||
evaluation_id: number;
|
||||
criteria_item_id?: number;
|
||||
feedback_type: 'overall' | 'criteria' | 'strength' | 'improvement';
|
||||
content: string;
|
||||
sort_order: number;
|
||||
created_at: Date;
|
||||
}
|
||||
|
||||
export interface SystemSetting {
|
||||
id: number;
|
||||
setting_key: string;
|
||||
setting_value?: string;
|
||||
description?: string;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
}
|
||||
|
||||
// 查詢結果類型
|
||||
export interface ProjectWithDetails extends Project {
|
||||
template: CriteriaTemplate;
|
||||
files: ProjectFile[];
|
||||
websites: ProjectWebsite[];
|
||||
evaluation?: Evaluation;
|
||||
}
|
||||
|
||||
export interface EvaluationWithDetails extends Evaluation {
|
||||
project: Project;
|
||||
scores: (EvaluationScore & { criteria_item: CriteriaItem })[];
|
||||
feedback: EvaluationFeedback[];
|
||||
}
|
||||
|
||||
export interface CriteriaTemplateWithItems extends CriteriaTemplate {
|
||||
items: CriteriaItem[];
|
||||
}
|
433
lib/services/database.ts
Normal file
433
lib/services/database.ts
Normal file
@@ -0,0 +1,433 @@
|
||||
import { query, transaction } from '../database';
|
||||
import type {
|
||||
User,
|
||||
CriteriaTemplate,
|
||||
CriteriaItem,
|
||||
Project,
|
||||
ProjectFile,
|
||||
ProjectWebsite,
|
||||
Evaluation,
|
||||
EvaluationScore,
|
||||
EvaluationFeedback,
|
||||
ProjectWithDetails,
|
||||
EvaluationWithDetails,
|
||||
CriteriaTemplateWithItems,
|
||||
} from '../models';
|
||||
|
||||
// 用戶相關操作
|
||||
export class UserService {
|
||||
static async create(userData: Omit<User, 'id' | 'created_at' | 'updated_at'>) {
|
||||
const sql = `
|
||||
INSERT INTO users (email, username, password_hash, full_name, avatar_url, role, is_active, email_verified_at, last_login_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
const result = await query(sql, [
|
||||
userData.email,
|
||||
userData.username,
|
||||
userData.password_hash,
|
||||
userData.full_name,
|
||||
userData.avatar_url,
|
||||
userData.role,
|
||||
userData.is_active,
|
||||
userData.email_verified_at,
|
||||
userData.last_login_at,
|
||||
]);
|
||||
return result;
|
||||
}
|
||||
|
||||
static async findByEmail(email: string): Promise<User | null> {
|
||||
const sql = 'SELECT * FROM users WHERE email = ?';
|
||||
const rows = await query(sql, [email]) as User[];
|
||||
return rows.length > 0 ? rows[0] : null;
|
||||
}
|
||||
|
||||
static async findById(id: number): Promise<User | null> {
|
||||
const sql = 'SELECT * FROM users WHERE id = ?';
|
||||
const rows = await query(sql, [id]) as User[];
|
||||
return rows.length > 0 ? rows[0] : null;
|
||||
}
|
||||
|
||||
static async updateLastLogin(id: number) {
|
||||
const sql = 'UPDATE users SET last_login_at = NOW() WHERE id = ?';
|
||||
await query(sql, [id]);
|
||||
}
|
||||
}
|
||||
|
||||
// 評分標準模板相關操作
|
||||
export class CriteriaTemplateService {
|
||||
static async create(templateData: Omit<CriteriaTemplate, 'id' | 'created_at' | 'updated_at'>) {
|
||||
const sql = `
|
||||
INSERT INTO criteria_templates (user_id, name, description, is_default, is_public, total_weight)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
const result = await query(sql, [
|
||||
templateData.user_id,
|
||||
templateData.name,
|
||||
templateData.description,
|
||||
templateData.is_default,
|
||||
templateData.is_public,
|
||||
templateData.total_weight,
|
||||
]);
|
||||
return result;
|
||||
}
|
||||
|
||||
static async findById(id: number): Promise<CriteriaTemplate | null> {
|
||||
const sql = 'SELECT * FROM criteria_templates WHERE id = ?';
|
||||
const rows = await query(sql, [id]) as CriteriaTemplate[];
|
||||
return rows.length > 0 ? rows[0] : null;
|
||||
}
|
||||
|
||||
static async findByUserId(userId: number): Promise<CriteriaTemplate[]> {
|
||||
const sql = 'SELECT * FROM criteria_templates WHERE user_id = ? ORDER BY created_at DESC';
|
||||
return await query(sql, [userId]) as CriteriaTemplate[];
|
||||
}
|
||||
|
||||
static async findDefault(): Promise<CriteriaTemplate | null> {
|
||||
const sql = 'SELECT * FROM criteria_templates WHERE is_default = 1 LIMIT 1';
|
||||
const rows = await query(sql) as CriteriaTemplate[];
|
||||
return rows.length > 0 ? rows[0] : null;
|
||||
}
|
||||
|
||||
static async findWithItems(id: number): Promise<CriteriaTemplateWithItems | null> {
|
||||
const template = await this.findById(id);
|
||||
if (!template) return null;
|
||||
|
||||
const itemsSql = 'SELECT * FROM criteria_items WHERE template_id = ? ORDER BY sort_order';
|
||||
const items = await query(itemsSql, [id]) as CriteriaItem[];
|
||||
|
||||
return { ...template, items };
|
||||
}
|
||||
|
||||
static async update(id: number, templateData: Partial<CriteriaTemplate>) {
|
||||
const fields = Object.keys(templateData).map(key => `${key} = ?`).join(', ');
|
||||
const values = Object.values(templateData);
|
||||
const sql = `UPDATE criteria_templates SET ${fields}, updated_at = NOW() WHERE id = ?`;
|
||||
await query(sql, [...values, id]);
|
||||
}
|
||||
|
||||
static async delete(id: number) {
|
||||
const sql = 'DELETE FROM criteria_templates WHERE id = ?';
|
||||
await query(sql, [id]);
|
||||
}
|
||||
}
|
||||
|
||||
// 評分項目相關操作
|
||||
export class CriteriaItemService {
|
||||
static async create(itemData: Omit<CriteriaItem, 'id' | 'created_at' | 'updated_at'>) {
|
||||
const sql = `
|
||||
INSERT INTO criteria_items (template_id, name, description, weight, max_score, sort_order)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
const result = await query(sql, [
|
||||
itemData.template_id,
|
||||
itemData.name,
|
||||
itemData.description,
|
||||
itemData.weight,
|
||||
itemData.max_score,
|
||||
itemData.sort_order,
|
||||
]);
|
||||
return result;
|
||||
}
|
||||
|
||||
static async findByTemplateId(templateId: number): Promise<CriteriaItem[]> {
|
||||
const sql = 'SELECT * FROM criteria_items WHERE template_id = ? ORDER BY sort_order';
|
||||
const rows = await query(sql, [templateId]) as any[];
|
||||
|
||||
// 映射資料庫欄位到前端期望的格式
|
||||
return rows.map(row => ({
|
||||
id: row.id,
|
||||
template_id: row.template_id,
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
weight: Number(row.weight) || 0,
|
||||
maxScore: Number(row.max_score) || 10, // 映射 max_score 到 maxScore 並轉換為數字
|
||||
sort_order: row.sort_order,
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at
|
||||
}));
|
||||
}
|
||||
|
||||
static async update(id: number, itemData: Partial<CriteriaItem>) {
|
||||
const fields = Object.keys(itemData).map(key => `${key} = ?`).join(', ');
|
||||
const values = Object.values(itemData);
|
||||
const sql = `UPDATE criteria_items SET ${fields}, updated_at = NOW() WHERE id = ?`;
|
||||
await query(sql, [...values, id]);
|
||||
}
|
||||
|
||||
static async delete(id: number) {
|
||||
const sql = 'DELETE FROM criteria_items WHERE id = ?';
|
||||
await query(sql, [id]);
|
||||
}
|
||||
|
||||
static async deleteByTemplateId(templateId: number) {
|
||||
const sql = 'DELETE FROM criteria_items WHERE template_id = ?';
|
||||
await query(sql, [templateId]);
|
||||
}
|
||||
}
|
||||
|
||||
// 專案相關操作
|
||||
export class ProjectService {
|
||||
static async create(projectData: Omit<Project, 'id' | 'created_at' | 'updated_at'>) {
|
||||
const sql = `
|
||||
INSERT INTO projects (user_id, template_id, title, description, status, analysis_started_at, analysis_completed_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
const result = await query(sql, [
|
||||
projectData.user_id,
|
||||
projectData.template_id,
|
||||
projectData.title,
|
||||
projectData.description,
|
||||
projectData.status,
|
||||
projectData.analysis_started_at,
|
||||
projectData.analysis_completed_at,
|
||||
]);
|
||||
return result;
|
||||
}
|
||||
|
||||
static async findById(id: number): Promise<Project | null> {
|
||||
const sql = 'SELECT * FROM projects WHERE id = ?';
|
||||
const rows = await query(sql, [id]) as Project[];
|
||||
return rows.length > 0 ? rows[0] : null;
|
||||
}
|
||||
|
||||
static async findByUserId(userId: number, limit = 20, offset = 0): Promise<Project[]> {
|
||||
const sql = `
|
||||
SELECT * FROM projects
|
||||
WHERE user_id = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
return await query(sql, [userId, limit, offset]) as Project[];
|
||||
}
|
||||
|
||||
static async findWithDetails(id: number): Promise<ProjectWithDetails | null> {
|
||||
const project = await this.findById(id);
|
||||
if (!project) return null;
|
||||
|
||||
const template = await CriteriaTemplateService.findById(project.template_id);
|
||||
if (!template) return null;
|
||||
|
||||
const files = await ProjectFileService.findByProjectId(id);
|
||||
const websites = await ProjectWebsiteService.findByProjectId(id);
|
||||
const evaluation = await EvaluationService.findByProjectId(id);
|
||||
|
||||
return {
|
||||
...project,
|
||||
template,
|
||||
files,
|
||||
websites,
|
||||
evaluation,
|
||||
};
|
||||
}
|
||||
|
||||
static async update(id: number, projectData: Partial<Project>) {
|
||||
const fields = Object.keys(projectData).map(key => `${key} = ?`).join(', ');
|
||||
const values = Object.values(projectData);
|
||||
const sql = `UPDATE projects SET ${fields}, updated_at = NOW() WHERE id = ?`;
|
||||
await query(sql, [...values, id]);
|
||||
}
|
||||
|
||||
static async delete(id: number) {
|
||||
const sql = 'DELETE FROM projects WHERE id = ?';
|
||||
await query(sql, [id]);
|
||||
}
|
||||
}
|
||||
|
||||
// 專案文件相關操作
|
||||
export class ProjectFileService {
|
||||
static async create(fileData: Omit<ProjectFile, 'id' | 'created_at' | 'updated_at'>) {
|
||||
const sql = `
|
||||
INSERT INTO project_files (project_id, original_name, file_name, file_path, file_size, file_type, mime_type, upload_status, upload_progress)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
const result = await query(sql, [
|
||||
fileData.project_id,
|
||||
fileData.original_name,
|
||||
fileData.file_name,
|
||||
fileData.file_path,
|
||||
fileData.file_size,
|
||||
fileData.file_type,
|
||||
fileData.mime_type,
|
||||
fileData.upload_status,
|
||||
fileData.upload_progress,
|
||||
]);
|
||||
return result;
|
||||
}
|
||||
|
||||
static async findByProjectId(projectId: number): Promise<ProjectFile[]> {
|
||||
const sql = 'SELECT * FROM project_files WHERE project_id = ? ORDER BY created_at';
|
||||
return await query(sql, [projectId]) as ProjectFile[];
|
||||
}
|
||||
|
||||
static async updateStatus(id: number, status: ProjectFile['upload_status'], progress?: number) {
|
||||
const sql = 'UPDATE project_files SET upload_status = ?, upload_progress = ?, updated_at = NOW() WHERE id = ?';
|
||||
await query(sql, [status, progress || 0, id]);
|
||||
}
|
||||
|
||||
static async delete(id: number) {
|
||||
const sql = 'DELETE FROM project_files WHERE id = ?';
|
||||
await query(sql, [id]);
|
||||
}
|
||||
}
|
||||
|
||||
// 專案網站相關操作
|
||||
export class ProjectWebsiteService {
|
||||
static async create(websiteData: Omit<ProjectWebsite, 'id' | 'created_at' | 'updated_at'>) {
|
||||
const sql = `
|
||||
INSERT INTO project_websites (project_id, url, title, description, status)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`;
|
||||
const result = await query(sql, [
|
||||
websiteData.project_id,
|
||||
websiteData.url,
|
||||
websiteData.title,
|
||||
websiteData.description,
|
||||
websiteData.status,
|
||||
]);
|
||||
return result;
|
||||
}
|
||||
|
||||
static async findByProjectId(projectId: number): Promise<ProjectWebsite[]> {
|
||||
const sql = 'SELECT * FROM project_websites WHERE project_id = ? ORDER BY created_at';
|
||||
return await query(sql, [projectId]) as ProjectWebsite[];
|
||||
}
|
||||
|
||||
static async updateStatus(id: number, status: ProjectWebsite['status']) {
|
||||
const sql = 'UPDATE project_websites SET status = ?, updated_at = NOW() WHERE id = ?';
|
||||
await query(sql, [status, id]);
|
||||
}
|
||||
|
||||
static async delete(id: number) {
|
||||
const sql = 'DELETE FROM project_websites WHERE id = ?';
|
||||
await query(sql, [id]);
|
||||
}
|
||||
}
|
||||
|
||||
// 評審相關操作
|
||||
export class EvaluationService {
|
||||
static async create(evaluationData: Omit<Evaluation, 'id' | 'created_at' | 'updated_at'>) {
|
||||
const sql = `
|
||||
INSERT INTO evaluations (project_id, overall_score, max_possible_score, grade, analysis_duration, ai_model_version, status, error_message)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
const result = await query(sql, [
|
||||
evaluationData.project_id,
|
||||
evaluationData.overall_score,
|
||||
evaluationData.max_possible_score,
|
||||
evaluationData.grade,
|
||||
evaluationData.analysis_duration,
|
||||
evaluationData.ai_model_version,
|
||||
evaluationData.status,
|
||||
evaluationData.error_message,
|
||||
]);
|
||||
return result;
|
||||
}
|
||||
|
||||
static async findById(id: number): Promise<Evaluation | null> {
|
||||
const sql = 'SELECT * FROM evaluations WHERE id = ?';
|
||||
const rows = await query(sql, [id]) as Evaluation[];
|
||||
return rows.length > 0 ? rows[0] : null;
|
||||
}
|
||||
|
||||
static async findByProjectId(projectId: number): Promise<Evaluation | null> {
|
||||
const sql = 'SELECT * FROM evaluations WHERE project_id = ? ORDER BY created_at DESC LIMIT 1';
|
||||
const rows = await query(sql, [projectId]) as Evaluation[];
|
||||
return rows.length > 0 ? rows[0] : null;
|
||||
}
|
||||
|
||||
static async findWithDetails(id: number): Promise<EvaluationWithDetails | null> {
|
||||
const evaluation = await this.findById(id);
|
||||
if (!evaluation) return null;
|
||||
|
||||
const project = await ProjectService.findById(evaluation.project_id);
|
||||
if (!project) return null;
|
||||
|
||||
const scoresSql = `
|
||||
SELECT es.*, ci.name as criteria_item_name, ci.description as criteria_item_description
|
||||
FROM evaluation_scores es
|
||||
JOIN criteria_items ci ON es.criteria_item_id = ci.id
|
||||
WHERE es.evaluation_id = ?
|
||||
ORDER BY ci.sort_order
|
||||
`;
|
||||
const scores = await query(scoresSql, [id]) as (EvaluationScore & { criteria_item: CriteriaItem })[];
|
||||
|
||||
const feedback = await EvaluationFeedbackService.findByEvaluationId(id);
|
||||
|
||||
return {
|
||||
...evaluation,
|
||||
project,
|
||||
scores,
|
||||
feedback,
|
||||
};
|
||||
}
|
||||
|
||||
static async update(id: number, evaluationData: Partial<Evaluation>) {
|
||||
const fields = Object.keys(evaluationData).map(key => `${key} = ?`).join(', ');
|
||||
const values = Object.values(evaluationData);
|
||||
const sql = `UPDATE evaluations SET ${fields}, updated_at = NOW() WHERE id = ?`;
|
||||
await query(sql, [...values, id]);
|
||||
}
|
||||
|
||||
static async delete(id: number) {
|
||||
const sql = 'DELETE FROM evaluations WHERE id = ?';
|
||||
await query(sql, [id]);
|
||||
}
|
||||
}
|
||||
|
||||
// 評分結果相關操作
|
||||
export class EvaluationScoreService {
|
||||
static async create(scoreData: Omit<EvaluationScore, 'id' | 'created_at'>) {
|
||||
const sql = `
|
||||
INSERT INTO evaluation_scores (evaluation_id, criteria_item_id, score, max_score, weight, weighted_score, percentage)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
const result = await query(sql, [
|
||||
scoreData.evaluation_id,
|
||||
scoreData.criteria_item_id,
|
||||
scoreData.score,
|
||||
scoreData.max_score,
|
||||
scoreData.weight,
|
||||
scoreData.weighted_score,
|
||||
scoreData.percentage,
|
||||
]);
|
||||
return result;
|
||||
}
|
||||
|
||||
static async findByEvaluationId(evaluationId: number): Promise<EvaluationScore[]> {
|
||||
const sql = 'SELECT * FROM evaluation_scores WHERE evaluation_id = ?';
|
||||
return await query(sql, [evaluationId]) as EvaluationScore[];
|
||||
}
|
||||
|
||||
static async deleteByEvaluationId(evaluationId: number) {
|
||||
const sql = 'DELETE FROM evaluation_scores WHERE evaluation_id = ?';
|
||||
await query(sql, [evaluationId]);
|
||||
}
|
||||
}
|
||||
|
||||
// 評語相關操作
|
||||
export class EvaluationFeedbackService {
|
||||
static async create(feedbackData: Omit<EvaluationFeedback, 'id' | 'created_at'>) {
|
||||
const sql = `
|
||||
INSERT INTO evaluation_feedback (evaluation_id, criteria_item_id, feedback_type, content, sort_order)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`;
|
||||
const result = await query(sql, [
|
||||
feedbackData.evaluation_id,
|
||||
feedbackData.criteria_item_id,
|
||||
feedbackData.feedback_type,
|
||||
feedbackData.content,
|
||||
feedbackData.sort_order,
|
||||
]);
|
||||
return result;
|
||||
}
|
||||
|
||||
static async findByEvaluationId(evaluationId: number): Promise<EvaluationFeedback[]> {
|
||||
const sql = 'SELECT * FROM evaluation_feedback WHERE evaluation_id = ? ORDER BY sort_order';
|
||||
return await query(sql, [evaluationId]) as EvaluationFeedback[];
|
||||
}
|
||||
|
||||
static async deleteByEvaluationId(evaluationId: number) {
|
||||
const sql = 'DELETE FROM evaluation_feedback WHERE evaluation_id = ?';
|
||||
await query(sql, [evaluationId]);
|
||||
}
|
||||
}
|
@@ -6,7 +6,9 @@
|
||||
"build": "next build",
|
||||
"dev": "next dev",
|
||||
"lint": "next lint",
|
||||
"start": "next start"
|
||||
"start": "next start",
|
||||
"db:init": "node scripts/init-database-simple.js",
|
||||
"db:test": "node scripts/test-database.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.10.0",
|
||||
@@ -47,8 +49,10 @@
|
||||
"geist": "latest",
|
||||
"input-otp": "1.4.1",
|
||||
"lucide-react": "^0.454.0",
|
||||
"mysql2": "^3.15.0",
|
||||
"next": "14.2.16",
|
||||
"next-themes": "^0.4.6",
|
||||
"node-fetch": "^3.3.2",
|
||||
"react": "^18",
|
||||
"react-day-picker": "9.8.0",
|
||||
"react-dom": "^18",
|
||||
|
141
pnpm-lock.yaml
generated
141
pnpm-lock.yaml
generated
@@ -122,12 +122,18 @@ importers:
|
||||
lucide-react:
|
||||
specifier: ^0.454.0
|
||||
version: 0.454.0(react@18.0.0)
|
||||
mysql2:
|
||||
specifier: ^3.15.0
|
||||
version: 3.15.0
|
||||
next:
|
||||
specifier: 14.2.16
|
||||
version: 14.2.16(react-dom@18.0.0(react@18.0.0))(react@18.0.0)
|
||||
next-themes:
|
||||
specifier: ^0.4.6
|
||||
version: 0.4.6(react-dom@18.0.0(react@18.0.0))(react@18.0.0)
|
||||
node-fetch:
|
||||
specifier: ^3.3.2
|
||||
version: 3.3.2
|
||||
react:
|
||||
specifier: ^18
|
||||
version: 18.0.0
|
||||
@@ -1366,6 +1372,10 @@ packages:
|
||||
peerDependencies:
|
||||
postcss: ^8.1.0
|
||||
|
||||
aws-ssl-profiles@1.1.2:
|
||||
resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==}
|
||||
engines: {node: '>= 6.0.0'}
|
||||
|
||||
baseline-browser-mapping@2.8.6:
|
||||
resolution: {integrity: sha512-wrH5NNqren/QMtKUEEJf7z86YjfqW/2uw3IL3/xpqZUC95SSVIFXYQeeGjL6FT/X68IROu6RMehZQS5foy2BXw==}
|
||||
hasBin: true
|
||||
@@ -1449,6 +1459,10 @@ packages:
|
||||
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
data-uri-to-buffer@4.0.1:
|
||||
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
date-fns-jalali@4.1.0-0:
|
||||
resolution: {integrity: sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==}
|
||||
|
||||
@@ -1458,6 +1472,10 @@ packages:
|
||||
decimal.js-light@2.5.1:
|
||||
resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
|
||||
|
||||
denque@2.1.0:
|
||||
resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
|
||||
engines: {node: '>=0.10'}
|
||||
|
||||
detect-libc@2.1.0:
|
||||
resolution: {integrity: sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1495,10 +1513,18 @@ packages:
|
||||
eventemitter3@5.0.1:
|
||||
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
|
||||
|
||||
fetch-blob@3.2.0:
|
||||
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
|
||||
engines: {node: ^12.20 || >= 14.13}
|
||||
|
||||
file-selector@2.1.2:
|
||||
resolution: {integrity: sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==}
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
formdata-polyfill@4.0.10:
|
||||
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
|
||||
fraction.js@4.3.7:
|
||||
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
|
||||
|
||||
@@ -1507,6 +1533,9 @@ packages:
|
||||
peerDependencies:
|
||||
next: '>=13.2.0'
|
||||
|
||||
generate-function@2.3.1:
|
||||
resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==}
|
||||
|
||||
get-nonce@1.0.1:
|
||||
resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -1514,6 +1543,10 @@ packages:
|
||||
graceful-fs@4.2.11:
|
||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||
|
||||
iconv-lite@0.7.0:
|
||||
resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
immer@10.1.3:
|
||||
resolution: {integrity: sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==}
|
||||
|
||||
@@ -1527,6 +1560,9 @@ packages:
|
||||
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
is-property@1.0.2:
|
||||
resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==}
|
||||
|
||||
jiti@2.5.1:
|
||||
resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==}
|
||||
hasBin: true
|
||||
@@ -1598,10 +1634,21 @@ packages:
|
||||
resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
|
||||
long@5.3.2:
|
||||
resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==}
|
||||
|
||||
loose-envify@1.4.0:
|
||||
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
||||
hasBin: true
|
||||
|
||||
lru-cache@7.18.3:
|
||||
resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
lru.min@1.1.2:
|
||||
resolution: {integrity: sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==}
|
||||
engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'}
|
||||
|
||||
lucide-react@0.454.0:
|
||||
resolution: {integrity: sha512-hw7zMDwykCLnEzgncEEjHeA6+45aeEzRYuKHuyRSOPkhko+J3ySGjGIzu+mmMfDFG1vazHepMaYFYHbTFAZAAQ==}
|
||||
peerDependencies:
|
||||
@@ -1623,6 +1670,14 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
mysql2@3.15.0:
|
||||
resolution: {integrity: sha512-tT6pomf5Z/I7Jzxu8sScgrYBMK9bUFWd7Kbo6Fs1L0M13OOIJ/ZobGKS3Z7tQ8Re4lj+LnLXIQVZZxa3fhYKzA==}
|
||||
engines: {node: '>= 8.0'}
|
||||
|
||||
named-placeholders@1.1.3:
|
||||
resolution: {integrity: sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
nanoid@3.3.11:
|
||||
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
@@ -1652,6 +1707,15 @@ packages:
|
||||
sass:
|
||||
optional: true
|
||||
|
||||
node-domexception@1.0.0:
|
||||
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||
engines: {node: '>=10.5.0'}
|
||||
deprecated: Use your platform's native DOMException instead
|
||||
|
||||
node-fetch@3.3.2:
|
||||
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
node-releases@2.0.21:
|
||||
resolution: {integrity: sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==}
|
||||
|
||||
@@ -1777,9 +1841,15 @@ packages:
|
||||
reselect@5.1.1:
|
||||
resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
|
||||
|
||||
safer-buffer@2.1.2:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
|
||||
scheduler@0.21.0:
|
||||
resolution: {integrity: sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==}
|
||||
|
||||
seq-queue@0.0.5:
|
||||
resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==}
|
||||
|
||||
sonner@1.7.4:
|
||||
resolution: {integrity: sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw==}
|
||||
peerDependencies:
|
||||
@@ -1790,6 +1860,10 @@ packages:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
sqlstring@2.3.3:
|
||||
resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
streamsearch@1.1.0:
|
||||
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
@@ -1883,6 +1957,10 @@ packages:
|
||||
victory-vendor@37.3.6:
|
||||
resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==}
|
||||
|
||||
web-streams-polyfill@3.3.3:
|
||||
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
yallist@5.0.0:
|
||||
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -3002,6 +3080,8 @@ snapshots:
|
||||
postcss: 8.5.0
|
||||
postcss-value-parser: 4.2.0
|
||||
|
||||
aws-ssl-profiles@1.1.2: {}
|
||||
|
||||
baseline-browser-mapping@2.8.6: {}
|
||||
|
||||
browserslist@4.26.2:
|
||||
@@ -3080,12 +3160,16 @@ snapshots:
|
||||
|
||||
d3-timer@3.0.1: {}
|
||||
|
||||
data-uri-to-buffer@4.0.1: {}
|
||||
|
||||
date-fns-jalali@4.1.0-0: {}
|
||||
|
||||
date-fns@4.1.0: {}
|
||||
|
||||
decimal.js-light@2.5.1: {}
|
||||
|
||||
denque@2.1.0: {}
|
||||
|
||||
detect-libc@2.1.0: {}
|
||||
|
||||
detect-node-es@1.1.0: {}
|
||||
@@ -3115,20 +3199,37 @@ snapshots:
|
||||
|
||||
eventemitter3@5.0.1: {}
|
||||
|
||||
fetch-blob@3.2.0:
|
||||
dependencies:
|
||||
node-domexception: 1.0.0
|
||||
web-streams-polyfill: 3.3.3
|
||||
|
||||
file-selector@2.1.2:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
formdata-polyfill@4.0.10:
|
||||
dependencies:
|
||||
fetch-blob: 3.2.0
|
||||
|
||||
fraction.js@4.3.7: {}
|
||||
|
||||
geist@1.5.1(next@14.2.16(react-dom@18.0.0(react@18.0.0))(react@18.0.0)):
|
||||
dependencies:
|
||||
next: 14.2.16(react-dom@18.0.0(react@18.0.0))(react@18.0.0)
|
||||
|
||||
generate-function@2.3.1:
|
||||
dependencies:
|
||||
is-property: 1.0.2
|
||||
|
||||
get-nonce@1.0.1: {}
|
||||
|
||||
graceful-fs@4.2.11: {}
|
||||
|
||||
iconv-lite@0.7.0:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
|
||||
immer@10.1.3: {}
|
||||
|
||||
input-otp@1.4.1(react-dom@18.0.0(react@18.0.0))(react@18.0.0):
|
||||
@@ -3138,6 +3239,8 @@ snapshots:
|
||||
|
||||
internmap@2.0.3: {}
|
||||
|
||||
is-property@1.0.2: {}
|
||||
|
||||
jiti@2.5.1: {}
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
@@ -3187,10 +3290,16 @@ snapshots:
|
||||
lightningcss-win32-arm64-msvc: 1.30.1
|
||||
lightningcss-win32-x64-msvc: 1.30.1
|
||||
|
||||
long@5.3.2: {}
|
||||
|
||||
loose-envify@1.4.0:
|
||||
dependencies:
|
||||
js-tokens: 4.0.0
|
||||
|
||||
lru-cache@7.18.3: {}
|
||||
|
||||
lru.min@1.1.2: {}
|
||||
|
||||
lucide-react@0.454.0(react@18.0.0):
|
||||
dependencies:
|
||||
react: 18.0.0
|
||||
@@ -3207,6 +3316,22 @@ snapshots:
|
||||
|
||||
mkdirp@3.0.1: {}
|
||||
|
||||
mysql2@3.15.0:
|
||||
dependencies:
|
||||
aws-ssl-profiles: 1.1.2
|
||||
denque: 2.1.0
|
||||
generate-function: 2.3.1
|
||||
iconv-lite: 0.7.0
|
||||
long: 5.3.2
|
||||
lru.min: 1.1.2
|
||||
named-placeholders: 1.1.3
|
||||
seq-queue: 0.0.5
|
||||
sqlstring: 2.3.3
|
||||
|
||||
named-placeholders@1.1.3:
|
||||
dependencies:
|
||||
lru-cache: 7.18.3
|
||||
|
||||
nanoid@3.3.11: {}
|
||||
|
||||
next-themes@0.4.6(react-dom@18.0.0(react@18.0.0))(react@18.0.0):
|
||||
@@ -3239,6 +3364,14 @@ snapshots:
|
||||
- '@babel/core'
|
||||
- babel-plugin-macros
|
||||
|
||||
node-domexception@1.0.0: {}
|
||||
|
||||
node-fetch@3.3.2:
|
||||
dependencies:
|
||||
data-uri-to-buffer: 4.0.1
|
||||
fetch-blob: 3.2.0
|
||||
formdata-polyfill: 4.0.10
|
||||
|
||||
node-releases@2.0.21: {}
|
||||
|
||||
normalize-range@0.1.2: {}
|
||||
@@ -3366,10 +3499,14 @@ snapshots:
|
||||
|
||||
reselect@5.1.1: {}
|
||||
|
||||
safer-buffer@2.1.2: {}
|
||||
|
||||
scheduler@0.21.0:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
|
||||
seq-queue@0.0.5: {}
|
||||
|
||||
sonner@1.7.4(react-dom@18.0.0(react@18.0.0))(react@18.0.0):
|
||||
dependencies:
|
||||
react: 18.0.0
|
||||
@@ -3377,6 +3514,8 @@ snapshots:
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
sqlstring@2.3.3: {}
|
||||
|
||||
streamsearch@1.1.0: {}
|
||||
|
||||
styled-jsx@5.1.1(react@18.0.0):
|
||||
@@ -3464,6 +3603,8 @@ snapshots:
|
||||
d3-time: 3.1.0
|
||||
d3-timer: 3.0.1
|
||||
|
||||
web-streams-polyfill@3.3.3: {}
|
||||
|
||||
yallist@5.0.0: {}
|
||||
|
||||
zod@3.25.67: {}
|
||||
|
319
scripts/init-database-simple.js
Normal file
319
scripts/init-database-simple.js
Normal file
@@ -0,0 +1,319 @@
|
||||
const mysql = require('mysql2/promise');
|
||||
|
||||
// 資料庫配置
|
||||
const dbConfig = {
|
||||
host: process.env.DB_HOST || 'mysql.theaken.com',
|
||||
port: parseInt(process.env.DB_PORT || '33306'),
|
||||
user: process.env.DB_USER || 'root',
|
||||
password: process.env.DB_PASSWORD || 'zh6161168',
|
||||
database: process.env.DB_NAME || 'db_AI_scoring',
|
||||
charset: 'utf8mb4',
|
||||
timezone: '+08:00',
|
||||
};
|
||||
|
||||
async function initializeDatabase() {
|
||||
let connection;
|
||||
|
||||
try {
|
||||
console.log('🔄 正在連接資料庫...');
|
||||
|
||||
// 先連接到 MySQL 伺服器(不指定資料庫)
|
||||
const serverConnection = await mysql.createConnection({
|
||||
host: dbConfig.host,
|
||||
port: dbConfig.port,
|
||||
user: dbConfig.user,
|
||||
password: dbConfig.password,
|
||||
charset: dbConfig.charset,
|
||||
timezone: dbConfig.timezone,
|
||||
});
|
||||
|
||||
console.log('✅ 成功連接到 MySQL 伺服器');
|
||||
|
||||
// 1. 建立資料庫
|
||||
console.log('🔄 正在建立資料庫...');
|
||||
await serverConnection.query(`
|
||||
CREATE DATABASE IF NOT EXISTS \`db_AI_scoring\`
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci
|
||||
`);
|
||||
console.log('✅ 資料庫建立完成');
|
||||
|
||||
// 2. 選擇資料庫
|
||||
await serverConnection.query('USE `db_AI_scoring`');
|
||||
console.log('✅ 已選擇資料庫');
|
||||
|
||||
// 3. 建立用戶表
|
||||
console.log('🔄 正在建立用戶表...');
|
||||
await serverConnection.query(`
|
||||
CREATE TABLE IF NOT EXISTS \`users\` (
|
||||
\`id\` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
\`email\` varchar(255) NOT NULL UNIQUE,
|
||||
\`username\` varchar(100) NOT NULL UNIQUE,
|
||||
\`password_hash\` varchar(255) NOT NULL,
|
||||
\`full_name\` varchar(255) DEFAULT NULL,
|
||||
\`avatar_url\` varchar(500) DEFAULT NULL,
|
||||
\`role\` enum('admin', 'user') DEFAULT 'user',
|
||||
\`is_active\` tinyint(1) DEFAULT 1,
|
||||
\`email_verified_at\` timestamp NULL DEFAULT NULL,
|
||||
\`last_login_at\` timestamp NULL DEFAULT NULL,
|
||||
\`created_at\` timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
\`updated_at\` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (\`id\`),
|
||||
KEY \`idx_email\` (\`email\`),
|
||||
KEY \`idx_username\` (\`username\`),
|
||||
KEY \`idx_created_at\` (\`created_at\`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
`);
|
||||
console.log('✅ 用戶表建立完成');
|
||||
|
||||
// 4. 建立評分標準模板表
|
||||
console.log('🔄 正在建立評分標準模板表...');
|
||||
await serverConnection.query(`
|
||||
CREATE TABLE IF NOT EXISTS \`criteria_templates\` (
|
||||
\`id\` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
\`user_id\` bigint(20) unsigned NOT NULL,
|
||||
\`name\` varchar(255) NOT NULL,
|
||||
\`description\` text DEFAULT NULL,
|
||||
\`is_default\` tinyint(1) DEFAULT 0,
|
||||
\`is_public\` tinyint(1) DEFAULT 0,
|
||||
\`total_weight\` decimal(5,2) DEFAULT 100.00,
|
||||
\`created_at\` timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
\`updated_at\` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (\`id\`),
|
||||
KEY \`idx_user_id\` (\`user_id\`),
|
||||
KEY \`idx_is_default\` (\`is_default\`),
|
||||
KEY \`idx_is_public\` (\`is_public\`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
`);
|
||||
console.log('✅ 評分標準模板表建立完成');
|
||||
|
||||
// 5. 建立評分項目表
|
||||
console.log('🔄 正在建立評分項目表...');
|
||||
await serverConnection.query(`
|
||||
CREATE TABLE IF NOT EXISTS \`criteria_items\` (
|
||||
\`id\` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
\`template_id\` bigint(20) unsigned NOT NULL,
|
||||
\`name\` varchar(255) NOT NULL,
|
||||
\`description\` text DEFAULT NULL,
|
||||
\`weight\` decimal(5,2) NOT NULL,
|
||||
\`max_score\` decimal(5,2) DEFAULT 10.00,
|
||||
\`sort_order\` int(11) DEFAULT 0,
|
||||
\`created_at\` timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
\`updated_at\` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (\`id\`),
|
||||
KEY \`idx_template_id\` (\`template_id\`),
|
||||
KEY \`idx_sort_order\` (\`sort_order\`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
`);
|
||||
console.log('✅ 評分項目表建立完成');
|
||||
|
||||
// 6. 建立專案表
|
||||
console.log('🔄 正在建立專案表...');
|
||||
await serverConnection.query(`
|
||||
CREATE TABLE IF NOT EXISTS \`projects\` (
|
||||
\`id\` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
\`user_id\` bigint(20) unsigned NOT NULL,
|
||||
\`template_id\` bigint(20) unsigned NOT NULL,
|
||||
\`title\` varchar(255) NOT NULL,
|
||||
\`description\` text DEFAULT NULL,
|
||||
\`status\` enum('draft', 'uploading', 'analyzing', 'completed', 'failed') DEFAULT 'draft',
|
||||
\`analysis_started_at\` timestamp NULL DEFAULT NULL,
|
||||
\`analysis_completed_at\` timestamp NULL DEFAULT NULL,
|
||||
\`created_at\` timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
\`updated_at\` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (\`id\`),
|
||||
KEY \`idx_user_id\` (\`user_id\`),
|
||||
KEY \`idx_template_id\` (\`template_id\`),
|
||||
KEY \`idx_status\` (\`status\`),
|
||||
KEY \`idx_created_at\` (\`created_at\`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
`);
|
||||
console.log('✅ 專案表建立完成');
|
||||
|
||||
// 7. 建立專案文件表
|
||||
console.log('🔄 正在建立專案文件表...');
|
||||
await serverConnection.query(`
|
||||
CREATE TABLE IF NOT EXISTS \`project_files\` (
|
||||
\`id\` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
\`project_id\` bigint(20) unsigned NOT NULL,
|
||||
\`original_name\` varchar(255) NOT NULL,
|
||||
\`file_name\` varchar(255) NOT NULL,
|
||||
\`file_path\` varchar(500) NOT NULL,
|
||||
\`file_size\` bigint(20) unsigned NOT NULL,
|
||||
\`file_type\` varchar(100) NOT NULL,
|
||||
\`mime_type\` varchar(100) NOT NULL,
|
||||
\`upload_status\` enum('uploading', 'completed', 'failed') DEFAULT 'uploading',
|
||||
\`upload_progress\` decimal(5,2) DEFAULT 0.00,
|
||||
\`created_at\` timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
\`updated_at\` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (\`id\`),
|
||||
KEY \`idx_project_id\` (\`project_id\`),
|
||||
KEY \`idx_file_type\` (\`file_type\`),
|
||||
KEY \`idx_upload_status\` (\`upload_status\`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
`);
|
||||
console.log('✅ 專案文件表建立完成');
|
||||
|
||||
// 8. 建立專案網站表
|
||||
console.log('🔄 正在建立專案網站表...');
|
||||
await serverConnection.query(`
|
||||
CREATE TABLE IF NOT EXISTS \`project_websites\` (
|
||||
\`id\` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
\`project_id\` bigint(20) unsigned NOT NULL,
|
||||
\`url\` varchar(500) NOT NULL,
|
||||
\`title\` varchar(255) DEFAULT NULL,
|
||||
\`description\` text DEFAULT NULL,
|
||||
\`status\` enum('pending', 'analyzing', 'completed', 'failed') DEFAULT 'pending',
|
||||
\`created_at\` timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
\`updated_at\` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (\`id\`),
|
||||
KEY \`idx_project_id\` (\`project_id\`),
|
||||
KEY \`idx_status\` (\`status\`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
`);
|
||||
console.log('✅ 專案網站表建立完成');
|
||||
|
||||
// 9. 建立評審記錄表
|
||||
console.log('🔄 正在建立評審記錄表...');
|
||||
await serverConnection.query(`
|
||||
CREATE TABLE IF NOT EXISTS \`evaluations\` (
|
||||
\`id\` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
\`project_id\` bigint(20) unsigned NOT NULL,
|
||||
\`overall_score\` decimal(5,2) DEFAULT NULL,
|
||||
\`max_possible_score\` decimal(5,2) DEFAULT 100.00,
|
||||
\`grade\` varchar(10) DEFAULT NULL,
|
||||
\`analysis_duration\` int(11) DEFAULT NULL,
|
||||
\`ai_model_version\` varchar(50) DEFAULT NULL,
|
||||
\`status\` enum('pending', 'analyzing', 'completed', 'failed') DEFAULT 'pending',
|
||||
\`error_message\` text DEFAULT NULL,
|
||||
\`created_at\` timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
\`updated_at\` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (\`id\`),
|
||||
KEY \`idx_project_id\` (\`project_id\`),
|
||||
KEY \`idx_status\` (\`status\`),
|
||||
KEY \`idx_created_at\` (\`created_at\`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
`);
|
||||
console.log('✅ 評審記錄表建立完成');
|
||||
|
||||
// 10. 建立評分結果表
|
||||
console.log('🔄 正在建立評分結果表...');
|
||||
await serverConnection.query(`
|
||||
CREATE TABLE IF NOT EXISTS \`evaluation_scores\` (
|
||||
\`id\` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
\`evaluation_id\` bigint(20) unsigned NOT NULL,
|
||||
\`criteria_item_id\` bigint(20) unsigned NOT NULL,
|
||||
\`score\` decimal(5,2) NOT NULL,
|
||||
\`max_score\` decimal(5,2) NOT NULL,
|
||||
\`weight\` decimal(5,2) NOT NULL,
|
||||
\`weighted_score\` decimal(5,2) NOT NULL,
|
||||
\`percentage\` decimal(5,2) NOT NULL,
|
||||
\`created_at\` timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (\`id\`),
|
||||
KEY \`idx_evaluation_id\` (\`evaluation_id\`),
|
||||
KEY \`idx_criteria_item_id\` (\`criteria_item_id\`),
|
||||
UNIQUE KEY \`unique_evaluation_criteria\` (\`evaluation_id\`, \`criteria_item_id\`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
`);
|
||||
console.log('✅ 評分結果表建立完成');
|
||||
|
||||
// 11. 建立評語表
|
||||
console.log('🔄 正在建立評語表...');
|
||||
await serverConnection.query(`
|
||||
CREATE TABLE IF NOT EXISTS \`evaluation_feedback\` (
|
||||
\`id\` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
\`evaluation_id\` bigint(20) unsigned NOT NULL,
|
||||
\`criteria_item_id\` bigint(20) unsigned DEFAULT NULL,
|
||||
\`feedback_type\` enum('overall', 'criteria', 'strength', 'improvement') NOT NULL,
|
||||
\`content\` text NOT NULL,
|
||||
\`sort_order\` int(11) DEFAULT 0,
|
||||
\`created_at\` timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (\`id\`),
|
||||
KEY \`idx_evaluation_id\` (\`evaluation_id\`),
|
||||
KEY \`idx_criteria_item_id\` (\`criteria_item_id\`),
|
||||
KEY \`idx_feedback_type\` (\`feedback_type\`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
`);
|
||||
console.log('✅ 評語表建立完成');
|
||||
|
||||
// 12. 建立系統設定表
|
||||
console.log('🔄 正在建立系統設定表...');
|
||||
await serverConnection.query(`
|
||||
CREATE TABLE IF NOT EXISTS \`system_settings\` (
|
||||
\`id\` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
\`setting_key\` varchar(100) NOT NULL UNIQUE,
|
||||
\`setting_value\` text DEFAULT NULL,
|
||||
\`description\` varchar(255) DEFAULT NULL,
|
||||
\`created_at\` timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
\`updated_at\` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (\`id\`),
|
||||
KEY \`idx_setting_key\` (\`setting_key\`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
`);
|
||||
console.log('✅ 系統設定表建立完成');
|
||||
|
||||
// 13. 插入預設數據
|
||||
console.log('🔄 正在插入預設數據...');
|
||||
|
||||
// 插入預設評分標準模板
|
||||
await serverConnection.query(`
|
||||
INSERT IGNORE INTO \`criteria_templates\` (\`user_id\`, \`name\`, \`description\`, \`is_default\`, \`is_public\`, \`total_weight\`)
|
||||
VALUES (1, '預設評分標準', '系統預設的評分標準模板', 1, 1, 100.00)
|
||||
`);
|
||||
|
||||
// 插入預設評分項目
|
||||
await serverConnection.query(`
|
||||
INSERT IGNORE INTO \`criteria_items\` (\`template_id\`, \`name\`, \`description\`, \`weight\`, \`max_score\`, \`sort_order\`) VALUES
|
||||
(1, '內容品質', '內容的準確性、完整性和專業度', 25.00, 10.00, 1),
|
||||
(1, '視覺設計', '版面設計、色彩搭配和視覺效果', 20.00, 10.00, 2),
|
||||
(1, '邏輯結構', '內容組織的邏輯性和條理性', 20.00, 10.00, 3),
|
||||
(1, '創新性', '創意思維和獨特觀點的展現', 15.00, 10.00, 4),
|
||||
(1, '實用性', '內容的實際應用價值和可操作性', 20.00, 10.00, 5)
|
||||
`);
|
||||
|
||||
// 插入系統設定
|
||||
await serverConnection.query(`
|
||||
INSERT IGNORE INTO \`system_settings\` (\`setting_key\`, \`setting_value\`, \`description\`) VALUES
|
||||
('max_file_size', '104857600', '最大文件上傳大小(位元組)'),
|
||||
('allowed_file_types', 'ppt,pptx,pdf,mp4,avi,mov,wmv,flv,webm', '允許上傳的文件類型'),
|
||||
('ai_analysis_timeout', '300', 'AI 分析超時時間(秒)'),
|
||||
('max_concurrent_analyses', '5', '最大並發分析數量'),
|
||||
('default_grade_thresholds', '{"A":90,"B":80,"C":70,"D":60}', '預設等級閾值')
|
||||
`);
|
||||
|
||||
console.log('✅ 預設數據插入完成');
|
||||
|
||||
// 關閉連接
|
||||
await serverConnection.end();
|
||||
|
||||
// 測試新建立的資料庫連接
|
||||
console.log('🔄 正在測試資料庫連接...');
|
||||
connection = await mysql.createConnection(dbConfig);
|
||||
|
||||
// 測試查詢
|
||||
const [rows] = await connection.query('SELECT COUNT(*) as count FROM criteria_templates');
|
||||
console.log(`✅ 資料庫測試成功,找到 ${rows[0].count} 個評分標準模板`);
|
||||
|
||||
// 顯示建立的資料表
|
||||
const [tables] = await connection.query('SHOW TABLES');
|
||||
console.log('📊 已建立的資料表:');
|
||||
tables.forEach(table => {
|
||||
console.log(` - ${Object.values(table)[0]}`);
|
||||
});
|
||||
|
||||
await connection.end();
|
||||
console.log('🎉 資料庫初始化完成!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 資料庫初始化失敗:', error.message);
|
||||
console.error('詳細錯誤:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果直接執行此腳本
|
||||
if (require.main === module) {
|
||||
initializeDatabase();
|
||||
}
|
||||
|
||||
module.exports = { initializeDatabase };
|
93
scripts/init-database.js
Normal file
93
scripts/init-database.js
Normal file
@@ -0,0 +1,93 @@
|
||||
const mysql = require('mysql2/promise');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// 資料庫配置
|
||||
const dbConfig = {
|
||||
host: process.env.DB_HOST || 'mysql.theaken.com',
|
||||
port: parseInt(process.env.DB_PORT || '33306'),
|
||||
user: process.env.DB_USER || 'root',
|
||||
password: process.env.DB_PASSWORD || 'zh6161168',
|
||||
database: process.env.DB_NAME || 'db_AI_scoring',
|
||||
charset: 'utf8mb4',
|
||||
timezone: '+08:00',
|
||||
multipleStatements: true,
|
||||
};
|
||||
|
||||
async function initializeDatabase() {
|
||||
let connection;
|
||||
|
||||
try {
|
||||
console.log('🔄 正在連接資料庫...');
|
||||
|
||||
// 先連接到 MySQL 伺服器(不指定資料庫)
|
||||
const serverConnection = await mysql.createConnection({
|
||||
host: dbConfig.host,
|
||||
port: dbConfig.port,
|
||||
user: dbConfig.user,
|
||||
password: dbConfig.password,
|
||||
charset: dbConfig.charset,
|
||||
timezone: dbConfig.timezone,
|
||||
});
|
||||
|
||||
console.log('✅ 成功連接到 MySQL 伺服器');
|
||||
|
||||
// 讀取 SQL 腳本
|
||||
const schemaPath = path.join(__dirname, '..', 'database', 'schema.sql');
|
||||
const schemaSQL = fs.readFileSync(schemaPath, 'utf8');
|
||||
|
||||
console.log('🔄 正在執行資料庫初始化腳本...');
|
||||
|
||||
// 分割 SQL 語句並逐個執行
|
||||
const statements = schemaSQL
|
||||
.split(';')
|
||||
.map(stmt => stmt.trim())
|
||||
.filter(stmt => stmt.length > 0 && !stmt.startsWith('--'));
|
||||
|
||||
for (const statement of statements) {
|
||||
if (statement.trim()) {
|
||||
try {
|
||||
await serverConnection.execute(statement);
|
||||
console.log(`✅ 執行: ${statement.substring(0, 50)}...`);
|
||||
} catch (error) {
|
||||
console.warn(`⚠️ 跳過語句: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✅ 資料庫初始化完成');
|
||||
|
||||
// 關閉連接
|
||||
await serverConnection.end();
|
||||
|
||||
// 測試新建立的資料庫連接
|
||||
console.log('🔄 正在測試資料庫連接...');
|
||||
connection = await mysql.createConnection(dbConfig);
|
||||
|
||||
// 測試查詢
|
||||
const [rows] = await connection.execute('SELECT COUNT(*) as count FROM criteria_templates');
|
||||
console.log(`✅ 資料庫測試成功,找到 ${rows[0].count} 個評分標準模板`);
|
||||
|
||||
// 顯示建立的資料表
|
||||
const [tables] = await connection.execute('SHOW TABLES');
|
||||
console.log('📊 已建立的資料表:');
|
||||
tables.forEach(table => {
|
||||
console.log(` - ${Object.values(table)[0]}`);
|
||||
});
|
||||
|
||||
await connection.end();
|
||||
console.log('🎉 資料庫初始化完成!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 資料庫初始化失敗:', error.message);
|
||||
console.error('詳細錯誤:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果直接執行此腳本
|
||||
if (require.main === module) {
|
||||
initializeDatabase();
|
||||
}
|
||||
|
||||
module.exports = { initializeDatabase };
|
69
scripts/test-criteria-api.js
Normal file
69
scripts/test-criteria-api.js
Normal file
@@ -0,0 +1,69 @@
|
||||
const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args));
|
||||
|
||||
const API_BASE = 'http://localhost:3000/api';
|
||||
|
||||
async function testCriteriaAPI() {
|
||||
try {
|
||||
console.log('🔄 測試評分標準 API...');
|
||||
|
||||
// 1. 測試獲取預設模板
|
||||
console.log('\n1. 測試獲取預設模板...');
|
||||
const defaultResponse = await fetch(`${API_BASE}/criteria-templates/default`);
|
||||
const defaultData = await defaultResponse.json();
|
||||
|
||||
if (defaultData.success) {
|
||||
console.log('✅ 預設模板獲取成功');
|
||||
console.log(` 模板名稱: ${defaultData.data.name}`);
|
||||
console.log(` 評分項目數量: ${defaultData.data.items.length}`);
|
||||
defaultData.data.items.forEach((item, index) => {
|
||||
console.log(` ${index + 1}. ${item.name} (權重: ${item.weight}%)`);
|
||||
});
|
||||
} else {
|
||||
console.log('❌ 預設模板獲取失敗:', defaultData.error);
|
||||
}
|
||||
|
||||
// 2. 測試創建新模板
|
||||
console.log('\n2. 測試創建新模板...');
|
||||
const newTemplate = {
|
||||
name: '測試模板',
|
||||
description: '這是一個測試模板',
|
||||
items: [
|
||||
{ name: '測試項目1', description: '測試描述1', weight: 50, maxScore: 10 },
|
||||
{ name: '測試項目2', description: '測試描述2', weight: 50, maxScore: 10 }
|
||||
]
|
||||
};
|
||||
|
||||
const createResponse = await fetch(`${API_BASE}/criteria-templates`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(newTemplate)
|
||||
});
|
||||
const createData = await createResponse.json();
|
||||
|
||||
if (createData.success) {
|
||||
console.log('✅ 新模板創建成功');
|
||||
console.log(` 模板 ID: ${createData.data.id}`);
|
||||
} else {
|
||||
console.log('❌ 新模板創建失敗:', createData.error);
|
||||
}
|
||||
|
||||
// 3. 測試獲取所有模板
|
||||
console.log('\n3. 測試獲取所有模板...');
|
||||
const allResponse = await fetch(`${API_BASE}/criteria-templates`);
|
||||
const allData = await allResponse.json();
|
||||
|
||||
if (allData.success) {
|
||||
console.log('✅ 所有模板獲取成功');
|
||||
console.log(` 模板總數: ${allData.data.length}`);
|
||||
} else {
|
||||
console.log('❌ 所有模板獲取失敗:', allData.error);
|
||||
}
|
||||
|
||||
console.log('\n🎉 API 測試完成!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 測試失敗:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
testCriteriaAPI();
|
54
scripts/test-database.js
Normal file
54
scripts/test-database.js
Normal file
@@ -0,0 +1,54 @@
|
||||
const mysql = require('mysql2/promise');
|
||||
|
||||
// 資料庫配置
|
||||
const dbConfig = {
|
||||
host: process.env.DB_HOST || 'mysql.theaken.com',
|
||||
port: parseInt(process.env.DB_PORT || '33306'),
|
||||
user: process.env.DB_USER || 'root',
|
||||
password: process.env.DB_PASSWORD || 'zh6161168',
|
||||
database: process.env.DB_NAME || 'db_AI_scoring',
|
||||
charset: 'utf8mb4',
|
||||
timezone: '+08:00',
|
||||
};
|
||||
|
||||
async function testDatabase() {
|
||||
let connection;
|
||||
|
||||
try {
|
||||
console.log('🔄 正在測試資料庫連接...');
|
||||
|
||||
connection = await mysql.createConnection(dbConfig);
|
||||
|
||||
// 測試基本連接
|
||||
await connection.ping();
|
||||
console.log('✅ 資料庫連接成功');
|
||||
|
||||
// 測試查詢
|
||||
const [rows] = await connection.query('SELECT COUNT(*) as count FROM criteria_templates');
|
||||
console.log(`✅ 找到 ${rows[0].count} 個評分標準模板`);
|
||||
|
||||
// 顯示所有資料表
|
||||
const [tables] = await connection.query('SHOW TABLES');
|
||||
console.log('📊 資料庫中的資料表:');
|
||||
tables.forEach(table => {
|
||||
console.log(` - ${Object.values(table)[0]}`);
|
||||
});
|
||||
|
||||
// 測試預設數據
|
||||
const [criteriaItems] = await connection.query('SELECT * FROM criteria_items ORDER BY sort_order');
|
||||
console.log('📋 預設評分項目:');
|
||||
criteriaItems.forEach(item => {
|
||||
console.log(` - ${item.name} (權重: ${item.weight}%, 滿分: ${item.max_score})`);
|
||||
});
|
||||
|
||||
await connection.end();
|
||||
console.log('🎉 資料庫測試完成!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 資料庫測試失敗:', error.message);
|
||||
console.error('詳細錯誤:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
testDatabase();
|
79
scripts/test-single-template.js
Normal file
79
scripts/test-single-template.js
Normal file
@@ -0,0 +1,79 @@
|
||||
const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args));
|
||||
|
||||
const API_BASE = 'http://localhost:3000/api';
|
||||
|
||||
async function testSingleTemplateMode() {
|
||||
try {
|
||||
console.log('🔄 測試單一模板模式...');
|
||||
|
||||
// 1. 測試獲取現有模板
|
||||
console.log('\n1. 測試獲取現有模板...');
|
||||
const getResponse = await fetch(`${API_BASE}/criteria-templates`);
|
||||
const getData = await getResponse.json();
|
||||
|
||||
if (getData.success) {
|
||||
console.log(`✅ 找到 ${getData.data.length} 個模板`);
|
||||
if (getData.data.length > 0) {
|
||||
const template = getData.data[0];
|
||||
console.log(` 模板名稱: ${template.name}`);
|
||||
console.log(` 評分項目數量: ${template.items.length}`);
|
||||
}
|
||||
} else {
|
||||
console.log('❌ 獲取模板失敗:', getData.error);
|
||||
}
|
||||
|
||||
// 2. 測試覆蓋模板
|
||||
console.log('\n2. 測試覆蓋模板...');
|
||||
const templateData = {
|
||||
name: '我的評分標準',
|
||||
description: '這是我的自定義評分標準',
|
||||
items: [
|
||||
{ name: '內容品質', description: '內容的準確性和完整性', weight: 30, maxScore: 10 },
|
||||
{ name: '視覺設計', description: '版面設計和視覺效果', weight: 25, maxScore: 10 },
|
||||
{ name: '邏輯結構', description: '內容組織的邏輯性', weight: 25, maxScore: 10 },
|
||||
{ name: '創新性', description: '創意思維的展現', weight: 20, maxScore: 10 }
|
||||
]
|
||||
};
|
||||
|
||||
const saveResponse = await fetch(`${API_BASE}/criteria-templates`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(templateData)
|
||||
});
|
||||
const saveData = await saveResponse.json();
|
||||
|
||||
if (saveData.success) {
|
||||
console.log('✅ 模板覆蓋成功');
|
||||
console.log(` 模板 ID: ${saveData.data.id}`);
|
||||
} else {
|
||||
console.log('❌ 模板覆蓋失敗:', saveData.error);
|
||||
}
|
||||
|
||||
// 3. 驗證覆蓋結果
|
||||
console.log('\n3. 驗證覆蓋結果...');
|
||||
const verifyResponse = await fetch(`${API_BASE}/criteria-templates`);
|
||||
const verifyData = await verifyResponse.json();
|
||||
|
||||
if (verifyData.success) {
|
||||
console.log(`✅ 驗證成功,現在有 ${verifyData.data.length} 個模板`);
|
||||
if (verifyData.data.length > 0) {
|
||||
const template = verifyData.data[0];
|
||||
console.log(` 模板名稱: ${template.name}`);
|
||||
console.log(` 模板描述: ${template.description}`);
|
||||
console.log(` 評分項目數量: ${template.items.length}`);
|
||||
template.items.forEach((item, index) => {
|
||||
console.log(` ${index + 1}. ${item.name} (權重: ${item.weight}%)`);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.log('❌ 驗證失敗:', verifyData.error);
|
||||
}
|
||||
|
||||
console.log('\n🎉 單一模板模式測試完成!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 測試失敗:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
testSingleTemplateMode();
|
23
scripts/test-weight-display.js
Normal file
23
scripts/test-weight-display.js
Normal file
@@ -0,0 +1,23 @@
|
||||
// 測試權重顯示格式
|
||||
const testWeights = [25.00, 20.00, 20.00, 15.00, 20.00];
|
||||
|
||||
console.log('🔄 測試權重顯示格式...');
|
||||
|
||||
// 計算總權重
|
||||
const totalWeight = testWeights.reduce((sum, weight) => sum + weight, 0);
|
||||
|
||||
console.log('個別權重:');
|
||||
testWeights.forEach((weight, index) => {
|
||||
console.log(` ${index + 1}. ${weight.toFixed(1)}%`);
|
||||
});
|
||||
|
||||
console.log(`\n總權重: ${totalWeight.toFixed(1)}%`);
|
||||
console.log(`是否等於 100%: ${totalWeight === 100 ? '✅' : '❌'}`);
|
||||
|
||||
// 測試權重顯示的各種格式
|
||||
console.log('\n權重顯示格式測試:');
|
||||
console.log(`原始格式: ${totalWeight}%`);
|
||||
console.log(`toFixed(1): ${totalWeight.toFixed(1)}%`);
|
||||
console.log(`toFixed(0): ${totalWeight.toFixed(0)}%`);
|
||||
|
||||
console.log('\n🎉 權重顯示格式測試完成!');
|
41
scripts/test-weight-fix.js
Normal file
41
scripts/test-weight-fix.js
Normal file
@@ -0,0 +1,41 @@
|
||||
// 測試權重修復
|
||||
console.log('🔄 測試權重修復...');
|
||||
|
||||
// 模擬可能出現的權重數據類型
|
||||
const testCases = [
|
||||
{ weight: 25.00 }, // 正常數字
|
||||
{ weight: "25.00" }, // 字符串數字
|
||||
{ weight: "25" }, // 字符串整數
|
||||
{ weight: null }, // null 值
|
||||
{ weight: undefined }, // undefined 值
|
||||
{ weight: "" }, // 空字符串
|
||||
{ weight: "abc" }, // 非數字字符串
|
||||
];
|
||||
|
||||
console.log('\n測試各種權重數據類型:');
|
||||
testCases.forEach((item, index) => {
|
||||
const originalWeight = item.weight;
|
||||
const safeWeight = Number(item.weight) || 0;
|
||||
const formattedWeight = safeWeight.toFixed(1);
|
||||
|
||||
console.log(`${index + 1}. 原始: ${originalWeight} (${typeof originalWeight})`);
|
||||
console.log(` 安全轉換: ${safeWeight} (${typeof safeWeight})`);
|
||||
console.log(` 格式化: ${formattedWeight}%`);
|
||||
console.log('');
|
||||
});
|
||||
|
||||
// 測試總權重計算
|
||||
console.log('測試總權重計算:');
|
||||
const criteria = [
|
||||
{ weight: 25.00 },
|
||||
{ weight: "20.00" },
|
||||
{ weight: 20 },
|
||||
{ weight: "15.00" },
|
||||
{ weight: null },
|
||||
];
|
||||
|
||||
const totalWeight = criteria.reduce((sum, item) => sum + (Number(item.weight) || 0), 0);
|
||||
console.log(`總權重: ${totalWeight} (${typeof totalWeight})`);
|
||||
console.log(`格式化總權重: ${Number(totalWeight).toFixed(1)}%`);
|
||||
|
||||
console.log('\n🎉 權重修復測試完成!');
|
Reference in New Issue
Block a user