文章详情

返回首页

CF上搭建订阅管理系统

分享文章 作者: Ws01 创建时间: 2026-03-12 📝 字数: 57,864 字 👁️ 阅读: 16 次

CF上搭建订阅管理系统

// 订阅管理系统 - Cloudflare Workers 主入口

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

    // 处理 CORS
    if (request.method === 'OPTIONS') {
      return new Response(null, {
        headers: {
          'Access-Control-Allow-Origin': '*',
          'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
          'Access-Control-Allow-Headers': 'Content-Type, Authorization',
        }
      });
    }

    // 静态页面路由
    if (path === '/' || path === '/index.html') {
      return new Response(await getLoginPage(), {
        headers: { 'Content-Type': 'text/html' }
      });
    }

    if (path === '/admin888/' || path === '/admin888/index.html') {
      return new Response(await getAdminPage(), {
        headers: { 'Content-Type': 'text/html' }
      });
    }

    // 备份文件路由
    if (path.startsWith('/backup/')) {
      return new Response('Backup endpoint', { status: 200 });
    }

    // API 路由
    if (path.startsWith('/api/')) {
      return handleApi(request, env, path);
    }

    // 默认返回 404
    return new Response('Not Found', { status: 404 });
  }
};


// 获取登录页面
async function getLoginPage() {
  return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>订阅管理 - 登录</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        .login-container {
            background: white;
            padding: 40px;
            border-radius: 10px;
            box-shadow: 0 10px 40px rgba(0,0,0,0.2);
            width: 100%;
            max-width: 400px;
        }
        .login-title {
            text-align: center;
            margin-bottom: 30px;
            color: #333;
            font-size: 24px;
        }
        .form-group {
            margin-bottom: 20px;
            display: flex;
            align-items: center;
            gap: 10px;
        }
        .form-group label {
            min-width: 80px;
            color: #555;
            font-weight: 500;
            display: flex;
            align-items: center;
            gap: 5px;
        }
        .form-group input {
            flex: 1;
            padding: 12px;
            border: 1px solid #ddd;
            border-radius: 5px;
            font-size: 14px;
            transition: border-color 0.3s;
        }
        .form-group input:focus {
            outline: none;
            border-color: #667eea;
        }
        .captcha-group {
            display: flex;
            gap: 10px;
            align-items: center;
            flex: 1;
            min-width: 0;
        }
        .captcha-group input {
            flex: 1;
            min-width: 0;
        }
        .captcha-canvas {
            border-radius: 5px;
            cursor: pointer;
            border: 1px solid #ddd;
            max-width: 120px;
            height: 40px;
            flex-shrink: 0;
        }
        .login-btn {
            width: 100%;
            padding: 12px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border: none;
            border-radius: 5px;
            font-size: 16px;
            cursor: pointer;
            transition: transform 0.2s;
        }
        .login-btn:hover {
            transform: translateY(-2px);
        }
        .error-msg {
            color: #e74c3c;
            text-align: center;
            margin-top: 15px;
            display: none;
        }
    </style>
</head>
<body>
    <div class="login-container">
        <h1 class="login-title">🔐 订阅管理系统</h1>
        <form id="loginForm">
            <div class="form-group">
                <label for="username">👤 帐号</label>
                <input type="text" id="username" name="username" required placeholder="请输入帐号">
            </div>
            <div class="form-group">
                <label for="password">🔑 密码</label>
                <input type="password" id="password" name="password" required placeholder="请输入密码">
            </div>
            <div class="form-group">
                <label for="captchaInput">🛡️ 验证码</label>
                <div class="captcha-group">
                    <input type="text" id="captchaInput" required placeholder="验证码.." maxlength="5">
                    <canvas id="captchaCanvas" class="captcha-canvas" width="120" height="40" title="点击刷新验证码"></canvas>
                </div>
            </div>
            <button type="submit" class="login-btn">登录</button>
            <p class="error-msg" id="errorMsg">帐号或密码错误</p>
        </form>
    </div>
    <script>
        // 验证码相关
        let currentCaptcha = '';
        
        // 生成随机颜色
        function randomColor() {
            return 'rgb(' + Math.floor(Math.random() * 256) + ',' + Math.floor(Math.random() * 256) + ',' + Math.floor(Math.random() * 256) + ')';
        }
        
        // 生成验证码
        function generateCaptcha() {
            const canvas = document.getElementById('captchaCanvas');
            const ctx = canvas.getContext('2d');
            const width = canvas.width;
            const height = canvas.height;
            
            // 清空画布
            ctx.clearRect(0, 0, width, height);
            
            // 生成随机背景色
            ctx.fillStyle = '#f0f0f0';
            ctx.fillRect(0, 0, width, height);
            
            // 绘制干扰点
            for (let i = 0; i < 50; i++) {
                ctx.fillStyle = randomColor();
                ctx.beginPath();
                ctx.arc(Math.random() * width, Math.random() * height, 2, 0, 2 * Math.PI);
                ctx.fill();
            }
            
            // 绘制干扰线
            for (let i = 0; i < 5; i++) {
                ctx.strokeStyle = randomColor();
                ctx.lineWidth = 1 + Math.random() * 2;
                ctx.beginPath();
                ctx.moveTo(Math.random() * width, Math.random() * height);
                ctx.lineTo(Math.random() * width, Math.random() * height);
                ctx.stroke();
            }
            
            // 生成 5 位数字验证码
            const chars = '0123456789';
            currentCaptcha = '';
            for (let i = 0; i < 5; i++) {
                currentCaptcha += chars.charAt(Math.floor(Math.random() * chars.length));
            }
            
            // 绘制验证码文字
            ctx.font = 'bold 24px Arial';
            ctx.textBaseline = 'middle';
            for (let i = 0; i < 5; i++) {
                ctx.fillStyle = randomColor();
                ctx.save();
                const x = 15 + i * 20;
                const y = 20 + (Math.random() - 0.5) * 10;
                const angle = (Math.random() - 0.5) * 0.4;
                ctx.translate(x, y);
                ctx.rotate(angle);
                ctx.fillText(currentCaptcha[i], 0, 0);
                ctx.restore();
            }
        }
        
        document.getElementById('loginForm').addEventListener('submit', async (e) => {
            e.preventDefault();
            const username = document.getElementById('username').value;
            const password = document.getElementById('password').value;
            const captchaInput = document.getElementById('captchaInput').value;
            
            // 验证验证码
            if (captchaInput !== currentCaptcha) {
                document.getElementById('errorMsg').textContent = '验证码错误';
                document.getElementById('errorMsg').style.display = 'block';
                generateCaptcha();
                document.getElementById('captchaInput').value = '';
                return;
            }
            
            try {
                const response = await fetch('/api/login', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ username, password })
                });
                const data = await response.json();
                
                if (data.success) {
                    localStorage.setItem('authToken', data.token);
                    window.location.href = '/admin888/';
                } else {
                    if (data.remainingAttempts !== undefined) {
                        document.getElementById('errorMsg').textContent = '帐号或密码错误,还剩 ' + data.remainingAttempts + ' 次尝试机会';
                    } else {
                        document.getElementById('errorMsg').textContent = '帐号或密码错误';
                    }
                    document.getElementById('errorMsg').style.display = 'block';
                    generateCaptcha();
                    document.getElementById('captchaInput').value = '';
                }
            } catch (err) {
                document.getElementById('errorMsg').textContent = '登录失败,请重试';
                document.getElementById('errorMsg').style.display = 'block';
                generateCaptcha();
                document.getElementById('captchaInput').value = '';
            }
        });
        
        // 点击验证码刷新
        document.getElementById('captchaCanvas').addEventListener('click', generateCaptcha);
        
        // 页面加载时生成验证码
        generateCaptcha();
    </script>
