当前位置: 首页 > news >正文

使用wrangler发布cf的workers项目

前言

想必大家都知道,在 cf 上可以免费搭建 pages 静态站点,比如我的博客 https://blog.qc7.org 就是在 github 上提交,然后在 cf 上部署的

但是有些人可能还不知道,cf 还提供了免费的数据库,对大部分个人用户来说,这个免费的额度已经足够使用了,在数据库的基础上,就可以实现动态的 web 服务了

与传统的 web 服务部同,cf 提供的基于 node 的 wrangler 开发部署模式,不支持传统的 php、java、net 等语言的动态 web 服务

本地安装wrangler

wrangler 是 cf 发布的一个 nodejs 组件包,在本地使用下面命令安装

npm install -g wrangler

全局包安装到 node 路径下的 node_modules 目录中,然后在 node 路径中添加命令脚本,对于非全局包,会安装到项目下的 node_modules 目录中

安装完毕后,在本地 cmd 窗口中运行 wrnagler 命令,显示它的帮助信息,表示安装成功

wrangler项目完整流程

控制台输入 wrangler init hello 一路回车创建一个默认的 hello 项目,最后两步可以选择 No,表示不使用 git 版本控制,不自动发布到 cf (使用手动发布)

创建完毕后,使用 vscode 打开项目,显示的目录结构以及构建脚本如下

命令行中输入 npm run dev 就可以在本地运行起来了,监听在 http://127.0.0.1:8787,使用浏览器输入 url 地址进行访问

默认 wrangler 创建的 example 的页面显示如下

使用 npm run deploy 命令可以将项目发布到 cf 中,初次发布的时候会弹出一个授权页面,在页面上登录你的 cf 账号授权即可

发布显示的信息如下,也可以通过日志中提示的域名进行访问 https://hello.?????.workers.dev/,这和正常的 web 服务没任何区别,还能自行绑定个人域名

创建数据库和表

cf 提供多种数据库,常见的有 D1(关系数据库,类似sqlite轻量的)、Workers KV(类似redis的键值存储)、R2(类似aws s3的对象存储),其他类型的一般用不到

官方这里有完整的指导文档 https://developers.cloudflare.com/d1/get-started/

通过以下命令创建一个类型为 d1 名为 prod-d1-tutorial 的数据库,前面已经完成授权,这里将在 cf 中创建数据库,并将配置添加到 wrangler.jsonc 文件中

生成配置中的 remote 字段为 true 表示使用远端的数据库

npx wrangler@latest d1 create prod-d1-tutorial

在前面创建好的数据库基础上,创建一个 Customers 表,并添加几条记录,数据库脚本如下

DROP TABLE IF EXISTS Customers;
CREATE TABLE IF NOT EXISTS Customers (CustomerId INTEGER PRIMARY KEY AUTOINCREMENT,CompanyName TEXT NOT NULL,ContactName TEXT NOT NULL,Email TEXT,Phone TEXT,CreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP
);INSERT INTO Customers (CompanyName, ContactName, Email, Phone) VALUES('Alfreds Futterkiste', 'Maria Anders', 'maria@alfreds.com', '123-456-7890'),('Around the Horn', 'Thomas Hardy', 'thomas@around.com', '123-456-7891'),('Bs Beverages', 'Victoria Ashworth', 'victoria@bsbeverages.com', '123-456-7892'),('Bs Beverages', 'Random Name', 'random@bsbeverages.com', '123-456-7893');

将脚本保存为 schema.sql,然后运行以下命令,这里指示为 --remote 表示在远程运行,如果本地运行,则使用 --local

npx wrangler d1 execute prod-d1-tutorial --remote --file=./schema.sql

上述 sql 执行后,可以通过以下命令查看 d1 中的数据内容

