瀏覽代碼

feat: add currency conversion and rates API

- Add /convert endpoint for currency conversion
- Add /rates endpoint to get exchange rates
- Update README with new API documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
kotoyuuko 3 天之前
父節點
當前提交
eb503dd29e
共有 2 個文件被更改,包括 239 次插入17 次删除
  1. 58 13
      README.md
  2. 181 4
      src/index.ts

+ 58 - 13
README.md

@@ -7,6 +7,7 @@ A Cloudflare Workers Scheduled Worker that automatically fetches exchange rates
 - 🔄 Hourly automatic sync via Cron trigger
 - 🔌 Manual sync via HTTP endpoint
 - 💱 Supports 170+ currencies
+- 🧮 Currency conversion API
 - ⚡ Fast and serverless
 
 ## Setup
@@ -81,18 +82,69 @@ wrangler secret put NOTION_TOKEN
 wrangler secret put NOTION_DATABASE_ID
 ```
 
-## Usage
+## API Reference
+
+### Endpoints
+
+| Endpoint | Method | Description |
+|----------|--------|-------------|
+| `/` | GET | Info page |
+| `/convert` | GET | Convert currency |
+| `/rates` | GET | Get exchange rates |
+| `/sync` | GET | Sync rates to Notion |
+
+### Convert Currency
+
+```bash
+GET /convert?from=USD&to=CNY&amount=100
+```
+
+Parameters:
+- `from` - Source currency code (e.g., USD, EUR, JPY)
+- `to` - Target currency code
+- `amount` - Amount to convert
+
+Response:
+```json
+{
+  "success": true,
+  "from": "USD",
+  "to": "CNY",
+  "amount": 100,
+  "rate": 6.9002,
+  "result": 690.02,
+  "timestamp": 1773900000000
+}
+```
 
-### Automatic Sync
+### Get Exchange Rates
+
+```bash
+GET /rates?base=USD
+```
 
-The worker runs automatically every hour via Cron trigger.
+Parameters:
+- `base` - Base currency (default: USD)
 
-### Manual Sync
+Response:
+```json
+{
+  "base": "USD",
+  "timestamp": 1773900000000,
+  "rates": {
+    "AED": 3.6725,
+    "CNY": 6.9002,
+    "EUR": 0.9175,
+    "JPY": 149.85,
+    ...
+  }
+}
+```
 
-Send a GET request to the `/sync` endpoint:
+### Manual Sync to Notion
 
 ```bash
-curl https://your-worker.workers.dev/sync
+GET /sync
 ```
 
 Response:
@@ -107,13 +159,6 @@ Response:
 }
 ```
 