</body>
</html>`;
}


// 获取管理页面
async function getAdminPage() {
  return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>订阅管理 - 后台</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            background: #f5f6fa;
            min-height: 100vh;
        }
        .header {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            padding: 15px 30px;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .header h1 { font-size: 20px; }
        .logout-btn {
            background: rgba(255,255,255,0.2);
            color: white;
            border: none;
            padding: 8px 16px;
            border-radius: 5px;
            cursor: pointer;
        }
        .container {
            max-width: 1400px;
            margin: 0 auto;
            padding: 20px;
        }
        .nav-tabs {
            display: flex;
            background: white;
            border-radius: 8px;
            margin-bottom: 20px;
            overflow: hidden;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        .nav-tab {
            padding: 15px 25px;
            cursor: pointer;
            border: none;
            background: white;
            font-size: 14px;
            transition: all 0.3s;
        }
        .nav-tab.active {
            background: #667eea;
            color: white;
        }
        .nav-tab:hover:not(.active) {
            background: #f0f0f0;
        }
        .content-area {
            background: white;
            border-radius: 8px;
            padding: 25px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        .section-title {
            font-size: 18px;
            margin-bottom: 20px;
            color: #333;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .add-btn {
            background: #27ae60;
            color: white;
            border: none;
            padding: 8px 16px;
            border-radius: 5px;
            cursor: pointer;
            font-size: 14px;
        }
        .add-btn:hover { background: #219a52; }
        table {
            width: 100%;
            border-collapse: collapse;
        }
        th, td {
            padding: 12px;
            text-align: left;
            border-bottom: 1px solid #eee;
        }
        th {
            background: #f8f9fa;
            font-weight: 600;
            color: #555;
        }
        tr:hover { background: #f8f9fa; }
        .action-btn {
            padding: 5px 10px;
            margin-right: 5px;
            border: none;
            border-radius: 3px;
            cursor: pointer;
            font-size: 12px;
        }
        .edit-btn { background: #3498db; color: white; }
        .delete-btn { background: #e74c3c; color: white; }
        .restore-btn { background: #f39c12; color: white; }
        .modal {
            display: none;
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0,0,0,0.5);
            z-index: 1000;
        }
        .modal-content {
            background: white;
            width: 90%;
            max-width: 500px;
            margin: 100px auto;
            border-radius: 8px;
            padding: 25px;
        }
        .modal-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 20px;
        }
        .close-btn {
            background: none;
            border: none;
            font-size: 24px;
            cursor: pointer;
            color: #999;
        }
        .form-group {
            margin-bottom: 15px;
        }
        .form-group label {
            display: block;
            margin-bottom: 5px;
            color: #555;
            font-weight: 500;
        }
        .form-group input, .form-group textarea {
            width: 100%;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 5px;
            font-size: 14px;
        }
        .form-group input:focus, .form-group textarea:focus {
            outline: none;
            border-color: #667eea;
        }
        .submit-btn {
            width: 100%;
            padding: 12px;
            background: #667eea;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            font-size: 16px;
        }
        .status-ok { color: #27ae60; }
        .status-warning { color: #f39c12; }
        .status-danger { color: #e74c3c; }
        .category-select {
            margin-bottom: 20px;
        }
        .category-select select {
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 5px;
            font-size: 14px;
        }
        .copyable {
            cursor: pointer;
            transition: all 0.2s;
            position: relative;
        }
        .copyable:hover {
            color: #667eea;
            text-decoration: underline;
        }
        .toast {
            position: fixed;
            top: 20px;
            left: 50%;
            transform: translateX(-50%);
            background: rgba(0,0,0,0.8);
            color: white;
            padding: 12px 24px;
            border-radius: 5px;
            z-index: 2000;
            display: none;
            font-size: 14px;
        }
        .backup-info {
            background: #f8f9fa;
            padding: 15px;
            border-radius: 5px;
            margin-bottom: 20px;
            font-size: 14px;
            color: #555;
        }
        .backup-btn {
            background: #667eea;
            color: white;
            border: none;
            padding: 10px 20px;
            border-radius: 5px;
            cursor: pointer;
            font-size: 14px;
            margin-right: 10px;
        }
        .backup-btn:hover { background: #5568d3; }
    </style>
</head>
<body>
    <div class="header">
        <h1>📋 订阅管理系统</h1>
        <button class="logout-btn" onclick="logout()">退出登录</button>
    </div>
    
    <div class="container">
        <div class="nav-tabs">
            <button class="nav-tab active" onclick="showSection('products')">商品管理</button>
            <button class="nav-tab" onclick="showSection('categories')">分类管理</button>
            <button class="nav-tab" onclick="showSection('backup')">数据备份</button>
        </div>
        
        <div id="productsSection" class="content-area">
            <div class="section-title">
                <span>商品列表</span>
                <div>
                    <button class="action-btn delete-btn" onclick="batchDeleteProducts()" style="margin-right: 10px;">🗑️ 批量删除</button>
                    <button class="add-btn" onclick="openProductModal()">+ 添加商品</button>
                </div>
            </div>
            <div class="category-select">
                <label>选择分类:</label>
                <select id="categoryFilter" onchange="loadProducts()">
                    <option value="">全部分类</option>
                </select>
            </div>
            <table>
                <thead>
                    <tr>
                        <th><input type="checkbox" id="selectAll" onclick="toggleSelectAll()"></th>
                        <th>序号</th>
                        <th>商品名称</th>
                        <th>分类</th>
                        <th>注册时间</th>
                        <th>续费时间</th>
                        <th>剩余天数</th>
                        <th>托管地址</th>
                        <th>备注</th>
                        <th>操作</th>
                    </tr>
                </thead>
                <tbody id="productTable"></tbody>
            </table>
        </div>
        
        <div id="categoriesSection" class="content-area" style="display:none;">
            <div class="section-title">
                <span>分类列表</span>
                <button class="add-btn" onclick="openCategoryModal()">+ 添加分类</button>
            </div>
            <table>
                <thead>
                    <tr>
                        <th>序号</th>
                        <th>分类名称</th>
                        <th>帐号</th>
                        <th>备注</th>
                        <th>操作</th>
                    </tr>
                </thead>
                <tbody id="categoryTable"></tbody>
            </table>
        </div>
        
        <div id="backupSection" class="content-area" style="display:none;">
            <div class="section-title">
                <span>备份管理</span>
                <button class="backup-btn" onclick="createBackup()">📦 创建备份</button>
            </div>
            <div class="backup-info">
                💡 备份说明:系统最多保留最近 5 个备份,创建新备份时会自动删除最旧的备份。恢复操作将覆盖当前所有数据,请谨慎操作。
            </div>
            <table>
                <thead>
                    <tr>
                        <th>序号</th>
                        <th>备份时间</th>
                        <th>分类数量</th>
                        <th>商品数量</th>
                        <th>操作</th>
                    </tr>
                </thead>
                <tbody id="backupTable"></tbody>
            </table>
        </div>
    </div>
    
    <!-- 分类模态框 -->
    <div id="categoryModal" class="modal">
        <div class="modal-content">
            <div class="modal-header">
                <h3 id="categoryModalTitle">添加分类</h3>
                <button class="close-btn" onclick="closeCategoryModal()">&times;</button>
            </div>
            <form id="categoryForm">
                <input type="hidden" id="categoryId">
                <div class="form-group">
                    <label>分类名称</label>
                    <input type="text" id="categoryName" required>
                </div>
                <div class="form-group">
                    <label>帐号</label>
                    <input type="text" id="categoryAccount">
                </div>
                <div class="form-group">
                    <label>密码</label>
                    <input type="text" id="categoryPassword">
                </div>
                <div class="form-group">
                    <label>备注</label>
                    <textarea id="categoryRemark" rows="3"></textarea>
                </div>
                <button type="submit" class="submit-btn">保存</button>
            </form>
        </div>
    </div>
    
    <!-- 商品模态框 -->
    <div id="productModal" class="modal">
        <div class="modal-content">
            <div class="modal-header">
                <h3 id="productModalTitle">添加商品</h3>
                <button class="close-btn" onclick="closeProductModal()">&times;</button>
            </div>
            <form id="productForm">
                <input type="hidden" id="productId">
                <div class="form-group">
                    <label>商品名称</label>
                    <input type="text" id="productName" required>
                </div>
                <div class="form-group">
                    <label>所属分类</label>
                    <select id="productCategory" required></select>
                </div>
                <div class="form-group">
                    <label>注册时间</label>
                    <input type="date" id="registerDate" required>
                </div>
                <div class="form-group">
                    <label>续费时间</label>
                    <div style="display: flex; gap: 10px; align-items: center; width: 100%;">
                        <input type="date" id="renewDate" style="flex: 1; min-width: 0;">
                        <label style="white-space: nowrap; display: flex; align-items: center; gap: 5px;"><input type="checkbox" id="renewDateLongterm" onchange="toggleRenewDate()"> 长期</label>
                    </div>
                </div>
                <div class="form-group">
                    <label>托管地址</label>
                    <input type="text" id="hostUrl">
                </div>
                <div class="form-group">
                    <label>备注</label>
                    <textarea id="productRemark" rows="3"></textarea>
                </div>
                <button type="submit" class="submit-btn">保存</button>
            </form>
        </div>
    </div>
    
    <script>
        // 认证检查
        function checkAuth() {
            const token = localStorage.getItem('authToken');
            if (!token) {
                window.location.href = '/';
            }
        }
        
        function logout() {
            localStorage.removeItem('authToken');
            window.location.href = '/';
        }
        
        // 页面切换
        function showSection(section) {
            document.querySelectorAll('.nav-tab').forEach(tab => tab.classList.remove('active'));
            document.querySelectorAll('.content-area').forEach(area => area.style.display = 'none');
            
            if (section === 'products') {
                document.querySelector('.nav-tab:nth-child(1)').classList.add('active');
                document.getElementById('productsSection').style.display = 'block';
                loadCategoriesForSelect();
                loadProducts();
            } else if (section === 'categories') {
                document.querySelector('.nav-tab:nth-child(2)').classList.add('active');
                document.getElementById('categoriesSection').style.display = 'block';
                loadCategories();
            } else if (section === 'backup') {
                document.querySelector('.nav-tab:nth-child(3)').classList.add('active');
                document.getElementById('backupSection').style.display = 'block';
                loadBackups();
            }
        }
        
        // 加载分类
        async function loadCategories() {
            const response = await fetch('/api/categories', {
                headers: { 'Authorization': 'Bearer ' + localStorage.getItem('authToken') }
            });
            const data = await response.json();
            // 按 sort 字段排序
            data.sort((a, b) => (a.sort || 0) - (b.sort || 0));
            const tbody = document.getElementById('categoryTable');
            tbody.innerHTML = '';
            
            data.forEach((cat, index) => {
                const tr = document.createElement('tr');
                tr.innerHTML = \`
                    <td>
                        \${index + 1}
                        <button class="action-btn" onclick="moveCategory('\${cat.id}', 'up')" style="margin-left:5px;">↑</button>
                        <button class="action-btn" onclick="moveCategory('\${cat.id}', 'down')">↓</button>
                    </td>
                    <td class="copyable" onclick="copyToClipboard('\${cat.name.replace(/'/g, "\\'")}')">\${cat.name}</td>
                    <td class="copyable" onclick="copyToClipboard('\${(cat.account || '-').replace(/'/g, "\\'")}')">\${cat.account || '-'}</td>
                    <td class="copyable" onclick="copyToClipboard('\${(cat.remark || '-').replace(/'/g, "\\'")}')">\${cat.remark || '-'}</td>
                    <td>
                        <button class="action-btn edit-btn" onclick="editCategory('\${cat.id}')">编辑</button>
                        <button class="action-btn delete-btn" onclick="deleteCategory('\${cat.id}')">删除</button>
                    </td>
                \`;
                tbody.appendChild(tr);
            });
        }
        
        // 加载分类到选择框
        async function loadCategoriesForSelect() {
            const response = await fetch('/api/categories', {
                headers: { 'Authorization': 'Bearer ' + localStorage.getItem('authToken') }
            });
            const data = await response.json();
            const select = document.getElementById('productCategory');
            const filter = document.getElementById('categoryFilter');
            
            select.innerHTML = '<option value="">请选择分类</option>';
            filter.innerHTML = '<option value="">全部分类</option>';
            
            data.forEach(cat => {
                select.innerHTML += \`<option value="\${cat.id}">\${cat.name}</option>\`;
                filter.innerHTML += \`<option value="\${cat.id}">\${cat.name}</option>\`;
            });
        }
        
        // 加载商品
        async function loadProducts() {
            const categoryId = document.getElementById('categoryFilter').value;
            const url = categoryId ? \`/api/products?categoryId=\${categoryId}\` : '/api/products';
            
            const response = await fetch(url, {
                headers: { 'Authorization': 'Bearer ' + localStorage.getItem('authToken') }
            });
            const data = await response.json();
            
            // 按剩余天数排序:剩余天数少的在前,长期在后
            data.sort((a, b) => {
                const daysA = calculateDays(a.renewDate);
                const daysB = calculateDays(b.renewDate);
                
                // 长期排在最后
                if (daysA.isLongterm && daysB.isLongterm) return 0;
                if (daysA.isLongterm) return 1;
                if (daysB.isLongterm) return -1;
                
                // 剩余天数少的在前
                return daysA.days - daysB.days;
            });
            
            const tbody = document.getElementById('productTable');
            tbody.innerHTML = '';
            
            data.forEach((prod, index) => {
                const daysInfo = calculateDays(prod.renewDate);
                const statusClass = daysInfo.isLongterm ? 'status-ok' : (daysInfo.days >= 180 ? 'status-ok' : (daysInfo.days >= 30 ? 'status-warning' : 'status-danger'));
                
                const tr = document.createElement('tr');
                tr.innerHTML = \`
                    <td><input type="checkbox" class="product-checkbox" value="\${prod.id}"></td>
                    <td>\${index + 1}</td>
                    <td class="copyable" onclick="copyToClipboard('\${prod.name.replace(/'/g, "\\'")}')">\${prod.name}</td>
                    <td>\${prod.categoryName || '-'}</td>
                    <td>\${prod.registerDate}</td>
                    <td>\${prod.renewDate}</td>
                    <td class="\${statusClass}">\${daysInfo.display}</td>
                    <td>\${prod.hostUrl || '-'}</td>
                    <td>\${prod.remark || '-'}</td>
                    <td>
                        <button class="action-btn edit-btn" onclick="editProduct('\${prod.id}')">编辑</button>
                        <button class="action-btn delete-btn" onclick="deleteProduct('\${prod.id}')">删除</button>
                    </td>
                \`;
                tbody.appendChild(tr);
            });
        }
        
        // 计算剩余天数
        function calculateDays(renewDate) {
            if (!renewDate || renewDate === 'longterm') {
                return { isLongterm: true, days: 9999, display: '长期' };
            }
            const renew = new Date(renewDate);
            const now = new Date();
            const diff = renew - now;
            const days = Math.ceil(diff / (1000 * 60 * 60 * 24));
            return { isLongterm: false, days: days, display: days + '天' };
        }
        
        // 切换长期选项
        function toggleRenewDate() {
            const isLongterm = document.getElementById('renewDateLongterm').checked;
            document.getElementById('renewDate').disabled = isLongterm;
            if (isLongterm) {
                document.getElementById('renewDate').value = '';
            }
        }
        
        // 分类模态框
        function openCategoryModal() {
            document.getElementById('categoryModalTitle').textContent = '添加分类';
            document.getElementById('categoryForm').reset();
            document.getElementById('categoryId').value = '';
            document.getElementById('categoryModal').style.display = 'block';
        }
        
        function closeCategoryModal() {
            document.getElementById('categoryModal').style.display = 'none';
        }
        
        async function editCategory(id) {
            const response = await fetch(\`/api/categories/\${id}\`, {
                headers: { 'Authorization': 'Bearer ' + localStorage.getItem('authToken') }
            });
            const data = await response.json();
            
            document.getElementById('categoryModalTitle').textContent = '编辑分类';
            document.getElementById('categoryId').value = data.id;
            document.getElementById('categoryName').value = data.name;
            document.getElementById('categoryAccount').value = data.account || '';
            document.getElementById('categoryPassword').value = data.password || '';
            document.getElementById('categoryRemark').value = data.remark || '';
            document.getElementById('categoryModal').style.display = 'block';
        }
        
        async function deleteCategory(id) {
            if (!confirm('确定要删除这个分类吗?')) return;
            
            await fetch(\`/api/categories/\${id}\`, {
                method: 'DELETE',
                headers: { 'Authorization': 'Bearer ' + localStorage.getItem('authToken') }
            });
            loadCategories();
        }
        
        document.getElementById('categoryForm').addEventListener('submit', async (e) => {
            e.preventDefault();
            const id = document.getElementById('categoryId').value;
            const data = {
                name: document.getElementById('categoryName').value,
                account: document.getElementById('categoryAccount').value,
                password: document.getElementById('categoryPassword').value,
                remark: document.getElementById('categoryRemark').value
            };
            
            const url = id ? \`/api/categories/\${id}\` : '/api/categories';
            const method = id ? 'PUT' : 'POST';
            
            await fetch(url, {
                method: method,
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': 'Bearer ' + localStorage.getItem('authToken')
                },
                body: JSON.stringify(data)
            });
            
            closeCategoryModal();
            loadCategories();
        });
        
        // 商品模态框
        function openProductModal() {
            document.getElementById('productModalTitle').textContent = '添加商品';
            document.getElementById('productForm').reset();
            document.getElementById('productId').value = '';
            document.getElementById('productModal').style.display = 'block';
        }
        
        function closeProductModal() {
            document.getElementById('productModal').style.display = 'none';
        }
        
        async function editProduct(id) {
            const response = await fetch(\`/api/products/\${id}\`, {
                headers: { 'Authorization': 'Bearer ' + localStorage.getItem('authToken') }
            });
            const data = await response.json();
            
            document.getElementById('productModalTitle').textContent = '编辑商品';
            document.getElementById('productId').value = data.id;
            document.getElementById('productName').value = data.name;
            document.getElementById('productCategory').value = data.categoryId;
            document.getElementById('registerDate').value = data.registerDate;
            if (data.renewDate === 'longterm') {
                document.getElementById('renewDateLongterm').checked = true;
                document.getElementById('renewDate').value = '';
                document.getElementById('renewDate').disabled = true;
            } else {
                document.getElementById('renewDateLongterm').checked = false;
                document.getElementById('renewDate').value = data.renewDate;
                document.getElementById('renewDate').disabled = false;
            }
            document.getElementById('hostUrl').value = data.hostUrl || '';
            document.getElementById('productRemark').value = data.remark || '';
            document.getElementById('productModal').style.display = 'block';
        }
        
        async function deleteProduct(id) {
            if (!confirm('确定要删除这个商品吗?')) return;
            
            await fetch(\`/api/products/\${id}\`, {
                method: 'DELETE',
                headers: { 'Authorization': 'Bearer ' + localStorage.getItem('authToken') }
            });
            loadProducts();
        }
        
        document.getElementById('productForm').addEventListener('submit', async (e) => {
            e.preventDefault();
            const id = document.getElementById('productId').value;
            const data = {
                name: document.getElementById('productName').value,
                categoryId: document.getElementById('productCategory').value,
                registerDate: document.getElementById('registerDate').value,
                renewDate: document.getElementById('renewDateLongterm').checked ? 'longterm' : document.getElementById('renewDate').value,
                hostUrl: document.getElementById('hostUrl').value,
                remark: document.getElementById('productRemark').value
            };
            
            const url = id ? \`/api/products/\${id}\` : '/api/products';
            const method = id ? 'PUT' : 'POST';
            
            await fetch(url, {
                method: method,
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': 'Bearer ' + localStorage.getItem('authToken')
                },
                body: JSON.stringify(data)
            });
            
            closeProductModal();
            loadProducts();
        });
        
        // 初始化
        checkAuth();
        loadCategoriesForSelect();
        loadProducts();
        
        // 全选/取消全选
        function toggleSelectAll() {
            const selectAll = document.getElementById('selectAll');
            const checkboxes = document.querySelectorAll('.product-checkbox');
            checkboxes.forEach(cb => {
                cb.checked = selectAll.checked;
            });
        }
        
        // 批量删除商品
        async function batchDeleteProducts() {
            const checkboxes = document.querySelectorAll('.product-checkbox:checked');
            if (checkboxes.length === 0) {
                showToast('请先选择要删除的商品');
                return;
            }
            
            if (!confirm(\`确定要删除选中的 \${checkboxes.length} 个商品吗?\`)) return;
            
            const ids = Array.from(checkboxes).map(cb => cb.value);
            
            try {
                const response = await fetch('/api/products/batch-delete', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'Authorization': 'Bearer ' + localStorage.getItem('authToken')
                    },
                    body: JSON.stringify({ ids })
                });
                const data = await response.json();
                
                if (data.success) {
                    showToast(\`成功删除 \${ids.length} 个商品\`);
                    document.getElementById('selectAll').checked = false;
                    loadProducts();
                } else {
                    showToast('删除失败:' + (data.message || '未知错误'));
                }
            } catch (err) {
                showToast('删除失败,请重试');
            }
        }
        
        // 复制到剪贴板
        function copyToClipboard(text) {
            if (!text || text === '-') return;
            
            navigator.clipboard.writeText(text).then(() => {
                showToast('已复制:' + text);
            }).catch(err => {
                // 降级方案
                const textarea = document.createElement('textarea');
                textarea.value = text;
                document.body.appendChild(textarea);
                textarea.select();
                document.execCommand('copy');
                document.body.removeChild(textarea);
                showToast('已复制:' + text);
            });
        }
        
        // 显示提示
        function showToast(message) {
            let toast = document.getElementById('toast');
            if (!toast) {
                toast = document.createElement('div');
                toast.id = 'toast';
                toast.className = 'toast';
                document.body.appendChild(toast);
            }
            toast.textContent = message;
            toast.style.display = 'block';
            
            setTimeout(() => {
                toast.style.display = 'none';
            }, 2000);
        }
        
        // 移动分类顺序
        async function moveCategory(id, direction) {
            try {
                const response = await fetch('/api/categories/move', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'Authorization': 'Bearer ' + localStorage.getItem('authToken')
                    },
                    body: JSON.stringify({ id, direction })
                });
                const data = await response.json();
                if (data.success) {
                    loadCategories();
                } else {
                    showToast('移动失败:' + (data.message || '未知错误'));
                }
            } catch (err) {
                showToast('移动失败,请重试');
            }
        }
        
        // 备份相关功能
        async function loadBackups() {
            const response = await fetch('/api/backups', {
                headers: { 'Authorization': 'Bearer ' + localStorage.getItem('authToken') }
            });
            const data = await response.json();
            const tbody = document.getElementById('backupTable');
            tbody.innerHTML = '';
            
            data.forEach((backup, index) => {
                const tr = document.createElement('tr');
                const backupTime = new Date(backup.createdAt).toLocaleString('zh-CN');
                tr.innerHTML = \`
                    <td>\${index + 1}</td>
                    <td>\${backupTime}</td>
                    <td>\${backup.categoriesCount || 0}</td>
                    <td>\${backup.productsCount || 0}</td>
                    <td>
                        <button class="action-btn restore-btn" onclick="restoreBackup('\${backup.id}')">恢复</button>
                        <button class="action-btn delete-btn" onclick="deleteBackup('\${backup.id}')">删除</button>
                    </td>
                \`;
                tbody.appendChild(tr);
            });
        }
        
        async function createBackup() {
            if (!confirm('确定要创建当前数据的备份吗?')) return;
            
            try {
                const response = await fetch('/api/backups', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'Authorization': 'Bearer ' + localStorage.getItem('authToken')
                    }
                });
                const data = await response.json();
                
                if (data.success) {
                    showToast('备份创建成功');
                    loadBackups();
                } else {
                    showToast('备份失败:' + (data.message || '未知错误'));
                }
            } catch (err) {
                showToast('备份失败,请重试');
            }
        }
        
        async function restoreBackup(id) {
            if (!confirm('⚠️ 警告:恢复操作将覆盖当前所有数据!确定要继续吗?')) return;
            if (!confirm('再次确认:此操作不可逆,确定要恢复此备份吗?')) return;
            
            try {
                const response = await fetch(\`/api/backups/\${id}/restore\`, {
                    method: 'POST',
                    headers: {
                        'Authorization': 'Bearer ' + localStorage.getItem('authToken')
                    }
                });
                const data = await response.json();
                
                if (data.success) {
                    showToast('恢复成功');
                    loadCategories();
                    loadProducts();
                    loadBackups();
                } else {
                    showToast('恢复失败:' + (data.message || '未知错误'));
                }
            } catch (err) {
                showToast('恢复失败,请重试');
            }
        }
        
        async function deleteBackup(id) {
            if (!confirm('确定要删除这个备份吗?')) return;
            
            try {
                await fetch(\`/api/backups/\${id}\`, {
                    method: 'DELETE',
                    headers: { 'Authorization': 'Bearer ' + localStorage.getItem('authToken') }
                });
                showToast('备份已删除');
                loadBackups();
            } catch (err) {
                showToast('删除失败,请重试');
            }
        }
    </script>
</body>
</html>`;
}


