diff --git a/cwd-api/schemas/comment.sql b/cwd-api/schemas/comment.sql index 0e752db..7af2071 100644 --- a/cwd-api/schemas/comment.sql +++ b/cwd-api/schemas/comment.sql @@ -17,6 +17,7 @@ CREATE TABLE IF NOT EXISTS Comment ( likes INTEGER NOT NULL DEFAULT 0, priority INTEGER NOT NULL DEFAULT 1, status TEXT DEFAULT 'approved', + site_id TEXT NOT NULL DEFAULT '', -- 建立自引用外键约束(父子评论关系) FOREIGN KEY (parent_id) REFERENCES Comment (id) ON DELETE SET NULL ); @@ -24,3 +25,4 @@ CREATE TABLE IF NOT EXISTS Comment ( -- 可选:为常用查询字段创建索引以提高性能 CREATE INDEX IF NOT EXISTS idx_post_slug ON Comment(post_slug); CREATE INDEX IF NOT EXISTS idx_status ON Comment(status); +CREATE INDEX IF NOT EXISTS idx_site_id ON Comment(site_id); diff --git a/cwd-api/scripts/auto_migrate.js b/cwd-api/scripts/auto_migrate.js index c98b27d..71cc677 100644 --- a/cwd-api/scripts/auto_migrate.js +++ b/cwd-api/scripts/auto_migrate.js @@ -50,29 +50,50 @@ function run() { const result = JSON.parse(output); const count = result[0]?.results?.[0]?.count; - if (count > 0) { - return; + if (count === 0) { + const sql = ` + ALTER TABLE Comment ADD COLUMN post_url TEXT; + UPDATE Comment SET post_url = post_slug; + UPDATE Comment + SET post_slug = SUBSTR( + REPLACE(REPLACE(post_slug, 'https://', ''), 'http://', ''), + INSTR(REPLACE(REPLACE(post_slug, 'https://', ''), 'http://', ''), '/') + ) + WHERE post_slug LIKE 'http%' AND INSTR(REPLACE(REPLACE(post_slug, 'https://', ''), 'http://', ''), '/') > 0; + `; + + const flatSql = sql.replace(/\s+/g, ' ').trim(); + const migrateCmd = `npx wrangler d1 execute ${dbName} --command "${flatSql}" --remote --yes`; + + execSync(migrateCmd, { stdio: 'inherit' }); + console.log('[Auto-Migrate] Migration applied for Comment.post_url.'); } } catch (e) { - return; + console.error('[Auto-Migrate] Failed post_url migration:', e.message); } - const sql = ` - ALTER TABLE Comment ADD COLUMN post_url TEXT; - UPDATE Comment SET post_url = post_slug; - UPDATE Comment - SET post_slug = SUBSTR( - REPLACE(REPLACE(post_slug, 'https://', ''), 'http://', ''), - INSTR(REPLACE(REPLACE(post_slug, 'https://', ''), 'http://', ''), '/') - ) - WHERE post_slug LIKE 'http%' AND INSTR(REPLACE(REPLACE(post_slug, 'https://', ''), 'http://', ''), '/') > 0; - `; - - const flatSql = sql.replace(/\s+/g, ' ').trim(); - const migrateCmd = `npx wrangler d1 execute ${dbName} --command "${flatSql}" --remote --yes`; - - execSync(migrateCmd, { stdio: 'inherit' }); - console.log('[Auto-Migrate] Migration applied for Comment.post_url.'); + try { + const checkCmd = `npx wrangler d1 execute ${dbName} --command "SELECT count(*) as count FROM pragma_table_info('Comment') WHERE name='site_id'" --remote --json`; + const output = execSync(checkCmd, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }); + + const result = JSON.parse(output); + const count = result[0]?.results?.[0]?.count; + + if (count === 0) { + const sql = ` + ALTER TABLE Comment ADD COLUMN site_id TEXT NOT NULL DEFAULT ''; + CREATE INDEX IF NOT EXISTS idx_site_id ON Comment(site_id); + `; + + const flatSql = sql.replace(/\s+/g, ' ').trim(); + const migrateCmd = `npx wrangler d1 execute ${dbName} --command "${flatSql}" --remote --yes`; + + execSync(migrateCmd, { stdio: 'inherit' }); + console.log('[Auto-Migrate] Migration applied for Comment.site_id.'); + } + } catch (e) { + console.error('[Auto-Migrate] Failed site_id migration:', e.message); + } } catch (error) { console.error('[Auto-Migrate] Failed:', error.message); diff --git a/cwd-api/src/api/admin/listComments.ts b/cwd-api/src/api/admin/listComments.ts index 9c87760..8b355a4 100644 --- a/cwd-api/src/api/admin/listComments.ts +++ b/cwd-api/src/api/admin/listComments.ts @@ -12,6 +12,8 @@ export const listComments = async (c: Context<{ Bindings: Bindings }>) => { const rawDomain = c.req.query('domain') || ''; const domain = rawDomain.trim(); + const rawSiteId = c.req.query('site_id') || ''; + const siteId = rawSiteId.trim(); let whereSql = ''; const params: (string | number)[] = []; @@ -21,6 +23,15 @@ export const listComments = async (c: Context<{ Bindings: Bindings }>) => { params.push(pattern, pattern); } + if (siteId) { + if (whereSql) { + whereSql += ' AND site_id = ?'; + } else { + whereSql = 'WHERE site_id = ?'; + } + params.push(siteId); + } + const totalCount = await c.env.CWD_DB.prepare( `SELECT COUNT(*) as count FROM Comment ${whereSql}` ) @@ -67,7 +78,8 @@ export const listComments = async (c: Context<{ Bindings: Bindings }>) => { : 0, ua: row.ua, avatar: await getCravatar(row.email, row.name, avatarPrefix || undefined), - isAdmin: adminEmail && row.email === adminEmail + isAdmin: adminEmail && row.email === adminEmail, + siteId: row.site_id })) ); diff --git a/cwd-api/src/api/public/getComments.ts b/cwd-api/src/api/public/getComments.ts index be25aa9..0e20199 100644 --- a/cwd-api/src/api/public/getComments.ts +++ b/cwd-api/src/api/public/getComments.ts @@ -9,6 +9,7 @@ export const getComments = async (c: Context<{ Bindings: Bindings }>) => { const limit = Math.min(parseInt(c.req.query('limit') || '20'), 50) const nested = c.req.query('nested') !== 'false' const avatar_prefix = c.req.query('avatar_prefix') + const siteId = c.req.query('site_id') const offset = (page - 1) * limit if (!postSlug) return c.json({ message: "post_slug is required" }, 400) @@ -54,15 +55,23 @@ export const getComments = async (c: Context<{ Bindings: Bindings }>) => { whereParts.length > 0 ? `status = "approved" AND (${whereParts.join(' OR ')})` : 'status = "approved"' + + let finalWhereClause = whereClause + const bindParams: unknown[] = [...equalSlugs, ...likePatterns] + + if (siteId) { + finalWhereClause += ' AND site_id = ?' + bindParams.push(siteId) + } + const query = ` SELECT id, name, email, url, content_text as contentText, content_html as contentHtml, created, parent_id as parentId, post_slug as postSlug, post_url as postUrl, priority, COALESCE(likes, 0) as likes FROM Comment - WHERE ${whereClause} + WHERE ${finalWhereClause} ORDER BY priority DESC, created DESC ` - const bindParams: unknown[] = [...equalSlugs, ...likePatterns] const [commentsResult, adminEmailRows] = await Promise.all([ c.env.CWD_DB.prepare(query).bind(...bindParams).all(), diff --git a/cwd-api/src/api/public/postComment.ts b/cwd-api/src/api/public/postComment.ts index af38224..3668461 100644 --- a/cwd-api/src/api/public/postComment.ts +++ b/cwd-api/src/api/public/postComment.ts @@ -24,6 +24,7 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => { return c.json({ message: '无效的请求体' }, 400); } const { post_slug, content: rawContent, name: rawName, email, url, post_title, post_url, adminToken } = data; + const site_id = data.site_id ? String(data.site_id).trim() : ""; const parentId = (data as any).parent_id ?? (data as any).parentId ?? null; if (!post_slug || typeof post_slug !== 'string') { return c.json({ message: 'post_slug 必填' }, 400); @@ -161,8 +162,8 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => { INSERT INTO Comment ( created, post_slug, post_url, name, email, url, ip_address, os, browser, device, ua, content_text, content_html, - parent_id, status - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + parent_id, status, site_id + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `).bind( Date.now(), post_slug, @@ -178,7 +179,8 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => { contentText, contentHtml, parentId || null, - defaultStatus + defaultStatus, + site_id ).run(); if (!result.success) throw new Error("Database insert failed"); diff --git a/docs/.vitepress/components/footerDoc.vue b/docs/.vitepress/components/footerDoc.vue index adad0bd..96bd41f 100644 --- a/docs/.vitepress/components/footerDoc.vue +++ b/docs/.vitepress/components/footerDoc.vue @@ -35,6 +35,7 @@ onMounted(async () => { const comments = new window.CWDComments({ el: commentsRoot.value, apiBaseUrl, + siteId: 'cwd-doc', theme: getTheme(), }); diff --git a/docs/widget/src/core/CWDComments.js b/docs/widget/src/core/CWDComments.js index 843b576..8742c8b 100644 --- a/docs/widget/src/core/CWDComments.js +++ b/docs/widget/src/core/CWDComments.js @@ -29,6 +29,9 @@ export class CWDComments { */ constructor(config) { this.config = { ...config }; + if (config.siteId) { + this.config.siteId = config.siteId; + } if (typeof window !== 'undefined') { this.config.postSlug = window.location.pathname; } @@ -571,6 +574,9 @@ export class CWDComments { const prevConfig = { ...this.config }; Object.assign(this.config, newConfig); + if (newConfig.siteId !== undefined) { + this.config.siteId = newConfig.siteId; + } if (typeof window !== 'undefined') { this.config.postSlug = window.location.pathname; } @@ -589,7 +595,8 @@ export class CWDComments { const shouldReload = this.config.apiBaseUrl !== prevConfig.apiBaseUrl || this.config.pageSize !== prevConfig.pageSize || - this.config.postSlug !== prevConfig.postSlug; + this.config.postSlug !== prevConfig.postSlug || + this.config.siteId !== prevConfig.siteId; if (shouldReload) { const api = createApiClient(this.config); diff --git a/docs/widget/src/core/api.js b/docs/widget/src/core/api.js index 9fcc6d3..2d8d1ad 100644 --- a/docs/widget/src/core/api.js +++ b/docs/widget/src/core/api.js @@ -50,6 +50,10 @@ export function createApiClient(config) { params.set('avatar_prefix', config.avatarPrefix); } + if (config.siteId) { + params.set('site_id', config.siteId); + } + const response = await fetch(`${baseUrl}/api/comments?${params}`); if (!response.ok) { throw new Error(`获取评论失败:${response.status} ${response.statusText}`); @@ -82,7 +86,8 @@ export function createApiClient(config) { url: data.url || undefined, content: data.content, parent_id: data.parentId, - adminToken: data.adminToken + adminToken: data.adminToken, + site_id: config.siteId }), });