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,
|
||||
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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}))
|
||||
);
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -35,6 +35,7 @@ onMounted(async () => {
|
||||
const comments = new window.CWDComments({
|
||||
el: commentsRoot.value,
|
||||
apiBaseUrl,
|
||||
siteId: 'cwd-doc',
|
||||
theme: getTheme(),
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user