// 处理 API 请求
async function handleApi(request, env, path) {
  const headers = {
    'Content-Type': 'application/json',
    'Access-Control-Allow-Origin': '*'
  };

  // 获取客户端 IP
  const clientIP = request.headers.get('CF-Connecting-IP') || 'unknown';

  try {
    // 登录
    if (path === '/api/login' && request.method === 'POST') {
      // 检查 IP 是否被限制
      const rateLimitCheck = await checkRateLimit(env, clientIP);
      if (!rateLimitCheck.allowed) {
        const remainingTime = Math.ceil((rateLimitCheck.unlockedAt - Date.now()) / (1000 * 60 * 60));
        return new Response(JSON.stringify({ 
          success: false, 
          message: `登录次数过多,请在${remainingTime}小时后重试` 
        }), { headers });
      }
      
      const { username, password } = await request.json();
      
      if (username === env.ADMIN_USERNAME && password === env.ADMIN_PASSWORD) {
        // 登录成功,清除该 IP 的失败记录
        await env.SUB_KV.delete(`login_attempt_${clientIP}`);
        const token = btoa(JSON.stringify({ username, time: Date.now() }));
        return new Response(JSON.stringify({ success: true, token }), { headers });
      }
      
      // 登录失败,记录尝试次数并获取剩余次数
      const attemptResult = await recordLoginAttempt(env, clientIP);
      return new Response(JSON.stringify({ 
        success: false, 
        message: '帐号或密码错误',
        remainingAttempts: attemptResult.remainingAttempts
      }), { headers });
    }

    // 验证 token
    const authHeader = request.headers.get('Authorization');
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      return new Response(JSON.stringify({ error: '未授权' }), { status: 401, headers });
    }

    // 分类管理
    if (path === '/api/categories' && request.method === 'GET') {
      const categories = await env.SUB_KV.get('categories', { type: 'json' }) || [];
      return new Response(JSON.stringify(categories), { headers });
    }

    if (path === '/api/categories' && request.method === 'POST') {
      const data = await request.json();
      const categories = await env.SUB_KV.get('categories', { type: 'json' }) || [];
      
      // 获取最大 sort 值
      const maxSort = categories.length > 0 ? Math.max(...categories.map(c => c.sort || 0)) : 0;
      
      const newCategory = {
        id: Date.now().toString(),
        name: data.name,
        account: data.account,
        password: data.password,
        remark: data.remark,
        sort: maxSort + 1,
        createdAt: new Date().toISOString()
      };
      
      categories.push(newCategory);
      await env.SUB_KV.put('categories', JSON.stringify(categories));
      
      return new Response(JSON.stringify(newCategory), { headers });
    }

    if (path.match(/^\/api\/categories\/[^\/]+$/) && request.method === 'GET') {
      const id = path.split('/').pop();
      const categories = await env.SUB_KV.get('categories', { type: 'json' }) || [];
      const category = categories.find(c => c.id === id);
      
      if (!category) {
        return new Response(JSON.stringify({ error: '未找到' }), { status: 404, headers });
      }
      
      return new Response(JSON.stringify(category), { headers });
    }

    if (path.match(/^\/api\/categories\/[^\/]+$/) && request.method === 'PUT') {
      const id = path.split('/').pop();
      const data = await request.json();
      const categories = await env.SUB_KV.get('categories', { type: 'json' }) || [];
      
      const index = categories.findIndex(c => c.id === id);
      if (index === -1) {
        return new Response(JSON.stringify({ error: '未找到' }), { status: 404, headers });
      }
      
      categories[index] = { ...categories[index], ...data, updatedAt: new Date().toISOString() };
      await env.SUB_KV.put('categories', JSON.stringify(categories));
      
      return new Response(JSON.stringify(categories[index]), { headers });
    }

    if (path.match(/^\/api\/categories\/[^\/]+$/) && request.method === 'DELETE') {
      const id = path.split('/').pop();
      const categories = await env.SUB_KV.get('categories', { type: 'json' }) || [];
      
      const filtered = categories.filter(c => c.id !== id);
      await env.SUB_KV.put('categories', JSON.stringify(filtered));
      
      // 删除该分类下的所有商品
      const products = await env.SUB_KV.get('products', { type: 'json' }) || [];
      const filteredProducts = products.filter(p => p.categoryId !== id);
      await env.SUB_KV.put('products', JSON.stringify(filteredProducts));
      
      return new Response(JSON.stringify({ success: true }), { headers });
    }

    // 移动分类顺序
    if (path === '/api/categories/move' && request.method === 'POST') {
      const { id, direction } = await request.json();
      let categories = await env.SUB_KV.get('categories', { type: 'json' }) || [];
      
      // 确保所有分类都有 sort 字段,初始化缺失的字段
      categories = categories.map((c, index) => ({
        ...c,
        sort: c.sort !== undefined ? c.sort : index + 1
      }));
      
      // 按 sort 排序
      categories.sort((a, b) => (a.sort || 0) - (b.sort || 0));
      
      const index = categories.findIndex(c => c.id === id);
      if (index === -1) {
        return new Response(JSON.stringify({ success: false, message: '分类不存在' }), { headers });
      }
      
      // 上移或下移
      const newIndex = direction === 'up' ? index - 1 : index + 1;
      if (newIndex < 0 || newIndex >= categories.length) {
        return new Response(JSON.stringify({ success: false, message: '无法移动' }), { headers });
      }
      
      // 交换 sort 值
      const temp = categories[index].sort;
      categories[index].sort = categories[newIndex].sort;
      categories[newIndex].sort = temp;
      
      // 保存更新后的分类数据
      await env.SUB_KV.put('categories', JSON.stringify(categories));
      
      return new Response(JSON.stringify({ success: true }), { headers });
    }

    // 商品管理
    if (path === '/api/products' && request.method === 'GET') {
      const url = new URL(request.url);
      const categoryId = url.searchParams.get('categoryId');
      
      let products = await env.SUB_KV.get('products', { type: 'json' }) || [];
      const categories = await env.SUB_KV.get('categories', { type: 'json' }) || [];
      
      // 添加分类名称
      products = products.map(p => {
        const category = categories.find(c => c.id === p.categoryId);
        return { ...p, categoryName: category ? category.name : '' };
      });
      
      if (categoryId) {
        products = products.filter(p => p.categoryId === categoryId);
      }
      
      return new Response(JSON.stringify(products), { headers });
    }

    if (path === '/api/products' && request.method === 'POST') {
      const data = await request.json();
      const products = await env.SUB_KV.get('products', { type: 'json' }) || [];
      
      const newProduct = {
        id: Date.now().toString(),
        name: data.name,
        categoryId: data.categoryId,
        registerDate: data.registerDate,
        renewDate: data.renewDate,
        hostUrl: data.hostUrl,
        remark: data.remark,
        createdAt: new Date().toISOString()
      };
      
      products.push(newProduct);
      await env.SUB_KV.put('products', JSON.stringify(products));
      
      return new Response(JSON.stringify(newProduct), { headers });
    }

    if (path.match(/^\/api\/products\/[^\/]+$/) && request.method === 'GET') {
      const id = path.split('/').pop();
      const products = await env.SUB_KV.get('products', { type: 'json' }) || [];
      const product = products.find(p => p.id === id);
      
      if (!product) {
        return new Response(JSON.stringify({ error: '未找到' }), { status: 404, headers });
      }
      
      return new Response(JSON.stringify(product), { headers });
    }

    if (path.match(/^\/api\/products\/[^\/]+$/) && request.method === 'PUT') {
      const id = path.split('/').pop();
      const data = await request.json();
      const products = await env.SUB_KV.get('products', { type: 'json' }) || [];
      
      const index = products.findIndex(p => p.id === id);
      if (index === -1) {
        return new Response(JSON.stringify({ error: '未找到' }), { status: 404, headers });
      }
      
      products[index] = { ...products[index], ...data, updatedAt: new Date().toISOString() };
      await env.SUB_KV.put('products', JSON.stringify(products));
      
      return new Response(JSON.stringify(products[index]), { headers });
    }

    if (path.match(/^\/api\/products\/[^\/]+$/) && request.method === 'DELETE') {
      const id = path.split('/').pop();
      const products = await env.SUB_KV.get('products', { type: 'json' }) || [];
      
      const filtered = products.filter(p => p.id !== id);
      await env.SUB_KV.put('products', JSON.stringify(filtered));
      
      return new Response(JSON.stringify({ success: true }), { headers });
    }

    if (path === '/api/products/batch-delete' && request.method === 'POST') {
      const { ids } = await request.json();
      const products = await env.SUB_KV.get('products', { type: 'json' }) || [];
      
      const filtered = products.filter(p => !ids.includes(p.id));
      await env.SUB_KV.put('products', JSON.stringify(filtered));
      
      return new Response(JSON.stringify({ success: true }), { headers });
    }

    // 备份管理
    if (path === '/api/backups' && request.method === 'GET') {
      const backups = await env.SUB_KV.get('backups', { type: 'json' }) || [];
      return new Response(JSON.stringify(backups), { headers });
    }

    if (path === '/api/backups' && request.method === 'POST') {
      const categories = await env.SUB_KV.get('categories', { type: 'json' }) || [];
      const products = await env.SUB_KV.get('products', { type: 'json' }) || [];
      const backups = await env.SUB_KV.get('backups', { type: 'json' }) || [];
      
      const backupId = Date.now().toString();
      const newBackup = {
        id: backupId,
        createdAt: new Date().toISOString(),
        categories: categories,
        products: products,
        categoriesCount: categories.length,
        productsCount: products.length
      };
      
      // 保存备份数据
      await env.SUB_KV.put(`backup_${backupId}`, JSON.stringify(newBackup));
      
      // 添加备份记录
      backups.push({
        id: backupId,
        createdAt: newBackup.createdAt,
        categoriesCount: categories.length,
        productsCount: products.length
      });
      
      // 保留最多 5 个备份,删除最旧的
      while (backups.length > 5) {
        const oldest = backups.shift();
        await env.SUB_KV.delete(`backup_${oldest.id}`);
      }
      
      await env.SUB_KV.put('backups', JSON.stringify(backups));
      
      return new Response(JSON.stringify({ success: true, backupId }), { headers });
    }

    if (path.match(/^\/api\/backups\/[^\/]+\/restore$/) && request.method === 'POST') {
      const id = path.split('/')[3];
      const backupData = await env.SUB_KV.get(`backup_${id}`, { type: 'json' });
      
      if (!backupData) {
        return new Response(JSON.stringify({ success: false, message: '备份不存在' }), { headers });
      }
      
      // 恢复数据
      await env.SUB_KV.put('categories', JSON.stringify(backupData.categories || []));
      await env.SUB_KV.put('products', JSON.stringify(backupData.products || []));
      
      return new Response(JSON.stringify({ success: true }), { headers });
    }

    if (path.match(/^\/api\/backups\/[^\/]+$/) && request.method === 'DELETE') {
      const id = path.split('/').pop();
      const backups = await env.SUB_KV.get('backups', { type: 'json' }) || [];
      
      const filtered = backups.filter(b => b.id !== id);
      await env.SUB_KV.put('backups', JSON.stringify(filtered));
      await env.SUB_KV.delete(`backup_${id}`);
      
      return new Response(JSON.stringify({ success: true }), { headers });
    }

    return new Response(JSON.stringify({ error: '未知接口' }), { status: 404, headers });

  } catch (error) {
    return new Response(JSON.stringify({ error: error.message }), { status: 500, headers });
  }
}