-## API Reference
-
-| Endpoint | Method | Description |
-|----------|--------|-------------|
-| `/` | GET | Info page |
-| `/sync` | GET | Manually trigger exchange rate sync |
-
 ## Tech Stack
 
 - [Cloudflare Workers](https://workers.cloudflare.com/)

+ 181 - 4
src/index.ts

@@ -41,6 +41,17 @@ interface SyncResult {
   errors: string[];
 }
 
+interface ConvertResult {
+  success: boolean;
+  from: string;
+  to: string;
+  amount: number;
+  rate: number;
+  result: number;
+  timestamp: number;
+  error?: string;
+}
+
 async function fetchExchangeRates(appId: string): Promise<OXRResponse> {
   const url = `https://openexchangerates.org/api/latest.json?app_id=${appId}`;
   const response = await fetch(url);
@@ -202,11 +213,68 @@ async function syncExchangeRates(env: Env): Promise<SyncResult> {
   return result;
 }
 
+async function convertCurrency(
+  env: Env,
+  from: string,
+  to: string,
+  amount: number
+): Promise<ConvertResult> {
+  const result: ConvertResult = {
+    success: false,
+    from: from.toUpperCase(),
+    to: to.toUpperCase(),
+    amount,
+    rate: 0,
+    result: 0,
+    timestamp: 0,
+  };
+
+  if (!env.OXR_APP_ID) {
+    result.error = 'Missing OXR_APP_ID';
+    return result;
+  }
+
+  try {
+    const oxrData = await fetchExchangeRates(env.OXR_APP_ID);
+    result.timestamp = oxrData.timestamp * 1000;
+
+    const fromRate = oxrData.rates[from.toUpperCase()];
+    const toRate = oxrData.rates[to.toUpperCase()];
+
+    if (!fromRate) {
+      result.error = `Unknown currency: ${from}`;
+      return result;
+    }
+
+    if (!toRate) {
+      result.error = `Unknown currency: ${to}`;
+      return result;
+    }
+
+    // Convert: amount in USD = amount / fromRate
+    // Then convert to target: result = amountInUSD * toRate
+    const amountInUSD = amount / fromRate;
+    const convertedAmount = amountInUSD * toRate;
+
+    // Calculate the direct rate (1 from = X to)
+    const directRate = toRate / fromRate;
+
+    result.rate = directRate;
+    result.result = convertedAmount;
+    result.success = true;
+  } catch (error) {
+    const errorMessage = error instanceof Error ? error.message : String(error);
+    result.error = errorMessage;
+  }
+
+  return result;
+}
+
 export default {
   async fetch(req, env, ctx): Promise<Response> {
     const url = new URL(req.url);
 
-    // Manual trigger endpoint
+    // Manual sync endpoint
     if (url.pathname === '/sync') {
       const result = await syncExchangeRates(env);
       return new Response(JSON.stringify(result, null, 2), {
@@ -214,9 +282,118 @@ export default {
       });
     }
 
-    return new Response('Notion Exchange Rate Worker\n\nUse /sync to manually trigger sync.', {
-      headers: { 'Content-Type': 'text/plain' },
-    });
+    // Currency conversion endpoint
+    if (url.pathname === '/convert') {
+      const from = url.searchParams.get('from');
+      const to = url.searchParams.get('to');
+      const amountStr = url.searchParams.get('amount');
+
+      if (!from || !to || !amountStr) {
+        return new Response(
+          JSON.stringify({
+            error: 'Missing required parameters: from, to, amount',
+            usage: '/convert?from=USD&to=CNY&amount=100',
+          }),
+          {
+            status: 400,
+            headers: { 'Content-Type': 'application/json' },
+          }
+        );
+      }
+
+      const amount = parseFloat(amountStr);
+      if (isNaN(amount)) {
+        return new Response(
+          JSON.stringify({ error: 'Invalid amount. Must be a number.' }),
+          {
+            status: 400,
+            headers: { 'Content-Type': 'application/json' },
+          }
+        );
+      }
+
+      const result = await convertCurrency(env, from, to, amount);
+      return new Response(JSON.stringify(result, null, 2), {
+        headers: { 'Content-Type': 'application/json' },
+      });
+    }
+
+    // Rates endpoint - get all rates for a base currency
+    if (url.pathname === '/rates') {
+      const base = url.searchParams.get('base') || 'USD';
+
+      if (!env.OXR_APP_ID) {
+        return new Response(
+          JSON.stringify({ error: 'Missing OXR_APP_ID' }),
+          { status: 500, headers: { 'Content-Type': 'application/json' } }
+        );
+      }
+
+      try {
+        const oxrData = await fetchExchangeRates(env.OXR_APP_ID);
+
+        // If base is USD, return as-is
+        if (base.toUpperCase() === 'USD') {
+          return new Response(
+            JSON.stringify(
+              {
+                base: oxrData.base,
+                timestamp: oxrData.timestamp * 1000,
+                rates: oxrData.rates,
+              },
+              null,
+              2
+            ),
+            { headers: { 'Content-Type': 'application/json' } }
+          );
+        }
+
+        // Otherwise, recalculate rates for the new base
+        const baseRate = oxrData.rates[base.toUpperCase()];
+        if (!baseRate) {
+          return new Response(
+            JSON.stringify({ error: `Unknown currency: ${base}` }),
+            { status: 400, headers: { 'Content-Type': 'application/json' } }
+          );
+        }
+
+        const convertedRates: Record<string, number> = {};
+        for (const [currency, rate] of Object.entries(oxrData.rates)) {
+          convertedRates[currency] = rate / baseRate;
+        }
+        convertedRates['USD'] = 1 / baseRate;
+
+        return new Response(
+          JSON.stringify(
+            {
+              base: base.toUpperCase(),
+              timestamp: oxrData.timestamp * 1000,
+              rates: convertedRates,
+            },
+            null,
+            2
+          ),
+          { headers: { 'Content-Type': 'application/json' } }
+        );
+      } catch (error) {
+        const errorMessage = error instanceof Error ? error.message : String(error);
+        return new Response(
+          JSON.stringify({ error: errorMessage }),
+          { status: 500, headers: { 'Content-Type': 'application/json' } }
+        );
+      }
+    }
+
+    // Home page
+    return new Response(
+      `Notion Exchange Rate Worker
+
+API Endpoints:
+  GET /convert?from=USD&to=CNY&amount=100  - Convert currency
+  GET /rates?base=USD                      - Get exchange rates
+  GET /sync                                - Sync rates to Notion`,
+      { headers: { 'Content-Type': 'text/plain' } }
+    );
   },
 
   async scheduled(event, env, ctx): Promise<void> {