feat: 实现多站点数据隔离和管理功能
- 新增站点管理功能,替换原有的域名管理 - 添加数据库迁移工具,为现有表增加 site_id 字段 - 创建 useSite 组合式 API 统一管理站点状态 - 修改前后端 API 以支持站点筛选参数 - 更新管理界面,将域名筛选改为站点筛选 - 优化数据导出功能,支持按站点筛选导出 - 更新文档,调整站点 ID 默认值为空字符串 - 升级 wrangler 依赖至最新版本
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
"@types/ua-parser-js": "^0.7.39",
|
||||
"typescript": "^5.5.2",
|
||||
"vitest": "~3.2.0",
|
||||
"wrangler": "^4.58.0"
|
||||
"wrangler": "^4.63.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"hono": "^4.11.3",
|
||||
|
||||
@@ -3,9 +3,18 @@ import { Bindings } from '../../bindings';
|
||||
|
||||
export const exportComments = async (c: Context<{ Bindings: Bindings }>) => {
|
||||
try {
|
||||
const { results } = await c.env.CWD_DB.prepare(
|
||||
'SELECT * FROM Comment ORDER BY priority DESC, created DESC'
|
||||
).all();
|
||||
const siteId = c.req.query('siteId');
|
||||
let query = 'SELECT * FROM Comment';
|
||||
const params: any[] = [];
|
||||
|
||||
if (siteId) {
|
||||
query += ' WHERE site_id = ?';
|
||||
params.push(siteId);
|
||||
}
|
||||
|
||||
query += ' ORDER BY priority DESC, created DESC';
|
||||
|
||||
const { results } = await c.env.CWD_DB.prepare(query).bind(...params).all();
|
||||
|
||||
return c.json(results);
|
||||
} catch (e: any) {
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
import { Context } from 'hono';
|
||||
import { Bindings } from '../../bindings';
|
||||
|
||||
async function ensureStatsTables(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 getStatsData = async (env: Bindings, siteId?: string) => {
|
||||
// Tables are ensured by dbMigration on startup
|
||||
|
||||
await 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();
|
||||
let statsQuery = 'SELECT * FROM page_stats';
|
||||
let dailyQuery = 'SELECT * FROM page_visit_daily';
|
||||
let likesQuery = 'SELECT * FROM Likes';
|
||||
const params: any[] = [];
|
||||
|
||||
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();
|
||||
}
|
||||
if (siteId) {
|
||||
statsQuery += ' WHERE site_id = ?';
|
||||
dailyQuery += ' WHERE site_id = ?';
|
||||
likesQuery += ' WHERE site_id = ?';
|
||||
params.push(siteId);
|
||||
}
|
||||
|
||||
export const getStatsData = async (env: Bindings) => {
|
||||
await ensureStatsTables(env);
|
||||
|
||||
const { results: pageStats } = await env.CWD_DB.prepare('SELECT * FROM page_stats').all();
|
||||
const { results: dailyVisits } = await env.CWD_DB.prepare('SELECT * FROM page_visit_daily').all();
|
||||
const { results: likes } = await env.CWD_DB.prepare('SELECT * FROM Likes').all();
|
||||
const { results: pageStats } = await env.CWD_DB.prepare(statsQuery).bind(...params).all();
|
||||
const { results: dailyVisits } = await env.CWD_DB.prepare(dailyQuery).bind(...params).all();
|
||||
const { results: likes } = await env.CWD_DB.prepare(likesQuery).bind(...params).all();
|
||||
|
||||
return {
|
||||
page_stats: pageStats,
|
||||
@@ -31,7 +29,8 @@ export const getStatsData = async (env: Bindings) => {
|
||||
|
||||
export const exportStats = async (c: Context<{ Bindings: Bindings }>) => {
|
||||
try {
|
||||
const data = await getStatsData(c.env);
|
||||
const siteId = c.req.query('siteId');
|
||||
const data = await getStatsData(c.env, siteId);
|
||||
return c.json(data);
|
||||
} catch (e: any) {
|
||||
return c.json({ message: e.message || '导出统计数据失败' }, 500);
|
||||
|
||||
@@ -1,74 +1,53 @@
|
||||
import type { Context } from 'hono';
|
||||
import type { Bindings } from '../../bindings';
|
||||
|
||||
function extractDomain(source: string | null | undefined): string | null {
|
||||
if (!source) {
|
||||
return null;
|
||||
}
|
||||
const value = source.trim();
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
if (!/^https?:\/\//i.test(value)) {
|
||||
return null;
|
||||
}
|
||||
export const getSites = async (c: Context<{ Bindings: Bindings }>) => {
|
||||
try {
|
||||
const url = new URL(value);
|
||||
return url.hostname.toLowerCase();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export const getDomains = async (c: Context<{ Bindings: Bindings }>) => {
|
||||
try {
|
||||
const domains = new Set<string>();
|
||||
const sites = new Set<string>();
|
||||
|
||||
// 1. Get sites from Comment
|
||||
const { results: commentRows } = await c.env.CWD_DB.prepare(
|
||||
'SELECT post_slug, post_url FROM Comment'
|
||||
).all<{
|
||||
post_slug: string;
|
||||
post_url: string | null;
|
||||
}>();
|
||||
'SELECT DISTINCT site_id FROM Comment'
|
||||
).all<{ site_id: string }>();
|
||||
|
||||
for (const row of commentRows) {
|
||||
const domain =
|
||||
extractDomain(row.post_url) || extractDomain(row.post_slug);
|
||||
if (domain) {
|
||||
domains.add(domain);
|
||||
if (row.site_id !== undefined && row.site_id !== null) {
|
||||
sites.add(row.site_id);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
// 2. Get sites from page_stats
|
||||
const { results: pageRows } = await c.env.CWD_DB.prepare(
|
||||
'SELECT post_slug, post_url FROM page_stats'
|
||||
).all<{
|
||||
post_slug: string;
|
||||
post_url: string | null;
|
||||
}>();
|
||||
'SELECT DISTINCT site_id FROM page_stats'
|
||||
).all<{ site_id: string }>();
|
||||
|
||||
for (const row of pageRows) {
|
||||
const domain =
|
||||
extractDomain(row.post_url) || extractDomain(row.post_slug);
|
||||
if (domain) {
|
||||
domains.add(domain);
|
||||
if (row.site_id !== undefined && row.site_id !== null) {
|
||||
sites.add(row.site_id);
|
||||
}
|
||||
}
|
||||
|
||||
const list = Array.from(domains);
|
||||
// 3. Get sites from page_visit_daily
|
||||
const { results: dailyRows } = await c.env.CWD_DB.prepare(
|
||||
'SELECT DISTINCT site_id FROM page_visit_daily'
|
||||
).all<{ site_id: string }>();
|
||||
|
||||
for (const row of dailyRows) {
|
||||
if (row.site_id !== undefined && row.site_id !== null) {
|
||||
sites.add(row.site_id);
|
||||
}
|
||||
}
|
||||
|
||||
const list = Array.from(sites);
|
||||
list.sort();
|
||||
|
||||
return c.json({
|
||||
domains: list
|
||||
sites: list
|
||||
});
|
||||
} catch (e: any) {
|
||||
return c.json(
|
||||
{ message: e.message || '获取域名列表失败' },
|
||||
{ message: e.message || '获取站点列表失败' },
|
||||
500
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -33,26 +33,25 @@ function extractDomain(source: string | null | undefined): string | null {
|
||||
|
||||
export const getStats = async (c: Context<{ Bindings: Bindings }>) => {
|
||||
try {
|
||||
const rawDomain = c.req.query('domain') || '';
|
||||
const domainFilter = rawDomain.trim().toLowerCase();
|
||||
const rawSiteId = c.req.query('siteId');
|
||||
const siteId = rawSiteId && rawSiteId !== 'default' ? rawSiteId : null;
|
||||
|
||||
const { results } = await c.env.CWD_DB.prepare(
|
||||
'SELECT created, post_slug, post_url, status FROM Comment'
|
||||
).all<{
|
||||
let sql = 'SELECT created, post_slug, post_url, status FROM Comment';
|
||||
const params: any[] = [];
|
||||
|
||||
if (siteId) {
|
||||
sql += ' WHERE site_id = ?';
|
||||
params.push(siteId);
|
||||
}
|
||||
|
||||
const { results } = await c.env.CWD_DB.prepare(sql).bind(...params).all<{
|
||||
created: number;
|
||||
post_slug: string;
|
||||
post_url: string | null;
|
||||
status: string;
|
||||
}>();
|
||||
|
||||
const summaryAll: StatusCounts = {
|
||||
total: 0,
|
||||
approved: 0,
|
||||
pending: 0,
|
||||
rejected: 0
|
||||
};
|
||||
|
||||
const summaryFiltered: StatusCounts = {
|
||||
const summary: StatusCounts = {
|
||||
total: 0,
|
||||
approved: 0,
|
||||
pending: 0,
|
||||
@@ -60,9 +59,7 @@ export const getStats = async (c: Context<{ Bindings: Bindings }>) => {
|
||||
};
|
||||
|
||||
const domainMap = new Map<string, StatusCounts>();
|
||||
|
||||
const dailyMapAll = new Map<string, number>();
|
||||
const dailyMapFiltered = new Map<string, number>();
|
||||
const dailyMap = new Map<string, number>();
|
||||
|
||||
const now = Date.now();
|
||||
const thirtyDaysAgo = now - 29 * 24 * 60 * 60 * 1000;
|
||||
@@ -90,26 +87,13 @@ export const getStats = async (c: Context<{ Bindings: Bindings }>) => {
|
||||
counts.rejected += 1;
|
||||
}
|
||||
|
||||
summaryAll.total += 1;
|
||||
summary.total += 1;
|
||||
if (row.status === 'approved') {
|
||||
summaryAll.approved += 1;
|
||||
summary.approved += 1;
|
||||
} else if (row.status === 'pending') {
|
||||
summaryAll.pending += 1;
|
||||
summary.pending += 1;
|
||||
} else if (row.status === 'rejected') {
|
||||
summaryAll.rejected += 1;
|
||||
}
|
||||
|
||||
const matchesFilter = domainFilter && domain === domainFilter;
|
||||
|
||||
if (matchesFilter) {
|
||||
summaryFiltered.total += 1;
|
||||
if (row.status === 'approved') {
|
||||
summaryFiltered.approved += 1;
|
||||
} else if (row.status === 'pending') {
|
||||
summaryFiltered.pending += 1;
|
||||
} else if (row.status === 'rejected') {
|
||||
summaryFiltered.rejected += 1;
|
||||
}
|
||||
summary.rejected += 1;
|
||||
}
|
||||
|
||||
if (row.created >= thirtyDaysAgo) {
|
||||
@@ -119,14 +103,7 @@ export const getStats = async (c: Context<{ Bindings: Bindings }>) => {
|
||||
const day = String(d.getUTCDate()).padStart(2, '0');
|
||||
const key = `${year}-${month}-${day}`;
|
||||
|
||||
dailyMapAll.set(key, (dailyMapAll.get(key) || 0) + 1);
|
||||
|
||||
if (matchesFilter) {
|
||||
dailyMapFiltered.set(
|
||||
key,
|
||||
(dailyMapFiltered.get(key) || 0) + 1
|
||||
);
|
||||
}
|
||||
dailyMap.set(key, (dailyMap.get(key) || 0) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,8 +117,6 @@ export const getStats = async (c: Context<{ Bindings: Bindings }>) => {
|
||||
}))
|
||||
.sort((a, b) => b.total - a.total);
|
||||
|
||||
const dailyMap = domainFilter ? dailyMapFiltered : dailyMapAll;
|
||||
|
||||
const last7Days: { date: string; total: number }[] = [];
|
||||
for (let i = 29; i >= 0; i--) {
|
||||
const d = new Date(now - i * 24 * 60 * 60 * 1000);
|
||||
@@ -155,8 +130,6 @@ export const getStats = async (c: Context<{ Bindings: Bindings }>) => {
|
||||
});
|
||||
}
|
||||
|
||||
const summary = domainFilter ? summaryFiltered : summaryAll;
|
||||
|
||||
return c.json({
|
||||
summary,
|
||||
domains,
|
||||
|
||||
@@ -10,17 +10,24 @@ type LikeStatsItem = {
|
||||
|
||||
export const getLikeStats = async (c: Context<{ Bindings: Bindings }>) => {
|
||||
try {
|
||||
await c.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();
|
||||
const siteId = c.req.query('siteId');
|
||||
|
||||
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();
|
||||
let query = `
|
||||
SELECT l.page_slug, COALESCE(p.post_title, NULL) AS page_title, COALESCE(p.post_url, NULL) AS page_url, COUNT(*) AS likes
|
||||
FROM Likes l
|
||||
LEFT JOIN page_stats p ON p.post_slug = l.page_slug AND p.site_id = l.site_id
|
||||
`;
|
||||
|
||||
const params: any[] = [];
|
||||
|
||||
const { results } = await c.env.CWD_DB.prepare(
|
||||
'SELECT l.page_slug, COALESCE(p.post_title, NULL) AS page_title, COALESCE(p.post_url, NULL) AS page_url, COUNT(*) AS likes FROM Likes l LEFT JOIN page_stats p ON p.post_slug = l.page_slug GROUP BY l.page_slug, p.post_title, p.post_url ORDER BY likes DESC LIMIT 50'
|
||||
).all<LikeStatsItem>();
|
||||
if (siteId) {
|
||||
query += ' WHERE l.site_id = ?';
|
||||
params.push(siteId);
|
||||
}
|
||||
|
||||
query += ' GROUP BY l.page_slug, l.site_id, p.post_title, p.post_url ORDER BY likes DESC LIMIT 50';
|
||||
|
||||
const { results } = await c.env.CWD_DB.prepare(query).bind(...params).all<LikeStatsItem>();
|
||||
|
||||
const items = results.map((row) => ({
|
||||
pageSlug: row.page_slug,
|
||||
|
||||
@@ -10,25 +10,14 @@ export const listComments = async (c: Context<{ Bindings: Bindings }>) => {
|
||||
const limit = 10;
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const rawDomain = c.req.query('domain') || '';
|
||||
const domain = rawDomain.trim();
|
||||
const rawSiteId = c.req.query('site_id') || '';
|
||||
const siteId = rawSiteId.trim();
|
||||
const rawSiteId = c.req.query('siteId'); // Changed from site_id to siteId to be consistent
|
||||
const siteId = rawSiteId && rawSiteId !== 'default' ? rawSiteId : null;
|
||||
|
||||
let whereSql = '';
|
||||
const params: (string | number)[] = [];
|
||||
if (domain) {
|
||||
const pattern = `%://${domain}/%`;
|
||||
whereSql = 'WHERE post_slug LIKE ? OR post_url LIKE ?';
|
||||
params.push(pattern, pattern);
|
||||
}
|
||||
|
||||
if (siteId) {
|
||||
if (whereSql) {
|
||||
whereSql += ' AND site_id = ?';
|
||||
} else {
|
||||
whereSql = 'WHERE site_id = ?';
|
||||
}
|
||||
whereSql = 'WHERE site_id = ?';
|
||||
params.push(siteId);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,46 +21,25 @@ type VisitPageItem = {
|
||||
postTitle: string | null;
|
||||
postUrl: string | null;
|
||||
pv: number;
|
||||
lastVisitAt: string | null;
|
||||
lastVisitAt: number | null;
|
||||
};
|
||||
|
||||
function extractDomain(source: string | null | undefined): string | null {
|
||||
if (!source) {
|
||||
return null;
|
||||
}
|
||||
const value = source.trim();
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
if (!/^https?:\/\//i.test(value)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const url = new URL(value);
|
||||
return url.hostname.toLowerCase();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export const getVisitOverview = async (
|
||||
c: Context<{ Bindings: Bindings }>
|
||||
) => {
|
||||
try {
|
||||
const rawDomain = c.req.query('domain') || '';
|
||||
const domainFilter = rawDomain.trim().toLowerCase();
|
||||
const rawSiteId = c.req.query('siteId');
|
||||
const siteId = rawSiteId && rawSiteId !== 'default' ? rawSiteId : null;
|
||||
|
||||
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();
|
||||
let statsSql = 'SELECT post_slug, post_title, post_url, pv, last_visit_at FROM page_stats';
|
||||
const statsParams: any[] = [];
|
||||
|
||||
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();
|
||||
if (siteId) {
|
||||
statsSql += ' WHERE site_id = ?';
|
||||
statsParams.push(siteId);
|
||||
}
|
||||
|
||||
const { results } = await c.env.CWD_DB.prepare(
|
||||
'SELECT post_slug, post_title, post_url, pv, last_visit_at FROM page_stats'
|
||||
).all<{
|
||||
const { results } = await c.env.CWD_DB.prepare(statsSql).bind(...statsParams).all<{
|
||||
post_slug: string;
|
||||
post_title: string | null;
|
||||
post_url: string | null;
|
||||
@@ -72,15 +51,6 @@ export const getVisitOverview = async (
|
||||
let totalPages = 0;
|
||||
|
||||
for (const row of results) {
|
||||
const domain =
|
||||
extractDomain(row.post_url) ||
|
||||
extractDomain(row.post_slug) ||
|
||||
null;
|
||||
|
||||
if (domainFilter && domain !== domainFilter) {
|
||||
continue;
|
||||
}
|
||||
|
||||
totalPv += row.pv || 0;
|
||||
totalPages += 1;
|
||||
}
|
||||
@@ -108,9 +78,6 @@ export const getVisitOverview = async (
|
||||
const lastMonthStartDate = new Date(Date.UTC(year, month - 1, 1));
|
||||
const lastMonthEndDate = new Date(monthStartDate.getTime() - 24 * 60 * 60 * 1000);
|
||||
|
||||
// For last week, we need the start date of last week.
|
||||
// weekStartDate is the start of current week.
|
||||
// So lastWeekStartDate is weekStartDate - 7 days.
|
||||
const weekStartDate = (() => {
|
||||
const d = new Date(Date.UTC(year, month, day));
|
||||
const weekday = d.getUTCDay();
|
||||
@@ -121,8 +88,6 @@ export const getVisitOverview = async (
|
||||
const lastWeekStartDate = new Date(weekStartDate.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||
const lastWeekEndDate = new Date(weekStartDate.getTime() - 24 * 60 * 60 * 1000);
|
||||
|
||||
// We need to fetch enough history for last month.
|
||||
// The earliest date could be either startDate30 or lastMonthStartDate.
|
||||
let earliestDate = startDate30;
|
||||
if (toKey(lastMonthStartDate) < earliestDate) {
|
||||
earliestDate = toKey(lastMonthStartDate);
|
||||
@@ -132,19 +97,18 @@ export const getVisitOverview = async (
|
||||
}
|
||||
|
||||
let dailySql =
|
||||
'SELECT date, domain, count FROM page_visit_daily WHERE date >= ?';
|
||||
'SELECT date, count FROM page_visit_daily WHERE date >= ?';
|
||||
const params: string[] = [earliestDate];
|
||||
|
||||
if (domainFilter) {
|
||||
dailySql += ' AND domain = ?';
|
||||
params.push(domainFilter);
|
||||
if (siteId) {
|
||||
dailySql += ' AND site_id = ?';
|
||||
params.push(siteId);
|
||||
}
|
||||
|
||||
const { results: dailyRows } = await c.env.CWD_DB.prepare(dailySql)
|
||||
.bind(...params)
|
||||
.all<{
|
||||
date: string;
|
||||
domain: string | null;
|
||||
count: number;
|
||||
}>();
|
||||
|
||||
@@ -159,6 +123,7 @@ export const getVisitOverview = async (
|
||||
dailyMap.set(key, (dailyMap.get(key) || 0) + value);
|
||||
}
|
||||
|
||||
// Fallback if no daily data but totalPv exists (rare edge case or initial migration)
|
||||
if (dailyMap.size === 0 && totalPv > 0) {
|
||||
const fallbackDate = now.toISOString().slice(0, 10);
|
||||
dailyMap.set(fallbackDate, totalPv);
|
||||
@@ -167,9 +132,6 @@ export const getVisitOverview = async (
|
||||
const todayKey = toKey(now);
|
||||
const yesterdayKey = toKey(new Date(now.getTime() - 24 * 60 * 60 * 1000));
|
||||
|
||||
// weekStartDate is already calculated above
|
||||
const weekStartKey = toKey(weekStartDate);
|
||||
|
||||
let todayPv = dailyMap.get(todayKey) || 0;
|
||||
let yesterdayPv = dailyMap.get(yesterdayKey) || 0;
|
||||
let weekPv = 0;
|
||||
@@ -260,18 +222,21 @@ export const getVisitOverview = async (
|
||||
|
||||
export const getVisitPages = async (c: Context<{ Bindings: Bindings }>) => {
|
||||
try {
|
||||
const rawDomain = c.req.query('domain') || '';
|
||||
const domainFilter = rawDomain.trim().toLowerCase();
|
||||
const rawSiteId = c.req.query('siteId');
|
||||
const siteId = rawSiteId && rawSiteId !== 'default' ? rawSiteId : null;
|
||||
|
||||
const rawOrder = c.req.query('order') || '';
|
||||
const order = rawOrder.trim().toLowerCase() === 'latest' ? 'latest' : 'pv';
|
||||
|
||||
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();
|
||||
let sql = 'SELECT post_slug, post_title, post_url, pv, last_visit_at FROM page_stats';
|
||||
const params: any[] = [];
|
||||
|
||||
const { results } = await c.env.CWD_DB.prepare(
|
||||
'SELECT post_slug, post_title, post_url, pv, last_visit_at FROM page_stats'
|
||||
).all<{
|
||||
if (siteId) {
|
||||
sql += ' WHERE site_id = ?';
|
||||
params.push(siteId);
|
||||
}
|
||||
|
||||
const { results } = await c.env.CWD_DB.prepare(sql).bind(...params).all<{
|
||||
post_slug: string;
|
||||
post_title: string | null;
|
||||
post_url: string | null;
|
||||
@@ -282,15 +247,6 @@ export const getVisitPages = async (c: Context<{ Bindings: Bindings }>) => {
|
||||
let items: VisitPageItem[] = [];
|
||||
|
||||
for (const row of results) {
|
||||
const domain =
|
||||
extractDomain(row.post_url) ||
|
||||
extractDomain(row.post_slug) ||
|
||||
null;
|
||||
|
||||
if (domainFilter && domain !== domainFilter) {
|
||||
continue;
|
||||
}
|
||||
|
||||
items.push({
|
||||
postSlug: row.post_slug,
|
||||
postTitle: row.post_title,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import { getAdminEmail } from './api/admin/getAdminEmail';
|
||||
import { setAdminEmail } from './api/admin/setAdminEmail';
|
||||
import { testEmail } from './api/admin/testEmail';
|
||||
import { getStats } from './api/admin/getStats';
|
||||
import { getDomains } from './api/admin/getDomains';
|
||||
import { getSites } from './api/admin/getDomains';
|
||||
import { trackVisit } from './api/public/trackVisit';
|
||||
import { getVisitOverview, getVisitPages } from './api/admin/visitAnalytics';
|
||||
import { getLikeStatus, likePage } from './api/public/like';
|
||||
@@ -36,10 +36,13 @@ import { getLikeStats } from './api/admin/likeStats';
|
||||
import { getFeatureSettings, updateFeatureSettings } from './api/admin/featureSettings';
|
||||
import { getTelegramSettings, updateTelegramSettings, setupTelegramWebhook, testTelegramMessage } from './api/admin/telegramSettings';
|
||||
import { telegramWebhook } from './api/telegram/webhook';
|
||||
import { ensureSchema } from './utils/dbMigration';
|
||||
|
||||
const app = new Hono<{ Bindings: Bindings }>();
|
||||
const VERSION = `${packageJson.version}`;
|
||||
|
||||
let MIGRATION_CHECKED = false;
|
||||
|
||||
const COMMENT_ADMIN_EMAIL_KEY = 'comment_admin_email';
|
||||
const COMMENT_ADMIN_BADGE_KEY = 'comment_admin_badge';
|
||||
const COMMENT_AVATAR_PREFIX_KEY = 'comment_avatar_prefix';
|
||||
@@ -226,6 +229,10 @@ async function saveCommentSettings(
|
||||
}
|
||||
|
||||
app.use('*', async (c, next) => {
|
||||
if (!MIGRATION_CHECKED) {
|
||||
await ensureSchema(c.env);
|
||||
MIGRATION_CHECKED = true;
|
||||
}
|
||||
console.log('Request:start', {
|
||||
method: c.req.method,
|
||||
path: c.req.path,
|
||||
@@ -290,7 +297,7 @@ app.post('/admin/import/backup', importBackup);
|
||||
app.put('/admin/comments/status', updateStatus);
|
||||
app.put('/admin/comments/update', updateComment);
|
||||
app.get('/admin/stats/comments', getStats);
|
||||
app.get('/admin/stats/domains', getDomains);
|
||||
app.get('/admin/stats/sites', getSites);
|
||||
app.get('/admin/analytics/overview', getVisitOverview);
|
||||
app.get('/admin/analytics/pages', getVisitPages);
|
||||
app.get('/admin/likes/list', listLikes);
|
||||
|
||||
133
cwd-api/src/utils/dbMigration.ts
Normal file
133
cwd-api/src/utils/dbMigration.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { Bindings } from '../bindings';
|
||||
|
||||
export async function ensureSchema(env: Bindings) {
|
||||
try {
|
||||
// 1. Check and migrate page_stats
|
||||
const statsInfo = await env.CWD_DB.prepare('PRAGMA table_info(page_stats)').all();
|
||||
const statsColumns = (statsInfo.results || []) as any[];
|
||||
const hasStatsSiteId = statsColumns.some((col) => col.name === 'site_id');
|
||||
|
||||
if (!hasStatsSiteId && statsColumns.length > 0) {
|
||||
console.log('Migrating page_stats table...');
|
||||
// Create new table with site_id and composite unique constraint
|
||||
await env.CWD_DB.prepare(
|
||||
`CREATE TABLE page_stats_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
site_id TEXT NOT NULL DEFAULT '',
|
||||
post_slug TEXT 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,
|
||||
UNIQUE(site_id, post_slug)
|
||||
)`
|
||||
).run();
|
||||
|
||||
// Copy data
|
||||
await env.CWD_DB.prepare(
|
||||
`INSERT INTO page_stats_new (post_slug, post_title, post_url, pv, last_visit_at, created_at, updated_at)
|
||||
SELECT post_slug, post_title, post_url, pv, last_visit_at, created_at, updated_at FROM page_stats`
|
||||
).run();
|
||||
|
||||
// Drop old table
|
||||
await env.CWD_DB.prepare('DROP TABLE page_stats').run();
|
||||
|
||||
// Rename new table
|
||||
await env.CWD_DB.prepare('ALTER TABLE page_stats_new RENAME TO page_stats').run();
|
||||
console.log('Migrated page_stats table successfully.');
|
||||
} else if (statsColumns.length === 0) {
|
||||
// Table doesn't exist, create it directly
|
||||
await env.CWD_DB.prepare(
|
||||
`CREATE TABLE IF NOT EXISTS page_stats (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
site_id TEXT NOT NULL DEFAULT '',
|
||||
post_slug TEXT 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,
|
||||
UNIQUE(site_id, post_slug)
|
||||
)`
|
||||
).run();
|
||||
}
|
||||
|
||||
// 2. Check and migrate page_visit_daily
|
||||
const dailyInfo = await env.CWD_DB.prepare('PRAGMA table_info(page_visit_daily)').all();
|
||||
const dailyColumns = (dailyInfo.results || []) as any[];
|
||||
const hasDailySiteId = dailyColumns.some((col) => col.name === 'site_id');
|
||||
|
||||
if (!hasDailySiteId && dailyColumns.length > 0) {
|
||||
console.log('Migrating page_visit_daily table...');
|
||||
await env.CWD_DB.prepare('ALTER TABLE page_visit_daily ADD COLUMN site_id TEXT NOT NULL DEFAULT ""').run();
|
||||
console.log('Migrated page_visit_daily table successfully.');
|
||||
} else if (dailyColumns.length === 0) {
|
||||
await 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,
|
||||
site_id TEXT NOT NULL DEFAULT ''
|
||||
)`
|
||||
).run();
|
||||
}
|
||||
|
||||
// 3. Check and migrate Likes
|
||||
const likesInfo = await env.CWD_DB.prepare('PRAGMA table_info(Likes)').all();
|
||||
const likesColumns = (likesInfo.results || []) as any[];
|
||||
const hasLikesSiteId = likesColumns.some((col) => col.name === 'site_id');
|
||||
|
||||
if (!hasLikesSiteId && likesColumns.length > 0) {
|
||||
console.log('Migrating Likes table...');
|
||||
await env.CWD_DB.prepare(
|
||||
`CREATE TABLE Likes_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
site_id TEXT NOT NULL DEFAULT '',
|
||||
page_slug TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
UNIQUE(site_id, page_slug, user_id)
|
||||
)`
|
||||
).run();
|
||||
|
||||
await env.CWD_DB.prepare(
|
||||
`INSERT INTO Likes_new (page_slug, user_id, created_at)
|
||||
SELECT page_slug, user_id, created_at FROM Likes`
|
||||
).run();
|
||||
|
||||
await env.CWD_DB.prepare('DROP TABLE Likes').run();
|
||||
await env.CWD_DB.prepare('ALTER TABLE Likes_new RENAME TO Likes').run();
|
||||
console.log('Migrated Likes table successfully.');
|
||||
} else if (likesColumns.length === 0) {
|
||||
await env.CWD_DB.prepare(
|
||||
`CREATE TABLE IF NOT EXISTS Likes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
site_id TEXT NOT NULL DEFAULT '',
|
||||
page_slug TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
UNIQUE(site_id, page_slug, user_id)
|
||||
)`
|
||||
).run();
|
||||
}
|
||||
|
||||
// 4. Create Indexes
|
||||
await env.CWD_DB.prepare('CREATE INDEX IF NOT EXISTS idx_page_stats_site_id ON page_stats(site_id)').run();
|
||||
await env.CWD_DB.prepare('CREATE INDEX IF NOT EXISTS idx_page_visit_daily_site_id ON page_visit_daily(site_id)').run();
|
||||
await env.CWD_DB.prepare('CREATE INDEX IF NOT EXISTS idx_likes_site_id ON Likes(site_id)').run();
|
||||
// Also ensure Comment index exists (just in case)
|
||||
await env.CWD_DB.prepare('CREATE INDEX IF NOT EXISTS idx_site_id ON Comment(site_id)').run();
|
||||
|
||||
} catch (e) {
|
||||
console.error('Database migration failed:', e);
|
||||
// Don't throw, to allow app to start, but log error.
|
||||
// Or maybe we should throw? If schema is wrong, queries will fail anyway.
|
||||
// For now, log error.
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user