|
|
@@ -794,19 +794,33 @@ export class ShopDatabase {
|
|
|
// 1. It's new content (no existing record)
|
|
|
// 2. The content hash has changed from the previous version
|
|
|
const changed = existing ? existing.content_hash !== contentHash : true;
|
|
|
- const id = uuidv4();
|
|
|
|
|
|
- // Insert new content record
|
|
|
- this.db.prepare(`
|
|
|
- INSERT INTO shop_content (id, shop_id, content_type, url, content, content_hash, changed, enabled, created_at, updated_at, title)
|
|
|
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
|
- `).run(id, shopId, contentType, url, content, contentHash, changed ? 1 : 0, 1, now, now, title || null);
|
|
|
+ if (existing) {
|
|
|
+ // Update existing record instead of creating a new one
|
|
|
+ // This prevents duplicate entries and maintains consistent content_id for Qdrant
|
|
|
+ this.db.prepare(`
|
|
|
+ UPDATE shop_content
|
|
|
+ SET content = ?, content_hash = ?, changed = ?, updated_at = ?, title = ?
|
|
|
+ WHERE id = ?
|
|
|
+ `).run(content, contentHash, changed ? 1 : 0, now, title || null, existing.id);
|
|
|
|
|
|
- if (changed) {
|
|
|
- logger.info(`Content changed detected for ${contentType} in shop ${shopId}`);
|
|
|
- }
|
|
|
+ if (changed) {
|
|
|
+ logger.info(`Content changed detected for ${contentType} in shop ${shopId} (URL: ${url})`);
|
|
|
+ }
|
|
|
+
|
|
|
+ return { changed, contentId: existing.id };
|
|
|
+ } else {
|
|
|
+ // Insert new content record only if it doesn't exist
|
|
|
+ const id = uuidv4();
|
|
|
+ this.db.prepare(`
|
|
|
+ INSERT INTO shop_content (id, shop_id, content_type, url, content, content_hash, changed, enabled, created_at, updated_at, title)
|
|
|
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
|
+ `).run(id, shopId, contentType, url, content, contentHash, 1, 1, now, now, title || null);
|
|
|
+
|
|
|
+ logger.info(`New content saved for ${contentType} in shop ${shopId} (URL: ${url})`);
|
|
|
|
|
|
- return { changed, contentId: id };
|
|
|
+ return { changed: true, contentId: id };
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
getLatestContent(shopId: string): { [key: string]: ShopContent } {
|
|
|
@@ -1029,6 +1043,54 @@ export class ShopDatabase {
|
|
|
return deletedCount;
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Clean up duplicate content entries in the database
|
|
|
+ * Keeps only the latest entry for each shop_id + content_type + url combination
|
|
|
+ * Returns the number of duplicate entries removed
|
|
|
+ */
|
|
|
+ cleanupDuplicateContent(shopId?: string): number {
|
|
|
+ // Find all duplicates (entries that are not the latest for their shop_id + content_type + url)
|
|
|
+ let query = `
|
|
|
+ DELETE FROM shop_content
|
|
|
+ WHERE id NOT IN (
|
|
|
+ SELECT id FROM (
|
|
|
+ SELECT id, ROW_NUMBER() OVER (
|
|
|
+ PARTITION BY shop_id, content_type, url
|
|
|
+ ORDER BY created_at DESC
|
|
|
+ ) as rn
|
|
|
+ FROM shop_content
|
|
|
+ ) WHERE rn = 1
|
|
|
+ )
|
|
|
+ `;
|
|
|
+
|
|
|
+ if (shopId) {
|
|
|
+ query = `
|
|
|
+ DELETE FROM shop_content
|
|
|
+ WHERE shop_id = ? AND id NOT IN (
|
|
|
+ SELECT id FROM (
|
|
|
+ SELECT id, ROW_NUMBER() OVER (
|
|
|
+ PARTITION BY shop_id, content_type, url
|
|
|
+ ORDER BY created_at DESC
|
|
|
+ ) as rn
|
|
|
+ FROM shop_content
|
|
|
+ WHERE shop_id = ?
|
|
|
+ ) WHERE rn = 1
|
|
|
+ )
|
|
|
+ `;
|
|
|
+ const result = this.db.prepare(query).run(shopId, shopId);
|
|
|
+ if (result.changes > 0) {
|
|
|
+ logger.info(`Cleaned up ${result.changes} duplicate content entries for shop ${shopId}`);
|
|
|
+ }
|
|
|
+ return result.changes;
|
|
|
+ } else {
|
|
|
+ const result = this.db.prepare(query).run();
|
|
|
+ if (result.changes > 0) {
|
|
|
+ logger.info(`Cleaned up ${result.changes} duplicate content entries across all shops`);
|
|
|
+ }
|
|
|
+ return result.changes;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
// Scheduled jobs operations
|
|
|
createScheduledJob(shopId: string, nextRunAt: string, frequency: string | null, lastModified: string | null): string {
|
|
|
const id = uuidv4();
|