完成評審評分機制
This commit is contained in:
@@ -27,10 +27,10 @@ interface DatabaseConfig {
|
||||
|
||||
// 主機資料庫配置
|
||||
const masterConfig = {
|
||||
host: process.env.DB_HOST || 'mysql.theaken.com',
|
||||
port: parseInt(process.env.DB_PORT || '33306'),
|
||||
user: process.env.DB_USER || 'AI_Platform',
|
||||
password: process.env.DB_PASSWORD || 'Aa123456',
|
||||
host: process.env.DB_HOST || '122.100.99.161',
|
||||
port: parseInt(process.env.DB_PORT || '43306'),
|
||||
user: process.env.DB_USER || 'A999',
|
||||
password: process.env.DB_PASSWORD || '1023',
|
||||
database: process.env.DB_NAME || 'db_AI_Platform',
|
||||
charset: 'utf8mb4',
|
||||
timezone: '+08:00',
|
||||
@@ -45,10 +45,10 @@ const masterConfig = {
|
||||
|
||||
// 備機資料庫配置
|
||||
const slaveConfig = {
|
||||
host: process.env.SLAVE_DB_HOST || '122.100.99.161',
|
||||
port: parseInt(process.env.SLAVE_DB_PORT || '43306'),
|
||||
user: process.env.SLAVE_DB_USER || 'A999',
|
||||
password: process.env.SLAVE_DB_PASSWORD || '1023',
|
||||
host: process.env.SLAVE_DB_HOST || 'mysql.theaken.com',
|
||||
port: parseInt(process.env.SLAVE_DB_PORT || '33306'),
|
||||
user: process.env.SLAVE_DB_USER || 'AI_Platform',
|
||||
password: process.env.SLAVE_DB_PASSWORD || 'Aa123456',
|
||||
database: process.env.SLAVE_DB_NAME || 'db_AI_Platform', // 修正為 AI 平台資料庫
|
||||
charset: 'utf8mb4',
|
||||
timezone: '+08:00',
|
||||
|
@@ -8,10 +8,10 @@ import { dbSync } from './database-sync';
|
||||
|
||||
// 資料庫配置
|
||||
const dbConfig = {
|
||||
host: process.env.DB_HOST || 'mysql.theaken.com',
|
||||
port: parseInt(process.env.DB_PORT || '33306'),
|
||||
user: process.env.DB_USER || 'AI_Platform',
|
||||
password: process.env.DB_PASSWORD || 'Aa123456',
|
||||
host: process.env.DB_HOST || '122.100.99.161',
|
||||
port: parseInt(process.env.DB_PORT || '43306'),
|
||||
user: process.env.DB_USER || 'A999',
|
||||
password: process.env.DB_PASSWORD || '1023',
|
||||
database: process.env.DB_NAME || 'db_AI_Platform',
|
||||
charset: 'utf8mb4',
|
||||
timezone: '+08:00',
|
||||
|
@@ -748,7 +748,7 @@ export class UserService {
|
||||
}
|
||||
|
||||
// 靜態方法保持向後兼容
|
||||
static async createUser(userData: Omit<User, 'id' | 'created_at' | 'updated_at'>): Promise<User> {
|
||||
static async createUser(userData: Omit<User, 'id' | 'created_at' | 'updated_at'> & { id?: string }): Promise<User> {
|
||||
const service = new UserService();
|
||||
return await service.create(userData);
|
||||
}
|
||||
@@ -932,6 +932,44 @@ export class JudgeService {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 獲取評審的評分任務
|
||||
static async getJudgeScoringTasks(judgeId: string, competitionId?: string): Promise<any[]> {
|
||||
let sql = `
|
||||
SELECT DISTINCT
|
||||
a.id,
|
||||
a.name,
|
||||
'app' as type,
|
||||
'individual' as participant_type,
|
||||
COALESCE(js.total_score, 0) as score,
|
||||
CASE
|
||||
WHEN js.total_score > 0 THEN 'completed'
|
||||
ELSE 'pending'
|
||||
END as status,
|
||||
js.submitted_at,
|
||||
t.name as team_name,
|
||||
CONCAT(COALESCE(t.name, '未知團隊'), ' - ', a.name) as display_name
|
||||
FROM apps a
|
||||
LEFT JOIN teams t ON a.team_id = t.id
|
||||
LEFT JOIN competition_apps ca ON a.id = ca.app_id
|
||||
LEFT JOIN judge_scores js ON a.id = js.app_id AND js.judge_id = ?
|
||||
WHERE ca.competition_id = ?
|
||||
`;
|
||||
|
||||
const params = [judgeId];
|
||||
|
||||
if (competitionId) {
|
||||
params.push(competitionId);
|
||||
} else {
|
||||
// 如果沒有指定競賽,獲取所有競賽的任務
|
||||
sql = sql.replace('WHERE ca.competition_id = ?', 'WHERE ca.competition_id IS NOT NULL');
|
||||
}
|
||||
|
||||
sql += ' ORDER BY a.name';
|
||||
|
||||
const results = await db.query(sql, params);
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
@@ -1722,16 +1760,6 @@ export class CompetitionService {
|
||||
return result.affectedRows > 0;
|
||||
}
|
||||
|
||||
// 獲取競賽的評分規則列表
|
||||
static async getCompetitionRules(competitionId: string): Promise<any[]> {
|
||||
const sql = `
|
||||
SELECT cr.*
|
||||
FROM competition_rules cr
|
||||
WHERE cr.competition_id = ?
|
||||
ORDER BY cr.order_index ASC, cr.created_at ASC
|
||||
`;
|
||||
return await db.query(sql, [competitionId]);
|
||||
}
|
||||
|
||||
// 為競賽添加評分規則
|
||||
static async addCompetitionRules(competitionId: string, rules: any[]): Promise<boolean> {
|
||||
@@ -1797,7 +1825,7 @@ export class CompetitionService {
|
||||
this.getCompetitionTeams(competitionId),
|
||||
this.getCompetitionApps(competitionId),
|
||||
this.getCompetitionAwardTypes(competitionId),
|
||||
this.getCompetitionRules(competitionId)
|
||||
ScoringService.getCompetitionRules(competitionId)
|
||||
]);
|
||||
|
||||
// 根據日期動態計算競賽狀態
|
||||
@@ -3186,7 +3214,7 @@ export class AppService {
|
||||
|
||||
apps.push({
|
||||
id: appId,
|
||||
name: appDetails.appName || '未知應用',
|
||||
name: (appDetails as any).appName || '未知應用',
|
||||
description: '應用描述不可用',
|
||||
category: '未分類',
|
||||
type: '未分類',
|
||||
@@ -3322,7 +3350,7 @@ export class AppService {
|
||||
details = activity.details;
|
||||
}
|
||||
}
|
||||
category = details.category || '未分類';
|
||||
category = (details as any).category || '未分類';
|
||||
}
|
||||
|
||||
if (!categoryCount[category]) {
|
||||
@@ -3332,12 +3360,12 @@ export class AppService {
|
||||
categoryCount[category].uniqueApps.add(activity.resource_id);
|
||||
} catch (error) {
|
||||
// 出錯時使用默認類別
|
||||
const category = '未分類';
|
||||
if (!categoryCount[category]) {
|
||||
categoryCount[category] = { count: 0, uniqueApps: new Set() };
|
||||
const defaultCategory = '未分類';
|
||||
if (!categoryCount[defaultCategory]) {
|
||||
categoryCount[defaultCategory] = { count: 0, uniqueApps: new Set() };
|
||||
}
|
||||
categoryCount[category].count++;
|
||||
categoryCount[category].uniqueApps.add(activity.resource_id);
|
||||
categoryCount[defaultCategory].count++;
|
||||
categoryCount[defaultCategory].uniqueApps.add(activity.resource_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3419,35 +3447,443 @@ export class AppService {
|
||||
// 評分服務
|
||||
// =====================================================
|
||||
export class ScoringService {
|
||||
// 提交應用評分
|
||||
static async submitAppScore(scoreData: Omit<AppJudgeScore, 'id' | 'submitted_at'>): Promise<AppJudgeScore> {
|
||||
const sql = `
|
||||
INSERT INTO app_judge_scores (id, judge_id, app_id, innovation_score, technical_score, usability_score, presentation_score, impact_score, total_score, comments)
|
||||
VALUES (UUID(), ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
innovation_score = VALUES(innovation_score),
|
||||
technical_score = VALUES(technical_score),
|
||||
usability_score = VALUES(usability_score),
|
||||
presentation_score = VALUES(presentation_score),
|
||||
impact_score = VALUES(impact_score),
|
||||
total_score = VALUES(total_score),
|
||||
comments = VALUES(comments),
|
||||
submitted_at = CURRENT_TIMESTAMP
|
||||
`;
|
||||
const params = [
|
||||
scoreData.judge_id,
|
||||
scoreData.app_id,
|
||||
scoreData.innovation_score,
|
||||
scoreData.technical_score,
|
||||
scoreData.usability_score,
|
||||
scoreData.presentation_score,
|
||||
scoreData.impact_score,
|
||||
scoreData.total_score,
|
||||
scoreData.comments || null
|
||||
];
|
||||
// 獲取競賽規則
|
||||
static async getCompetitionRules(competitionId: string): Promise<any[]> {
|
||||
console.log('🔍 獲取競賽規則,competitionId:', competitionId);
|
||||
|
||||
await db.insert(sql, params);
|
||||
return await this.getAppScore(scoreData.judge_id, scoreData.app_id) as AppJudgeScore;
|
||||
const sql = `
|
||||
SELECT id, name, description, weight, order_index
|
||||
FROM competition_rules
|
||||
WHERE competition_id = ?
|
||||
ORDER BY order_index ASC
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await db.query(sql, [competitionId]);
|
||||
console.log('🔍 競賽規則查詢結果:', result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('❌ 獲取競賽規則失敗:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 根據APP ID獲取競賽ID
|
||||
static async getCompetitionIdByAppId(appId: string): Promise<string | null> {
|
||||
const sql = `
|
||||
SELECT ca.competition_id
|
||||
FROM competition_apps ca
|
||||
WHERE ca.app_id = ?
|
||||
`;
|
||||
const result = await db.query(sql, [appId]);
|
||||
return result.length > 0 ? result[0].competition_id : null;
|
||||
}
|
||||
|
||||
// 提交應用評分(基於競賽規則的動態評分)
|
||||
static async submitAppScore(scoreData: any): Promise<any> {
|
||||
console.log('🔍 開始提交評分,數據:', scoreData);
|
||||
|
||||
const { judge_id, app_id, competition_id, scores, total_score, comments, isEdit, recordId } = scoreData;
|
||||
|
||||
// 驗證必要參數
|
||||
console.log('🔍 參數驗證:');
|
||||
console.log('judge_id:', judge_id, typeof judge_id);
|
||||
console.log('app_id:', app_id, typeof app_id);
|
||||
console.log('competition_id:', competition_id, typeof competition_id);
|
||||
console.log('scores:', scores, typeof scores);
|
||||
console.log('total_score:', total_score, typeof total_score);
|
||||
|
||||
if (!judge_id || !app_id || !competition_id || !scores || total_score === undefined) {
|
||||
throw new Error('缺少必要的評分參數');
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. 生成唯一的評分記錄ID
|
||||
const judgeScoreId = `js_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
console.log('🔍 生成的評分記錄ID:', judgeScoreId);
|
||||
|
||||
let finalJudgeScoreId;
|
||||
|
||||
if (isEdit && recordId) {
|
||||
// 編輯模式:使用傳入的記錄ID
|
||||
finalJudgeScoreId = recordId;
|
||||
console.log('🔍 編輯模式,使用記錄ID:', finalJudgeScoreId);
|
||||
} else {
|
||||
// 新增模式:檢查是否已存在評分記錄
|
||||
const existingScore = await db.query(
|
||||
'SELECT id FROM judge_scores WHERE judge_id = ? AND app_id = ?',
|
||||
[judge_id, app_id]
|
||||
);
|
||||
|
||||
if (existingScore.length > 0) {
|
||||
// 使用現有的評分記錄ID
|
||||
finalJudgeScoreId = existingScore[0].id;
|
||||
console.log('🔍 使用現有評分記錄ID:', finalJudgeScoreId);
|
||||
} else {
|
||||
// 創建新記錄
|
||||
finalJudgeScoreId = `js_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
console.log('🔍 創建新評分記錄ID:', finalJudgeScoreId);
|
||||
}
|
||||
}
|
||||
|
||||
// 檢查記錄是否存在,決定是更新還是插入
|
||||
const existingRecord = await db.query(
|
||||
'SELECT id FROM judge_scores WHERE id = ?',
|
||||
[finalJudgeScoreId]
|
||||
);
|
||||
|
||||
if (existingRecord.length > 0) {
|
||||
// 更新現有記錄
|
||||
await db.update(`
|
||||
UPDATE judge_scores
|
||||
SET total_score = ?, comments = ?, submitted_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
`, [total_score, comments || null, finalJudgeScoreId]);
|
||||
console.log('🔍 更新現有評分記錄');
|
||||
} else {
|
||||
// 檢查是否已有相同 judge_id + app_id + competition_id 的記錄
|
||||
const duplicateRecord = await db.query(
|
||||
'SELECT id FROM judge_scores WHERE judge_id = ? AND app_id = ? AND competition_id = ?',
|
||||
[judge_id, app_id, competition_id]
|
||||
);
|
||||
|
||||
if (duplicateRecord.length > 0) {
|
||||
// 更新現有記錄
|
||||
finalJudgeScoreId = duplicateRecord[0].id;
|
||||
await db.update(`
|
||||
UPDATE judge_scores
|
||||
SET total_score = ?, comments = ?, submitted_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
`, [total_score, comments || null, finalJudgeScoreId]);
|
||||
console.log('🔍 更新重複的評分記錄');
|
||||
} else {
|
||||
// 創建新記錄
|
||||
await db.insert(`
|
||||
INSERT INTO judge_scores (id, judge_id, app_id, competition_id, total_score, comments)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`, [
|
||||
finalJudgeScoreId,
|
||||
judge_id,
|
||||
app_id,
|
||||
competition_id,
|
||||
total_score,
|
||||
comments || null
|
||||
]);
|
||||
console.log('🔍 創建新評分記錄');
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 獲取競賽規則
|
||||
const rules = await this.getCompetitionRules(competition_id);
|
||||
console.log('🔍 競賽規則:', rules);
|
||||
|
||||
// 3. 刪除現有的評分詳情
|
||||
await db.delete(
|
||||
'DELETE FROM judge_score_details WHERE judge_score_id = ?',
|
||||
[finalJudgeScoreId]
|
||||
);
|
||||
|
||||
// 4. 插入新的評分詳情
|
||||
for (const [ruleName, score] of Object.entries(scores)) {
|
||||
if (typeof score === 'number' && score > 0) {
|
||||
// 找到對應的規則
|
||||
const rule = rules.find((r: any) => r.name === ruleName);
|
||||
console.log(`🔍 尋找規則 ${ruleName}:`, rule);
|
||||
|
||||
if (rule) {
|
||||
const detailId = `jsd_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
console.log('🔍 插入評分詳情:', {
|
||||
detailId,
|
||||
finalJudgeScoreId,
|
||||
ruleId: rule.id,
|
||||
ruleName: rule.name,
|
||||
score,
|
||||
weight: rule.weight
|
||||
});
|
||||
|
||||
await db.insert(`
|
||||
INSERT INTO judge_score_details (id, judge_score_id, rule_id, rule_name, score, weight)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`, [
|
||||
detailId,
|
||||
finalJudgeScoreId,
|
||||
rule.id,
|
||||
rule.name,
|
||||
score,
|
||||
rule.weight
|
||||
]);
|
||||
} else {
|
||||
console.log(`⚠️ 找不到規則: ${ruleName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 返回完整的評分記錄
|
||||
return await this.getJudgeScoreById(finalJudgeScoreId);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 提交評分失敗:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 根據ID獲取評分記錄
|
||||
static async getJudgeScoreById(judgeScoreId: string): Promise<any> {
|
||||
const sql = `
|
||||
SELECT
|
||||
js.*,
|
||||
GROUP_CONCAT(
|
||||
CONCAT(jsd.rule_name, ':', jsd.score, ':', jsd.weight)
|
||||
SEPARATOR '|'
|
||||
) as score_details
|
||||
FROM judge_scores js
|
||||
LEFT JOIN judge_score_details jsd ON js.id = jsd.judge_score_id
|
||||
WHERE js.id = ?
|
||||
GROUP BY js.id
|
||||
`;
|
||||
|
||||
const result = await db.query(sql, [judgeScoreId]);
|
||||
return result.length > 0 ? result[0] : null;
|
||||
}
|
||||
|
||||
// 獲取競賽評分進度
|
||||
static async getCompetitionScoringProgress(competitionId: string): Promise<{
|
||||
completed: number;
|
||||
total: number;
|
||||
percentage: number;
|
||||
}> {
|
||||
try {
|
||||
console.log('🔍 獲取競賽評分進度,competitionId:', competitionId);
|
||||
|
||||
// 獲取競賽的評審數量
|
||||
const judgesResult = await db.query(`
|
||||
SELECT COUNT(DISTINCT cj.judge_id) as judge_count
|
||||
FROM competition_judges cj
|
||||
WHERE cj.competition_id = ?
|
||||
`, [competitionId]);
|
||||
|
||||
const judgeCount = judgesResult[0]?.judge_count || 0;
|
||||
console.log('🔍 評審數量:', judgeCount);
|
||||
|
||||
// 獲取競賽的參賽APP數量
|
||||
const appsResult = await db.query(`
|
||||
SELECT COUNT(DISTINCT ca.app_id) as app_count
|
||||
FROM competition_apps ca
|
||||
WHERE ca.competition_id = ?
|
||||
`, [competitionId]);
|
||||
|
||||
const appCount = appsResult[0]?.app_count || 0;
|
||||
console.log('🔍 參賽APP數量:', appCount);
|
||||
|
||||
// 如果沒有評審或APP關聯,嘗試從其他方式獲取
|
||||
let finalJudgeCount = judgeCount;
|
||||
let finalAppCount = appCount;
|
||||
|
||||
if (judgeCount === 0) {
|
||||
// 嘗試從 judges 表獲取所有評審
|
||||
const allJudgesResult = await db.query('SELECT COUNT(*) as judge_count FROM judges');
|
||||
finalJudgeCount = allJudgesResult[0]?.judge_count || 0;
|
||||
console.log('🔍 使用所有評審數量:', finalJudgeCount);
|
||||
}
|
||||
|
||||
if (appCount === 0) {
|
||||
// 嘗試從 apps 表獲取所有APP
|
||||
const allAppsResult = await db.query('SELECT COUNT(*) as app_count FROM apps');
|
||||
finalAppCount = allAppsResult[0]?.app_count || 0;
|
||||
console.log('🔍 使用所有APP數量:', finalAppCount);
|
||||
}
|
||||
|
||||
// 獲取已完成的評分數量
|
||||
const completedResult = await db.query(`
|
||||
SELECT COUNT(*) as completed_count
|
||||
FROM judge_scores js
|
||||
WHERE js.competition_id = ?
|
||||
`, [competitionId]);
|
||||
|
||||
const completed = completedResult[0]?.completed_count || 0;
|
||||
const total = finalJudgeCount * finalAppCount;
|
||||
const percentage = total > 0 ? Math.min(Math.round((completed / total) * 100), 100) : 0;
|
||||
|
||||
console.log('🔍 評分進度結果:', { completed, total, percentage });
|
||||
|
||||
return {
|
||||
completed,
|
||||
total,
|
||||
percentage
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('獲取評分進度失敗:', error);
|
||||
return { completed: 0, total: 0, percentage: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
// 獲取評分完成度匯總
|
||||
static async getScoringSummary(competitionId: string): Promise<{
|
||||
judges: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
completedCount: number;
|
||||
totalCount: number;
|
||||
completionRate: number;
|
||||
status: 'completed' | 'partial' | 'not_started';
|
||||
lastScoredAt?: string;
|
||||
}>;
|
||||
apps: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
teamName?: string;
|
||||
scoredCount: number;
|
||||
totalJudges: number;
|
||||
completionRate: number;
|
||||
status: 'completed' | 'partial' | 'not_scored';
|
||||
averageScore?: number;
|
||||
}>;
|
||||
overallStats: {
|
||||
totalJudges: number;
|
||||
totalApps: number;
|
||||
totalPossibleScores: number;
|
||||
completedScores: number;
|
||||
overallCompletionRate: number;
|
||||
};
|
||||
}> {
|
||||
try {
|
||||
console.log('🔍 獲取評分完成度匯總,competitionId:', competitionId);
|
||||
|
||||
// 獲取競賽的評審列表 - 先嘗試從關聯表獲取,如果沒有則獲取所有評審
|
||||
let judgesResult = await db.query(`
|
||||
SELECT j.id, j.name
|
||||
FROM judges j
|
||||
LEFT JOIN competition_judges cj ON j.id = cj.judge_id
|
||||
WHERE cj.competition_id = ?
|
||||
ORDER BY j.name
|
||||
`, [competitionId]);
|
||||
|
||||
// 如果沒有關聯的評審,獲取所有評審
|
||||
if (judgesResult.length === 0) {
|
||||
judgesResult = await db.query(`
|
||||
SELECT id, name FROM judges ORDER BY name
|
||||
`);
|
||||
}
|
||||
|
||||
// 獲取競賽的APP列表 - 先嘗試從關聯表獲取,如果沒有則獲取所有APP
|
||||
let appsResult = await db.query(`
|
||||
SELECT a.id, a.name, t.name as team_name
|
||||
FROM apps a
|
||||
LEFT JOIN competition_apps ca ON a.id = ca.app_id
|
||||
LEFT JOIN teams t ON a.team_id = t.id
|
||||
WHERE ca.competition_id = ?
|
||||
ORDER BY a.name
|
||||
`, [competitionId]);
|
||||
|
||||
// 如果沒有關聯的APP,獲取所有APP
|
||||
if (appsResult.length === 0) {
|
||||
appsResult = await db.query(`
|
||||
SELECT a.id, a.name, t.name as team_name
|
||||
FROM apps a
|
||||
LEFT JOIN teams t ON a.team_id = t.id
|
||||
ORDER BY a.name
|
||||
`);
|
||||
}
|
||||
|
||||
// 獲取已完成的評分記錄
|
||||
const scoresResult = await db.query(`
|
||||
SELECT js.judge_id, js.app_id, js.total_score, js.submitted_at,
|
||||
j.name as judge_name, a.name as app_name
|
||||
FROM judge_scores js
|
||||
LEFT JOIN judges j ON js.judge_id = j.id
|
||||
LEFT JOIN apps a ON js.app_id = a.id
|
||||
WHERE js.competition_id = ?
|
||||
ORDER BY js.submitted_at DESC
|
||||
`, [competitionId]);
|
||||
|
||||
const judges = judgesResult.map((judge: any) => {
|
||||
const judgeScores = scoresResult.filter((score: any) => score.judge_id === judge.id);
|
||||
const completedCount = judgeScores.length;
|
||||
const totalCount = appsResult.length;
|
||||
const completionRate = totalCount > 0 ? Math.min(Math.round((completedCount / totalCount) * 100), 100) : 0;
|
||||
|
||||
let status: 'completed' | 'partial' | 'not_started' = 'not_started';
|
||||
if (completedCount === totalCount && totalCount > 0) status = 'completed';
|
||||
else if (completedCount > 0) status = 'partial';
|
||||
|
||||
const lastScoredAt = judgeScores.length > 0 ?
|
||||
judgeScores[0].submitted_at : undefined;
|
||||
|
||||
return {
|
||||
id: judge.id,
|
||||
name: judge.name,
|
||||
email: '', // judges 表沒有 email 字段
|
||||
completedCount,
|
||||
totalCount,
|
||||
completionRate,
|
||||
status,
|
||||
lastScoredAt
|
||||
};
|
||||
});
|
||||
|
||||
const apps = appsResult.map((app: any) => {
|
||||
const appScores = scoresResult.filter((score: any) => score.app_id === app.id);
|
||||
const scoredCount = appScores.length;
|
||||
const totalJudges = judges.length;
|
||||
const completionRate = totalJudges > 0 ? Math.min(Math.round((scoredCount / totalJudges) * 100), 100) : 0;
|
||||
|
||||
let status: 'completed' | 'partial' | 'not_scored' = 'not_scored';
|
||||
if (scoredCount >= totalJudges && totalJudges > 0) status = 'completed';
|
||||
else if (scoredCount > 0) status = 'partial';
|
||||
|
||||
const averageScore = appScores.length > 0 ?
|
||||
Math.round(appScores.reduce((sum: number, score: any) => sum + (score.total_score || 0), 0) / appScores.length) : undefined;
|
||||
|
||||
return {
|
||||
id: app.id,
|
||||
name: app.name,
|
||||
teamName: app.team_name,
|
||||
scoredCount,
|
||||
totalJudges,
|
||||
completionRate,
|
||||
status,
|
||||
averageScore
|
||||
};
|
||||
});
|
||||
|
||||
const totalJudges = judges.length;
|
||||
const totalApps = apps.length;
|
||||
const totalPossibleScores = totalJudges * totalApps;
|
||||
const completedScores = scoresResult.length;
|
||||
const overallCompletionRate = totalPossibleScores > 0 ?
|
||||
Math.min(Math.round((completedScores / totalPossibleScores) * 100), 100) : 0;
|
||||
|
||||
const summary = {
|
||||
judges,
|
||||
apps,
|
||||
overallStats: {
|
||||
totalJudges,
|
||||
totalApps,
|
||||
totalPossibleScores,
|
||||
completedScores,
|
||||
overallCompletionRate
|
||||
}
|
||||
};
|
||||
|
||||
console.log('🔍 評分完成度匯總結果:', summary);
|
||||
|
||||
return summary;
|
||||
} catch (error) {
|
||||
console.error('獲取評分完成度匯總失敗:', error);
|
||||
return {
|
||||
judges: [],
|
||||
apps: [],
|
||||
overallStats: {
|
||||
totalJudges: 0,
|
||||
totalApps: 0,
|
||||
totalPossibleScores: 0,
|
||||
completedScores: 0,
|
||||
overallCompletionRate: 0
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 獲取應用評分
|
||||
@@ -3504,6 +3940,291 @@ export class ScoringService {
|
||||
const sql = 'SELECT * FROM proposal_judge_scores WHERE proposal_id = ? ORDER BY submitted_at DESC';
|
||||
return await db.query<ProposalJudgeScore>(sql, [proposalId]);
|
||||
}
|
||||
|
||||
// 獲取競賽的所有評分記錄(包含評審和參賽者信息)
|
||||
static async getCompetitionScores(competitionId: string): Promise<any[]> {
|
||||
const sql = `
|
||||
SELECT
|
||||
js.id,
|
||||
js.judge_id,
|
||||
js.app_id,
|
||||
js.total_score,
|
||||
js.comments,
|
||||
js.submitted_at,
|
||||
j.name as judge_name,
|
||||
j.title as judge_title,
|
||||
j.department as judge_department,
|
||||
a.name as app_name,
|
||||
a.creator_id,
|
||||
u.name as creator_name,
|
||||
u.department as creator_department,
|
||||
'app' as participant_type,
|
||||
-- 從 judge_score_details 表獲取詳細評分
|
||||
(SELECT GROUP_CONCAT(CONCAT(jsd.rule_name, ':', jsd.score) SEPARATOR ',')
|
||||
FROM judge_score_details jsd
|
||||
WHERE jsd.judge_score_id = js.id) as score_details
|
||||
FROM judge_scores js
|
||||
JOIN judges j ON js.judge_id = j.id
|
||||
JOIN apps a ON js.app_id = a.id
|
||||
JOIN users u ON a.creator_id = u.id
|
||||
WHERE js.competition_id = ?
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
js.id,
|
||||
js.judge_id,
|
||||
js.app_id,
|
||||
js.total_score,
|
||||
js.comments,
|
||||
js.submitted_at,
|
||||
j.name as judge_name,
|
||||
j.title as judge_title,
|
||||
j.department as judge_department,
|
||||
t.name as app_name,
|
||||
t.leader_id as creator_id,
|
||||
u.name as creator_name,
|
||||
u.department as creator_department,
|
||||
'team' as participant_type,
|
||||
-- 從 judge_score_details 表獲取詳細評分
|
||||
(SELECT GROUP_CONCAT(CONCAT(jsd.rule_name, ':', jsd.score) SEPARATOR ',')
|
||||
FROM judge_score_details jsd
|
||||
WHERE jsd.judge_score_id = js.id) as score_details
|
||||
FROM judge_scores js
|
||||
JOIN judges j ON js.judge_id = j.id
|
||||
JOIN teams t ON js.app_id = t.id
|
||||
JOIN users u ON t.leader_id = u.id
|
||||
WHERE js.competition_id = ?
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
js.id,
|
||||
js.judge_id,
|
||||
js.app_id,
|
||||
js.total_score,
|
||||
js.comments,
|
||||
js.submitted_at,
|
||||
j.name as judge_name,
|
||||
j.title as judge_title,
|
||||
j.department as judge_department,
|
||||
p.title as app_name,
|
||||
p.team_id as creator_id,
|
||||
t.name as creator_name,
|
||||
t.department as creator_department,
|
||||
'proposal' as participant_type,
|
||||
-- 從 judge_score_details 表獲取詳細評分
|
||||
(SELECT GROUP_CONCAT(CONCAT(jsd.rule_name, ':', jsd.score) SEPARATOR ',')
|
||||
FROM judge_score_details jsd
|
||||
WHERE jsd.judge_score_id = js.id) as score_details
|
||||
FROM judge_scores js
|
||||
JOIN judges j ON js.judge_id = j.id
|
||||
JOIN proposals p ON js.app_id = p.id
|
||||
JOIN teams t ON p.team_id = t.id
|
||||
WHERE js.competition_id = ?
|
||||
|
||||
ORDER BY submitted_at DESC
|
||||
`;
|
||||
return await db.query(sql, [competitionId, competitionId, competitionId]);
|
||||
}
|
||||
|
||||
// 提交團隊評分(使用應用評分表,但標記為團隊類型)
|
||||
static async submitTeamScore(scoreData: Omit<AppJudgeScore, 'id' | 'submitted_at'> & { teamId: string }): Promise<AppJudgeScore> {
|
||||
// 創建一個虛擬的應用ID來存儲團隊評分
|
||||
// 格式:team_{teamId} 以便識別這是團隊評分
|
||||
const virtualAppId = `team_${scoreData.teamId}`;
|
||||
|
||||
const sql = `
|
||||
INSERT INTO app_judge_scores (id, judge_id, app_id, innovation_score, technical_score, usability_score, presentation_score, impact_score, total_score, comments)
|
||||
VALUES (UUID(), ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
innovation_score = VALUES(innovation_score),
|
||||
technical_score = VALUES(technical_score),
|
||||
usability_score = VALUES(usability_score),
|
||||
presentation_score = VALUES(presentation_score),
|
||||
impact_score = VALUES(impact_score),
|
||||
total_score = VALUES(total_score),
|
||||
comments = VALUES(comments),
|
||||
submitted_at = CURRENT_TIMESTAMP
|
||||
`;
|
||||
const params = [
|
||||
scoreData.judge_id,
|
||||
virtualAppId, // 使用虛擬應用ID
|
||||
scoreData.innovation_score,
|
||||
scoreData.technical_score,
|
||||
scoreData.usability_score,
|
||||
scoreData.presentation_score,
|
||||
scoreData.impact_score,
|
||||
scoreData.total_score,
|
||||
scoreData.comments || null
|
||||
];
|
||||
|
||||
await db.insert(sql, params);
|
||||
return await this.getAppScore(scoreData.judge_id, virtualAppId) as AppJudgeScore;
|
||||
}
|
||||
|
||||
// 獲取競賽評分統計
|
||||
static async getCompetitionScoreStats(competitionId: string): Promise<{
|
||||
totalScores: number;
|
||||
completedScores: number;
|
||||
pendingScores: number;
|
||||
completionRate: number;
|
||||
totalParticipants: number;
|
||||
}> {
|
||||
// 獲取競賽參與者數量
|
||||
const participantsSql = `
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM competition_apps WHERE competition_id = ?) +
|
||||
(SELECT COUNT(*) FROM competition_teams WHERE competition_id = ?) +
|
||||
(SELECT COUNT(*) FROM competition_proposals WHERE competition_id = ?) as total_participants
|
||||
`;
|
||||
const participantsResult = await db.queryOne<{ total_participants: number }>(participantsSql, [competitionId, competitionId, competitionId]);
|
||||
const totalParticipants = participantsResult?.total_participants || 0;
|
||||
|
||||
// 獲取評審數量
|
||||
const judgesSql = 'SELECT COUNT(*) as judge_count FROM competition_judges WHERE competition_id = ?';
|
||||
const judgesResult = await db.queryOne<{ judge_count: number }>(judgesSql, [competitionId]);
|
||||
const judgeCount = judgesResult?.judge_count || 0;
|
||||
|
||||
// 計算總評分項目數
|
||||
const totalScores = totalParticipants * judgeCount;
|
||||
|
||||
// 獲取已完成評分數量
|
||||
const completedScoresSql = `
|
||||
SELECT COUNT(*) as completed_count FROM judge_scores
|
||||
WHERE competition_id = ?
|
||||
`;
|
||||
const completedResult = await db.queryOne<{ completed_count: number }>(completedScoresSql, [competitionId]);
|
||||
const completedScores = completedResult?.completed_count || 0;
|
||||
|
||||
const pendingScores = Math.max(0, totalScores - completedScores);
|
||||
const completionRate = totalScores > 0 ? Math.round((completedScores / totalScores) * 100) : 0;
|
||||
|
||||
return {
|
||||
totalScores,
|
||||
completedScores,
|
||||
pendingScores,
|
||||
completionRate,
|
||||
totalParticipants
|
||||
};
|
||||
}
|
||||
|
||||
// 獲取評審的評分記錄
|
||||
static async getJudgeScores(judgeId: string, competitionId?: string): Promise<any[]> {
|
||||
let sql = `
|
||||
SELECT
|
||||
ajs.id,
|
||||
ajs.judge_id,
|
||||
ajs.app_id,
|
||||
ajs.innovation_score,
|
||||
ajs.technical_score,
|
||||
ajs.usability_score,
|
||||
ajs.presentation_score,
|
||||
ajs.impact_score,
|
||||
ajs.total_score,
|
||||
ajs.comments,
|
||||
ajs.submitted_at,
|
||||
a.name as app_name,
|
||||
a.creator_id,
|
||||
u.name as creator_name,
|
||||
u.department as creator_department,
|
||||
'app' as participant_type
|
||||
FROM app_judge_scores ajs
|
||||
JOIN apps a ON ajs.app_id = a.id
|
||||
JOIN users u ON a.creator_id = u.id
|
||||
WHERE ajs.judge_id = ?
|
||||
`;
|
||||
|
||||
const params = [judgeId];
|
||||
|
||||
if (competitionId) {
|
||||
sql += ' AND EXISTS (SELECT 1 FROM competition_apps ca WHERE ca.app_id = a.id AND ca.competition_id = ?)';
|
||||
params.push(competitionId);
|
||||
}
|
||||
|
||||
sql += `
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
pjs.id,
|
||||
pjs.judge_id,
|
||||
pjs.proposal_id as app_id,
|
||||
pjs.problem_identification_score as innovation_score,
|
||||
pjs.solution_feasibility_score as technical_score,
|
||||
pjs.innovation_score as usability_score,
|
||||
pjs.impact_score as presentation_score,
|
||||
pjs.presentation_score as impact_score,
|
||||
pjs.total_score,
|
||||
pjs.comments,
|
||||
pjs.submitted_at,
|
||||
p.title as app_name,
|
||||
p.team_id as creator_id,
|
||||
t.name as creator_name,
|
||||
t.department as creator_department,
|
||||
'proposal' as participant_type
|
||||
FROM proposal_judge_scores pjs
|
||||
JOIN proposals p ON pjs.proposal_id = p.id
|
||||
JOIN teams t ON p.team_id = t.id
|
||||
WHERE pjs.judge_id = ?
|
||||
`;
|
||||
|
||||
params.push(judgeId);
|
||||
|
||||
if (competitionId) {
|
||||
sql += ' AND EXISTS (SELECT 1 FROM competition_proposals cp WHERE cp.proposal_id = p.id AND cp.competition_id = ?)';
|
||||
params.push(competitionId);
|
||||
}
|
||||
|
||||
sql += ' ORDER BY submitted_at DESC';
|
||||
|
||||
return await db.query(sql, params);
|
||||
}
|
||||
|
||||
// 刪除評分記錄
|
||||
static async deleteScore(scoreId: string, scoreType: 'app' | 'proposal'): Promise<boolean> {
|
||||
const tableName = scoreType === 'app' ? 'app_judge_scores' : 'proposal_judge_scores';
|
||||
const sql = `DELETE FROM ${tableName} WHERE id = ?`;
|
||||
const result = await db.delete(sql, [scoreId]);
|
||||
return result.affectedRows > 0;
|
||||
}
|
||||
|
||||
// 更新評分記錄
|
||||
static async updateAppScore(scoreId: string, updates: Partial<AppJudgeScore>): Promise<boolean> {
|
||||
const fields = Object.keys(updates).filter(key =>
|
||||
key !== 'id' &&
|
||||
key !== 'judge_id' &&
|
||||
key !== 'app_id' &&
|
||||
key !== 'submitted_at'
|
||||
);
|
||||
|
||||
if (fields.length === 0) return true;
|
||||
|
||||
const setClause = fields.map(field => `${field} = ?`).join(', ');
|
||||
const values = fields.map(field => (updates as any)[field]);
|
||||
|
||||
const sql = `UPDATE app_judge_scores SET ${setClause}, submitted_at = CURRENT_TIMESTAMP WHERE id = ?`;
|
||||
const result = await db.update(sql, [...values, scoreId]);
|
||||
return result.affectedRows > 0;
|
||||
}
|
||||
|
||||
// 更新提案評分記錄
|
||||
static async updateProposalScore(scoreId: string, updates: Partial<ProposalJudgeScore>): Promise<boolean> {
|
||||
const fields = Object.keys(updates).filter(key =>
|
||||
key !== 'id' &&
|
||||
key !== 'judge_id' &&
|
||||
key !== 'proposal_id' &&
|
||||
key !== 'submitted_at'
|
||||
);
|
||||
|
||||
if (fields.length === 0) return true;
|
||||
|
||||
const setClause = fields.map(field => `${field} = ?`).join(', ');
|
||||
const values = fields.map(field => (updates as any)[field]);
|
||||
|
||||
const sql = `UPDATE proposal_judge_scores SET ${setClause}, submitted_at = CURRENT_TIMESTAMP WHERE id = ?`;
|
||||
const result = await db.update(sql, [...values, scoreId]);
|
||||
return result.affectedRows > 0;
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
|
Reference in New Issue
Block a user