This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
104
migration_scripts/add-status-etc-fields.js
Normal file
104
migration_scripts/add-status-etc-fields.js
Normal 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();
|
||||||
53
migration_scripts/check-null-ref-id.js
Normal file
53
migration_scripts/check-null-ref-id.js
Normal 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();
|
||||||
57
migration_scripts/fix-reference-ids.js
Normal file
57
migration_scripts/fix-reference-ids.js
Normal 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();
|
||||||
34
migration_scripts/migrate-status-6-to-5.js
Normal file
34
migration_scripts/migrate-status-6-to-5.js
Normal 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();
|
||||||
BIN
migration_scripts/output.txt
Normal file
BIN
migration_scripts/output.txt
Normal file
Binary file not shown.
108
migration_scripts/sync-images-to-all.js
Normal file
108
migration_scripts/sync-images-to-all.js
Normal 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();
|
||||||
67
migration_scripts/sync-ref-id-from-sentences.js
Normal file
67
migration_scripts/sync-ref-id-from-sentences.js
Normal 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();
|
||||||
67
migration_scripts/sync-ref-id-from-vocab.js
Normal file
67
migration_scripts/sync-ref-id-from-vocab.js
Normal 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();
|
||||||
70
migration_scripts/update-type-image-from-url.js
Normal file
70
migration_scripts/update-type-image-from-url.js
Normal 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();
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user