Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -12,6 +12,52 @@ use serde::{Deserialize, Serialize};
|
|||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
async fn connect_mysql_pool(env_var: &str) -> Result<sqlx::MySqlPool, (StatusCode, String)> {
|
||||||
|
use sqlx::mysql::MySqlPoolOptions;
|
||||||
|
|
||||||
|
let mysql_url = std::env::var(env_var)
|
||||||
|
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, format!("{} not configured", env_var)))?;
|
||||||
|
|
||||||
|
let mut last_error = String::new();
|
||||||
|
|
||||||
|
for attempt in 1..=3 {
|
||||||
|
let result = MySqlPoolOptions::new()
|
||||||
|
// Keep per-request pools small to avoid exhausting remote MySQL.
|
||||||
|
.max_connections(2)
|
||||||
|
.min_connections(0)
|
||||||
|
.acquire_timeout(std::time::Duration::from_secs(15))
|
||||||
|
.idle_timeout(std::time::Duration::from_secs(30))
|
||||||
|
.max_lifetime(std::time::Duration::from_secs(300))
|
||||||
|
.connect(&mysql_url)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(pool) => return Ok(pool),
|
||||||
|
Err(e) => {
|
||||||
|
last_error = e.to_string();
|
||||||
|
tracing::warn!(
|
||||||
|
"MySQL connection attempt {}/3 failed for {}: {}",
|
||||||
|
attempt,
|
||||||
|
env_var,
|
||||||
|
last_error
|
||||||
|
);
|
||||||
|
|
||||||
|
if attempt < 3 {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(2 * attempt)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err((
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!(
|
||||||
|
"Failed to connect to MySQL after 3 attempts: {}",
|
||||||
|
last_error
|
||||||
|
),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== MySQL Study Plans & Courses ====================
|
// ==================== MySQL Study Plans & Courses ====================
|
||||||
|
|
||||||
#[derive(Debug, sqlx::FromRow, Serialize, Deserialize)]
|
#[derive(Debug, sqlx::FromRow, Serialize, Deserialize)]
|
||||||
@@ -376,12 +422,7 @@ pub async fn import_from_mysql(
|
|||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
// Connect to MySQL
|
// Connect to MySQL
|
||||||
let mysql_url = std::env::var("MYSQL_DATABASE_URL")
|
let mysql_pool = connect_mysql_pool("MYSQL_DATABASE_URL").await?;
|
||||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "MYSQL_DATABASE_URL not configured".to_string()))?;
|
|
||||||
|
|
||||||
let mysql_pool = sqlx::MySqlPool::connect(&mysql_url)
|
|
||||||
.await
|
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to connect to MySQL: {}", e)))?;
|
|
||||||
|
|
||||||
// Fetch all study plans and courses from MySQL to sync them
|
// Fetch all study plans and courses from MySQL to sync them
|
||||||
let mysql_plans: Vec<MySqlPlanInfo> = sqlx::query_as(
|
let mysql_plans: Vec<MySqlPlanInfo> = sqlx::query_as(
|
||||||
@@ -746,12 +787,7 @@ pub async fn list_mysql_courses(
|
|||||||
State(_pool): State<PgPool>,
|
State(_pool): State<PgPool>,
|
||||||
) -> Result<Json<Vec<MySqlCourseInfo>>, (StatusCode, String)> {
|
) -> Result<Json<Vec<MySqlCourseInfo>>, (StatusCode, String)> {
|
||||||
// Connect to MySQL
|
// Connect to MySQL
|
||||||
let mysql_url = std::env::var("MYSQL_DATABASE_URL")
|
let mysql_pool = connect_mysql_pool("MYSQL_DATABASE_URL").await?;
|
||||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "MYSQL_DATABASE_URL not configured".to_string()))?;
|
|
||||||
|
|
||||||
let mysql_pool = sqlx::MySqlPool::connect(&mysql_url)
|
|
||||||
.await
|
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to connect to MySQL: {}", e)))?;
|
|
||||||
|
|
||||||
// Fetch courses with their plan names
|
// Fetch courses with their plan names
|
||||||
let courses: Vec<MySqlCourseInfo> = sqlx::query_as(
|
let courses: Vec<MySqlCourseInfo> = sqlx::query_as(
|
||||||
@@ -881,12 +917,7 @@ pub async fn import_all_from_mysql(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Connect to MySQL
|
// Connect to MySQL
|
||||||
let mysql_url = std::env::var("MYSQL_DATABASE_URL")
|
let mysql_pool = connect_mysql_pool("MYSQL_DATABASE_URL").await?;
|
||||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "MYSQL_DATABASE_URL not configured".to_string()))?;
|
|
||||||
|
|
||||||
let mysql_pool = sqlx::MySqlPool::connect(&mysql_url)
|
|
||||||
.await
|
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to connect to MySQL: {}", e)))?;
|
|
||||||
|
|
||||||
// Fetch all study plans and courses from MySQL to sync them
|
// Fetch all study plans and courses from MySQL to sync them
|
||||||
let mysql_plans: Vec<MySqlPlanInfo> = sqlx::query_as(
|
let mysql_plans: Vec<MySqlPlanInfo> = sqlx::query_as(
|
||||||
@@ -1292,12 +1323,7 @@ pub async fn import_course_from_mysql(
|
|||||||
use common::models::Course;
|
use common::models::Course;
|
||||||
|
|
||||||
// Connect to MySQL
|
// Connect to MySQL
|
||||||
let mysql_url = std::env::var("MYSQL_DATABASE_URL")
|
let mysql_pool = connect_mysql_pool("MYSQL_DATABASE_URL").await?;
|
||||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "MYSQL_DATABASE_URL not configured".to_string()))?;
|
|
||||||
|
|
||||||
let mysql_pool = sqlx::MySqlPool::connect(&mysql_url)
|
|
||||||
.await
|
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to connect to MySQL: {}", e)))?;
|
|
||||||
|
|
||||||
// Fetch course info from MySQL
|
// Fetch course info from MySQL
|
||||||
let mysql_course: MySqlCourseInfo = sqlx::query_as(
|
let mysql_course: MySqlCourseInfo = sqlx::query_as(
|
||||||
@@ -1529,22 +1555,7 @@ pub async fn import_from_sam_diagnostico(
|
|||||||
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
let sam_diag_url = std::env::var("SAM_DIAGNOSTICO_DATABASE_URL")
|
let mysql_pool = connect_mysql_pool("SAM_DIAGNOSTICO_DATABASE_URL").await?;
|
||||||
.map_err(|_| {
|
|
||||||
(
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
"SAM_DIAGNOSTICO_DATABASE_URL not configured".to_string(),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let mysql_pool = sqlx::MySqlPool::connect(&sam_diag_url)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
(
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
format!("Failed to connect to SAM_diagnostico: {}", e),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Determinar qué tablas procesar según la audiencia solicitada
|
// Determinar qué tablas procesar según la audiencia solicitada
|
||||||
let tables: Vec<(&str, &str)> = match payload.audience.as_deref() {
|
let tables: Vec<(&str, &str)> = match payload.audience.as_deref() {
|
||||||
@@ -1571,14 +1582,14 @@ pub async fn import_from_sam_diagnostico(
|
|||||||
idTest AS id_test,
|
idTest AS id_test,
|
||||||
idCurso AS id_curso,
|
idCurso AS id_curso,
|
||||||
idPregunta AS id_pregunta,
|
idPregunta AS id_pregunta,
|
||||||
MAX(preguntaNombre) AS pregunta_nombre,
|
CAST(MAX(preguntaNombre) AS CHAR CHARACTER SET utf8mb4) AS pregunta_nombre,
|
||||||
MAX(tipoPregunta) AS tipo_pregunta,
|
CAST(MAX(tipoPregunta) AS CHAR CHARACTER SET utf8mb4) AS tipo_pregunta,
|
||||||
GROUP_CONCAT(
|
GROUP_CONCAT(
|
||||||
respuestaNombre
|
CAST(respuestaNombre AS CHAR CHARACTER SET utf8mb4)
|
||||||
ORDER BY idOpcion
|
ORDER BY idOpcion
|
||||||
SEPARATOR '|||'
|
SEPARATOR '|||'
|
||||||
) AS opciones,
|
) AS opciones,
|
||||||
MAX(CASE WHEN valorRespuesta = 1 THEN respuestaNombre ELSE NULL END)
|
CAST(MAX(CASE WHEN valorRespuesta = 1 THEN respuestaNombre ELSE NULL END) AS CHAR CHARACTER SET utf8mb4)
|
||||||
AS respuesta_correcta
|
AS respuesta_correcta
|
||||||
FROM {}
|
FROM {}
|
||||||
WHERE 1=1
|
WHERE 1=1
|
||||||
@@ -1683,7 +1694,7 @@ pub async fn import_from_sam_diagnostico(
|
|||||||
options, correct_answer, source, source_metadata,
|
options, correct_answer, source, source_metadata,
|
||||||
audio_status, is_active
|
audio_status, is_active
|
||||||
)
|
)
|
||||||
VALUES ($1, $2, $3, 'multiple_choice', $4, $5, 'sam-diagnostico', $6, 'pending', true)
|
VALUES ($1, $2, $3, 'multiple-choice', $4, $5, 'sam-diagnostico', $6, 'pending', true)
|
||||||
"#
|
"#
|
||||||
)
|
)
|
||||||
.bind(org_ctx.id)
|
.bind(org_ctx.id)
|
||||||
|
|||||||
@@ -17,9 +17,6 @@ RUN cargo build --release -p lms-service
|
|||||||
FROM node:20-alpine AS node-builder
|
FROM node:20-alpine AS node-builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Configure DNS for Google Fonts access during build
|
|
||||||
RUN echo "nameserver 8.8.8.8" > /etc/resolv.conf
|
|
||||||
|
|
||||||
COPY web/experience/package*.json ./
|
COPY web/experience/package*.json ./
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
COPY web/experience/ .
|
COPY web/experience/ .
|
||||||
|
|||||||
@@ -17,9 +17,6 @@ RUN cargo build --release -p cms-service
|
|||||||
FROM node:20-alpine AS node-builder
|
FROM node:20-alpine AS node-builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Configure DNS for Google Fonts access during build
|
|
||||||
RUN echo "nameserver 8.8.8.8" > /etc/resolv.conf
|
|
||||||
|
|
||||||
COPY web/studio/package*.json ./
|
COPY web/studio/package*.json ./
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
COPY web/studio/ .
|
COPY web/studio/ .
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { X, Download, Database, Check, AlertCircle, Upload, FileSpreadsheet } from 'lucide-react';
|
import { X, Download, Database, Check, AlertCircle, Upload, FileSpreadsheet } from 'lucide-react';
|
||||||
import { apiFetch } from '@/lib/api';
|
import { apiFetch } from '@/lib/api';
|
||||||
import ExcelImportModal from './ExcelImportModal';
|
import ExcelImportModal from './ExcelImportModal';
|
||||||
@@ -17,7 +17,7 @@ export default function MySQLImportModal({ onSuccess, onCancel }: MySQLImportMod
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const handleImportAll = async () => {
|
const handleImportAll = async () => {
|
||||||
if (!confirm('¿Estás seguro de importar TODAS las preguntas de MySQL? Esto puede tomar varios minutos.')) {
|
if (!confirm('¿Estás seguro de importar preguntas desde SAM Diagnóstico (adultos, kids y teens)? Esto puede tomar varios minutos.')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,18 +25,18 @@ export default function MySQLImportModal({ onSuccess, onCancel }: MySQLImportMod
|
|||||||
setImporting(true);
|
setImporting(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
const result = await apiFetch('/question-bank/import-mysql-all', {
|
const result = await apiFetch('/question-bank/import-sam-diagnostico', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
body: JSON.stringify({}),
|
||||||
});
|
});
|
||||||
|
|
||||||
setImportResult(result);
|
setImportResult(result);
|
||||||
|
|
||||||
setTimeout(() => {
|
// Refresh parent data but keep modal open so the user can inspect counts/errors.
|
||||||
onSuccess?.();
|
onSuccess?.();
|
||||||
}, 1500);
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Import all failed:', error);
|
console.error('SAM diagnostico import failed:', error);
|
||||||
setError(error.message || 'Error al importar todas las preguntas');
|
setError(error.message || 'Error al importar preguntas desde SAM Diagnóstico');
|
||||||
} finally {
|
} finally {
|
||||||
setImporting(false);
|
setImporting(false);
|
||||||
}
|
}
|
||||||
@@ -77,18 +77,18 @@ export default function MySQLImportModal({ onSuccess, onCancel }: MySQLImportMod
|
|||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="p-6 space-y-4">
|
<div className="p-6 space-y-4">
|
||||||
{/* MySQL Import */}
|
{/* SAM Diagnostico Import */}
|
||||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||||
<div className="flex items-start gap-3 mb-3">
|
<div className="flex items-start gap-3 mb-3">
|
||||||
<Database className="w-5 h-5 text-blue-600 shrink-0 mt-0.5" />
|
<Database className="w-5 h-5 text-blue-600 shrink-0 mt-0.5" />
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-semibold text-blue-900 dark:text-blue-100 text-sm mb-2">
|
<h4 className="font-semibold text-blue-900 dark:text-blue-100 text-sm mb-2">
|
||||||
Importar desde MySQL
|
Importar desde SAM Diagnostico
|
||||||
</h4>
|
</h4>
|
||||||
<ul className="text-xs text-blue-800 dark:text-blue-200 space-y-1">
|
<ul className="text-xs text-blue-800 dark:text-blue-200 space-y-1">
|
||||||
<li>• Todas las preguntas del banco de diagnóstico</li>
|
<li>• Origen: tablas SAM diagnostico (adultos, kids y teens)</li>
|
||||||
<li>• Sin duplicados automáticos</li>
|
<li>• Sin duplicados (usa sam_id en source_metadata)</li>
|
||||||
<li>• Mapeo automático de tipos</li>
|
<li>• Mapeo directo al banco de preguntas de OpenCCB</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -97,7 +97,7 @@ export default function MySQLImportModal({ onSuccess, onCancel }: MySQLImportMod
|
|||||||
disabled={importing}
|
disabled={importing}
|
||||||
className="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 text-sm font-medium"
|
className="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 text-sm font-medium"
|
||||||
>
|
>
|
||||||
{importing ? 'Importando...' : 'Importar Todo desde MySQL'}
|
{importing ? 'Importando...' : 'Importar Todo desde SAM Diagnostico'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -155,6 +155,12 @@ export default function MySQLImportModal({ onSuccess, onCancel }: MySQLImportMod
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{Array.isArray(importResult.errors) && importResult.errors.length > 0 && (
|
||||||
|
<div className="mt-3 rounded border border-yellow-300 bg-yellow-50 p-3 text-xs text-yellow-900 dark:border-yellow-700 dark:bg-yellow-900/20 dark:text-yellow-200">
|
||||||
|
<p className="font-semibold mb-1">Errores reportados por el backend: {importResult.errors.length}</p>
|
||||||
|
<p>Primer error: {String(importResult.errors[0])}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user