新增評分項目設定、資料庫整合

This commit is contained in:
2025-09-22 00:33:12 +08:00
parent 8de09129be
commit 9d4c586ad3
20 changed files with 2321 additions and 79 deletions

View 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 }
);
}
}

View 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 }
);
}
}

View 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 }
);
}
}

View File

@@ -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 儲存評分標準
toast({
title: "儲存成功",
description: "評分標準已成功儲存",
})
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("預設評分標準")
toast({
title: "已重置",
description: "評分標準已重置為預設值",
})
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>
<Input
value={templateName}
onChange={(e) => setTemplateName(e.target.value)}
placeholder="輸入評分標準名稱"
className="max-w-md"
/>
<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="輸入評分標準名稱"
/>
</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">
<Save className="h-4 w-4 mr-2" />
<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>

View File

@@ -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,

View File

@@ -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
View 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
View 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
View 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
View 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
View 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]);
}
}

View File

@@ -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
View File

@@ -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: {}

View 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
View 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 };

View 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
View 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();

View 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();

View 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🎉 權重顯示格式測試完成!');

View 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🎉 權重修復測試完成!');