|
|
@@ -777,11 +777,24 @@ export class ShopDatabase {
|
|
|
url: string,
|
|
|
content: string,
|
|
|
title?: string
|
|
|
- ): { changed: boolean; contentId: string } {
|
|
|
+ ): { changed: boolean; contentId: string; skipped?: boolean } {
|
|
|
const crypto = require('crypto');
|
|
|
const contentHash = crypto.createHash('md5').update(content).digest('hex');
|
|
|
const now = new Date().toISOString();
|
|
|
|
|
|
+ // Check if this exact content_hash already exists for this shop (in ANY content type)
|
|
|
+ // This prevents the same page from being stored multiple times under different categories
|
|
|
+ const duplicateContent = this.db.prepare(`
|
|
|
+ SELECT id, content_type, url FROM shop_content
|
|
|
+ WHERE shop_id = ? AND content_hash = ? AND content_type != ?
|
|
|
+ LIMIT 1
|
|
|
+ `).get(shopId, contentHash, contentType) as { id: string; content_type: string; url: string } | undefined;
|
|
|
+
|
|
|
+ if (duplicateContent) {
|
|
|
+ logger.debug(`Skipping duplicate content for ${contentType} (URL: ${url}) - same content already exists in ${duplicateContent.content_type} (URL: ${duplicateContent.url})`);
|
|
|
+ return { changed: false, contentId: duplicateContent.id, skipped: true };
|
|
|
+ }
|
|
|
+
|
|
|
// Check if content exists and get previous hash for this specific URL
|
|
|
const existing = this.db.prepare(`
|
|
|
SELECT id, content_hash FROM shop_content
|
|
|
@@ -1091,6 +1104,54 @@ export class ShopDatabase {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Clean up duplicate content entries by content_hash
|
|
|
+ * When the same content appears in multiple categories, keep only the first occurrence
|
|
|
+ * Returns the number of duplicate entries removed
|
|
|
+ */
|
|
|
+ cleanupDuplicateContentByHash(shopId?: string): number {
|
|
|
+ // Find entries where the same content_hash appears in multiple content_types
|
|
|
+ // Keep the one with the earliest created_at, delete the rest
|
|
|
+ let query: string;
|
|
|
+ let params: any[] = [];
|
|
|
+
|
|
|
+ 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_hash
|
|
|
+ ORDER BY created_at ASC
|
|
|
+ ) as rn
|
|
|
+ FROM shop_content
|
|
|
+ WHERE shop_id = ?
|
|
|
+ ) WHERE rn = 1
|
|
|
+ )
|
|
|
+ `;
|
|
|
+ params = [shopId, shopId];
|
|
|
+ } else {
|
|
|
+ query = `
|
|
|
+ DELETE FROM shop_content
|
|
|
+ WHERE id NOT IN (
|
|
|
+ SELECT id FROM (
|
|
|
+ SELECT id, ROW_NUMBER() OVER (
|
|
|
+ PARTITION BY shop_id, content_hash
|
|
|
+ ORDER BY created_at ASC
|
|
|
+ ) as rn
|
|
|
+ FROM shop_content
|
|
|
+ ) WHERE rn = 1
|
|
|
+ )
|
|
|
+ `;
|
|
|
+ }
|
|
|
+
|
|
|
+ const result = this.db.prepare(query).run(...params);
|
|
|
+ if (result.changes > 0) {
|
|
|
+ logger.info(`Cleaned up ${result.changes} duplicate content entries by hash${shopId ? ` for shop ${shopId}` : ' across all shops'}`);
|
|
|
+ }
|
|
|
+ return result.changes;
|
|
|
+ }
|
|
|
+
|
|
|
// Scheduled jobs operations
|
|
|
createScheduledJob(shopId: string, nextRunAt: string, frequency: string | null, lastModified: string | null): string {
|
|
|
const id = uuidv4();
|