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

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