npx wrangler d1 execute prod-d1-tutorial --remote --command="SELECT * FROM Customers"` 

也可以在 cf 的 web 页面上查看数据记录

代码读写表记录

前面已经创建了数据库和表,并插入了一些数据,现在实现一个简单的页面,对数据库进行增删改用户数据,测试后将代码通过 npm run deploy 发布到 cf 中

发布的最终效果如下,可以实现新增记录、修改记录、删除记录

项目原有两个代码文件 public/index.htmlsrc/index.ts,修改后最终的代码如下

<!DOCTYPE html>
<html lang="zh-CN" data-theme="light">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>D1 CRM - 现代化客户管理系统</title><script src="https://unpkg.com/htmx.org@1.9.10"></script><script src="https://unpkg.com/hyperscript.org@0.9.11"></script><link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"><style>:root {--primary: #6366f1;--primary-dark: #4f46e5;--secondary: #f8fafc;--accent: #f59e0b;--text: #1e293b;--text-light: #64748b;--border: #e2e8f0;--success: #10b981;--warning: #f59e0b;--error: #ef4444;--shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1);--radius: 12px;--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);}[data-theme="dark"] {--primary: #818cf8;--primary-dark: #6366f1;--secondary: #1e293b;--accent: #fbbf24;--text: #f1f5f9;--text-light: #94a3b8;--border: #334155;--shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3);}* {margin: 0;padding: 0;box-sizing: border-box;}body {font-family: 'Inter', 'Segoe UI', system-ui, sans-serif;background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);min-height: 100vh;color: var(--text);line-height: 1.6;}.app-container {min-height: 100vh;display: flex;flex-direction: column;}/* Header */.header {background: rgba(255, 255, 255, 0.95);backdrop-filter: blur(20px);border-bottom: 1px solid var(--border);padding: 1rem 2rem;display: flex;justify-content: space-between;align-items: center;box-shadow: var(--shadow);}[data-theme="dark"] .header {background: rgba(30, 41, 59, 0.95);}.logo {display: flex;align-items: center;gap: 0.75rem;font-size: 1.5rem;font-weight: 700;color: var(--primary);}.theme-toggle {background: none;border: none;color: var(--text-light);cursor: pointer;padding: 0.5rem;border-radius: var(--radius);transition: var(--transition);}.theme-toggle:hover {color: var(--text);background: var(--secondary);}/* Main Content */.main-content {flex: 1;padding: 2rem;max-width: 1400px;margin: 0 auto;width: 100%;}.dashboard {display: grid;gap: 2rem;grid-template-columns: 1fr 400px;}@media (max-width: 1024px) {.dashboard {grid-template-columns: 1fr;}}/* Cards */.card {background: rgba(255, 255, 255, 0.95);backdrop-filter: blur(20px);border-radius: var(--radius);padding: 1.5rem;box-shadow: var(--shadow);border: 1px solid var(--border);transition: var(--transition);}[data-theme="dark"] .card {background: rgba(30, 41, 59, 0.95);}.card:hover {transform: translateY(-2px);box-shadow: 0 20px 40px -10px rgba(0, 0, 0, 0.15);}.card-header {display: flex;justify-content: space-between;align-items: center;margin-bottom: 1.5rem;}.card-title {font-size: 1.25rem;font-weight: 600;color: var(--text);}/* Form Styles */.form-grid {display: grid;gap: 1rem;grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));}.form-group {display: flex;flex-direction: column;gap: 0.5rem;}.form-label {font-weight: 500;color: var(--text);font-size: 0.875rem;}.form-input {padding: 0.75rem 1rem;border: 2px solid var(--border);border-radius: var(--radius);background: var(--secondary);color: var(--text);transition: var(--transition);font-size: 0.875rem;}.form-input:focus {outline: none;border-color: var(--primary);box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);}/* Button Styles */.btn {padding: 0.75rem 1.5rem;border: none;border-radius: var(--radius);font-weight: 500;font-size: 0.875rem;cursor: pointer;transition: var(--transition);display: inline-flex;align-items: center;gap: 0.5rem;text-decoration: none;}.btn-primary {background: var(--primary);color: white;}.btn-primary:hover {background: var(--primary-dark);transform: translateY(-1px);}.btn-success {background: var(--success);color: white;}.btn-warning {background: var(--warning);color: white;}.btn-danger {background: var(--error);color: white;}.btn-outline {background: transparent;border: 2px solid var(--border);color: var(--text);}.btn-sm {padding: 0.5rem 1rem;font-size: 0.75rem;}/* Table Styles */.table-container {overflow-x: auto;border-radius: var(--radius);}.data-table {width: 100%;border-collapse: collapse;background: var(--secondary);}.data-table th,.data-table td {padding: 1rem;text-align: left;border-bottom: 1px solid var(--border);}.data-table th {background: var(--secondary);font-weight: 600;font-size: 0.875rem;color: var(--text-light);text-transform: uppercase;letter-spacing: 0.05em;}.data-table tr:hover {background: rgba(99, 102, 241, 0.05);}.actions-cell {display: flex;gap: 0.5rem;}/* Status Indicators */.status-badge {padding: 0.25rem 0.75rem;border-radius: 20px;font-size: 0.75rem;font-weight: 500;}.status-active {background: rgba(16, 185, 129, 0.1);color: var(--success);}/* Loading States */.skeleton {background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);background-size: 200% 100%;animation: loading 1.5s infinite;border-radius: 4px;}@keyframes loading {0% { background-position: 200% 0; }100% { background-position: -200% 0; }}/* Toast Notifications */.toast-container {position: fixed;top: 2rem;right: 2rem;z-index: 1000;}.toast {background: var(--secondary);border-left: 4px solid var(--primary);padding: 1rem;border-radius: var(--radius);box-shadow: var(--shadow);margin-bottom: 1rem;animation: slideIn 0.3s ease-out;}@keyframes slideIn {from { transform: translateX(100%); opacity: 0; }to { transform: translateX(0); opacity: 1; }}/* Responsive */@media (max-width: 768px) {.main-content {padding: 1rem;}.header {padding: 1rem;}.form-grid {grid-template-columns: 1fr;}.actions-cell {flex-direction: column;}}</style>
</head>
<body><div class="app-container" id="app"><!-- Header --><header class="header"><div class="logo"><i class="fas fa-database"></i><span>D1 CRM</span></div><button class="theme-toggle" onclick="toggleTheme()"><i class="fas fa-moon"></i></button></header><!-- Main Content --><main class="main-content"><div class="dashboard"><!-- Customer Form --><div class="card"><div class="card-header"><h2 class="card-title" id="formTitle"><i class="fas fa-user-plus"></i>添加新客户</h2></div><form id="customerForm" _="on submit halt the event then call submitCustomerForm()"><input type="hidden" id="customerId"><div class="form-grid"><div class="form-group"><label class="form-label" for="CompanyName"><i class="fas fa-building"></i> 公司名称 *</label><input type="text" id="CompanyName" class="form-input" requiredplaceholder="输入公司名称"></div><div class="form-group"><label class="form-label" for="ContactName"><i class="fas fa-user"></i> 联系人 *</label><input type="text" id="ContactName" class="form-input" requiredplaceholder="输入联系人姓名"></div><div class="form-group"><label class="form-label" for="Email"><i class="fas fa-envelope"></i> 邮箱</label><input type="Email" id="Email" class="form-input"placeholder="contact@company.com"></div><div class="form-group"><label class="form-label" for="Phone"><i class="fas fa-Phone"></i> 电话</label><input type="tel" id="Phone" class="form-input"placeholder="+86 138 0000 0000"></div></div><div style="display: flex; gap: 1rem; margin-top: 1.5rem; flex-wrap: wrap;"><button type="submit" class="btn btn-primary" id="submitBtn"><i class="fas fa-plus"></i> 添加客户</button><button type="submit" class="btn btn-success" id="updateBtn" style="display: none;"><i class="fas fa-save"></i> 更新客户</button><button type="button" class="btn btn-outline" onclick="resetForm()"><i class="fas fa-times"></i> 取消编辑</button></div></form></div><!-- Quick Actions --><div class="card"><div class="card-header"><h2 class="card-title"><i class="fas fa-bolt"></i> 快速操作</h2></div><div style="display: flex; flex-direction: column; gap: 1rem;"><button class="btn btn-outline" onclick="loadCustomers()"><i class="fas fa-sync-alt"></i> 刷新数据</button><button class="btn btn-warning" onclick="exportData()"><i class="fas fa-download"></i> 导出数据</button><button class="btn btn-danger" onclick="confirmDeleteAll()"><i class="fas fa-trash"></i> 清空数据</button></div></div></div><!-- Customer Table --><div class="card" style="margin-top: 2rem;"><div class="card-header"><h2 class="card-title"><i class="fas fa-users"></i> 客户列表<span class="status-badge status-active" id="customerCount">加载中...</span></h2><div style="display: flex; gap: 1rem; align-items: center;"><div class="form-group" style="flex-direction: row; align-items: center;"><label class="form-label" for="search"><i class="fas fa-search"></i></label><input type="text" id="search" class="form-input"placeholder="搜索客户..." oninput="filterCustomers()"style="width: 200px;"></div></div></div><div class="table-container"><table class="data-table" id="customersTable"><thead><tr><th>ID</th><th>公司名称</th><th>联系人</th><th>邮箱</th><th>电话</th><th>创建时间</th><th>操作</th></tr></thead><tbody id="customersTableBody"><tr><td colspan="7" style="text-align: center; padding: 3rem;"><div class="skeleton" style="height: 20px; margin-bottom: 0.5rem;"></div><div class="skeleton" style="height: 20px; width: 60%; margin: 0 auto;"></div></td></tr></tbody></table></div></div></main><!-- Toast Container --><div class="toast-container" id="toastContainer"></div></div><script>// 现代化 JavaScript 代码class D1CRM {constructor() {this.currentEditingId = null;this.customers = [];this.init();}async init() {await this.loadCustomers();this.setupEventListeners();this.showToast('系统初始化完成', 'success');}setupEventListeners() {// 表单提交事件document.getElementById('customerForm').addEventListener('submit', (e) => {e.preventDefault();this.submitCustomerForm();});// 实时搜索document.getElementById('search').addEventListener('input', (e) => {this.filterCustomers(e.target.value);});}async loadCustomers() {try {this.showLoading();const response = await fetch('/api/customers');if (!response.ok) throw new Error('获取数据失败');const result = await response.json();if (result && result.success && Array.isArray(result.data)) {this.customers = result.data;} else {this.customers = [];console.warn('Unexpected API response format:', result);}this.renderCustomers();this.updateCustomerCount();} catch (error) {this.showToast(`加载失败: ${error.message}`, 'error');}}renderCustomers(customers = this.customers) {const tbody = document.getElementById('customersTableBody');if (customers.length === 0) {tbody.innerHTML = `<tr><td colspan="7" style="text-align: center; padding: 3rem; color: var(--text-light);"><i class="fas fa-inbox" style="font-size: 3rem; margin-bottom: 1rem; opacity: 0.5;"></i><div>暂无客户数据</div></td></tr>`;return;}tbody.innerHTML = customers.map(customer => `<tr><td>${customer.CustomerId}</td><td><div style="font-weight: 600;">${customer.CompanyName}</div></td><td>${customer.ContactName}</td><td>${customer.Email || '<span style="opacity: 0.5;">未设置</span>'}</td><td>${customer.Phone || '<span style="opacity: 0.5;">未设置</span>'}</td><td>${new Date(customer.CreatedAt).toLocaleDateString('zh-CN')}</td><td class="actions-cell"><button class="btn btn-outline btn-sm"onclick="app.editCustomer(${customer.CustomerId})"title="编辑"><i class="fas fa-edit"></i></button><button class="btn btn-danger btn-sm"onclick="app.deleteCustomer(${customer.CustomerId})"title="删除"><i class="fas fa-trash"></i></button></td></tr>`).join('');}async submitCustomerForm() {const formData = this.getFormData();if (!this.validateForm(formData)) return;try {const url = this.currentEditingId? `/api/customers/${this.currentEditingId}`: '/api/customers';const method = this.currentEditingId ? 'PUT' : 'POST';const response = await fetch(url, {method,headers: { 'Content-Type': 'application/json' },body: JSON.stringify(formData)});if (!response.ok) throw new Error('操作失败');const result = await response.json();this.showToast(this.currentEditingId ? '客户更新成功' : '客户添加成功','success');this.resetForm();await this.loadCustomers();} catch (error) {this.showToast(`操作失败: ${error.message}`, 'error');}}async editCustomer(id) {try {const response = await fetch(`/api/customers/${id}`);if (!response.ok) throw new Error('获取客户信息失败');const result = await response.json();if (!result || !result.success) {throw new Error('客户信息格式错误');}const customer = result.data;if (!customer || !customer.CompanyName) {throw new Error('客户数据不完整');}this.populateForm(customer);this.showToast(`正在编辑: ${customer.CompanyName}`, 'info');} catch (error) {this.showToast(`编辑失败: ${error.message}`, 'error');}}async deleteCustomer(id) {if (!confirm('确定要删除这个客户吗?此操作不可恢复!')) return;try {const response = await fetch(`/api/customers/${id}`, {method: 'DELETE'});if (!response.ok) throw new Error('删除失败');this.showToast('客户删除成功', 'success');await this.loadCustomers();} catch (error) {this.showToast(`删除失败: ${error.message}`, 'error');}}// 辅助方法getFormData() {return {CompanyName: document.getElementById('CompanyName').value.trim(),ContactName: document.getElementById('ContactName').value.trim(),Email: document.getElementById('Email').value.trim(),Phone: document.getElementById('Phone').value.trim()};}validateForm(data) {if (!data.CompanyName || !data.ContactName) {this.showToast('公司名称和联系人为必填项', 'warning');return false;}return true;}populateForm(customer) {document.getElementById('customerId').value = customer.CustomerId;document.getElementById('CompanyName').value = customer.CompanyName;document.getElementById('ContactName').value = customer.ContactName;document.getElementById('Email').value = customer.Email || '';document.getElementById('Phone').value = customer.Phone || '';this.currentEditingId = customer.CustomerId;document.getElementById('submitBtn').style.display = 'none';document.getElementById('updateBtn').style.display = 'inline-flex';document.getElementById('formTitle').innerHTML ='<i class="fas fa-edit"></i> 编辑客户';}resetForm() {document.getElementById('customerForm').reset();this.currentEditingId = null;document.getElementById('submitBtn').style.display = 'inline-flex';document.getElementById('updateBtn').style.display = 'none';document.getElementById('formTitle').innerHTML ='<i class="fas fa-user-plus"></i> 添加新客户';}filterCustomers(searchTerm = '') {const filtered = this.customers.filter(customer =>customer.CompanyName.toLowerCase().includes(searchTerm.toLowerCase()) ||customer.ContactName.toLowerCase().includes(searchTerm.toLowerCase()) ||(customer.Email && customer.Email.toLowerCase().includes(searchTerm.toLowerCase())));this.renderCustomers(filtered);}updateCustomerCount() {document.getElementById('customerCount').textContent =`${this.customers.length} 个客户`;}showLoading() {const tbody = document.getElementById('customersTableBody');tbody.innerHTML = `<tr><td colspan="7" style="text-align: center; padding: 2rem;"><i class="fas fa-spinner fa-spin" style="font-size: 2rem; color: var(--primary);"></i><div style="margin-top: 1rem; color: var(--text-light);">加载中...</div></td></tr>`;}showToast(message, type = 'info') {const toastContainer = document.getElementById('toastContainer');const toast = document.createElement('div');toast.className = 'toast';toast.innerHTML = `<div style="display: flex; align-items: center; gap: 0.5rem;"><i class="fas fa-${this.getToastIcon(type)}"style="color: var(--${type});"></i><span>${message}</span></div>`;toastContainer.appendChild(toast);setTimeout(() => {toast.remove();}, 3000);}getToastIcon(type) {const icons = {success: 'check-circle',error: 'exclamation-circle',warning: 'exclamation-triangle',info: 'info-circle'};return icons[type] || 'info-circle';}}// 全局函数function toggleTheme() {const currentTheme = document.documentElement.getAttribute('data-theme');const newTheme = currentTheme === 'dark' ? 'light' : 'dark';document.documentElement.setAttribute('data-theme', newTheme);const icon = document.querySelector('.theme-toggle i');icon.className = newTheme === 'dark' ? 'fas fa-sun' : 'fas fa-moon';localStorage.setItem('theme', newTheme);}function confirmDeleteAll() {if (!confirm('确定要删除所有客户数据吗?此操作不可恢复!')) return;app.deleteAllCustomers();}async function exportData() {// 简单的导出功能const data = JSON.stringify(app.customers, null, 2);const blob = new Blob([data], { type: 'application/json' });const url = URL.createObjectURL(blob);const a = document.createElement('a');a.href = url;a.download = `customers-${new Date().toISOString().split('T')[0]}.json`;a.click();URL.revokeObjectURL(url);app.showToast('数据导出成功', 'success');}// 初始化应用const app = new D1CRM();// 设置主题const savedTheme = localStorage.getItem('theme') || 'light';document.documentElement.setAttribute('data-theme', savedTheme);</script>
</body>
</html>
// 类型定义
interface Customer {CustomerId?: number;CompanyName: string;ContactName: string;Email?: string;Phone?: string;CreatedAt?: string;}interface ApiResponse<T = any> {success: boolean;data?: T;error?: string;message?: string;metadata?: {total?: number;page?: number;limit?: number;};}// 环境变量类型export interface Env {prod_d1_tutorial: D1Database;}// 错误处理类class AppError extends Error {constructor(message: string,public statusCode: number = 500,public code?: string) {super(message);this.name = 'AppError';}}// 响应工具类class ResponseBuilder {static json<T>(data: T, status: number = 200, headers?: Record<string, string>): Response {const response: ApiResponse<T> = {success: status < 400,data};return new Response(JSON.stringify(response), {status,headers: {'Content-Type': 'application/json','Access-Control-Allow-Origin': '*','Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS','Access-Control-Allow-Headers': 'Content-Type, Authorization',...headers,},});}static error(error: string, status: number = 500, code?: string): Response {const response: ApiResponse = {success: false,error,...(code && { code })};return new Response(JSON.stringify(response), {status,headers: {'Content-Type': 'application/json','Access-Control-Allow-Origin': '*','Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS','Access-Control-Allow-Headers': 'Content-Type, Authorization',},});}static success(message: string, data?: any): Response {const response: ApiResponse = {success: true,message,...(data && { data })};return ResponseBuilder.json(response, 200);}}// 验证工具类class Validator {static validateCustomer(data: any): Partial<Customer> {const errors: string[] = [];if (!data.CompanyName || typeof data.CompanyName !== 'string' || data.CompanyName.trim().length === 0) {errors.push('CompanyName is required and must be a non-empty string');}if (!data.ContactName || typeof data.ContactName !== 'string' || data.ContactName.trim().length === 0) {errors.push('ContactName is required and must be a non-empty string');}if (data.Email && typeof data.Email !== 'string') {errors.push('Email must be a string');}if (data.Phone && typeof data.Phone !== 'string') {errors.push('Phone must be a string');}if (errors.length > 0) {throw new AppError(`Validation failed: ${errors.join(', ')}`, 400, 'VALIDATION_ERROR');}return {CompanyName: data.CompanyName.trim(),ContactName: data.ContactName.trim(),Email: data.Email ? data.Email.trim() : null,Phone: data.Phone ? data.Phone.trim() : null,};}}// 数据库服务类class CustomerService {constructor(private db: D1Database) {}async getAllCustomers(): Promise<Customer[]> {try {const query = `SELECTCustomerId,CompanyName,ContactName,Email,Phone,CreatedAtFROM CustomersORDER BY CustomerId DESC`;const result = await this.db.prepare(query).all();return result.results as Customer[];} catch (error) {console.error('Database error in getAllCustomers:', error);throw new AppError('Failed to fetch customers', 500, 'DATABASE_ERROR');}}async getCustomerById(id: number): Promise<Customer | null> {try {const query = `SELECTCustomerId,CompanyName,ContactName,Email,Phone,CreatedAtFROM CustomersWHERE CustomerId = ?`;const result = await this.db.prepare(query).bind(id).all();return result.results.length > 0 ? result.results[0] as Customer : null;} catch (error) {console.error('Database error in getCustomerById:', error);throw new AppError('Failed to fetch customer', 500, 'DATABASE_ERROR');}}async createCustomer(customerData: Omit<Customer, 'CustomerId'>): Promise<{ id: number }> {try {const query = `INSERT INTO Customers (CompanyName, ContactName, Email, Phone)VALUES (?, ?, ?, ?)`;const result = await this.db.prepare(query).bind(customerData.CompanyName,customerData.ContactName,customerData.Email || null,customerData.Phone || null).run();return { id: Number(result.meta.last_row_id) };} catch (error) {console.error('Database error in createCustomer:', error);throw new AppError('Failed to create customer', 500, 'DATABASE_ERROR');}}async updateCustomer(id: number, customerData: Partial<Customer>): Promise<{ changes: number }> {try {const query = `UPDATE CustomersSET CompanyName = ?, ContactName = ?, Email = ?, Phone = ?WHERE CustomerId = ?`;const result = await this.db.prepare(query).bind(customerData.CompanyName,customerData.ContactName,customerData.Email || null,customerData.Phone || null,id).run();return { changes: result.meta.changes || 0 };} catch (error) {console.error('Database error in updateCustomer:', error);throw new AppError('Failed to update customer', 500, 'DATABASE_ERROR');}}async deleteCustomer(id: number): Promise<{ changes: number }> {try {const query = 'DELETE FROM Customers WHERE CustomerId = ?';const result = await this.db.prepare(query).bind(id).run();return { changes: result.meta.changes || 0 };} catch (error) {console.error('Database error in deleteCustomer:', error);throw new AppError('Failed to delete customer', 500, 'DATABASE_ERROR');}}async deleteAllCustomers(): Promise<{ changes: number }> {try {const query = 'DELETE FROM Customers';const result = await this.db.prepare(query).run();return { changes: result.meta.changes || 0 };} catch (error) {console.error('Database error in deleteAllCustomers:', error);throw new AppError('Failed to delete all customers', 500, 'DATABASE_ERROR');}}}// 路由处理器类class CustomerHandler {private service: CustomerService;constructor(private env: Env) {this.service = new CustomerService(env.prod_d1_tutorial);}async handleRequest(request: Request, pathname: string, customerId?: number): Promise<Response> {try {switch (request.method) {case 'GET':if (customerId) {return await this.getCustomer(customerId);} else {return await this.getAllCustomers();}case 'POST':if (!customerId) {return await this.createCustomer(request);}break;case 'PUT':if (customerId) {return await this.updateCustomer(request, customerId);}break;case 'DELETE':if (customerId) {return await this.deleteCustomer(customerId);} else {return await this.deleteAllCustomers();}default:return ResponseBuilder.error('Method not allowed', 405);}return ResponseBuilder.error('Not found', 404);} catch (error) {return this.handleError(error);}}private async getAllCustomers(): Promise<Response> {const customers = await this.service.getAllCustomers();return ResponseBuilder.json(customers, 200, {'Cache-Control': 'no-cache',});}private async getCustomer(id: number): Promise<Response> {const customer = await this.service.getCustomerById(id);if (!customer) {return ResponseBuilder.error('Customer not found', 404, 'NOT_FOUND');}return ResponseBuilder.json(customer);}private async createCustomer(request: Request): Promise<Response> {let body: any;try {body = await request.json();} catch {return ResponseBuilder.error('Invalid JSON body', 400, 'INVALID_JSON');}const validatedData = Validator.validateCustomer(body);const result = await this.service.createCustomer(validatedData);return ResponseBuilder.success('Customer created successfully', { id: result.id });}private async updateCustomer(request: Request, id: number): Promise<Response> {// 检查客户是否存在const existingCustomer = await this.service.getCustomerById(id);if (!existingCustomer) {return ResponseBuilder.error('Customer not found', 404, 'NOT_FOUND');}let body: any;try {body = await request.json();} catch {return ResponseBuilder.error('Invalid JSON body', 400, 'INVALID_JSON');}const validatedData = Validator.validateCustomer(body);const result = await this.service.updateCustomer(id, validatedData);if (result.changes === 0) {return ResponseBuilder.error('No changes made', 400, 'NO_CHANGES');}return ResponseBuilder.success('Customer updated successfully');}private async deleteCustomer(id: number): Promise<Response> {// 检查客户是否存在const existingCustomer = await this.service.getCustomerById(id);if (!existingCustomer) {return ResponseBuilder.error('Customer not found', 404, 'NOT_FOUND');}const result = await this.service.deleteCustomer(id);if (result.changes === 0) {return ResponseBuilder.error('Failed to delete customer', 500, 'DELETE_FAILED');}return ResponseBuilder.success('Customer deleted successfully');}private async deleteAllCustomers(): Promise<Response> {const result = await this.service.deleteAllCustomers();return ResponseBuilder.success(`Deleted ${result.changes} customers successfully`);}private handleError(error: any): Response {if (error instanceof AppError) {return ResponseBuilder.error(error.message, error.statusCode, error.code);}console.error('Unhandled error:', error);return ResponseBuilder.error('Internal server error', 500, 'INTERNAL_ERROR');}}// 静态资源服务class StaticAssetHandler {private readonly htmlContent = `<!DOCTYPE html><html lang="zh-CN" data-theme="light"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>D1 CRM - 现代化客户管理系统</title><script src="https://unpkg.com/htmx.org@1.9.10"></script><script src="https://unpkg.com/hyperscript.org@0.9.11"></script><link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"><style>/* 这里包含之前提供的完整CSS样式 */:root { --primary: #6366f1; --primary-dark: #4f46e5; /* ... 完整CSS样式 */ }</style></head><body><div class="app-container" id="app"><!-- 这里包含之前提供的完整HTML结构 --><header class="header">...</header><main class="main-content">...</main><div class="toast-container" id="toastContainer"></div></div><script>// 这里包含之前提供的完整JavaScript代码class D1CRM { /* ... 完整JavaScript代码 */ }const app = new D1CRM();</script></body></html>`;handleRequest(request: Request): Response {const acceptHeader = request.headers.get('accept') || '';if (acceptHeader.includes('text/html')) {return new Response(this.htmlContent, {status: 200,headers: {'Content-Type': 'text/html; charset=utf-8','Cache-Control': 'no-cache',},});}return ResponseBuilder.error('Not found', 404);}}// 主处理器export default {async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {const url = new URL(request.url);const pathname = url.pathname;// 处理预检请求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','Access-Control-Max-Age': '86400',},});}// API 路由if (pathname.startsWith('/api/customers')) {const customerHandler = new CustomerHandler(env);// 提取客户IDconst idMatch = pathname.match(/^\/api\/customers\/(\d+)$/);const customerId = idMatch ? parseInt(idMatch[1]) : undefined;return customerHandler.handleRequest(request, pathname, customerId);}// 静态资源路由 (根路径)if (pathname === '/' || pathname === '/index.html') {const staticHandler = new StaticAssetHandler();return staticHandler.handleRequest(request);}// 默认返回首页const staticHandler = new StaticAssetHandler();return staticHandler.handleRequest(request);},} satisfies ExportedHandler<Env>;
http://www.dtcms.com/a/606875.html

