feat: 实现多站点数据隔离和管理功能
- 新增站点管理功能,替换原有的域名管理 - 添加数据库迁移工具,为现有表增加 site_id 字段 - 创建 useSite 组合式 API 统一管理站点状态 - 修改前后端 API 以支持站点筛选参数 - 更新管理界面,将域名筛选改为站点筛选 - 优化数据导出功能,支持按站点筛选导出 - 更新文档,调整站点 ID 默认值为空字符串 - 升级 wrangler 依赖至最新版本
This commit is contained in:
@@ -11,6 +11,7 @@ type LikeRequestBody = {
|
||||
postSlug?: string;
|
||||
postTitle?: string;
|
||||
postUrl?: string;
|
||||
siteId?: string;
|
||||
};
|
||||
|
||||
function getUserIdFromRequest(c: Context<{ Bindings: Bindings }>): string {
|
||||
@@ -30,24 +31,13 @@ function getUserIdFromRequest(c: Context<{ Bindings: Bindings }>): string {
|
||||
return 'anonymous';
|
||||
}
|
||||
|
||||
async function ensureLikesTable(env: Bindings) {
|
||||
await env.CWD_DB.prepare(
|
||||
'CREATE TABLE IF NOT EXISTS Likes (id INTEGER PRIMARY KEY AUTOINCREMENT, page_slug TEXT NOT NULL, user_id TEXT NOT NULL, created_at INTEGER NOT NULL, UNIQUE(page_slug, user_id))'
|
||||
).run();
|
||||
}
|
||||
|
||||
async function ensurePageStatsTable(env: Bindings) {
|
||||
await env.CWD_DB.prepare(
|
||||
'CREATE TABLE IF NOT EXISTS page_stats (id INTEGER PRIMARY KEY AUTOINCREMENT, post_slug TEXT UNIQUE NOT NULL, post_title TEXT, post_url TEXT, pv INTEGER NOT NULL DEFAULT 0, last_visit_at INTEGER, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL)'
|
||||
).run();
|
||||
}
|
||||
|
||||
export const getLikeStatus = async (
|
||||
c: Context<{ Bindings: Bindings }>
|
||||
): Promise<Response> => {
|
||||
try {
|
||||
const rawPostSlug = c.req.query('post_slug') || '';
|
||||
const postSlug = rawPostSlug.trim();
|
||||
const siteId = c.req.query('siteId') || '';
|
||||
|
||||
if (!postSlug) {
|
||||
return c.json({ message: 'post_slug is required' }, 400);
|
||||
@@ -59,20 +49,18 @@ export const getLikeStatus = async (
|
||||
'';
|
||||
const userId = userIdHeader.trim();
|
||||
|
||||
await ensureLikesTable(c.env);
|
||||
|
||||
const totalRow = await c.env.CWD_DB.prepare(
|
||||
'SELECT COUNT(*) AS count FROM Likes WHERE page_slug = ?'
|
||||
'SELECT COUNT(*) AS count FROM Likes WHERE page_slug = ? AND site_id = ?'
|
||||
)
|
||||
.bind(postSlug)
|
||||
.bind(postSlug, siteId)
|
||||
.first<{ count: number }>();
|
||||
|
||||
let liked = false;
|
||||
if (userId) {
|
||||
const row = await c.env.CWD_DB.prepare(
|
||||
'SELECT id FROM Likes WHERE page_slug = ? AND user_id = ?'
|
||||
'SELECT id FROM Likes WHERE page_slug = ? AND user_id = ? AND site_id = ?'
|
||||
)
|
||||
.bind(postSlug, userId)
|
||||
.bind(postSlug, userId, siteId)
|
||||
.first<{ id: number }>();
|
||||
liked = !!row;
|
||||
}
|
||||
@@ -108,6 +96,7 @@ export const likePage = async (
|
||||
typeof body.postTitle === 'string' ? body.postTitle.trim() : '';
|
||||
const rawPostUrl =
|
||||
typeof body.postUrl === 'string' ? body.postUrl.trim() : '';
|
||||
const siteId = typeof body.siteId === 'string' ? body.siteId.trim() : '';
|
||||
|
||||
if (!rawPostSlug) {
|
||||
return c.json({ message: 'postSlug is required' }, 400);
|
||||
@@ -115,38 +104,35 @@ export const likePage = async (
|
||||
|
||||
const userId = getUserIdFromRequest(c);
|
||||
|
||||
await ensureLikesTable(c.env);
|
||||
await ensurePageStatsTable(c.env);
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
const existingLike = await c.env.CWD_DB.prepare(
|
||||
'SELECT id FROM Likes WHERE page_slug = ? AND user_id = ?'
|
||||
'SELECT id FROM Likes WHERE page_slug = ? AND user_id = ? AND site_id = ?'
|
||||
)
|
||||
.bind(rawPostSlug, userId)
|
||||
.bind(rawPostSlug, userId, siteId)
|
||||
.first<{ id: number }>();
|
||||
|
||||
let alreadyLiked = false;
|
||||
|
||||
if (!existingLike) {
|
||||
await c.env.CWD_DB.prepare(
|
||||
'INSERT INTO Likes (page_slug, user_id, created_at) VALUES (?, ?, ?)'
|
||||
'INSERT INTO Likes (page_slug, user_id, created_at, site_id) VALUES (?, ?, ?, ?)'
|
||||
)
|
||||
.bind(rawPostSlug, userId, now)
|
||||
.bind(rawPostSlug, userId, now, siteId)
|
||||
.run();
|
||||
} else {
|
||||
alreadyLiked = true;
|
||||
}
|
||||
|
||||
const pageStatsRow = await c.env.CWD_DB.prepare(
|
||||
'SELECT id FROM page_stats WHERE post_slug = ?'
|
||||
'SELECT id FROM page_stats WHERE post_slug = ? AND site_id = ?'
|
||||
)
|
||||
.bind(rawPostSlug)
|
||||
.bind(rawPostSlug, siteId)
|
||||
.first<{ id: number }>();
|
||||
|
||||
if (!pageStatsRow) {
|
||||
await c.env.CWD_DB.prepare(
|
||||
'INSERT INTO page_stats (post_slug, post_title, post_url, pv, last_visit_at, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
||||
'INSERT INTO page_stats (post_slug, post_title, post_url, pv, last_visit_at, created_at, updated_at, site_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)'
|
||||
)
|
||||
.bind(
|
||||
rawPostSlug,
|
||||
@@ -155,7 +141,8 @@ export const likePage = async (
|
||||
0,
|
||||
now,
|
||||
now,
|
||||
now
|
||||
now,
|
||||
siteId
|
||||
)
|
||||
.run();
|
||||
} else if (rawPostTitle || rawPostUrl) {
|
||||
@@ -172,9 +159,9 @@ export const likePage = async (
|
||||
}
|
||||
|
||||
const totalRow = await c.env.CWD_DB.prepare(
|
||||
'SELECT COUNT(*) AS count FROM Likes WHERE page_slug = ?'
|
||||
'SELECT COUNT(*) AS count FROM Likes WHERE page_slug = ? AND site_id = ?'
|
||||
)
|
||||
.bind(rawPostSlug)
|
||||
.bind(rawPostSlug, siteId)
|
||||
.first<{ count: number }>();
|
||||
|
||||
const totalLikes = totalRow?.count || 0;
|
||||
@@ -193,4 +180,3 @@ export const likePage = async (
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ type TrackVisitBody = {
|
||||
postSlug?: string;
|
||||
postTitle?: string;
|
||||
postUrl?: string;
|
||||
siteId?: string;
|
||||
};
|
||||
|
||||
function extractDomain(source: string | null | undefined): string | null {
|
||||
@@ -33,19 +34,12 @@ export const trackVisit = async (c: Context<{ Bindings: Bindings }>) => {
|
||||
const rawPostSlug = typeof body.postSlug === 'string' ? body.postSlug.trim() : '';
|
||||
const rawPostTitle = typeof body.postTitle === 'string' ? body.postTitle.trim() : '';
|
||||
const rawPostUrl = typeof body.postUrl === 'string' ? body.postUrl.trim() : '';
|
||||
const rawSiteId = typeof body.siteId === 'string' ? body.siteId.trim() : '';
|
||||
|
||||
if (!rawPostSlug) {
|
||||
return c.json({ message: 'postSlug is required' }, 400);
|
||||
}
|
||||
|
||||
await c.env.CWD_DB.prepare(
|
||||
'CREATE TABLE IF NOT EXISTS page_stats (id INTEGER PRIMARY KEY AUTOINCREMENT, post_slug TEXT UNIQUE NOT NULL, post_title TEXT, post_url TEXT, pv INTEGER NOT NULL DEFAULT 0, last_visit_at INTEGER, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL)'
|
||||
).run();
|
||||
|
||||
await c.env.CWD_DB.prepare(
|
||||
'CREATE TABLE IF NOT EXISTS page_visit_daily (id INTEGER PRIMARY KEY AUTOINCREMENT, date TEXT NOT NULL, domain TEXT, count INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL)'
|
||||
).run();
|
||||
|
||||
const nowDate = new Date();
|
||||
const nowTs = nowDate.getTime();
|
||||
const nowIso = nowDate.toISOString();
|
||||
@@ -56,68 +50,51 @@ export const trackVisit = async (c: Context<{ Bindings: Bindings }>) => {
|
||||
extractDomain(rawPostSlug) ||
|
||||
null;
|
||||
|
||||
const existing = await c.env.CWD_DB.prepare(
|
||||
'SELECT id, pv FROM page_stats WHERE post_slug = ?'
|
||||
// Upsert page_stats using ON CONFLICT (site_id, post_slug)
|
||||
await c.env.CWD_DB.prepare(
|
||||
`INSERT INTO page_stats (site_id, post_slug, post_title, post_url, pv, last_visit_at, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, 1, ?, ?, ?)
|
||||
ON CONFLICT(site_id, post_slug) DO UPDATE SET
|
||||
pv = pv + 1,
|
||||
last_visit_at = ?,
|
||||
updated_at = ?,
|
||||
post_title = excluded.post_title,
|
||||
post_url = excluded.post_url`
|
||||
)
|
||||
.bind(rawPostSlug)
|
||||
.first<{ id: number; pv: number }>();
|
||||
|
||||
if (!existing) {
|
||||
await c.env.CWD_DB.prepare(
|
||||
'INSERT INTO page_stats (post_slug, post_title, post_url, pv, last_visit_at, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
||||
.bind(
|
||||
rawSiteId,
|
||||
rawPostSlug,
|
||||
rawPostTitle || null,
|
||||
rawPostUrl || null,
|
||||
nowTs,
|
||||
nowTs,
|
||||
nowTs,
|
||||
nowTs,
|
||||
nowTs
|
||||
)
|
||||
.bind(
|
||||
rawPostSlug,
|
||||
rawPostTitle || null,
|
||||
rawPostUrl || null,
|
||||
1,
|
||||
nowTs,
|
||||
nowTs,
|
||||
nowTs
|
||||
)
|
||||
.run();
|
||||
} else {
|
||||
const newPv = (existing.pv || 0) + 1;
|
||||
await c.env.CWD_DB.prepare(
|
||||
'UPDATE page_stats SET post_title = ?, post_url = ?, pv = ?, last_visit_at = ?, updated_at = ? WHERE id = ?'
|
||||
)
|
||||
.bind(
|
||||
rawPostTitle || null,
|
||||
rawPostUrl || null,
|
||||
newPv,
|
||||
nowTs,
|
||||
nowTs,
|
||||
existing.id
|
||||
)
|
||||
.run();
|
||||
}
|
||||
.run();
|
||||
|
||||
let dailyRow:
|
||||
| {
|
||||
id: number;
|
||||
count: number;
|
||||
}
|
||||
| null = null;
|
||||
// Update page_visit_daily
|
||||
// We try to find a row for (date, site_id, domain)
|
||||
let dailySql = 'SELECT id, count FROM page_visit_daily WHERE date = ? AND site_id = ?';
|
||||
const params: any[] = [today, rawSiteId];
|
||||
|
||||
if (domain) {
|
||||
dailyRow = await c.env.CWD_DB.prepare(
|
||||
'SELECT id, count FROM page_visit_daily WHERE date = ? AND domain = ?'
|
||||
)
|
||||
.bind(today, domain)
|
||||
.first<{ id: number; count: number }>();
|
||||
dailySql += ' AND domain = ?';
|
||||
params.push(domain);
|
||||
} else {
|
||||
dailyRow = await c.env.CWD_DB.prepare(
|
||||
'SELECT id, count FROM page_visit_daily WHERE date = ? AND domain IS NULL'
|
||||
)
|
||||
.bind(today)
|
||||
.first<{ id: number; count: number }>();
|
||||
dailySql += ' AND domain IS NULL';
|
||||
}
|
||||
|
||||
const dailyRow = await c.env.CWD_DB.prepare(dailySql)
|
||||
.bind(...params)
|
||||
.first<{ id: number; count: number }>();
|
||||
|
||||
if (!dailyRow) {
|
||||
await c.env.CWD_DB.prepare(
|
||||
'INSERT INTO page_visit_daily (date, domain, count, created_at, updated_at) VALUES (?, ?, ?, ?, ?)'
|
||||
'INSERT INTO page_visit_daily (date, site_id, domain, count, created_at, updated_at) VALUES (?, ?, ?, 1, ?, ?)'
|
||||
)
|
||||
.bind(today, domain, 1, nowTs, nowTs)
|
||||
.bind(today, rawSiteId, domain, nowTs, nowTs)
|
||||
.run();
|
||||
} else {
|
||||
const newCount = (dailyRow.count || 0) + 1;
|
||||
@@ -133,4 +110,3 @@ export const trackVisit = async (c: Context<{ Bindings: Bindings }>) => {
|
||||
return c.json({ message: e.message || '记录访问数据失败' }, 500);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user