feat: 添加多站点支持,引入site_id字段
- 在Comment表中添加site_id字段,用于区分不同站点的评论 - 更新前后端API以支持site_id参数传递 - 修改自动迁移脚本,添加site_id字段迁移逻辑 - 更新前端组件配置,支持设置siteId参数
This commit is contained in:
@@ -17,6 +17,7 @@ CREATE TABLE IF NOT EXISTS Comment (
|
|||||||
likes INTEGER NOT NULL DEFAULT 0,
|
likes INTEGER NOT NULL DEFAULT 0,
|
||||||
priority INTEGER NOT NULL DEFAULT 1,
|
priority INTEGER NOT NULL DEFAULT 1,
|
||||||
status TEXT DEFAULT 'approved',
|
status TEXT DEFAULT 'approved',
|
||||||
|
site_id TEXT NOT NULL DEFAULT '',
|
||||||
-- 建立自引用外键约束(父子评论关系)
|
-- 建立自引用外键约束(父子评论关系)
|
||||||
FOREIGN KEY (parent_id) REFERENCES Comment (id) ON DELETE SET NULL
|
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_post_slug ON Comment(post_slug);
|
||||||
CREATE INDEX IF NOT EXISTS idx_status ON Comment(status);
|
CREATE INDEX IF NOT EXISTS idx_status ON Comment(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_site_id ON Comment(site_id);
|
||||||
|
|||||||
@@ -50,29 +50,50 @@ function run() {
|
|||||||
const result = JSON.parse(output);
|
const result = JSON.parse(output);
|
||||||
const count = result[0]?.results?.[0]?.count;
|
const count = result[0]?.results?.[0]?.count;
|
||||||
|
|
||||||
if (count > 0) {
|
if (count === 0) {
|
||||||
return;
|
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) {
|
} catch (e) {
|
||||||
return;
|
console.error('[Auto-Migrate] Failed post_url migration:', e.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sql = `
|
try {
|
||||||
ALTER TABLE Comment ADD COLUMN post_url TEXT;
|
const checkCmd = `npx wrangler d1 execute ${dbName} --command "SELECT count(*) as count FROM pragma_table_info('Comment') WHERE name='site_id'" --remote --json`;
|
||||||
UPDATE Comment SET post_url = post_slug;
|
const output = execSync(checkCmd, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
||||||
UPDATE Comment
|
|
||||||
SET post_slug = SUBSTR(
|
const result = JSON.parse(output);
|
||||||
REPLACE(REPLACE(post_slug, 'https://', ''), 'http://', ''),
|
const count = result[0]?.results?.[0]?.count;
|
||||||
INSTR(REPLACE(REPLACE(post_slug, 'https://', ''), 'http://', ''), '/')
|
|
||||||
)
|
if (count === 0) {
|
||||||
WHERE post_slug LIKE 'http%' AND INSTR(REPLACE(REPLACE(post_slug, 'https://', ''), 'http://', ''), '/') > 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`;
|
|
||||||
|
const flatSql = sql.replace(/\s+/g, ' ').trim();
|
||||||
execSync(migrateCmd, { stdio: 'inherit' });
|
const migrateCmd = `npx wrangler d1 execute ${dbName} --command "${flatSql}" --remote --yes`;
|
||||||
console.log('[Auto-Migrate] Migration applied for Comment.post_url.');
|
|
||||||
|
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) {
|
} catch (error) {
|
||||||
console.error('[Auto-Migrate] Failed:', error.message);
|
console.error('[Auto-Migrate] Failed:', error.message);
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ export const listComments = async (c: Context<{ Bindings: Bindings }>) => {
|
|||||||
|
|
||||||
const rawDomain = c.req.query('domain') || '';
|
const rawDomain = c.req.query('domain') || '';
|
||||||
const domain = rawDomain.trim();
|
const domain = rawDomain.trim();
|
||||||
|
const rawSiteId = c.req.query('site_id') || '';
|
||||||
|
const siteId = rawSiteId.trim();
|
||||||
|
|
||||||
let whereSql = '';
|
let whereSql = '';
|
||||||
const params: (string | number)[] = [];
|
const params: (string | number)[] = [];
|
||||||
@@ -21,6 +23,15 @@ export const listComments = async (c: Context<{ Bindings: Bindings }>) => {
|
|||||||
params.push(pattern, pattern);
|
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(
|
const totalCount = await c.env.CWD_DB.prepare(
|
||||||
`SELECT COUNT(*) as count FROM Comment ${whereSql}`
|
`SELECT COUNT(*) as count FROM Comment ${whereSql}`
|
||||||
)
|
)
|
||||||
@@ -67,7 +78,8 @@ export const listComments = async (c: Context<{ Bindings: Bindings }>) => {
|
|||||||
: 0,
|
: 0,
|
||||||
ua: row.ua,
|
ua: row.ua,
|
||||||
avatar: await getCravatar(row.email, row.name, avatarPrefix || undefined),
|
avatar: await getCravatar(row.email, row.name, avatarPrefix || undefined),
|
||||||
isAdmin: adminEmail && row.email === adminEmail
|
isAdmin: adminEmail && row.email === adminEmail,
|
||||||
|
siteId: row.site_id
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export const getComments = async (c: Context<{ Bindings: Bindings }>) => {
|
|||||||
const limit = Math.min(parseInt(c.req.query('limit') || '20'), 50)
|
const limit = Math.min(parseInt(c.req.query('limit') || '20'), 50)
|
||||||
const nested = c.req.query('nested') !== 'false'
|
const nested = c.req.query('nested') !== 'false'
|
||||||
const avatar_prefix = c.req.query('avatar_prefix')
|
const avatar_prefix = c.req.query('avatar_prefix')
|
||||||
|
const siteId = c.req.query('site_id')
|
||||||
const offset = (page - 1) * limit
|
const offset = (page - 1) * limit
|
||||||
|
|
||||||
if (!postSlug) return c.json({ message: "post_slug is required" }, 400)
|
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
|
whereParts.length > 0
|
||||||
? `status = "approved" AND (${whereParts.join(' OR ')})`
|
? `status = "approved" AND (${whereParts.join(' OR ')})`
|
||||||
: 'status = "approved"'
|
: 'status = "approved"'
|
||||||
|
|
||||||
|
let finalWhereClause = whereClause
|
||||||
|
const bindParams: unknown[] = [...equalSlugs, ...likePatterns]
|
||||||
|
|
||||||
|
if (siteId) {
|
||||||
|
finalWhereClause += ' AND site_id = ?'
|
||||||
|
bindParams.push(siteId)
|
||||||
|
}
|
||||||
|
|
||||||
const query = `
|
const query = `
|
||||||
SELECT id, name, email, url, content_text as contentText,
|
SELECT id, name, email, url, content_text as contentText,
|
||||||
content_html as contentHtml, created, parent_id as parentId,
|
content_html as contentHtml, created, parent_id as parentId,
|
||||||
post_slug as postSlug, post_url as postUrl, priority, COALESCE(likes, 0) as likes
|
post_slug as postSlug, post_url as postUrl, priority, COALESCE(likes, 0) as likes
|
||||||
FROM Comment
|
FROM Comment
|
||||||
WHERE ${whereClause}
|
WHERE ${finalWhereClause}
|
||||||
ORDER BY priority DESC, created DESC
|
ORDER BY priority DESC, created DESC
|
||||||
`
|
`
|
||||||
const bindParams: unknown[] = [...equalSlugs, ...likePatterns]
|
|
||||||
|
|
||||||
const [commentsResult, adminEmailRows] = await Promise.all([
|
const [commentsResult, adminEmailRows] = await Promise.all([
|
||||||
c.env.CWD_DB.prepare(query).bind(...bindParams).all(),
|
c.env.CWD_DB.prepare(query).bind(...bindParams).all(),
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => {
|
|||||||
return c.json({ message: '无效的请求体' }, 400);
|
return c.json({ message: '无效的请求体' }, 400);
|
||||||
}
|
}
|
||||||
const { post_slug, content: rawContent, name: rawName, email, url, post_title, post_url, adminToken } = data;
|
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;
|
const parentId = (data as any).parent_id ?? (data as any).parentId ?? null;
|
||||||
if (!post_slug || typeof post_slug !== 'string') {
|
if (!post_slug || typeof post_slug !== 'string') {
|
||||||
return c.json({ message: 'post_slug 必填' }, 400);
|
return c.json({ message: 'post_slug 必填' }, 400);
|
||||||
@@ -161,8 +162,8 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => {
|
|||||||
INSERT INTO Comment (
|
INSERT INTO Comment (
|
||||||
created, post_slug, post_url, name, email, url, ip_address,
|
created, post_slug, post_url, name, email, url, ip_address,
|
||||||
os, browser, device, ua, content_text, content_html,
|
os, browser, device, ua, content_text, content_html,
|
||||||
parent_id, status
|
parent_id, status, site_id
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`).bind(
|
`).bind(
|
||||||
Date.now(),
|
Date.now(),
|
||||||
post_slug,
|
post_slug,
|
||||||
@@ -178,7 +179,8 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => {
|
|||||||
contentText,
|
contentText,
|
||||||
contentHtml,
|
contentHtml,
|
||||||
parentId || null,
|
parentId || null,
|
||||||
defaultStatus
|
defaultStatus,
|
||||||
|
site_id
|
||||||
).run();
|
).run();
|
||||||
|
|
||||||
if (!result.success) throw new Error("Database insert failed");
|
if (!result.success) throw new Error("Database insert failed");
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ onMounted(async () => {
|
|||||||
const comments = new window.CWDComments({
|
const comments = new window.CWDComments({
|
||||||
el: commentsRoot.value,
|
el: commentsRoot.value,
|
||||||
apiBaseUrl,
|
apiBaseUrl,
|
||||||
|
siteId: 'cwd-doc',
|
||||||
theme: getTheme(),
|
theme: getTheme(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ export class CWDComments {
|
|||||||
*/
|
*/
|
||||||
constructor(config) {
|
constructor(config) {
|
||||||
this.config = { ...config };
|
this.config = { ...config };
|
||||||
|
if (config.siteId) {
|
||||||
|
this.config.siteId = config.siteId;
|
||||||
|
}
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
this.config.postSlug = window.location.pathname;
|
this.config.postSlug = window.location.pathname;
|
||||||
}
|
}
|
||||||
@@ -571,6 +574,9 @@ export class CWDComments {
|
|||||||
const prevConfig = { ...this.config };
|
const prevConfig = { ...this.config };
|
||||||
|
|
||||||
Object.assign(this.config, newConfig);
|
Object.assign(this.config, newConfig);
|
||||||
|
if (newConfig.siteId !== undefined) {
|
||||||
|
this.config.siteId = newConfig.siteId;
|
||||||
|
}
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
this.config.postSlug = window.location.pathname;
|
this.config.postSlug = window.location.pathname;
|
||||||
}
|
}
|
||||||
@@ -589,7 +595,8 @@ export class CWDComments {
|
|||||||
const shouldReload =
|
const shouldReload =
|
||||||
this.config.apiBaseUrl !== prevConfig.apiBaseUrl ||
|
this.config.apiBaseUrl !== prevConfig.apiBaseUrl ||
|
||||||
this.config.pageSize !== prevConfig.pageSize ||
|
this.config.pageSize !== prevConfig.pageSize ||
|
||||||
this.config.postSlug !== prevConfig.postSlug;
|
this.config.postSlug !== prevConfig.postSlug ||
|
||||||
|
this.config.siteId !== prevConfig.siteId;
|
||||||
|
|
||||||
if (shouldReload) {
|
if (shouldReload) {
|
||||||
const api = createApiClient(this.config);
|
const api = createApiClient(this.config);
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ export function createApiClient(config) {
|
|||||||
params.set('avatar_prefix', config.avatarPrefix);
|
params.set('avatar_prefix', config.avatarPrefix);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.siteId) {
|
||||||
|
params.set('site_id', config.siteId);
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch(`${baseUrl}/api/comments?${params}`);
|
const response = await fetch(`${baseUrl}/api/comments?${params}`);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`获取评论失败:${response.status} ${response.statusText}`);
|
throw new Error(`获取评论失败:${response.status} ${response.statusText}`);
|
||||||
@@ -82,7 +86,8 @@ export function createApiClient(config) {
|
|||||||
url: data.url || undefined,
|
url: data.url || undefined,
|
||||||
content: data.content,
|
content: data.content,
|
||||||
parent_id: data.parentId,
|
parent_id: data.parentId,
|
||||||
adminToken: data.adminToken
|
adminToken: data.adminToken,
|
||||||
|
site_id: config.siteId
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user