// ============================================================ // 续命项目 v2.0 - API Worker(精简版,不含前端HTML) // ============================================================ // ============================================================ // 工具函数 // ============================================================ async function hashPassword(password, salt) { const encoder = new TextEncoder(); const data = encoder.encode(password + salt); const hash = await crypto.subtle.digest('SHA-256', data); return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join(''); } function generateRandom(length) { const arr = new Uint8Array(length); crypto.getRandomValues(arr); return Array.from(arr).map(b => b.toString(16).padStart(2, '0')).join(''); } function generateRecoveryCode() { const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; let code = ''; for (let i = 0; i < 12; i++) { code += chars[Math.floor(Math.random() * chars.length)]; if (i === 3 || i === 7) code += '-'; } return code; } function jsonResponse(data, status = 200, token = null) { const headers = { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': 'https://pub-cc89fa4aa8f64bedb0b11ecf843d7215.r2.dev', 'Access-Control-Allow-Credentials': 'true' }; if (token) { headers['Set-Cookie'] = 'token=' + token + '; Path=/; Max-Age=2592000; SameSite=Lax'; data.token = token; } return new Response(JSON.stringify(data), { status, headers }); } function parseCookies(cookieHeader) { if (!cookieHeader) return {}; const result = {}; cookieHeader.split('; ').filter(c => c).forEach(c => { const [key, ...rest] = c.split('='); result[key] = rest.join('='); }); return result; } function isValidEmail(email) { return email && email.includes('@') && email.includes('.'); } function extractToken(request) { const cookieHeader = request.headers.get('Cookie') || ''; const cookies = parseCookies(cookieHeader); if (cookies.token) return cookies.token; const authHeader = request.headers.get('Authorization'); if (authHeader && authHeader.startsWith('Bearer ')) { return authHeader.substring(7); } return null; } function formatBytes(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } function escapeHtml(text) { if (!text) return ''; return text.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); } // ============================================================ // 邮件类 // ============================================================ class Email { constructor(env) { this.env = env; this.apiKey = env.MAIL_API_KEY; this.from = env.MAIL_FROM || 'noreply@xuming.com'; } async send(to, subject, html, attachments = []) { if (!this.apiKey) { console.error('MAIL_API_KEY 未配置'); return { success: false, error: '邮件服务未配置' }; } try { const payload = { from: this.from, to: to, subject: subject, html: html, attachments: attachments.map(a => ({ filename: a.filename || 'file', content: a.content })) }; console.log('调用 Resend API,收件人:', to); const response = await fetch('https://api.resend.com/emails', { method: 'POST', headers: { 'Authorization': 'Bearer ' + this.apiKey, 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); console.log('Resend 响应状态:', response.status); const result = await response.json(); console.log('Resend 响应体:', result); if (response.ok) { return { success: true, id: result.id }; } else { console.error('邮件发送失败:', result); return { success: false, error: result.message || '邮件发送失败' }; } } catch (error) { console.error('邮件发送异常:', error); return { success: false, error: error.message }; } } async sendDataEmail(to, dataItems, attachments = []) { if (!to || dataItems.length === 0) { return { success: false, error: '收件人或数据为空' }; } let html = `

📬 您有一份数据待查收

此邮件由「续命」系统自动发送,共 ${dataItems.length} 条数据

`; for (const item of dataItems) { html += `

${escapeHtml(item.title || '(无标题)')}

${escapeHtml(item.content || '(无内容)')}

