diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/api.md b/api.md new file mode 100644 index 0000000..6b9add5 --- /dev/null +++ b/api.md @@ -0,0 +1,193 @@ +# API 接口文檔 + +## 訂單管理系統 API 接口文檔 + +### 1. 添加訂單 + +#### 接口信息 +- **接口路徑**: `POST /api/orders` +- **功能描述**: 添加新的訂單記錄 +- **請求格式**: JSON +- **響應格式**: JSON + +#### 請求參數 +| 參數名 | 類型 | 必填 | 說明 | +|--------|------|------|------| +| customer_name | String | 是 | 客戶姓名 | +| product_name | String | 是 | 商品名稱 | +| quantity | Number | 是 | 商品數量,必須大於0 | +| price | Number | 是 | 商品單價,必須大於0 | +| status | String | 否 | 訂單狀態,默認為"pending" | + +#### 響應參數 +| 參數名 | 類型 | 說明 | +|--------|------|------| +| success | Boolean | 請求是否成功 | +| message | String | 響應消息 | +| data | Object | 返回的數據對象 | +| data.id | Number | 新增訂單的ID | +| data.customer_name | String | 客戶姓名 | +| data.product_name | String | 商品名稱 | +| data.quantity | Number | 商品數量 | +| data.price | Number | 商品單價 | +| data.status | String | 訂單狀態 | +| data.created_at | String | 創建時間 | + +#### 成功響應示例 +```json +{ + "success": true, + "message": "訂單創建成功", + "data": { + "id": 6, + "customer_name": "陳八", + "product_name": "HomePod Mini", + "quantity": 1, + "price": 99.99, + "status": "pending", + "created_at": "2023-12-01 10:30:00" + } +} +``` + +#### 失敗響應示例 +```json +{ + "success": false, + "message": "缺少必需字段或字段值無效" +} +``` + +#### Curl 請求示例 +```bash +curl -X POST http://localhost:3000/api/orders \ + -H "Content-Type: application/json" \ + -H "Accept: application/json" \ + -d '{ + "customer_name": "陳八", + "product_name": "HomePod Mini", + "quantity": 1, + "price": 99.99, + "status": "pending" + }' +``` + +### 2. 查詢訂單列表 + +#### 接口信息 +- **接口路徑**: `GET /api/orders` +- **功能描述**: 查詢訂單列表,支持條件篩選和分頁 +- **請求格式**: Query String +- **響應格式**: JSON + +#### 查詢參數 +| 參數名 | 類型 | 必填 | 默認值 | 說明 | +|--------|------|------|--------|------| +| page | Number | 否 | 1 | 頁碼 | +| pageSize | Number | 否 | 10 | 每頁顯示數量 | +| customerName | String | 否 | - | 客戶姓名模糊匹配 | +| productName | String | 否 | - | 商品名稱模糊匹配 | +| status | String | 否 | - | 訂單狀態精確匹配 | + +#### 響應參數 +| 參數名 | 類型 | 說明 | +|--------|------|------| +| success | Boolean | 請求是否成功 | +| data | Object | 返回的數據對象 | +| data.orders | Array | 訂單列表 | +| data.pagination | Object | 分頁信息 | +| data.pagination.page | Number | 當前頁碼 | +| data.pagination.pageSize | Number | 每頁顯示數量 | +| data.pagination.total | Number | 總記錄數 | +| data.pagination.totalPages | Number | 總頁數 | + +#### 成功響應示例 +```json +{ + "success": true, + "data": { + "orders": [ + { + "id": 1, + "customer_name": "張三", + "product_name": "iPhone 15", + "quantity": 1, + "price": 999.99, + "status": "completed", + "created_at": "2023-12-01 10:30:00" + }, + { + "id": 2, + "customer_name": "李四", + "product_name": "MacBook Pro", + "quantity": 1, + "price": 1999.99, + "status": "pending", + "created_at": "2023-12-01 10:25:00" + } + ], + "pagination": { + "page": 1, + "pageSize": 10, + "total": 2, + "totalPages": 1 + } + } +} +``` + +#### Curl 請求示例 + +##### 查詢所有訂單(默認分頁) +```bash +curl "http://localhost:3000/api/orders" +``` + +##### 分頁查詢 +```bash +curl "http://localhost:3000/api/orders?page=1&pageSize=5" +``` + +##### 按客戶姓名查詢 +```bash +curl "http://localhost:3000/api/orders?customerName=張" +``` + +##### 按商品名稱查詢 +```bash +curl "http://localhost:3000/api/orders?productName=iPhone" +``` + +##### 按狀態查詢 +```bash +curl "http://localhost:3000/api/orders?status=pending" +``` + +##### 組合條件查詢 +```bash +curl "http://localhost:3000/api/orders?page=1&pageSize=10&customerName=張&status=completed" +``` + +### 3. 錯誤碼說明 + +| 錯誤碼 | 狀態碼 | 說明 | +|--------|--------|------| +| 400 | 400 Bad Request | 請求參數錯誤 | +| 404 | 404 Not Found | 路由不存在 | +| 500 | 500 Internal Server Error | 服務器內部錯誤 | + +### 4. 公共響應格式 + +所有接口響應都遵循以下格式: + +```json +{ + "success": true/false, + "message": "響應消息", + "data": {} +} +``` + +- `success`: 請求是否成功的布爾值 +- `message`: 響應消息,成功時可選,失敗時必填 +- `data`: 返回的業務數據,根據接口不同而異 \ No newline at end of file diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..3c58493 --- /dev/null +++ b/bun.lock @@ -0,0 +1,26 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "bun_lite", + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], + + "@types/node": ["@types/node@25.2.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w=="], + + "bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + } +} diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..d356f03 --- /dev/null +++ b/index.ts @@ -0,0 +1,181 @@ +import { Database } from "bun:sqlite"; + +// 初始化 SQLite 數據庫 +const db = new Database("orders.db"); + +// 創建 orders 表 +db.run(` + CREATE TABLE IF NOT EXISTS orders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + customer_name TEXT NOT NULL, + product_name TEXT NOT NULL, + quantity INTEGER NOT NULL, + price REAL NOT NULL, + status TEXT DEFAULT 'pending', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) +`); + +// 定義類型 +interface Order { + id?: number; + customer_name: string; + product_name: string; + quantity: number; + price: number; + status?: string; + created_at?: string; +} + +// 準備 SQL 語句 +const insertOrderStmt = db.prepare(` + INSERT INTO orders (customer_name, product_name, quantity, price, status) + VALUES ($customerName, $productName, $quantity, $price, $status) +`); + +const getOrdersStmt = db.prepare(` + SELECT * FROM orders + WHERE ($customerName IS NULL OR customer_name LIKE $customerName) + AND ($productName IS NULL OR product_name LIKE $productName) + AND ($status IS NULL OR status = $status) + ORDER BY created_at DESC + LIMIT $limit OFFSET $offset +`); + +const getOrderCountStmt = db.prepare(` + SELECT COUNT(*) as count FROM orders + WHERE ($customerName IS NULL OR customer_name LIKE $customerName) + AND ($productName IS NULL OR product_name LIKE $productName) + AND ($status IS NULL OR status = $status) +`); + +// 創建服務器 +const server = Bun.serve({ + port: 3000, + async fetch(req) { + const url = new URL(req.url); + const path = url.pathname; + const method = req.method; + + // CORS headers + const headers = { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type", + }; + + // 處理預檢請求 + if (method === "OPTIONS") { + return new Response(null, { status: 204, headers }); + } + + try { + if (path === "/api/orders" && method === "GET") { + // 解析查詢參數 + const page = parseInt(url.searchParams.get("page") || "1"); + const pageSize = parseInt(url.searchParams.get("pageSize") || "10"); + const customerName = url.searchParams.get("customerName") || null; + const productName = url.searchParams.get("productName") || null; + const status = url.searchParams.get("status") || null; + + // 計算偏移量 + const offset = (page - 1) * pageSize; + + // 準備查詢參數 + const params: any = { + $customerName: customerName ? `%${customerName}%` : null, + $productName: productName ? `%${productName}%` : null, + $status: status, + $limit: pageSize, + $offset: offset + }; + + // 查詢訂單列表 + const orders = getOrdersStmt.all(params) as Order[]; + + // 獲取總數 + const countResult: any = getOrderCountStmt.get({ + $customerName: customerName ? `%${customerName}%` : null, + $productName: productName ? `%${productName}%` : null, + $status: status + }); + + const total = countResult.count; + + return Response.json({ + success: true, + data: { + orders, + pagination: { + page, + pageSize, + total, + totalPages: Math.ceil(total / pageSize) + } + } + }, { headers }); + } else if (path === "/api/orders" && method === "POST") { + // 添加新訂單 + const body: any = await req.json(); + + const order: Order = { + customer_name: body.customer_name, + product_name: body.product_name, + quantity: body.quantity, + price: body.price, + status: body.status || 'pending' + }; + + // 驗證必需字段 + if (!order.customer_name || !order.product_name || order.quantity <= 0 || order.price <= 0) { + return Response.json({ + success: false, + message: "缺少必需字段或字段值無效" + }, { + status: 400, + headers + }); + } + + // 插入數據 + const result = insertOrderStmt.run({ + $customerName: order.customer_name, + $productName: order.product_name, + $quantity: order.quantity, + $price: order.price, + $status: order.status || 'pending' + }); + + return Response.json({ + success: true, + message: "訂單創建成功", + data: { + id: result.lastInsertRowid as number, + ...order + } + }, { headers }); + } else { + return Response.json({ + success: false, + message: "路由不存在" + }, { + status: 404, + headers + }); + } + } catch (error: any) { + console.error("服務器錯誤:", error); + + return Response.json({ + success: false, + message: error.message || "服務器內部錯誤" + }, { + status: 500, + headers + }); + } + } +}); + +console.log(`Server running at http://localhost:${server.port}`); \ No newline at end of file diff --git a/install.sql b/install.sql new file mode 100644 index 0000000..5645463 --- /dev/null +++ b/install.sql @@ -0,0 +1,26 @@ +-- SQLite 數據庫初始化腳本 +-- 創建 orders 表 + +CREATE TABLE IF NOT EXISTS orders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + customer_name TEXT NOT NULL, + product_name TEXT NOT NULL, + quantity INTEGER NOT NULL, + price REAL NOT NULL, + status TEXT DEFAULT 'pending', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +-- 插入示例數據 +INSERT INTO orders (customer_name, product_name, quantity, price, status) VALUES +('張三', 'iPhone 15', 1, 999.99, 'completed'), +('李四', 'MacBook Pro', 1, 1999.99, 'pending'), +('王五', 'iPad Air', 2, 599.99, 'shipped'), +('趙六', 'Apple Watch', 1, 399.99, 'delivered'), +('錢七', 'AirPods Pro', 1, 249.99, 'processing'); + +-- 創建索引以提高查詢性能 +CREATE INDEX IF NOT EXISTS idx_customer_name ON orders(customer_name); +CREATE INDEX IF NOT EXISTS idx_product_name ON orders(product_name); +CREATE INDEX IF NOT EXISTS idx_status ON orders(status); +CREATE INDEX IF NOT EXISTS idx_created_at ON orders(created_at); \ No newline at end of file diff --git a/orders.db b/orders.db new file mode 100644 index 0000000..4faa860 Binary files /dev/null and b/orders.db differ diff --git a/package.json b/package.json new file mode 100644 index 0000000..42bdc2c --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "bun_lite", + "module": "index.ts", + "type": "module", + "private": true, + "scripts": { + "dev": "bun run index.ts", + "start": "bun run index.ts" + }, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..bfa0fea --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +}