update
All checks were successful
Deploy to Production / deploy (push) Successful in 20s

This commit is contained in:
vuongps38770
2026-02-28 20:00:38 +07:00
parent f96833a7e4
commit 72283443ab
15 changed files with 972 additions and 318 deletions

View File

@@ -1,4 +1,5 @@
const { Context, Vocab } = require('../models'); const { Context, Vocab } = require('../models');
const { Op } = require('sequelize');
/** /**
* Context Controller - Workflow-based status management * Context Controller - Workflow-based status management
@@ -11,7 +12,7 @@ class ContextController {
*/ */
async createContext(req, res, next) { async createContext(req, res, next) {
try { try {
const { title, desc, grade, type, type_image , reference_id } = req.body; const { title, desc, grade, type, type_image, reference_id } = req.body;
// Validate required fields // Validate required fields
if (!title || !desc || !grade) { if (!title || !desc || !grade) {
@@ -58,18 +59,96 @@ class ContextController {
async getContextsByStatus(req, res, next) { async getContextsByStatus(req, res, next) {
try { try {
const { status } = req.params; const { status } = req.params;
const { page = 1, limit = 50, type, grade } = req.query; const { page = 1, limit = 50, type, grade, title, date, from, to, sort } = req.query;
const offset = (page - 1) * limit; const offset = (page - 1) * limit;
const where = { status: parseInt(status) }; const where = { status: parseInt(status) };
if (type) where.type = type; if (type) where.type = type;
if (grade) where.grade = parseInt(grade); if (grade) where.grade = parseInt(grade);
if (title) where.title = { [Op.like]: `%${title}%` };
// Hàm helper để chuẩn hóa và parse date cho dải thời gian
const parseDateRange = (dStr, isEndDate = false) => {
if (!dStr) return null;
let s = String(dStr).replace('_', 'T').replace(' ', 'T');
if (s.includes(':') && !s.includes('Z') && !s.match(/[+-]\d{2}:?\d{2}$/)) {
s += 'Z';
}
const d = new Date(s);
if (isNaN(d.getTime())) return null;
if (!s.includes(':') && isEndDate) {
d.setHours(23, 59, 59, 999);
}
return d;
};
if (from || to) {
where.updated_at = {};
if (from) {
const startDate = parseDateRange(from);
if (startDate) where.updated_at[Op.gte] = startDate;
}
if (to) {
const endDate = parseDateRange(to, true);
if (endDate) where.updated_at[Op.lte] = endDate;
}
} else if (date) {
// Chuẩn hóa chuỗi ngày tháng: thay '_' hoặc khoảng cách bằng 'T' để dễ xử lý
let dateString = String(date).replace('_', 'T').replace(' ', 'T');
// Nếu chuỗi có giờ phút (có dấu :) nhưng chưa có múi giờ (Z hoặc +/-)
// Ta mặc định là giờ UTC để khớp chính xác với những gì người dùng thấy trong DB
if (dateString.includes(':') && !dateString.includes('Z') && !dateString.match(/[+-]\d{2}:?\d{2}$/)) {
dateString += 'Z';
}
const searchDate = new Date(dateString);
if (!isNaN(searchDate.getTime())) {
// Kiểm tra xem có dấu ':' (có giờ phút) không
const hasTime = dateString.includes(':');
if (hasTime) {
// Kiểm tra xem có cung cấp đến giây không (vd: 09:08:18 -> 3 phần)
const isSecondsProvided = dateString.split(':').length === 3;
if (isSecondsProvided) {
// Tìm chính xác giây đó (dải 1000ms)
const startTime = Math.floor(searchDate.getTime() / 1000) * 1000;
const startRange = new Date(startTime);
const endRange = new Date(startTime + 1000);
where.updated_at = { [Op.gte]: startRange, [Op.lt]: endRange };
} else {
// Chỉ có giờ:phút, tìm trong cả phút đó
const startTime = Math.floor(searchDate.getTime() / 60000) * 60000;
const startRange = new Date(startTime);
const endRange = new Date(startTime + 60000);
where.updated_at = { [Op.gte]: startRange, [Op.lt]: endRange };
}
} else {
// Nếu chỉ có ngày (vd: 2026-02-28), tìm cả ngày theo giờ server
const startOfDay = new Date(searchDate);
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date(searchDate);
endOfDay.setHours(23, 59, 59, 999);
where.updated_at = { [Op.gte]: startOfDay, [Op.lte]: endOfDay };
}
}
}
// Sort order
let order = [['title', 'ASC']]; // default alphabet
if (sort === 'created_at') {
order = [['created_at', 'DESC']];
} else if (sort === 'updated_at') {
order = [['updated_at', 'DESC']];
}
const { count, rows } = await Context.findAndCountAll({ const { count, rows } = await Context.findAndCountAll({
where, where,
limit: parseInt(limit), limit: parseInt(limit),
offset: parseInt(offset), offset: parseInt(offset),
order: [['created_at', 'DESC']] order
}); });
res.json({ res.json({
@@ -416,11 +495,11 @@ class ContextController {
type, type,
status, status,
grade, grade,
reference_id,
page = 1, page = 1,
limit = 50 limit = 50
} = req.body; } = req.body;
const { Op } = require('sequelize');
const offset = (page - 1) * limit; const offset = (page - 1) * limit;
const where = {}; const where = {};
@@ -431,6 +510,7 @@ class ContextController {
if (type) where.type = type; if (type) where.type = type;
if (status !== undefined && status !== null) where.status = parseInt(status); if (status !== undefined && status !== null) where.status = parseInt(status);
if (grade !== undefined && grade !== null) where.grade = parseInt(grade); if (grade !== undefined && grade !== null) where.grade = parseInt(grade);
if (reference_id) where.reference_id = reference_id;
// ── Text search ────────────────────────────────────────────────────── // ── Text search ──────────────────────────────────────────────────────
// `search` → title OR context (cả hai cùng lúc) // `search` → title OR context (cả hai cùng lúc)

View File

@@ -127,6 +127,8 @@ exports.getAllSentences = async (req, res) => {
topic, topic,
text, text,
search, search,
status,
sort,
is_active = true is_active = true
} = req.query; } = req.query;
@@ -137,6 +139,11 @@ exports.getAllSentences = async (req, res) => {
where.is_active = is_active === 'true' || is_active === true; where.is_active = is_active === 'true' || is_active === true;
} }
// Status filter (0 = chưa duyệt, 1 = đã duyệt, undefined = all)
if (status !== undefined && status !== '' && status !== 'all') {
where.status = parseInt(status);
}
if (category) { if (category) {
where.category = category; where.category = category;
} }
@@ -156,11 +163,19 @@ exports.getAllSentences = async (req, res) => {
]; ];
} }
// Sort order
let order = [['created_at', 'DESC']]; // default
if (sort === 'updated_at') {
order = [['updated_at', 'DESC']];
} else if (sort === 'alphabet') {
order = [['text', 'ASC']];
}
const { count, rows } = await Sentences.findAndCountAll({ const { count, rows } = await Sentences.findAndCountAll({
where, where,
limit: parseInt(limit), limit: parseInt(limit),
offset: parseInt(offset), offset: parseInt(offset),
order: [['created_at', 'DESC']] order
}); });
res.json({ res.json({

View File

@@ -170,6 +170,8 @@ exports.getAllVocabs = async (req, res) => {
search, search,
grade_start, grade_start,
grade_end, grade_end,
status,
sort,
is_active = true is_active = true
} = req.query; } = req.query;
@@ -181,6 +183,11 @@ exports.getAllVocabs = async (req, res) => {
where.is_active = is_active === 'true' || is_active === true; where.is_active = is_active === 'true' || is_active === true;
} }
// Status filter (0 = chưa duyệt, 1 = đã duyệt, undefined = all)
if (status !== undefined && status !== '' && status !== 'all') {
where.status = parseInt(status);
}
if (category) { if (category) {
where.category = category; where.category = category;
} }
@@ -221,11 +228,19 @@ exports.getAllVocabs = async (req, res) => {
where.grade_number = { [Op.lte]: parseInt(grade_end) }; where.grade_number = { [Op.lte]: parseInt(grade_end) };
} }
// Sort order
let order = [['created_at', 'DESC']]; // default
if (sort === 'updated_at') {
order = [['updated_at', 'DESC']];
} else if (sort === 'alphabet') {
order = [['text', 'ASC']];
}
const { count, rows } = await Vocab.findAndCountAll({ const { count, rows } = await Vocab.findAndCountAll({
where, where,
limit: parseInt(limit), limit: parseInt(limit),
offset: parseInt(offset), offset: parseInt(offset),
order: [['created_at', 'DESC']] order
}); });
res.json({ res.json({

View File

@@ -1,59 +1,26 @@
/**
* Import Context (status=5) → Sentences
*
* Logic:
* 1. Lấy tất cả Context có status = 5
* 2. So sánh Context.title với Sentences.text
* - Nếu chưa có → tạo mới Sentence
* - Nếu đã có → cập nhật image vào đúng mảng
* 3. Xác định slot image dựa vào Context.image.split("_")[1]:
* - "square" → image_square
* - "small" → image_small
* - "normal" → image_normal
* 4. Chuyển Context.status → 6
*/
const { sequelize } = require('./config/database'); const { sequelize } = require('./config/database');
const Context = require('./models/Context'); const { Context, Vocab, Sentences } = require('./models');
const Sentences = require('./models/Sentences');
// ─── helper: thêm URL vào mảng JSON (không trùng lặp) ───────────────────────
function addToArray(existing, url) {
const arr = Array.isArray(existing) ? [...existing] : [];
if (!arr.includes(url)) arr.push(url);
return arr;
}
// ─── helper: xác định slot image từ tên file/URL ──────────────────────────── // ─── helper: xác định slot image từ tên file/URL ────────────────────────────
function resolveImageSlot(imageUrl) { function resolveImageSlot(imageUrl) {
if (!imageUrl) return null; if (!imageUrl) return null;
// Lấy phần tên file (bỏ path/query)
const filename = imageUrl.split('/').pop().split('?')[0]; const filename = imageUrl.split('/').pop().split('?')[0];
const parts = filename.split('_'); const parts = filename.split('_');
// Duyệt tất cả phần tử, tìm keyword
for (const part of parts) { for (const part of parts) {
const key = part.toLowerCase(); const key = part.toLowerCase();
if (key === 'square') return 'image_square'; if (key === 'square') return 'image_square';
if (key === 'small') return 'image_small'; if (key === 'small') return 'image_small';
if (key === 'normal') return 'image_normal'; if (key === 'normal') return 'image_normal';
} }
return null;
return null; // không xác định được
} }
// ─── main ────────────────────────────────────────────────────────────────────
async function run() { async function run() {
try { try {
await sequelize.authenticate(); await sequelize.authenticate();
console.log('✅ Database connected\n'); console.log('✅ Database connected\n');
// 1. Lấy tất cả Context có status = 5 const contexts = await Context.findAll({ where: { status: 5 } });
const contexts = await Context.findAll({
where: { status: 5 }
});
console.log(`📦 Tìm thấy ${contexts.length} Context(s) có status = 5\n`); console.log(`📦 Tìm thấy ${contexts.length} Context(s) có status = 5\n`);
if (contexts.length === 0) { if (contexts.length === 0) {
@@ -61,76 +28,78 @@ async function run() {
process.exit(0); process.exit(0);
} }
let created = 0; let updatedVocab = 0;
let updated = 0; let updatedSentences = 0;
let skipped = 0; let skipped = 0;
for (const ctx of contexts) { for (const ctx of contexts) {
const text = ctx.context; // so sánh với Sentences.text
const imageUrl = ctx.image; const imageUrl = ctx.image;
const imageSlot = resolveImageSlot(imageUrl); const slot = resolveImageSlot(imageUrl);
if (!text) { if (!imageUrl || !slot) {
console.warn(` ⚠️ Context [${ctx.uuid}] không có context — bỏ qua`); console.warn(` ⚠️ [BỎ QUA] ID ${ctx.uuid}: Không xác định được ảnh/slot`);
skipped++; skipped++;
continue; continue;
} }
// 2. Tìm Sentence có text khớp let targetEntity = null;
let sentence = await Sentences.findOne({ let entityType = '';
where: { text }
// Xử lý Vocab
if (ctx.type === 'vocabulary') {
// Ưu tiên tìm bản ghi đang active
targetEntity = await Vocab.findOne({
where: { text: ctx.title, is_active: true }
}); });
// Nếu không có bản ghi active, mới tìm bản ghi bất kỳ
if (!targetEntity) targetEntity = await Vocab.findOne({ where: { text: ctx.title } });
entityType = 'Vocab';
} else {
// Xử lý Sentence (ưu tiên active)
targetEntity = await Sentences.findOne({
where: { text: ctx.context, is_active: true }
});
if (!targetEntity) targetEntity = await Sentences.findOne({ where: { text: ctx.context } });
if (!sentence) { if (!targetEntity) {
// ── Tạo mới ────────────────────────────────────────────────────────── targetEntity = await Sentences.findOne({
const newData = { where: { text: ctx.title, is_active: true }
text, });
is_active: true, if (!targetEntity) targetEntity = await Sentences.findOne({ where: { text: ctx.title } });
image_small: [], }
image_square: [], entityType = 'Sentence';
image_normal: [],
};
// Gán image vào đúng slot
if (imageUrl && imageSlot) {
newData[imageSlot] = [imageUrl];
} else if (imageUrl) {
console.warn(` ⚠️ Không xác định slot image từ URL: "${imageUrl}" — bỏ qua image`);
} }
await Sentences.create(newData); if (targetEntity) {
console.log(` ✅ [TẠO MỚI] "${text}"${imageSlot ? `${imageSlot}` : ''}`); // 1. Cập nhật ảnh (Ghi đè - để mảng chỉ có 1 ảnh)
created++; await targetEntity.update({ [slot]: [imageUrl] });
// 2. Kiểm tra và sửa reference_id nếu lệch
const correctId = targetEntity.vocab_id || targetEntity.id;
const updates = { status: 6 };
if (ctx.reference_id !== correctId) {
updates.reference_id = correctId;
}
await ctx.update(updates);
if (entityType === 'Vocab') updatedVocab++; else updatedSentences++;
console.log(` ✅ [${entityType}] "${ctx.title}" -> ${slot} (Đã ghi đè)`);
} else { } else {
// ── Cập nhật image ─────────────────────────────────────────────────── console.warn(` ❌ [KHÔNG THẤY] "${ctx.title}" (Type: ${ctx.type})`);
if (imageUrl && imageSlot) {
const updatedArr = addToArray(sentence[imageSlot], imageUrl);
await sentence.update({ [imageSlot]: updatedArr });
console.log(` 🔄 [CẬP NHẬT] "${text}" → ${imageSlot} (+1 ảnh)`);
updated++;
} else {
console.warn(` ⚠️ [BỎ QUA IMAGE] "${text}" — URL trống hoặc không xác định slot`);
skipped++; skipped++;
} }
} }
// 3. Chuyển Context.status → 6
await ctx.update({ status: 6 });
}
console.log('\n─────────────────────────────────────'); console.log('\n─────────────────────────────────────');
console.log(`📊 Kết quả:`); console.log(`📊 Kết quả:`);
console.log(`Tạo mới : ${created}`); console.log(`Cập nhật Vocab : ${updatedVocab}`);
console.log(` 🔄 Cập nhật : ${updated}`); console.log(` Cập nhật Sentences: ${updatedSentences}`);
console.log(` ⚠️ Bỏ qua : ${skipped}`); console.log(` ⚠️ Bỏ qua : ${skipped}`);
console.log(` 📌 Tổng : ${contexts.length}`);
console.log('─────────────────────────────────────'); console.log('─────────────────────────────────────');
process.exit(0); process.exit(0);
} catch (error) { } catch (error) {
console.error('❌ Lỗi:', error.message); console.error('❌ Lỗi:', error.message);
console.error(error.stack);
process.exit(1); process.exit(1);
} }
} }

View File

@@ -0,0 +1,104 @@
const { sequelize } = require('../config/database');
/**
* Migration: Thêm trường `status` (INT, default 0) và `etc` (TEXT, default '')
* cho cả bảng `vocab` và `sentences`.
*
* - status: dùng để confirm lại vocab/sentence (0 = chưa confirm, etc.)
* - etc: thông tin bổ sung dạng chuỗi
*/
async function run() {
try {
await sequelize.authenticate();
console.log('✅ Database connected\n');
const queryInterface = sequelize.getQueryInterface();
// ─── VOCAB ───────────────────────────────────────────
console.log('📦 Đang migrate bảng VOCAB...');
// Kiểm tra & thêm status
try {
await queryInterface.addColumn('vocab', 'status', {
type: sequelize.Sequelize.DataTypes.INTEGER,
defaultValue: 0,
allowNull: false,
comment: 'Trạng thái confirm (0 = chưa confirm)'
});
console.log(' ✅ Đã thêm cột "status" vào bảng vocab');
} catch (e) {
if (e.message.includes('Duplicate') || e.message.includes('already exists') || e.original?.code === 'ER_DUP_FIELDNAME') {
console.log(' ⚠️ Cột "status" đã tồn tại trong bảng vocab, bỏ qua.');
} else {
throw e;
}
}
// Kiểm tra & thêm etc
try {
await queryInterface.addColumn('vocab', 'etc', {
type: sequelize.Sequelize.DataTypes.TEXT,
defaultValue: '',
allowNull: true,
comment: 'Thông tin bổ sung'
});
console.log(' ✅ Đã thêm cột "etc" vào bảng vocab');
} catch (e) {
if (e.message.includes('Duplicate') || e.message.includes('already exists') || e.original?.code === 'ER_DUP_FIELDNAME') {
console.log(' ⚠️ Cột "etc" đã tồn tại trong bảng vocab, bỏ qua.');
} else {
throw e;
}
}
// ─── SENTENCES ───────────────────────────────────────
console.log('\n📦 Đang migrate bảng SENTENCES...');
// Kiểm tra & thêm status
try {
await queryInterface.addColumn('sentences', 'status', {
type: sequelize.Sequelize.DataTypes.INTEGER,
defaultValue: 0,
allowNull: false,
comment: 'Trạng thái confirm (0 = chưa confirm)'
});
console.log(' ✅ Đã thêm cột "status" vào bảng sentences');
} catch (e) {
if (e.message.includes('Duplicate') || e.message.includes('already exists') || e.original?.code === 'ER_DUP_FIELDNAME') {
console.log(' ⚠️ Cột "status" đã tồn tại trong bảng sentences, bỏ qua.');
} else {
throw e;
}
}
// Kiểm tra & thêm etc (sentences đã có etc trong model nhưng chưa chắc có trong DB)
try {
await queryInterface.addColumn('sentences', 'etc', {
type: sequelize.Sequelize.DataTypes.TEXT,
defaultValue: '',
allowNull: true,
comment: 'Thông tin bổ sung'
});
console.log(' ✅ Đã thêm cột "etc" vào bảng sentences');
} catch (e) {
if (e.message.includes('Duplicate') || e.message.includes('already exists') || e.original?.code === 'ER_DUP_FIELDNAME') {
console.log(' ⚠️ Cột "etc" đã tồn tại trong bảng sentences, bỏ qua.');
} else {
throw e;
}
}
console.log('\n─────────────────────────────────────');
console.log('✅ Migration hoàn tất!');
console.log(' - vocab: status (INT, default 0), etc (TEXT, default "")');
console.log(' - sentences: status (INT, default 0), etc (TEXT, default "")');
console.log('─────────────────────────────────────');
process.exit(0);
} catch (error) {
console.error('❌ Lỗi:', error.message);
process.exit(1);
}
}
run();

View File

@@ -0,0 +1,53 @@
const { Context } = require('../models');
const { Op } = require('sequelize');
const { sequelize } = require('../config/database');
async function checkNullRefId() {
try {
await sequelize.authenticate();
console.log('✅ Kết nối database thành công.');
// Kiểm tra tất cả các bản ghi có reference_id là null hoặc chuỗi rỗng
const nullRefCount = await Context.count({
where: {
[Op.or]: [
{ reference_id: null },
{ reference_id: '' }
]
}
});
const totalCount = await Context.count();
// Thêm thống kê theo status để dễ hình dung
const statsByStatus = await Context.findAll({
attributes: ['status', [sequelize.fn('COUNT', sequelize.col('uuid')), 'count']],
where: {
[Op.or]: [
{ reference_id: null },
{ reference_id: '' }
]
},
group: ['status']
});
console.log('\n--- KẾT QUẢ KIỂM TRA REFERENCE_ID ---');
console.log(`📊 Tổng số bản ghi trong bảng: ${totalCount}`);
console.log(`❌ Số bản ghi có reference_id bị NULL/Trống: ${nullRefCount}`);
console.log(`📈 Tỷ lệ lỗi: ${((nullRefCount / totalCount) * 100).toFixed(2)}%`);
if (statsByStatus.length > 0) {
console.log('\nPhân loại theo Status:');
statsByStatus.forEach(stat => {
console.log(` - Status ${stat.get('status')}: ${stat.get('count')} bản ghi`);
});
}
process.exit(0);
} catch (error) {
console.error('❌ Lỗi khi kiểm tra:', error);
process.exit(1);
}
}
checkNullRefId();

View File

@@ -0,0 +1,57 @@
const { Context, Vocab, Sentences } = require('../models');
const { sequelize } = require('../config/database');
async function fixReferenceIds() {
try {
await sequelize.authenticate();
console.log('✅ Kết nối database thành công.');
// Chỉ kiểm tra các bản ghi status 5 (hoặc bạn có thể bỏ where để check tất cả)
const contexts = await Context.findAll({
where: { status: 5 }
});
console.log(`🔍 Đang kiểm tra ${contexts.length} bản ghi Context để sửa Reference ID...`);
let fixedCount = 0;
let verifiedCount = 0;
for (const ctx of contexts) {
// 1. Thử tìm trong Vocab trước
const vocab = await Vocab.findOne({ where: { text: ctx.title } });
if (vocab) {
if (ctx.reference_id !== vocab.vocab_id) {
await ctx.update({ reference_id: vocab.vocab_id });
fixedCount++;
console.log(`✅ Fixed ID: '${ctx.title}' -> ${vocab.vocab_id} (Vocab)`);
} else {
verifiedCount++;
}
continue;
}
// 2. Nếu không thấy trong Vocab, thử tìm trong Sentences bằng trường context
const sentence = await Sentences.findOne({ where: { text: ctx.context } });
if (sentence) {
if (ctx.reference_id !== sentence.id) {
await ctx.update({ reference_id: sentence.id });
fixedCount++;
console.log(`✅ Fixed ID: '${ctx.title}' -> ${sentence.id} (Sentence)`);
} else {
verifiedCount++;
}
}
}
console.log('\n--- KẾT QUẢ SỬA LỖI ---');
console.log(`✅ Đã sửa: ${fixedCount} bản ghi.`);
console.log(`✔️ Đã chuẩn sẵn: ${verifiedCount} bản ghi.`);
process.exit(0);
} catch (error) {
console.error('❌ Lỗi:', error);
process.exit(1);
}
}
fixReferenceIds();

View File

@@ -0,0 +1,34 @@
const { Context } = require('../models');
const { Op } = require('sequelize');
const { sequelize } = require('../config/database');
async function migrate() {
try {
await sequelize.authenticate();
console.log('✅ Kết nối database thành công.');
const fromDate = new Date('2026-02-28T09:00:00Z');
console.log(`🔍 Tìm kiếm các context có status = 6 và update từ ${fromDate.toISOString()} trở đi...`);
const [affectedCount] = await Context.update(
{ status: 5 },
{
where: {
status: 6,
updated_at: {
[Op.gte]: fromDate
}
}
}
);
console.log(`✅ Đã cập nhật thành công ${affectedCount} bản ghi từ status 6 sang status 5.`);
process.exit(0);
} catch (error) {
console.error('❌ Lỗi khi migration:', error);
process.exit(1);
}
}
migrate();

Binary file not shown.

View File

@@ -0,0 +1,108 @@
const { Context, Vocab, Sentences } = require('../models');
const { sequelize } = require('../config/database');
// --- helper: xác định slot image từ tên file/URL ---
function resolveImageSlot(imageUrl) {
if (!imageUrl) return null;
const filename = imageUrl.split('/').pop().split('?')[0];
const parts = filename.split('_');
for (const part of parts) {
const key = part.toLowerCase();
if (key === 'square') return 'image_square';
if (key === 'small') return 'image_small';
if (key === 'normal') return 'image_normal';
}
// Nếu không tìm thấy trong underscore, thử tìm trong toàn bộ URL
if (imageUrl.toLowerCase().includes('square')) return 'image_square';
if (imageUrl.toLowerCase().includes('small')) return 'image_small';
if (imageUrl.toLowerCase().includes('normal')) return 'image_normal';
return null;
}
async function syncImages() {
try {
await sequelize.authenticate();
console.log('✅ Kết nối database thành công.');
const contexts = await Context.findAll({
where: { status: 5 }
});
console.log(`📦 Tìm thấy ${contexts.length} bản ghi Context ở status 5.`);
let vocabUpdated = 0;
let sentencesUpdated = 0;
let skipped = 0;
for (const ctx of contexts) {
const imageUrl = ctx.image;
const slot = resolveImageSlot(imageUrl);
if (!imageUrl || !slot) {
console.warn(`⚠️ ID ${ctx.uuid}: Không xác định được ảnh hoặc slot (URL: ${imageUrl}). Bỏ qua.`);
skipped++;
continue;
}
let updated = false;
// Ưu tiên 1: Dùng reference_id
if (ctx.reference_id) {
// Thử tìm trong Vocab
const vocab = await Vocab.findByPk(ctx.reference_id);
if (vocab) {
await vocab.update({ [slot]: [imageUrl] });
vocabUpdated++;
updated = true;
} else {
// Thử tìm trong Sentences
const sentence = await Sentences.findByPk(ctx.reference_id);
if (sentence) {
await sentence.update({ [slot]: [imageUrl] });
sentencesUpdated++;
updated = true;
}
}
}
// Ưu tiên 2: Nếu chưa update được (hoặc ko có ref_id), thử match theo text
if (!updated) {
// Thử khớp Vocab.text = Context.title
const vocabByText = await Vocab.findOne({ where: { text: ctx.title } });
if (vocabByText) {
await vocabByText.update({ [slot]: [imageUrl] });
vocabUpdated++;
updated = true;
} else if (ctx.context) {
// Thử khớp Sentences.text = Context.context
const sentenceByText = await Sentences.findOne({ where: { text: ctx.context } });
if (sentenceByText) {
await sentenceByText.update({ [slot]: [imageUrl] });
sentencesUpdated++;
updated = true;
}
}
}
if (updated) {
await ctx.update({ status: 6 });
} else {
console.warn(`❌ ID ${ctx.uuid}: Không tìm thấy entity đích để cập nhật ảnh (Title: ${ctx.title}).`);
skipped++;
}
}
console.log('\n--- KẾT QUẢ ĐỒNG BỘ ẢNH ---');
console.log(`✅ Cập nhật Vocab: ${vocabUpdated} bản ghi.`);
console.log(`✅ Cập nhật Sentences: ${sentencesUpdated} bản ghi.`);
console.log(`⚠️ Bỏ qua hoặc lỗi: ${skipped} bản ghi.`);
console.log(`📊 Tổng xử lý: ${contexts.length}`);
process.exit(0);
} catch (error) {
console.error('❌ Lỗi khi đồng bộ:', error);
process.exit(1);
}
}
syncImages();

View File

@@ -0,0 +1,67 @@
const { Context, Sentences } = require('../models');
const { Op } = require('sequelize');
const { sequelize } = require('../config/database');
async function syncRefIdFromSentences() {
try {
await sequelize.authenticate();
console.log('✅ Kết nối database thành công.');
// 1. Tìm các bản ghi status = 5 và reference_id đang null hoặc trống
const contexts = await Context.findAll({
where: {
status: 5,
[Op.or]: [
{ reference_id: null },
{ reference_id: '' }
]
}
});
console.log(`📊 Tìm thấy ${contexts.length} bản ghi status = 5 thiếu reference_id.`);
let updatedCount = 0;
let notFoundCount = 0;
for (const context of contexts) {
// Tìm câu trong bảng Sentences có text khớp chính xác với title của Context
const sentence = await Sentences.findOne({
where: {
text: context.title
}
});
if (sentence) {
await context.update({ reference_id: sentence.id });
updatedCount++;
// console.log(`✅ ID ${context.uuid}: Đã khớp '${context.title}' -> Sentence ID ${sentence.id}`);
} else {
notFoundCount++;
// console.log(`⚠️ ID ${context.uuid}: Không tìm thấy câu '${context.title}' trong bảng Sentences.`);
}
}
// 2. Kiểm tra lại số lượng còn sót sau khi update
const remainingCount = await Context.count({
where: {
status: 5,
[Op.or]: [
{ reference_id: null },
{ reference_id: '' }
]
}
});
console.log('\n--- KẾT QUẢ CẬP NHẬT REFERENCE_ID TỪ SENTENCES ---');
console.log(`✅ Đã cập nhật thành công: ${updatedCount} bản ghi.`);
console.log(`❌ Không tìm thấy Sentence khớp: ${notFoundCount} bản ghi.`);
console.log(`📊 Tổng số bản ghi status 5 vẫn còn thiếu reference_id: ${remainingCount}`);
process.exit(0);
} catch (error) {
console.error('❌ Lỗi khi migration:', error);
process.exit(1);
}
}
syncRefIdFromSentences();

View File

@@ -0,0 +1,67 @@
const { Context, Vocab } = require('../models');
const { Op } = require('sequelize');
const { sequelize } = require('../config/database');
async function syncRefIdFromVocab() {
try {
await sequelize.authenticate();
console.log('✅ Kết nối database thành công.');
// 1. Tìm các bản ghi status = 5 và reference_id đang null hoặc trống
const contexts = await Context.findAll({
where: {
status: 5,
[Op.or]: [
{ reference_id: null },
{ reference_id: '' }
]
}
});
console.log(`📊 Tìm thấy ${contexts.length} bản ghi status = 5 thiếu reference_id.`);
let updatedCount = 0;
let notFoundCount = 0;
for (const context of contexts) {
// Tìm từ trong bảng Vocab có text khớp chính xác với title của Context
const vocab = await Vocab.findOne({
where: {
text: context.title
}
});
if (vocab) {
await context.update({ reference_id: vocab.vocab_id });
updatedCount++;
// console.log(`✅ ID ${context.uuid}: Đã khớp '${context.title}' -> Vocab ID ${vocab.vocab_id}`);
} else {
notFoundCount++;
console.log(`⚠️ ID ${context.uuid}: Không tìm thấy từ '${context.title}' trong bảng Vocab.`);
}
}
// 2. Kiểm tra lại số lượng còn sót sau khi update
const remainingCount = await Context.count({
where: {
status: 5,
[Op.or]: [
{ reference_id: null },
{ reference_id: '' }
]
}
});
console.log('\n--- KẾT QUẢ CẬP NHẬT REFERENCE_ID ---');
console.log(`✅ Đã cập nhật thành công: ${updatedCount} bản ghi.`);
console.log(`❌ Không tìm thấy Vocab khớp: ${notFoundCount} bản ghi.`);
console.log(`📊 Tổng số bản ghi status 5 vẫn còn thiếu reference_id: ${remainingCount}`);
process.exit(0);
} catch (error) {
console.error('❌ Lỗi khi migration:', error);
process.exit(1);
}
}
syncRefIdFromVocab();

View File

@@ -0,0 +1,70 @@
const { Context } = require('../models');
const { Op } = require('sequelize');
const { sequelize } = require('../config/database');
async function updateTypeImage() {
try {
await sequelize.authenticate();
console.log('✅ Kết nối database thành công.');
// 1. Tìm các bản ghi status = 5 và type_image đang null
const contexts = await Context.findAll({
where: {
status: 5,
[Op.or]: [
{ type_image: null },
{ type_image: '' }
]
}
});
console.log(`📊 Tìm thấy ${contexts.length} bản ghi status = 5 thiếu type_image.`);
let updatedCount = 0;
let skippedCount = 0;
for (const context of contexts) {
if (!context.image) {
skippedCount++;
continue;
}
// Logic trích xuất type_image từ URL: https://image.senaai.tech/type/filename.jpg
// Ví dụ: https://image.senaai.tech/square/cool_square_1772002797233.jpg -> type là 'square'
const match = context.image.match(/image\.senaai\.tech\/([^/]+)\//);
if (match && match[1]) {
const extractedType = match[1];
await context.update({ type_image: extractedType });
updatedCount++;
console.log(`✅ ID ${context.id}: Cập nhật type_image = '${extractedType}' từ URL.`);
} else {
skippedCount++;
console.log(`⚠️ ID ${context.id}: Không thể trích xuất type từ URL: ${context.image}`);
}
}
// 2. Kiểm tra xem còn sót cái nào không
const remainingCount = await Context.count({
where: {
status: 5,
[Op.or]: [
{ type_image: null },
{ type_image: '' }
]
}
});
console.log('\n--- KẾT QUẢ ---');
console.log(`✅ Đã cập nhật: ${updatedCount} bản ghi.`);
console.log(`⏭️ Bỏ qua (không có ảnh hoặc URL không khớp): ${skippedCount} bản ghi.`);
console.log(`❌ Số lượng còn sót lại vẫn thiếu type_image: ${remainingCount} bản ghi.`);
process.exit(0);
} catch (error) {
console.error('❌ Lỗi khi cập nhật:', error);
process.exit(1);
}
}
updateTypeImage();

View File

@@ -13,7 +13,7 @@ const Sentences = sequelize.define('Sentences', {
type: DataTypes.TEXT, type: DataTypes.TEXT,
allowNull: false allowNull: false
}, },
ipa : { ipa: {
type: DataTypes.TEXT, type: DataTypes.TEXT,
comment: 'International Phonetic Alphabet representation' comment: 'International Phonetic Alphabet representation'
}, },
@@ -23,7 +23,7 @@ const Sentences = sequelize.define('Sentences', {
defaultValue: '', defaultValue: '',
comment: 'Vietnamese meaning' comment: 'Vietnamese meaning'
}, },
grade : { grade: {
type: DataTypes.TEXT, type: DataTypes.TEXT,
defaultValue: '0', defaultValue: '0',
comment: 'Grade level (e.g., Grade 1, Grade 2)' comment: 'Grade level (e.g., Grade 1, Grade 2)'
@@ -51,7 +51,7 @@ const Sentences = sequelize.define('Sentences', {
defaultValue: [], defaultValue: [],
comment: 'Array of image URLs' comment: 'Array of image URLs'
}, },
audio : { audio: {
type: DataTypes.JSON, type: DataTypes.JSON,
comment: 'Array of audio URLs' comment: 'Array of audio URLs'
}, },
@@ -65,11 +65,16 @@ const Sentences = sequelize.define('Sentences', {
defaultValue: '', defaultValue: '',
comment: 'Lưu ý về ngữ cảnh sử dụng câu này' comment: 'Lưu ý về ngữ cảnh sử dụng câu này'
}, },
etc : { etc: {
type: DataTypes.TEXT, type: DataTypes.TEXT,
defaultValue: '', defaultValue: '',
comment: 'Các thông tin khác liên quan đến câu này (ví dụ: level, grammar points, etc.)' comment: 'Các thông tin khác liên quan đến câu này (ví dụ: level, grammar points, etc.)'
}, },
status: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: 'Trạng thái confirm (0 = chưa confirm)'
},
is_active: { is_active: {
type: DataTypes.BOOLEAN, type: DataTypes.BOOLEAN,
defaultValue: true, defaultValue: true,
@@ -83,7 +88,7 @@ const Sentences = sequelize.define('Sentences', {
type: DataTypes.DATE, type: DataTypes.DATE,
defaultValue: DataTypes.NOW defaultValue: DataTypes.NOW
} }
}, { }, {
tableName: 'sentences', tableName: 'sentences',
timestamps: true, timestamps: true,
createdAt: 'created_at', createdAt: 'created_at',
@@ -98,6 +103,6 @@ const Sentences = sequelize.define('Sentences', {
fields: [{ name: 'category', length: 191 }] fields: [{ name: 'category', length: 191 }]
} }
] ]
}); });
module.exports = Sentences; module.exports = Sentences;

View File

@@ -15,7 +15,7 @@ const Vocab = sequelize.define('Vocab', {
allowNull: false, allowNull: false,
index: true index: true
}, },
ipa : { ipa: {
type: DataTypes.STRING(100), type: DataTypes.STRING(100),
comment: 'International Phonetic Alphabet representation' comment: 'International Phonetic Alphabet representation'
}, },
@@ -62,7 +62,7 @@ const Vocab = sequelize.define('Vocab', {
type: DataTypes.JSON, type: DataTypes.JSON,
comment: 'Array of image URLs' comment: 'Array of image URLs'
}, },
audio : { audio: {
type: DataTypes.JSON, type: DataTypes.JSON,
comment: 'Array of audio URLs' comment: 'Array of audio URLs'
}, },
@@ -86,6 +86,16 @@ const Vocab = sequelize.define('Vocab', {
type: DataTypes.JSON, type: DataTypes.JSON,
comment: 'Grammar constraints (followed_by, match_subject, match_with, phonetic_rules, etc.)' comment: 'Grammar constraints (followed_by, match_subject, match_with, phonetic_rules, etc.)'
}, },
status: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: 'Trạng thái confirm (0 = chưa confirm)'
},
etc: {
type: DataTypes.TEXT,
defaultValue: '',
comment: 'Thông tin bổ sung'
},
is_active: { is_active: {
type: DataTypes.BOOLEAN, type: DataTypes.BOOLEAN,
defaultValue: true, defaultValue: true,
@@ -99,7 +109,7 @@ const Vocab = sequelize.define('Vocab', {
type: DataTypes.DATE, type: DataTypes.DATE,
defaultValue: DataTypes.NOW defaultValue: DataTypes.NOW
} }
}, { }, {
tableName: 'vocab', tableName: 'vocab',
timestamps: true, timestamps: true,
createdAt: 'created_at', createdAt: 'created_at',
@@ -118,6 +128,6 @@ const Vocab = sequelize.define('Vocab', {
fields: ['category'] fields: ['category']
} }
] ]
}); });
module.exports = Vocab; module.exports = Vocab;