// 检查 IP 速率限制
async function checkRateLimit(env, ip) {
  const key = `login_attempt_${ip}`;
  const data = await env.SUB_KV.get(key, { type: 'json' });
  
  if (!data) {
    return { allowed: true };
  }
  
  // 检查是否处于限制期
  if (data.locked && data.unlockedAt) {
    if (Date.now() < data.unlockedAt) {
      return { 
        allowed: false, 
        unlockedAt: data.unlockedAt 
      };
    } else {
      // 限制期已过,清除记录
      await env.SUB_KV.delete(key);
      return { allowed: true };
    }
  }
  
  return { allowed: true };
}

// 记录登录尝试
async function recordLoginAttempt(env, ip) {
  const key = `login_attempt_${ip}`;
  const data = await env.SUB_KV.get(key, { type: 'json' }) || { attempts: 0, locked: false };
  
  data.attempts = (data.attempts || 0) + 1;
  
  // 计算剩余尝试次数
  const remainingAttempts = Math.max(0, 10 - data.attempts);
  
  // 达到 10 次失败,锁定 24 小时
  if (data.attempts >= 10) {
    data.locked = true;
    data.unlockedAt = Date.now() + (24 * 60 * 60 * 1000); // 24 小时后解锁
  }
  
  // 设置过期时间(24 小时)
  await env.SUB_KV.put(key, JSON.stringify(data), { expirationTtl: 24 * 60 * 60 });
  
  return {
    attempts: data.attempts,
    remainingAttempts: remainingAttempts,
    locked: data.locked
  };
}

留言

暂无留言

0 / 100