feat: 添加多站点支持,引入site_id字段

- 在Comment表中添加site_id字段,用于区分不同站点的评论
- 更新前后端API以支持site_id参数传递
- 修改自动迁移脚本,添加site_id字段迁移逻辑
- 更新前端组件配置,支持设置siteId参数
This commit is contained in:
anghunk
2026-02-09 13:56:35 +08:00
parent 5948949400
commit 32603d281a
8 changed files with 86 additions and 27 deletions

View File

@@ -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);

View File

@@ -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);

View File

@@ -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
}))
);

View File

@@ -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(),

View File

@@ -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");

View File

@@ -35,6 +35,7 @@ onMounted(async () => {
const comments = new window.CWDComments({
el: commentsRoot.value,
apiBaseUrl,
siteId: 'cwd-doc',
theme: getTheme(),
});

View File

@@ -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);

View File

@@ -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
}),
});