今天配合 AI 写了一个简单的带统计的短链接中间页程序,部署在 Cloudflare Workers 上,配合 KV 存储实现。不仅能实现长链接转短链接,还能记录每一次点击的次数。

简单的说,就是部署在Worker上的,通过KV储存链接和次数,每次访问都会刷新KV的短链接程序。用pico.min.css优化了一些样式的显示。并且做了简单的管理功能。

代码实现

export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    const path = url.pathname;

    if (path === '/admin' || path.startsWith('/admin/')) {
      return handleAdmin(request, env);
    }

    const shortCode = path.slice(1);
    if (shortCode && shortCode !== 'favicon.ico' && shortCode !== '') {
      return handleRedirect(shortCode, env, ctx);
    }

    return new Response('Short Link Service is Running.', { status: 200 });
  }
};

async function handleRedirect(shortCode, env, ctx) {
  const key = `link:${shortCode}`;
  const dataStr = await env.SHORT_LINKS.get(key);
  
  if (!dataStr) {
    return new Response('链接不存在或已被删除', { status: 404 });
  }
  
  const data = JSON.parse(dataStr);
  
  ctx.waitUntil((async () => {
    data.visits = (data.visits || 0) + 1;
    await env.SHORT_LINKS.put(key, JSON.stringify(data));
  })());
  
  return Response.redirect(data.originalUrl, 302);
}

async function handleAdmin(request, env) {
  const auth = request.headers.get('Authorization');
  if (!auth) {
    return new Response('Unauthorized', { 
      status: 401, 
      headers: { 'WWW-Authenticate': 'Basic realm="Admin"' } 
    });
  }
  const credentials = atob(auth.slice(6)).split(':');
  if (credentials[1] !== env.ADMIN_PASSWORD) {
    return new Response('密码错误', { status: 403 });
  }

  const url = new URL(request.url);

  if (request.method === 'POST' && url.pathname === '/admin/create') {
    const formData = await request.formData();
    const originalUrl = formData.get('originalUrl');
    let shortCode = formData.get('customCode') || Math.random().toString(36).substring(2, 8);

    if (!originalUrl || !originalUrl.startsWith('http')) {
      return new Response('URL 格式不正确', { status: 400 });
    }

    await env.SHORT_LINKS.put(`link:${shortCode}`, JSON.stringify({
      originalUrl,
      visits: 0,
      createdAt: new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })
    }));
    
    return Response.redirect(`${url.origin}/admin`, 303);
  }

  if (url.pathname === '/admin/delete') {
    const code = url.searchParams.get('code');
    if (code) await env.SHORT_LINKS.delete(`link:${code}`);
    return Response.redirect(`${url.origin}/admin`, 303);
  }

  const list = await env.SHORT_LINKS.list({ prefix: 'link:' });
  const links = [];
  for (const key of list.keys) {
    const val = await env.SHORT_LINKS.get(key.name);
    if (val) {
      const data = JSON.parse(val);
      links.push({
        code: key.name.replace('link:', ''),
        ...data
      });
    }
  }

  return new Response(renderAdminPage(links), {
    headers: { 'Content-Type': 'text/html;charset=UTF-8' }
  });
}

function renderAdminPage(links) {
  const rows = links.map(link => `
    <tr>
      <td><a href="/${link.code}" target="_blank">/${link.code}</a></td>
      <td style="word-break: break-all;"><small>${link.originalUrl}</small></td>
      <td><mark>${link.visits}</mark></td>
      <td><small>${link.createdAt || '未知'}</small></td>
      <td><a href="/admin/delete?code=${link.code}" onclick="return confirm('确定删除吗?')" style="color: #e53935;">删除</a></td>
    </tr>
  `).join('');

  return `
  <!DOCTYPE html>
  <html lang="zh">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>短链接管理后台</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/picocss/1.5.13/pico.min.css">
    <style>
      body { padding: 20px 0; }
      .container { max-width: 900px; }
      .header { margin-bottom: 40px; text-align: center; }
      mark { font-weight: bold; padding: 2px 8px; border-radius: 4px; }
    </style>
  </head>
  <body>
    <main class="container">
      <div class="header">
        <h1>🔗 Short Link Admin</h1>
        <p>轻量、高效的 Cloudflare Workers 短链接生成器</p>
      </div>
      <article>
        <header><strong>新建短链接</strong></header>
        <form action="/admin/create" method="POST">
          <div class="grid">
            <input type="url" name="originalUrl" placeholder="粘贴长链接 (https://...)" required>
            <input type="text" name="customCode" placeholder="自定义短码 (选填)">
            <button type="submit">立即创建</button>
          </div>
        </form>
      </article>
      <figure>
        <table role="grid">
          <thead>
            <tr>
              <th>短码</th>
              <th>原始链接</th>
              <th>点击量</th>
              <th>创建时间</th>
              <th>操作</th>
            </tr>
          </thead>
          <tbody>
            ${rows || '<tr><td colspan="5" style="text-align:center;">暂无数据</td></tr>'}
          </tbody>
        </table>
      </figure>
    </main>
  </body>
  </html>
  `;
}

注意

  1. KV空间我命名的是SHORT_LINKS,你可以自己改
  2. 环境变量需要有ADMIN_PASSWORD用来储存管理密码
  3. 注意绑定WORKER和KV
  4. 第一次打开/admin,提示输入账户和密码,只输入密码即可!
  5. 免费的KV每天的PUT次数好像是有限制的,并且还有很多问题。小玩具,不用太在意。

你可以点击这里跳转本文章: https://link.1tb.site/short-link