相关文章:

  • 如东网站制作网站建设工具哪家好
  • 零知IDE——基于STM32F103RBT6和SHT40温湿度传感器的环境监测系统
  • 建立手机个人网站福田企业网站优化有用吗
  • C语言反编译 | 如何高效实现C语言程序反编译及相关技术解析
  • 佛山网站建设过程做游乐设施模型的网站
  • 网站建设培训珠海招商广告
  • nginx wordpress 目录 伪静态seo北京公司
  • C++ 三分查找:在单调与凸函数中高效定位极值的算法
  • wordpress建站教程 cms浙江信息港
  • C++备忘录模式:优雅实现对象状态保存与恢复
  • 网站的数据库选择网页价格表
  • react的框架UmiJs(五米)
  • PySide6/PyQt信号总线的实现、使用以及使用建议
  • 制作网站参考wordpress设置摘要还是显示全文
  • 2012年网站设计方法网站建设吉金手指排名15
  • 二十九、STM32的USART (串口发送)
  • dw做网站链接数据库渭南 网站建设
  • 站长工具查询网站信息1 建设网站目的是什么意思
  • 企业网站开发课程的能力应用图片直链在线生成网站
  • 设计模式学习(3)-行为型模式
  • Angular 2 架构:深度解析与最佳实践
  • 09-微服务原理篇(XXLJOB-幂等-MySQL)
  • 代码随想录训练营打卡Day45| 动态规划part12
  • 网站开发要考什么证wordpress xy 变装
  • 已知两个端点的位置、切矢量和曲率,构造一条五阶Bézier 曲线实现 G² 连续的方法
  • 显示 Markdown 的文件
  • 算法学习入门---模拟(C++)
  • 列表网做优化网站怎么样成都网站注册
  • 文库网站开发教程wordpress 绑定熊掌号
  • C语言在线编译器网站 | 提供快速、便捷的在线编程与调试平台