Add "Create Short URL" link to Header, remove Navbar component, and implement Create Short URL page with form handling and validation.

This commit is contained in:
2025-04-22 13:07:20 +08:00
parent 05af4aae70
commit 42f5be4dcb
4 changed files with 409 additions and 74 deletions

View File

@@ -4,7 +4,7 @@
// 描述: 此脚本从PostgreSQL数据库获取所有shorturl类型的资源及其关联数据并同步到ClickHouse
import { Pool } from "https://deno.land/x/postgres@v0.17.0/mod.ts";
import { getResource, getVariable } from "https://deno.land/x/windmill@v1.183.0/mod.ts";
import { getResource, getVariable, setVariable } from "https://deno.land/x/windmill@v1.183.0/mod.ts";
// 资源属性接口
interface ResourceAttributes {
@@ -37,6 +37,15 @@ interface PgConfig {
[key: string]: unknown;
}
// 上次同步状态接口
interface SyncState {
lastSyncTime: string;
lastRunTime: string;
}
// 状态变量名称
const STATE_VARIABLE_PATH = "f/shorturl_analytics/shorturl_sync_state";
// Windmill函数定义
export async function main(
/** PostgreSQL和ClickHouse同步脚本 */
@@ -47,9 +56,11 @@ export async function main(
includeDeleted?: boolean;
/** 是否执行实际写入操作 */
dryRun?: boolean;
/** 开始时间ISO格式*/
/** 是否强制全量同步 */
forceFullSync?: boolean;
/** 手动指定开始时间ISO格式- 会覆盖自动增量设置 */
startTime?: string;
/** 结束时间ISO格式*/
/** 手动指定结束时间ISO格式*/
endTime?: string;
}
) {
@@ -57,8 +68,41 @@ export async function main(
const limit = params.limit || 500;
const includeDeleted = params.includeDeleted || false;
const dryRun = params.dryRun || false;
const startTime = params.startTime ? new Date(params.startTime) : undefined;
const endTime = params.endTime ? new Date(params.endTime) : undefined;
const forceFullSync = params.forceFullSync || false;
// 获取当前时间作为本次运行时间
const currentRunTime = new Date().toISOString();
// 初始化同步状态
let syncState: SyncState;
let startTime: Date | undefined;
const endTime: Date | undefined = params.endTime ? new Date(params.endTime) : new Date();
// 如果强制全量同步或手动指定了开始时间,则使用指定的开始时间
if (forceFullSync || params.startTime) {
startTime = params.startTime ? new Date(params.startTime) : undefined;
console.log(`使用${params.startTime ? '手动指定' : '全量同步'} - 开始时间: ${startTime ? startTime.toISOString() : '无限制'}`);
}
// 否则尝试获取上次同步时间作为增量同步的开始时间点
else {
try {
// 获取上次同步状态
const stateStr = await getVariable(STATE_VARIABLE_PATH);
if (stateStr) {
syncState = JSON.parse(stateStr);
console.log(`获取到上次同步状态: 同步时间=${syncState.lastSyncTime}, 运行时间=${syncState.lastRunTime}`);
// 使用上次运行时间作为本次的开始时间 (减去1分钟防止边界问题)
const lastRunTime = new Date(syncState.lastRunTime);
lastRunTime.setMinutes(lastRunTime.getMinutes() - 1);
startTime = lastRunTime;
} else {
console.log("未找到上次同步状态,将执行全量同步");
}
} catch (error: unknown) {
console.log(`获取同步状态出错: ${error instanceof Error ? error.message : String(error)},将执行全量同步`);
}
}
console.log(`开始同步PostgreSQL shorturl数据到ClickHouse`);
console.log(`参数: limit=${limit}, includeDeleted=${includeDeleted}, dryRun=${dryRun}`);
@@ -67,7 +111,7 @@ export async function main(
// 获取数据库配置
console.log("获取PostgreSQL数据库配置...");
const pgConfig = await getResource('f/limq/postgresql') as PgConfig;
const pgConfig = await getResource('f/limq/production_supabase') as PgConfig;
console.log(`数据库连接配置: host=${pgConfig.host}, port=${pgConfig.port}, database=${pgConfig.dbname || 'postgres'}, user=${pgConfig.user}`);
let pgPool: Pool | null = null;
@@ -106,6 +150,8 @@ export async function main(
console.log(`获取到 ${shorturls.length} 个shorturl资源`);
if (shorturls.length === 0) {
// 即使没有数据也更新状态
await updateSyncState(currentRunTime);
return { synced: 0, message: "没有找到需要同步的shorturl资源" };
}
@@ -120,7 +166,11 @@ export async function main(
// 写入ClickHouse
const inserted = await insertToClickhouse(clickhouseData);
console.log(`成功写入 ${inserted} 条记录到ClickHouse`);
return { synced: inserted, message: "同步完成" };
// 更新同步状态
await updateSyncState(currentRunTime);
return { synced: inserted, message: "同步完成", lastSyncTime: currentRunTime };
} else {
console.log("Dry run模式 - 不执行实际写入");
console.log(`将写入 ${clickhouseData.length} 条记录到ClickHouse`);
@@ -146,6 +196,22 @@ export async function main(
}
}
// 更新同步状态
async function updateSyncState(currentRunTime: string): Promise<void> {
try {
const syncState: SyncState = {
lastSyncTime: new Date().toISOString(), // 记录数据同步完成的时间
lastRunTime: currentRunTime // 记录本次运行的时间点
};
console.log(`更新同步状态: ${JSON.stringify(syncState)}`);
await setVariable(STATE_VARIABLE_PATH, JSON.stringify(syncState));
} catch (error: unknown) {
console.error(`更新同步状态失败: ${error instanceof Error ? error.message : String(error)}`);
// 不中断主流程,即使状态更新失败
}
}
// 从PostgreSQL获取所有shorturl资源
async function fetchShorturlResources(
pgPool: Pool,
@@ -185,8 +251,9 @@ async function fetchShorturlResources(
query += ` AND r.deleted_at IS NULL`;
}
// 修改为同时考虑created_at和updated_at确保捕获自上次同步以来创建或更新的记录
if (options.startTime) {
query += ` AND r.created_at >= $${paramCount}`;
query += ` AND (r.created_at >= $${paramCount} OR r.updated_at >= $${paramCount})`;
params.push(options.startTime);
paramCount++;
}
@@ -197,7 +264,8 @@ async function fetchShorturlResources(
paramCount++;
}
query += ` ORDER BY r.created_at DESC LIMIT $${paramCount}`;
// 优先按更新时间排序,确保最近更新的记录先处理
query += ` ORDER BY r.updated_at DESC, r.created_at DESC LIMIT $${paramCount}`;
params.push(options.limit);
const client = await pgPool.connect();