// ============================================================
// 续命项目 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 = `
`;
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);
}
}
};