This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
const { Context, Vocab } = require('../models');
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
/**
|
||||
* Context Controller - Workflow-based status management
|
||||
@@ -11,7 +12,7 @@ class ContextController {
|
||||
*/
|
||||
async createContext(req, res, next) {
|
||||
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
|
||||
if (!title || !desc || !grade) {
|
||||
@@ -58,18 +59,96 @@ class ContextController {
|
||||
async getContextsByStatus(req, res, next) {
|
||||
try {
|
||||
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 where = { status: parseInt(status) };
|
||||
if (type) where.type = type;
|
||||
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({
|
||||
where,
|
||||
limit: parseInt(limit),
|
||||
offset: parseInt(offset),
|
||||
order: [['created_at', 'DESC']]
|
||||
order
|
||||
});
|
||||
|
||||
res.json({
|
||||
@@ -308,8 +387,8 @@ class ContextController {
|
||||
const updatedImagesSquare = currentVocab.image_square || [];
|
||||
updatedImagesSquare.push(context.image);
|
||||
await currentVocab.update({ image_square: updatedImagesSquare });
|
||||
} else if (context.type_image === 'normal') {
|
||||
const updatedImagesNormal = currentVocab.image_normal || [];
|
||||
} else if (context.type_image === 'normal') {
|
||||
const updatedImagesNormal = currentVocab.image_normal || [];
|
||||
updatedImagesNormal.push(context.image);
|
||||
await currentVocab.update({ image_normal: updatedImagesNormal });
|
||||
}
|
||||
@@ -416,11 +495,11 @@ class ContextController {
|
||||
type,
|
||||
status,
|
||||
grade,
|
||||
reference_id,
|
||||
page = 1,
|
||||
limit = 50
|
||||
} = req.body;
|
||||
|
||||
const { Op } = require('sequelize');
|
||||
const offset = (page - 1) * limit;
|
||||
const where = {};
|
||||
|
||||
@@ -430,7 +509,8 @@ class ContextController {
|
||||
}
|
||||
if (type) where.type = type;
|
||||
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 ──────────────────────────────────────────────────────
|
||||
// `search` → title OR context (cả hai cùng lúc)
|
||||
@@ -440,7 +520,7 @@ class ContextController {
|
||||
|
||||
if (search) {
|
||||
textConditions.push(
|
||||
{ title: { [Op.like]: `%${search}%` } },
|
||||
{ title: { [Op.like]: `%${search}%` } },
|
||||
{ context: { [Op.like]: `%${search}%` } }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -127,6 +127,8 @@ exports.getAllSentences = async (req, res) => {
|
||||
topic,
|
||||
text,
|
||||
search,
|
||||
status,
|
||||
sort,
|
||||
is_active = true
|
||||
} = req.query;
|
||||
|
||||
@@ -137,6 +139,11 @@ exports.getAllSentences = async (req, res) => {
|
||||
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) {
|
||||
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({
|
||||
where,
|
||||
limit: parseInt(limit),
|
||||
offset: parseInt(offset),
|
||||
order: [['created_at', 'DESC']]
|
||||
order
|
||||
});
|
||||
|
||||
res.json({
|
||||
|
||||
@@ -60,10 +60,10 @@ const { Op } = require('sequelize');
|
||||
*/
|
||||
exports.createVocab = async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
const {
|
||||
text,
|
||||
ipa,
|
||||
base_word,
|
||||
base_word,
|
||||
vi,
|
||||
category,
|
||||
topic,
|
||||
@@ -160,16 +160,18 @@ exports.createVocab = async (req, res) => {
|
||||
*/
|
||||
exports.getAllVocabs = async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
limit = 20,
|
||||
category,
|
||||
const {
|
||||
page = 1,
|
||||
limit = 20,
|
||||
category,
|
||||
topic,
|
||||
base_word,
|
||||
text,
|
||||
search,
|
||||
grade_start,
|
||||
grade_end,
|
||||
status,
|
||||
sort,
|
||||
is_active = true
|
||||
} = req.query;
|
||||
|
||||
@@ -181,6 +183,11 @@ exports.getAllVocabs = async (req, res) => {
|
||||
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) {
|
||||
where.category = category;
|
||||
}
|
||||
@@ -221,11 +228,19 @@ exports.getAllVocabs = async (req, res) => {
|
||||
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({
|
||||
where,
|
||||
limit: parseInt(limit),
|
||||
offset: parseInt(offset),
|
||||
order: [['created_at', 'DESC']]
|
||||
order
|
||||
});
|
||||
|
||||
res.json({
|
||||
@@ -276,9 +291,9 @@ exports.getVocabById = async (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
const vocab = await Vocab.findOne({
|
||||
where: {
|
||||
where: {
|
||||
vocab_id: id,
|
||||
is_active: true
|
||||
is_active: true
|
||||
}
|
||||
});
|
||||
|
||||
@@ -339,7 +354,7 @@ exports.getVocabsWithoutIpa = async (req, res) => {
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const { count, rows } = await Vocab.findAndCountAll({
|
||||
where: {
|
||||
where: {
|
||||
is_active: true,
|
||||
[Op.or]: [
|
||||
{ ipa: null },
|
||||
@@ -407,7 +422,7 @@ exports.getVocabsWithoutImages = async (req, res) => {
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const { count, rows } = await Vocab.findAndCountAll({
|
||||
where: {
|
||||
where: {
|
||||
is_active: true,
|
||||
[Op.or]: [
|
||||
{ image_small: null },
|
||||
@@ -798,7 +813,7 @@ exports.getVocabStats = async (req, res) => {
|
||||
*/
|
||||
exports.searchVocabs = async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
const {
|
||||
topic,
|
||||
category,
|
||||
base_word,
|
||||
@@ -876,7 +891,7 @@ exports.searchVocabs = async (req, res) => {
|
||||
// Handle shuffle_pos: find replacement words based on syntax
|
||||
if (shuffle_pos && typeof shuffle_pos === 'object') {
|
||||
const syntaxConditions = [];
|
||||
|
||||
|
||||
if (shuffle_pos.is_subject === true) {
|
||||
syntaxConditions.push({ 'syntax.is_subject': true });
|
||||
}
|
||||
@@ -911,7 +926,7 @@ exports.searchVocabs = async (req, res) => {
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
if (orConditions.length === 1) {
|
||||
where[Op.and] = where[Op.and] || [];
|
||||
where[Op.and].push(orConditions[0]);
|
||||
@@ -975,7 +990,7 @@ exports.searchVocabs = async (req, res) => {
|
||||
exports.getAllCategories = async (req, res) => {
|
||||
try {
|
||||
const categories = await Vocab.findAll({
|
||||
where: {
|
||||
where: {
|
||||
is_active: true,
|
||||
category: { [Op.ne]: null }
|
||||
},
|
||||
@@ -1026,7 +1041,7 @@ exports.getAllCategories = async (req, res) => {
|
||||
exports.getAllTopics = async (req, res) => {
|
||||
try {
|
||||
const topics = await Vocab.findAll({
|
||||
where: {
|
||||
where: {
|
||||
is_active: true,
|
||||
topic: { [Op.ne]: null }
|
||||
},
|
||||
|
||||
@@ -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 Context = require('./models/Context');
|
||||
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;
|
||||
}
|
||||
const { Context, Vocab, Sentences } = require('./models');
|
||||
|
||||
// ─── helper: xác định slot image từ tên file/URL ────────────────────────────
|
||||
function resolveImageSlot(imageUrl) {
|
||||
if (!imageUrl) return null;
|
||||
|
||||
// Lấy phần tên file (bỏ path/query)
|
||||
const filename = imageUrl.split('/').pop().split('?')[0];
|
||||
const parts = filename.split('_');
|
||||
|
||||
// Duyệt tất cả phần tử, tìm keyword
|
||||
for (const part of parts) {
|
||||
const key = part.toLowerCase();
|
||||
if (key === 'square') return 'image_square';
|
||||
if (key === 'small') return 'image_small';
|
||||
if (key === 'small') return 'image_small';
|
||||
if (key === 'normal') return 'image_normal';
|
||||
}
|
||||
|
||||
return null; // không xác định được
|
||||
return null;
|
||||
}
|
||||
|
||||
// ─── main ────────────────────────────────────────────────────────────────────
|
||||
async function run() {
|
||||
try {
|
||||
await sequelize.authenticate();
|
||||
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`);
|
||||
|
||||
if (contexts.length === 0) {
|
||||
@@ -61,76 +28,78 @@ async function run() {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
let created = 0;
|
||||
let updated = 0;
|
||||
let updatedVocab = 0;
|
||||
let updatedSentences = 0;
|
||||
let skipped = 0;
|
||||
|
||||
for (const ctx of contexts) {
|
||||
const text = ctx.context; // so sánh với Sentences.text
|
||||
const imageUrl = ctx.image;
|
||||
const imageSlot = resolveImageSlot(imageUrl);
|
||||
const imageUrl = ctx.image;
|
||||
const slot = resolveImageSlot(imageUrl);
|
||||
|
||||
if (!text) {
|
||||
console.warn(` ⚠️ Context [${ctx.uuid}] không có context — bỏ qua`);
|
||||
if (!imageUrl || !slot) {
|
||||
console.warn(` ⚠️ [BỎ QUA] ID ${ctx.uuid}: Không xác định được ảnh/slot`);
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. Tìm Sentence có text khớp
|
||||
let sentence = await Sentences.findOne({
|
||||
where: { text }
|
||||
});
|
||||
|
||||
if (!sentence) {
|
||||
// ── Tạo mới ──────────────────────────────────────────────────────────
|
||||
const newData = {
|
||||
text,
|
||||
is_active: true,
|
||||
image_small: [],
|
||||
image_square: [],
|
||||
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);
|
||||
console.log(` ✅ [TẠO MỚI] "${text}"${imageSlot ? ` → ${imageSlot}` : ''}`);
|
||||
created++;
|
||||
let targetEntity = null;
|
||||
let entityType = '';
|
||||
|
||||
// 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 {
|
||||
// ── Cập nhật image ───────────────────────────────────────────────────
|
||||
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++;
|
||||
// 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 (!targetEntity) {
|
||||
targetEntity = await Sentences.findOne({
|
||||
where: { text: ctx.title, is_active: true }
|
||||
});
|
||||
if (!targetEntity) targetEntity = await Sentences.findOne({ where: { text: ctx.title } });
|
||||
}
|
||||
entityType = 'Sentence';
|
||||
}
|
||||
|
||||
// 3. Chuyển Context.status → 6
|
||||
await ctx.update({ status: 6 });
|
||||
if (targetEntity) {
|
||||
// 1. Cập nhật ảnh (Ghi đè - để mảng chỉ có 1 ảnh)
|
||||
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 {
|
||||
console.warn(` ❌ [KHÔNG THẤY] "${ctx.title}" (Type: ${ctx.type})`);
|
||||
skipped++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n─────────────────────────────────────');
|
||||
console.log(`📊 Kết quả:`);
|
||||
console.log(` ✅ Tạo mới : ${created}`);
|
||||
console.log(` 🔄 Cập nhật : ${updated}`);
|
||||
console.log(` ⚠️ Bỏ qua : ${skipped}`);
|
||||
console.log(` 📌 Tổng : ${contexts.length}`);
|
||||
console.log(` ✅ Cập nhật Vocab : ${updatedVocab}`);
|
||||
console.log(` ✅ Cập nhật Sentences: ${updatedSentences}`);
|
||||
console.log(` ⚠️ Bỏ qua : ${skipped}`);
|
||||
console.log('─────────────────────────────────────');
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('❌ Lỗi:', error.message);
|
||||
console.error(error.stack);
|
||||
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();
|
||||
@@ -2,102 +2,107 @@ const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const Sentences = sequelize.define('Sentences', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
comment: 'Unique identifier for sentence entry'
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
comment: 'Unique identifier for sentence entry'
|
||||
},
|
||||
// Từ thực tế (wash, washes, washing, ate, eaten...)
|
||||
text: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false
|
||||
},
|
||||
ipa: {
|
||||
type: DataTypes.TEXT,
|
||||
comment: 'International Phonetic Alphabet representation'
|
||||
},
|
||||
// Nội dung dùng chung (có thể lưu JSON để dễ quản lý hoặc dùng chung cho cả group)
|
||||
vi: {
|
||||
type: DataTypes.TEXT,
|
||||
defaultValue: '',
|
||||
comment: 'Vietnamese meaning'
|
||||
},
|
||||
grade: {
|
||||
type: DataTypes.TEXT,
|
||||
defaultValue: '0',
|
||||
comment: 'Grade level (e.g., Grade 1, Grade 2)'
|
||||
},
|
||||
category: {
|
||||
type: DataTypes.TEXT,
|
||||
comment: 'Category of the sentence (e.g., Action Verbs, Nouns)'
|
||||
},
|
||||
topic: {
|
||||
type: DataTypes.TEXT,
|
||||
comment: 'Topic of the sentence (e.g., Food, Travel, Education)'
|
||||
},
|
||||
image_small: {
|
||||
type: DataTypes.JSON,
|
||||
defaultValue: [],
|
||||
comment: 'Array of image URLs'
|
||||
},
|
||||
image_square: {
|
||||
type: DataTypes.JSON,
|
||||
defaultValue: [],
|
||||
comment: 'Array of image URLs'
|
||||
},
|
||||
image_normal: {
|
||||
type: DataTypes.JSON,
|
||||
defaultValue: [],
|
||||
comment: 'Array of image URLs'
|
||||
},
|
||||
audio: {
|
||||
type: DataTypes.JSON,
|
||||
comment: 'Array of audio URLs'
|
||||
},
|
||||
tags: {
|
||||
type: DataTypes.JSON,
|
||||
defaultValue: [],
|
||||
comment: 'Array of tags for categorization'
|
||||
},
|
||||
usage_note: {
|
||||
type: DataTypes.TEXT,
|
||||
defaultValue: '',
|
||||
comment: 'Lưu ý về ngữ cảnh sử dụng câu này'
|
||||
},
|
||||
etc: {
|
||||
type: DataTypes.TEXT,
|
||||
defaultValue: '',
|
||||
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: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: true,
|
||||
comment: 'Whether this sentence entry is active'
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
updated_at: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW
|
||||
}
|
||||
}, {
|
||||
tableName: 'sentences',
|
||||
timestamps: true,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at',
|
||||
indexes: [
|
||||
{
|
||||
name: 'idx_sentences_text',
|
||||
fields: [{ name: 'text', length: 191 }]
|
||||
},
|
||||
// Từ thực tế (wash, washes, washing, ate, eaten...)
|
||||
text: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false
|
||||
},
|
||||
ipa : {
|
||||
type: DataTypes.TEXT,
|
||||
comment: 'International Phonetic Alphabet representation'
|
||||
},
|
||||
// Nội dung dùng chung (có thể lưu JSON để dễ quản lý hoặc dùng chung cho cả group)
|
||||
vi: {
|
||||
type: DataTypes.TEXT,
|
||||
defaultValue: '',
|
||||
comment: 'Vietnamese meaning'
|
||||
},
|
||||
grade : {
|
||||
type: DataTypes.TEXT,
|
||||
defaultValue: '0',
|
||||
comment: 'Grade level (e.g., Grade 1, Grade 2)'
|
||||
},
|
||||
category: {
|
||||
type: DataTypes.TEXT,
|
||||
comment: 'Category of the sentence (e.g., Action Verbs, Nouns)'
|
||||
},
|
||||
topic: {
|
||||
type: DataTypes.TEXT,
|
||||
comment: 'Topic of the sentence (e.g., Food, Travel, Education)'
|
||||
},
|
||||
image_small: {
|
||||
type: DataTypes.JSON,
|
||||
defaultValue: [],
|
||||
comment: 'Array of image URLs'
|
||||
},
|
||||
image_square: {
|
||||
type: DataTypes.JSON,
|
||||
defaultValue: [],
|
||||
comment: 'Array of image URLs'
|
||||
},
|
||||
image_normal: {
|
||||
type: DataTypes.JSON,
|
||||
defaultValue: [],
|
||||
comment: 'Array of image URLs'
|
||||
},
|
||||
audio : {
|
||||
type: DataTypes.JSON,
|
||||
comment: 'Array of audio URLs'
|
||||
},
|
||||
tags: {
|
||||
type: DataTypes.JSON,
|
||||
defaultValue: [],
|
||||
comment: 'Array of tags for categorization'
|
||||
},
|
||||
usage_note: {
|
||||
type: DataTypes.TEXT,
|
||||
defaultValue: '',
|
||||
comment: 'Lưu ý về ngữ cảnh sử dụng câu này'
|
||||
},
|
||||
etc : {
|
||||
type: DataTypes.TEXT,
|
||||
defaultValue: '',
|
||||
comment: 'Các thông tin khác liên quan đến câu này (ví dụ: level, grammar points, etc.)'
|
||||
},
|
||||
is_active: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: true,
|
||||
comment: 'Whether this sentence entry is active'
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
updated_at: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW
|
||||
{
|
||||
name: 'idx_category',
|
||||
fields: [{ name: 'category', length: 191 }]
|
||||
}
|
||||
}, {
|
||||
tableName: 'sentences',
|
||||
timestamps: true,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at',
|
||||
indexes: [
|
||||
{
|
||||
name: 'idx_sentences_text',
|
||||
fields: [{ name: 'text', length: 191 }]
|
||||
},
|
||||
{
|
||||
name: 'idx_category',
|
||||
fields: [{ name: 'category', length: 191 }]
|
||||
}
|
||||
]
|
||||
});
|
||||
]
|
||||
});
|
||||
|
||||
module.exports = Sentences;
|
||||
|
||||
236
models/Vocab.js
236
models/Vocab.js
@@ -3,121 +3,131 @@ const { sequelize } = require('../config/database');
|
||||
const Grade = require('./Grade');
|
||||
|
||||
const Vocab = sequelize.define('Vocab', {
|
||||
vocab_id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
comment: 'Unique identifier for vocabulary entry'
|
||||
vocab_id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
comment: 'Unique identifier for vocabulary entry'
|
||||
},
|
||||
// Từ thực tế (wash, washes, washing, ate, eaten...)
|
||||
text: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
index: true
|
||||
},
|
||||
ipa: {
|
||||
type: DataTypes.STRING(100),
|
||||
comment: 'International Phonetic Alphabet representation'
|
||||
},
|
||||
// Từ gốc để nhóm lại (wash, eat...)
|
||||
base_word: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
index: true
|
||||
},
|
||||
// Loại biến thể (V1, V2, V3, V_ing, Noun_Form...)
|
||||
form_key: {
|
||||
type: DataTypes.TEXT,
|
||||
defaultValue: 'base',
|
||||
comment: 'Form key indicating the type of word form (e.g., base, V1, V2, V3, V_ing, Noun_Form)'
|
||||
},
|
||||
// Nội dung dùng chung (có thể lưu JSON để dễ quản lý hoặc dùng chung cho cả group)
|
||||
vi: {
|
||||
type: DataTypes.TEXT,
|
||||
defaultValue: '',
|
||||
comment: 'Vietnamese meaning'
|
||||
},
|
||||
Grade_number: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0,
|
||||
comment: 'Numeric representation of grade unit lesson as GG UU LL for easier querying and sorting'
|
||||
},
|
||||
category: {
|
||||
type: DataTypes.STRING(100),
|
||||
comment: 'Category of the word (e.g., Action Verbs, Nouns)'
|
||||
},
|
||||
topic: {
|
||||
type: DataTypes.STRING(100),
|
||||
comment: 'Topic of the word (e.g., Food, Travel, Education)'
|
||||
},
|
||||
image_small: {
|
||||
type: DataTypes.JSON,
|
||||
comment: 'Array of image URLs'
|
||||
},
|
||||
image_square: {
|
||||
type: DataTypes.JSON,
|
||||
comment: 'Array of image URLs'
|
||||
},
|
||||
image_normal: {
|
||||
type: DataTypes.JSON,
|
||||
comment: 'Array of image URLs'
|
||||
},
|
||||
audio: {
|
||||
type: DataTypes.JSON,
|
||||
comment: 'Array of audio URLs'
|
||||
},
|
||||
example_sentences: {
|
||||
type: DataTypes.JSON,
|
||||
comment: 'Array of example sentences'
|
||||
},
|
||||
tags: {
|
||||
type: DataTypes.JSON,
|
||||
comment: 'Array of tags for categorization'
|
||||
},
|
||||
syntax: {
|
||||
type: DataTypes.JSON,
|
||||
comment: 'Syntax roles for Grammar Engine (is_subject, is_verb, is_object, is_be, is_adj, is_adv, is_article, verb_type, article_type, adv_type, position, priority, etc.)'
|
||||
},
|
||||
semantics: {
|
||||
type: DataTypes.JSON,
|
||||
comment: 'Semantic constraints (can_be_subject_type, can_take_object_type, can_modify, cannot_modify, word_type, is_countable, person_type, etc.)'
|
||||
},
|
||||
constraints: {
|
||||
type: DataTypes.JSON,
|
||||
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: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: true,
|
||||
comment: 'Whether this vocab entry is active'
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
updated_at: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW
|
||||
}
|
||||
}, {
|
||||
tableName: 'vocab',
|
||||
timestamps: true,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at',
|
||||
indexes: [
|
||||
{
|
||||
name: 'idx_vocab_text',
|
||||
fields: ['text']
|
||||
},
|
||||
// Từ thực tế (wash, washes, washing, ate, eaten...)
|
||||
text: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
index: true
|
||||
{
|
||||
name: 'idx_base_word',
|
||||
fields: ['base_word']
|
||||
},
|
||||
ipa : {
|
||||
type: DataTypes.STRING(100),
|
||||
comment: 'International Phonetic Alphabet representation'
|
||||
},
|
||||
// Từ gốc để nhóm lại (wash, eat...)
|
||||
base_word: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
index: true
|
||||
},
|
||||
// Loại biến thể (V1, V2, V3, V_ing, Noun_Form...)
|
||||
form_key: {
|
||||
type: DataTypes.TEXT,
|
||||
defaultValue: 'base',
|
||||
comment: 'Form key indicating the type of word form (e.g., base, V1, V2, V3, V_ing, Noun_Form)'
|
||||
},
|
||||
// Nội dung dùng chung (có thể lưu JSON để dễ quản lý hoặc dùng chung cho cả group)
|
||||
vi: {
|
||||
type: DataTypes.TEXT,
|
||||
defaultValue: '',
|
||||
comment: 'Vietnamese meaning'
|
||||
},
|
||||
Grade_number: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0,
|
||||
comment: 'Numeric representation of grade unit lesson as GG UU LL for easier querying and sorting'
|
||||
},
|
||||
category: {
|
||||
type: DataTypes.STRING(100),
|
||||
comment: 'Category of the word (e.g., Action Verbs, Nouns)'
|
||||
},
|
||||
topic: {
|
||||
type: DataTypes.STRING(100),
|
||||
comment: 'Topic of the word (e.g., Food, Travel, Education)'
|
||||
},
|
||||
image_small: {
|
||||
type: DataTypes.JSON,
|
||||
comment: 'Array of image URLs'
|
||||
},
|
||||
image_square: {
|
||||
type: DataTypes.JSON,
|
||||
comment: 'Array of image URLs'
|
||||
},
|
||||
image_normal: {
|
||||
type: DataTypes.JSON,
|
||||
comment: 'Array of image URLs'
|
||||
},
|
||||
audio : {
|
||||
type: DataTypes.JSON,
|
||||
comment: 'Array of audio URLs'
|
||||
},
|
||||
example_sentences: {
|
||||
type: DataTypes.JSON,
|
||||
comment: 'Array of example sentences'
|
||||
},
|
||||
tags: {
|
||||
type: DataTypes.JSON,
|
||||
comment: 'Array of tags for categorization'
|
||||
},
|
||||
syntax: {
|
||||
type: DataTypes.JSON,
|
||||
comment: 'Syntax roles for Grammar Engine (is_subject, is_verb, is_object, is_be, is_adj, is_adv, is_article, verb_type, article_type, adv_type, position, priority, etc.)'
|
||||
},
|
||||
semantics: {
|
||||
type: DataTypes.JSON,
|
||||
comment: 'Semantic constraints (can_be_subject_type, can_take_object_type, can_modify, cannot_modify, word_type, is_countable, person_type, etc.)'
|
||||
},
|
||||
constraints: {
|
||||
type: DataTypes.JSON,
|
||||
comment: 'Grammar constraints (followed_by, match_subject, match_with, phonetic_rules, etc.)'
|
||||
},
|
||||
is_active: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: true,
|
||||
comment: 'Whether this vocab entry is active'
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
updated_at: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW
|
||||
{
|
||||
name: 'idx_category',
|
||||
fields: ['category']
|
||||
}
|
||||
}, {
|
||||
tableName: 'vocab',
|
||||
timestamps: true,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at',
|
||||
indexes: [
|
||||
{
|
||||
name: 'idx_vocab_text',
|
||||
fields: ['text']
|
||||
},
|
||||
{
|
||||
name: 'idx_base_word',
|
||||
fields: ['base_word']
|
||||
},
|
||||
{
|
||||
name: 'idx_category',
|
||||
fields: ['category']
|
||||
}
|
||||
]
|
||||
});
|
||||
]
|
||||
});
|
||||
|
||||
module.exports = Vocab;
|
||||
|
||||
Reference in New Issue
Block a user