${item.createdAt ? '

创建时间:' + new Date(item.createdAt).toLocaleString('zh-CN') + '

' : ''} ${item.hasFile ? '

📎 包含附件(见邮件附件)

' : ''}
`; } html += ` `; const subject = '📬 [续命] 您有一份数据待查收(' + dataItems.length + ' 条)'; return await this.send(to, subject, html, attachments); } async prepareAttachments(files) { const attachments = []; let totalSize = 0; for (const file of files) { let data = file.data; let size = data.byteLength || data.length; let name = file.name || 'file'; if (size > 5 * 1024 * 1024) { console.log('附件 ' + name + ' 过大 (' + formatBytes(size) + '),跳过'); continue; } if (totalSize + size > 5 * 1024 * 1024) { console.log('附件总大小超过5MB,停止添加'); break; } attachments.push({ filename: name, content: data }); totalSize += size; } return attachments; } } // ============================================================ // 加密类 // ============================================================ class Encryption { constructor(env) { this.env = env; } async getSalt(userId) { const r = await this.env.DB.prepare('SELECT encryption_salt FROM user_settings WHERE user_id = ?').bind(userId).first(); return r ? r.encryption_salt : null; } async encrypt(text, userId) { if (!text) return ''; const salt = await this.getSalt(userId); if (!salt) throw new Error('加密盐值不存在'); const encoder = new TextEncoder(); const keyMaterial = encoder.encode((this.env.ENCRYPTION_MASTER_KEY || 'default') + salt); const keyHash = await crypto.subtle.digest('SHA-256', keyMaterial); const key = await crypto.subtle.importKey('raw', keyHash, { name: 'AES-CBC' }, false, ['encrypt']); const iv = crypto.getRandomValues(new Uint8Array(16)); const data = encoder.encode(text); const encrypted = await crypto.subtle.encrypt({ name: 'AES-CBC', iv }, key, data); const combined = new Uint8Array(iv.length + encrypted.byteLength); combined.set(iv); combined.set(new Uint8Array(encrypted), iv.length); return btoa(String.fromCharCode(...combined)); } async decrypt(encryptedBase64, userId) { if (!encryptedBase64) return ''; try { const salt = await this.getSalt(userId); if (!salt) throw new Error('加密盐值不存在'); const combined = Uint8Array.from(atob(encryptedBase64), c => c.charCodeAt(0)); const iv = combined.slice(0, 16); const encrypted = combined.slice(16); const encoder = new TextEncoder(); const keyMaterial = encoder.encode((this.env.ENCRYPTION_MASTER_KEY || 'default') + salt); const keyHash = await crypto.subtle.digest('SHA-256', keyMaterial); const key = await crypto.subtle.importKey('raw', keyHash, { name: 'AES-CBC' }, false, ['decrypt']); const decrypted = await crypto.subtle.decrypt({ name: 'AES-CBC', iv }, key, encrypted); return new TextDecoder().decode(decrypted); } catch (e) { return '[解密失败]'; } } } // ============================================================ // 认证类 // ============================================================ class Auth { constructor(env) { this.env = env; } async register(request) { try { const { email, password } = await request.json(); if (!email || !password) return jsonResponse({ error: '邮箱和密码不能为空' }, 400); if (password.length < 8) return jsonResponse({ error: '密码至少8位' }, 400); if (!isValidEmail(email)) return jsonResponse({ error: '邮箱格式不正确' }, 400); const count = await this.env.DB.prepare('SELECT COUNT(*) as c FROM users WHERE is_active = 1').first(); if (count.c >= 50) return jsonResponse({ error: '注册已满(50人上限)' }, 403); const exist = await this.env.DB.prepare('SELECT id FROM users WHERE email = ?').bind(email).first(); if (exist) return jsonResponse({ error: '该邮箱已注册' }, 409); const salt = generateRandom(16); const passwordHash = await hashPassword(password, salt); const recoveryCode = generateRecoveryCode(); const recoveryHash = await hashPassword(recoveryCode, salt); const userId = crypto.randomUUID(); const now = Date.now(); await this.env.DB.prepare( 'INSERT INTO users (id, email, password_hash, salt, recovery_code_hash, last_renew_at, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)' ).bind(userId, email, passwordHash, salt, recoveryHash, now, now, now).run(); const encryptionSalt = generateRandom(32); await this.env.DB.prepare( 'INSERT INTO user_settings (user_id, encryption_salt, updated_at) VALUES (?, ?, ?)' ).bind(userId, encryptionSalt, now).run(); const token = await this.generateToken(userId); return jsonResponse({ success: true, userId, email, recoveryCode, message: '注册成功' }, 201, token); } catch (e) { return jsonResponse({ error: '注册失败:' + e.message }, 500); } } async login(request) { try { const { email, password } = await request.json(); if (!email || !password) return jsonResponse({ error: '邮箱和密码不能为空' }, 400); const user = await this.env.DB.prepare( 'SELECT id, password_hash, salt, is_active, is_readonly FROM users WHERE email = ?' ).bind(email).first(); if (!user || user.is_active === 0) return jsonResponse({ error: '邮箱或密码错误' }, 401); if (await hashPassword(password, user.salt) !== user.password_hash) { return jsonResponse({ error: '邮箱或密码错误' }, 401); } const now = Date.now(); await this.env.DB.prepare('UPDATE users SET last_renew_at = ?, updated_at = ? WHERE id = ?') .bind(now, now, user.id).run(); const token = await this.generateToken(user.id); return jsonResponse({ success: true, userId: user.id, email, isReadonly: user.is_readonly === 1 }, 200, token); } catch (e) { return jsonResponse({ error: '登录失败:' + e.message }, 500); } } async logout() { const headers = { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': 'https://pub-cc89fa4aa8f64bedb0b11ecf843d7215.r2.dev', 'Access-Control-Allow-Credentials': 'true', 'Set-Cookie': 'token=; HttpOnly; Path=/; Max-Age=0; SameSite=Lax' }; return new Response(JSON.stringify({ success: true }), { status: 200, headers }); } async verifyToken(token) { if (!token) return null; try { const parts = token.split('.'); if (parts.length !== 3) return null; const payload = JSON.parse(atob(parts[1])); if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) return null; const user = await this.env.DB.prepare('SELECT id, is_active FROM users WHERE id = ?').bind(payload.userId).first(); if (!user || user.is_active === 0) return null; return { userId: payload.userId }; } catch { return null; } } async checkReadonly(userId) { const r = await this.env.DB.prepare('SELECT is_readonly FROM users WHERE id = ?').bind(userId).first(); return r ? r.is_readonly === 1 : false; } async generateToken(userId) { const payload = { userId, exp: Math.floor(Date.now() / 1000) + (30 * 24 * 60 * 60) }; const encoded = btoa(JSON.stringify(payload)); const encoder = new TextEncoder(); const hash = await crypto.subtle.digest('SHA-256', encoder.encode(encoded + (this.env.JWT_SECRET || 'default'))); const signature = btoa(String.fromCharCode(...new Uint8Array(hash))).slice(0, 32); return 'xuming.' + encoded + '.' + signature; } } // ============================================================ // 续命类 // ============================================================ class Renew { constructor(env) { this.env = env; } async renew(userId, request) { try { const { password } = await request.json(); if (!password) return jsonResponse({ error: '请输入密码验证' }, 400); const user = await this.env.DB.prepare('SELECT salt, password_hash FROM users WHERE id = ?').bind(userId).first(); if (!user) return jsonResponse({ error: '用户不存在' }, 404); if (await hashPassword(password, user.salt) !== user.password_hash) { return jsonResponse({ error: '密码错误' }, 401); } const now = Date.now(); await this.env.DB.prepare('UPDATE users SET last_renew_at = ?, is_readonly = 0, updated_at = ? WHERE id = ?') .bind(now, now, userId).run(); const threshold = await this.getThreshold(userId); return jsonResponse({ success: true, message: '续命成功,已延长 ' + threshold + ' 天', remainingDays: threshold }); } catch (e) { return jsonResponse({ error: '续命失败' }, 500); } } async updateThreshold(userId, request) { try { const { days } = await request.json(); if (!days || days < 1 || days > 15) return jsonResponse({ error: '阈值必须在1~15天' }, 400); const now = Date.now(); await this.env.DB.prepare( 'UPDATE users SET renew_threshold_days = ?, last_renew_at = ?, is_readonly = 0, updated_at = ? WHERE id = ?' ).bind(days, now, now, userId).run(); return jsonResponse({ success: true, message: '阈值已改为 ' + days + ' 天,已自动续命', threshold: days }); } catch (e) { return jsonResponse({ error: '修改阈值失败' }, 500); } } async getStatus(userId) { try { const user = await this.env.DB.prepare( 'SELECT last_renew_at, renew_threshold_days, is_readonly FROM users WHERE id = ?' ).bind(userId).first(); if (!user) return jsonResponse({ error: '用户不存在' }, 404); const remainingMs = (user.last_renew_at + user.renew_threshold_days * 86400000) - Date.now(); const remainingDays = Math.ceil(remainingMs / 86400000); const isExpired = remainingDays <= 0; if (isExpired && user.is_readonly === 0) { await this.env.DB.prepare('UPDATE users SET is_readonly = 1 WHERE id = ?').bind(userId).run(); } return jsonResponse({ success: true, data: { remainingDays: isExpired ? 0 : remainingDays, isExpired: isExpired, isReadonly: isExpired ? 1 : user.is_readonly, thresholdDays: user.renew_threshold_days, lastRenewAt: user.last_renew_at } }); } catch (e) { return jsonResponse({ error: '获取续命状态失败' }, 500); } } async getThreshold(userId) { const r = await this.env.DB.prepare('SELECT renew_threshold_days FROM users WHERE id = ?').bind(userId).first(); return r ? r.renew_threshold_days : 2; } } // ============================================================ // 用户类 // ============================================================ class User { constructor(env) { this.env = env; } async getProfile(userId) { try { const user = await this.env.DB.prepare( 'SELECT id, email, storage_quota, storage_used, last_renew_at, renew_threshold_days, is_readonly FROM users WHERE id = ?' ).bind(userId).first(); if (!user) return jsonResponse({ error: '用户不存在' }, 404); const remainingMs = (user.last_renew_at + user.renew_threshold_days * 86400000) - Date.now(); const remainingDays = Math.ceil(remainingMs / 86400000); const usagePercent = user.storage_quota > 0 ? Math.round((user.storage_used / user.storage_quota) * 100) : 0; return jsonResponse({ success: true, data: { id: user.id, email: user.email, storageQuota: user.storage_quota, storageUsed: user.storage_used, storageUsagePercent: usagePercent, renewThresholdDays: user.renew_threshold_days, isReadonly: user.is_readonly === 1, remainingDays: remainingDays, isExpired: remainingDays <= 0, lastRenewAt: user.last_renew_at } }); } catch (e) { return jsonResponse({ error: '获取资料失败' }, 500); } } async changePassword(userId, request) { try { const { oldPassword, newPassword } = await request.json(); if (!oldPassword || !newPassword) return jsonResponse({ error: '旧密码和新密码不能为空' }, 400); if (newPassword.length < 8) return jsonResponse({ error: '新密码至少8位' }, 400); const user = await this.env.DB.prepare('SELECT salt, password_hash FROM users WHERE id = ?').bind(userId).first(); if (!user) return jsonResponse({ error: '用户不存在' }, 404); if (await hashPassword(oldPassword, user.salt) !== user.password_hash) { return jsonResponse({ error: '旧密码错误' }, 401); } const newSalt = generateRandom(16); const newHash = await hashPassword(newPassword, newSalt); await this.env.DB.prepare('UPDATE users SET password_hash = ?, salt = ?, updated_at = ? WHERE id = ?') .bind(newHash, newSalt, Date.now(), userId).run(); return jsonResponse({ success: true, message: '密码修改成功' }); } catch (e) { return jsonResponse({ error: '修改密码失败' }, 500); } } async regenerateRecoveryCode(userId) { try { const user = await this.env.DB.prepare('SELECT salt FROM users WHERE id = ?').bind(userId).first(); if (!user) return jsonResponse({ error: '用户不存在' }, 404); const recoveryCode = generateRecoveryCode(); const recoveryHash = await hashPassword(recoveryCode, user.salt); await this.env.DB.prepare('UPDATE users SET recovery_code_hash = ?, updated_at = ? WHERE id = ?') .bind(recoveryHash, Date.now(), userId).run(); return jsonResponse({ success: true, recoveryCode: recoveryCode, message: '恢复码已重新生成' }); } catch (e) { return jsonResponse({ error: '重新生成恢复码失败' }, 500); } } async deleteAccount(userId, request) { try { const { password } = await request.json(); if (!password) return jsonResponse({ error: '请输入密码确认' }, 400); const user = await this.env.DB.prepare('SELECT salt, password_hash FROM users WHERE id = ?').bind(userId).first(); if (!user) return jsonResponse({ error: '用户不存在' }, 404); if (await hashPassword(password, user.salt) !== user.password_hash) { return jsonResponse({ error: '密码错误' }, 401); } const records = await this.env.DB.prepare('SELECT file_key FROM data_records WHERE user_id = ? AND file_key IS NOT NULL').bind(userId).all(); for (const r of records.results) { try { await this.env.STORAGE.delete(r.file_key); } catch {} } await this.env.DB.prepare('DELETE FROM data_records WHERE user_id = ?').bind(userId).run(); await this.env.DB.prepare('DELETE FROM user_settings WHERE user_id = ?').bind(userId).run(); await this.env.DB.prepare('DELETE FROM users WHERE id = ?').bind(userId).run(); const headers = { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': 'https://pub-cc89fa4aa8f64bedb0b11ecf843d7215.r2.dev', 'Access-Control-Allow-Credentials': 'true', 'Set-Cookie': 'token=; HttpOnly; Path=/; Max-Age=0; SameSite=Lax' }; return new Response(JSON.stringify({ success: true, message: '账号已注销' }), { status: 200, headers }); } catch (e) { return jsonResponse({ error: '注销失败' }, 500); } } } // ============================================================ // 数据类 // ============================================================ class Data { constructor(env) { this.env = env; this.enc = new Encryption(env); } async create(userId, request) { try { const { title, content, recipientEmail } = await request.json(); if (!title || !content) return jsonResponse({ error: '标题和内容不能为空' }, 400); if (recipientEmail && !isValidEmail(recipientEmail)) return jsonResponse({ error: '邮箱格式不正确' }, 400); const quota = await this.getQuota(userId); if (quota.storageUsed >= quota.storageQuota) return jsonResponse({ error: '存储空间已满(50MB)' }, 413); const id = crypto.randomUUID(); const now = Date.now(); await this.env.DB.prepare( 'INSERT INTO data_records (id, user_id, title, content, recipient_email, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)' ).bind(id, userId, await this.enc.encrypt(title, userId), await this.enc.encrypt(content, userId), recipientEmail || null, now, now).run(); return jsonResponse({ success: true, id: id, message: '创建成功' }); } catch (e) { return jsonResponse({ error: '创建数据失败' }, 500); } } async list(userId, request) { try { const url = new URL(request.url); const sort = url.searchParams.get('sort') || 'created_desc'; const filter = url.searchParams.get('filter') || 'all'; let sql = 'SELECT id, title, file_key, file_name, file_size, recipient_email, is_sent, created_at, updated_at FROM data_records WHERE user_id = ? AND is_deleted = 0'; const params = [userId]; if (filter === 'sent') sql += ' AND is_sent = 1'; else if (filter === 'unsent') sql += ' AND is_sent = 0'; else if (filter === 'has_file') sql += ' AND file_key IS NOT NULL'; else if (filter === 'no_file') sql += ' AND file_key IS NULL'; const sortMap = { 'created_asc': 'created_at ASC', 'created_desc': 'created_at DESC', 'updated_asc': 'updated_at ASC', 'updated_desc': 'updated_at DESC', 'title_asc': 'title ASC', 'title_desc': 'title DESC', 'sent_first': 'is_sent ASC, created_at DESC' }; sql += ' ORDER BY ' + (sortMap[sort] || 'created_at DESC'); const result = await this.env.DB.prepare(sql).bind(...params).all(); const records = []; for (const row of result.results) { records.push({ id: row.id, title: await this.enc.decrypt(row.title, userId), hasFile: !!row.file_key, fileName: row.file_name, fileSize: row.file_size, recipientEmail: row.recipient_email, isSent: row.is_sent === 1, createdAt: row.created_at, updatedAt: row.updated_at }); } const search = url.searchParams.get('search') || ''; let filtered = records; if (search) filtered = records.filter(r => r.title.includes(search)); return jsonResponse({ success: true, data: filtered, total: filtered.length }); } catch (e) { return jsonResponse({ error: '获取列表失败' }, 500); } } async get(userId, id) { try { const row = await this.env.DB.prepare( 'SELECT id, title, content, file_key, file_name, file_size, recipient_email, is_sent, sent_at, created_at, updated_at FROM data_records WHERE id = ? AND user_id = ? AND is_deleted = 0' ).bind(id, userId).first(); if (!row) return jsonResponse({ error: '数据不存在' }, 404); return jsonResponse({ success: true, data: { id: row.id, title: await this.enc.decrypt(row.title, userId), content: await this.enc.decrypt(row.content, userId), hasFile: !!row.file_key, fileKey: row.file_key, fileName: row.file_name, fileSize: row.file_size, recipientEmail: row.recipient_email, isSent: row.is_sent === 1, sentAt: row.sent_at, createdAt: row.created_at, updatedAt: row.updated_at } }); } catch (e) { return jsonResponse({ error: '获取数据失败' }, 500); } } async update(userId, id, request) { try { const { title, content, recipientEmail } = await request.json(); const existing = await this.env.DB.prepare('SELECT id FROM data_records WHERE id = ? AND user_id = ? AND is_deleted = 0').bind(id, userId).first(); if (!existing) return jsonResponse({ error: '数据不存在' }, 404); if (recipientEmail && !isValidEmail(recipientEmail)) return jsonResponse({ error: '邮箱格式不正确' }, 400); let sql = 'UPDATE data_records SET updated_at = ?'; const params = [Date.now()]; if (title !== undefined) { sql += ', title = ?'; params.push(await this.enc.encrypt(title, userId)); } if (content !== undefined) { sql += ', content = ?'; params.push(await this.enc.encrypt(content, userId)); } if (recipientEmail !== undefined) { sql += ', recipient_email = ?'; params.push(recipientEmail || null); } sql += ' WHERE id = ? AND user_id = ?'; params.push(id, userId); await this.env.DB.prepare(sql).bind(...params).run(); return jsonResponse({ success: true, message: '更新成功' }); } catch (e) { return jsonResponse({ error: '更新数据失败' }, 500); } } async delete(userId, id) { try { const row = await this.env.DB.prepare('SELECT file_key, file_size FROM data_records WHERE id = ? AND user_id = ? AND is_deleted = 0').bind(id, userId).first(); if (!row) return jsonResponse({ error: '数据不存在' }, 404); if (row.file_key) { try { await this.env.STORAGE.delete(row.file_key); } catch {} if (row.file_size) { await this.env.DB.prepare('UPDATE users SET storage_used = storage_used - ? WHERE id = ?').bind(row.file_size, userId).run(); } } await this.env.DB.prepare('UPDATE data_records SET is_deleted = 1, updated_at = ? WHERE id = ? AND user_id = ?').bind(Date.now(), id, userId).run(); return jsonResponse({ success: true, message: '删除成功' }); } catch (e) { return jsonResponse({ error: '删除数据失败' }, 500); } } async upload(userId, request) { try { const formData = await request.formData(); const file = formData.get('file'); if (!file) return jsonResponse({ error: '请选择文件' }, 400); const fileSize = file.size; const fileName = file.name; const quota = await this.getQuota(userId); if (quota.storageUsed + fileSize > quota.storageQuota) { return jsonResponse({ error: '存储空间不足(已用 ' + Math.round(quota.storageUsed/1024/1024*100)/100 + 'MB / 50MB)' }, 413); } const fileKey = 'users/' + userId + '/' + Date.now() + '_' + fileName.replace(/[^a-zA-Z0-9.\-_\u4e00-\u9fa5]/g, '_'); await this.env.STORAGE.put(fileKey, file.stream(), { httpMetadata: { contentType: file.type || 'application/octet-stream', contentDisposition: 'attachment; filename="' + encodeURIComponent(fileName) + '"' } }); await this.env.DB.prepare('UPDATE users SET storage_used = storage_used + ? WHERE id = ?').bind(fileSize, userId).run(); return jsonResponse({ success: true, fileKey: fileKey, fileName: fileName, fileSize: fileSize, message: '上传成功' }); } catch (e) { return jsonResponse({ error: '上传文件失败' }, 500); } } async resetSent(userId, request) { try { const { id } = await request.json(); if (!id) return jsonResponse({ error: '请指定数据ID' }, 400); const existing = await this.env.DB.prepare('SELECT id FROM data_records WHERE id = ? AND user_id = ? AND is_deleted = 0').bind(id, userId).first(); if (!existing) return jsonResponse({ error: '数据不存在' }, 404); await this.env.DB.prepare('UPDATE data_records SET is_sent = 0, sent_at = NULL, updated_at = ? WHERE id = ? AND user_id = ?').bind(Date.now(), id, userId).run(); return jsonResponse({ success: true, message: '发送状态已重置' }); } catch (e) { return jsonResponse({ error: '重置发送状态失败' }, 500); } } async getQuota(userId) { const r = await this.env.DB.prepare('SELECT storage_quota, storage_used FROM users WHERE id = ?').bind(userId).first(); return { storageQuota: r ? r.storage_quota : 52428800, storageUsed: r ? r.storage_used : 0 }; } } // ============================================================ // 邮件发送函数 // ============================================================ async function checkExpiredUsersAndSend(env) { console.log('=== checkExpiredUsersAndSend 被调用 ==='); const email = new Email(env); const encryption = new Encryption(env); console.log('=== 准备查询数据库 ==='); const users = await env.DB.prepare( 'SELECT id, email FROM users WHERE is_readonly = 1 AND is_active = 1' ).all(); console.log('=== 查询完成,找到 ' + users.results.length + ' 个只读用户 ==='); console.log('找到 ' + users.results.length + ' 个只读用户'); for (const user of users.results) { try { const records = await env.DB.prepare( 'SELECT id, title, content, file_key, file_name, created_at, recipient_email FROM data_records WHERE user_id = ? AND is_deleted = 0 AND is_sent = 0 AND recipient_email IS NOT NULL AND recipient_email != \'\'' ).bind(user.id).all(); if (records.results.length === 0) continue; console.log('用户 ' + user.email + ' 有 ' + records.results.length + ' 条未发送数据'); console.log('=== 查询到的数据条数: ' + records.results.length); const decryptedItems = []; const attachments = []; for (const record of records.results) { console.log('=== 开始解密记录 ' + record.id + ' ==='); try { const title = await encryption.decrypt(record.title, user.id); console.log('=== 标题解密成功: ' + title + ' ==='); const content = await encryption.decrypt(record.content || '', user.id); console.log('=== 内容解密成功 ==='); decryptedItems.push({ id: record.id, title: title || '(无标题)', content: content || '(无内容)', hasFile: !!record.file_key, createdAt: record.created_at }); if (record.file_key) { try { const fileData = await env.STORAGE.get(record.file_key); if (fileData) { const arrayBuffer = await fileData.arrayBuffer(); attachments.push({ name: record.file_name || 'file', data: arrayBuffer }); } } catch (e) { console.error('获取文件失败 ' + record.file_key + ':', e); } } } catch (e) { console.error('解密记录 ' + record.id + ' 失败:', e); } } if (decryptedItems.length === 0) { console.log('=== decryptedItems 为空,跳过发送 ==='); continue; } console.log('=== 开始查找收件邮箱 ==='); const firstRecord = records.results.find(r => r.recipient_email); console.log('=== 找到收件邮箱: ' + (firstRecord ? firstRecord.recipient_email : '无') + ' ==='); if (!firstRecord) { console.log('=== 没有收件邮箱,跳过 ==='); continue; } const to = firstRecord.recipient_email; console.log('=== 准备发送邮件到 ' + to + ',数据 ' + decryptedItems.length + ' 条 ==='); const preparedAttachments = await email.prepareAttachments(attachments); console.log('=== 附件准备完成,共 ' + preparedAttachments.length + ' 个 ==='); console.log('准备发送邮件到 ' + to + ',数据 ' + decryptedItems.length + ' 条'); const result = await email.sendDataEmail(to, decryptedItems, preparedAttachments); console.log('邮件发送结果:', result); if (result.success) { const ids = records.results.map(r => r.id); const placeholders = ids.map(() => '?').join(','); await env.DB.prepare( 'UPDATE data_records SET is_sent = 1, sent_at = ? WHERE id IN (' + placeholders + ')' ).bind(Date.now(), ...ids).run(); console.log('✅ 已发送 ' + ids.length + ' 条数据到 ' + to); } else { console.error('❌ 发送失败 ' + user.email + ':', result.error); } } catch (error) { console.error('处理用户 ' + user.id + ' 失败:', error); } } } // ============================================================ // Worker 入口 // ============================================================ export default { async fetch(request, env, ctx) { const url = new URL(request.url); const path = url.pathname; const method = request.method; // OPTIONS 预检请求 - 必须返回正确的 CORS 头 if (method === 'OPTIONS') { return new Response(null, { headers: { 'Access-Control-Allow-Origin': 'https://pub-cc89fa4aa8f64bedb0b11ecf843d7215.r2.dev', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization', 'Access-Control-Allow-Credentials': 'true', 'Access-Control-Max-Age': '86400' } }); } const token = extractToken(request); const auth = new Auth(env); const user = new User(env); const renew = new Renew(env); const data = new Data(env); if (path === '/api/auth/register' && method === 'POST') return await auth.register(request); if (path === '/api/auth/login' && method === 'POST') return await auth.login(request); const payload = await auth.verifyToken(token); if (!payload) return jsonResponse({ error: '未登录或登录已过期' }, 401); const userId = payload.userId; const isReadonly = await auth.checkReadonly(userId); const isRenewApi = (path === '/api/renew' && method === 'POST') || (path === '/api/renew/threshold' && method === 'PUT'); if (!isRenewApi && isReadonly) { return jsonResponse({ error: '账号已过期,请续命后操作', readonly: true }, 403); } if (path === '/api/user/profile' && method === 'GET') return await user.getProfile(userId); if (path === '/api/user/password' && method === 'PUT') return await user.changePassword(userId, request); if (path === '/api/user/recovery' && method === 'POST') return await user.regenerateRecoveryCode(userId); if (path === '/api/user/delete' && method === 'DELETE') return await user.deleteAccount(userId, request); if (path === '/api/auth/logout' && method === 'POST') return await auth.logout(); if (path === '/api/renew' && method === 'POST') return await renew.renew(userId, request); if (path === '/api/renew/threshold' && method === 'PUT') return await renew.updateThreshold(userId, request); if (path === '/api/renew/status' && method === 'GET') return await renew.getStatus(userId); if (path === '/api/data' && method === 'GET') return await data.list(userId, request); if (path === '/api/data' && method === 'POST') return await data.create(userId, request); if (path === '/api/data/reset-sent' && method === 'POST') return await data.resetSent(userId, request); if (path === '/api/upload' && method === 'POST') return await data.upload(userId, request); const dataMatch = path.match(/^\/api\/data\/([^\/]+)$/); if (dataMatch) { const id = dataMatch[1]; if (method === 'GET') return await data.get(userId, id); if (method === 'PUT') return await data.update(userId, id, request); if (method === 'DELETE') return await data.delete(userId, id); } return jsonResponse({ error: '接口不存在' }, 404); }, // ============================================================ // 定时任务:每天凌晨 2:00 执行 // ============================================================ async scheduled(event, env, ctx) { console.log('Cron job started at', new Date().toISOString()); try { const threeMonthsAgo = Date.now() - 90 * 24 * 60 * 60 * 1000; await env.DB.prepare('DELETE FROM access_logs WHERE timestamp < ?').bind(threeMonthsAgo).run(); console.log('清理访问日志完成'); const fifteenMinsAgo = Date.now() - 15 * 60 * 1000; await env.DB.prepare('DELETE FROM login_failures WHERE timestamp < ?').bind(fifteenMinsAgo).run(); console.log('清理登录失败记录完成'); await checkExpiredUsersAndSend(env); console.log('Cron job completed at', new Date().toISOString()); } catch (error) { console.error('Cron job error:', error); } } };