技术日志2026.07.02

Astro 网站接入 OneSend 询盘教程:Cloudflare D1 备份小白版

这是一份给新手看的完整教程,用来给已经做好的 Astro 外贸网站接入 OneSend 询盘,并使用 Cloudflare D1 做备份,防止丢单。

真实工作流程通常是:

网站页面已经做好
  -> 已经有 inquiry / contact / quote 表单
  -> 再把这个表单接到 OneSend
  -> 再配置 Cloudflare D1 备份

所以本文档的重点不是让你重新做一个询盘页,而是教你如何检查和改造现有表单。

这套方案的目标不是“只要 Cloudflare D1 有记录就算成功”,而是双层保障:

第一层:Cloudflare D1 能查到询盘备份
第二层:OneSend / 网站询盘后台也能查到同一条询盘

只有两边都能查到,才说明完整链路真正跑通。

你最终要实现的效果是:

买家填写询盘表单
  -> 提交到你自己网站的 /f/项目名
  -> Cloudflare Pages Function 接收
  -> 先保存一份到 Cloudflare D1
  -> 再转发到 OneSend
  -> 买家跳转到 thank-you 页面

一句话理解:

表单不要直接交给 OneSend。先交给自己网站的 Cloudflare Function,让它先备份,再转发。

换句话说:

D1 是保险柜,负责防丢单。
OneSend / 网站后台是正式业务系统,负责销售查看和跟进询盘。

1. 你需要准备什么

开始前,你需要有:

  • 一个 Astro 网站项目
  • 网站里已经有询盘表单,或者已经确定要用哪个表单收询盘
  • 网站部署在 Cloudflare Pages
  • 一个 OneSend 项目路径,例如 newsite
  • 一个 Cloudflare D1 数据库
  • 一个 Cloudflare Turnstile site key

如果你已经有其他网站接入过 OneSend,并且 D1 表已经建好,那么新网站通常只需要:

  • 复用同一个 D1 数据库
  • 在新站 Cloudflare Pages 里绑定 D1
  • 新表单使用新的项目名
  • Turnstile 允许新域名

2. 先理解几个名字

Astro

Astro 是你的网站框架。询盘页可能是:

src/pages/inquiry.astro

也可能是:

src/pages/contact.astro
src/pages/contact-us.astro
src/pages/quote.astro

具体用哪个页面不重要,重要的是找到真正的表单,并把它的 action 改成 /f/项目名

OneSend

OneSend 是接收询盘的后端服务。你的表单最终会转发到:

https://api.oneuie.me/f/项目名

例如项目名叫 newsite,最终地址就是:

https://api.oneuie.me/f/newsite

Cloudflare Pages Function

Cloudflare Pages Function 是部署在 Cloudflare 边缘上的小后端。

它放在项目根目录:

functions/f/[[path]].js

它的作用是:

  1. 接收表单
  2. 写入 D1 备份
  3. 转发给 OneSend
  4. 处理失败兜底

Cloudflare D1

D1 是 Cloudflare 的数据库。这里用来保存询盘备份。

即使 OneSend 临时失败,只要 D1 写入成功,询盘数据就不会丢。

Turnstile

Turnstile 是 Cloudflare 的人机验证,用来减少垃圾询盘。

前端表单会生成:

cf-turnstile-response

Cloudflare Function 会把它转成 OneSend 后端需要的:

turnstile_token

3. 第一步:创建 D1 表

如果你是第一次配置 D1,需要在 Cloudflare D1 控制台执行:

CREATE TABLE IF NOT EXISTS submissions_backup (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  project TEXT NOT NULL,
  data TEXT NOT NULL,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

如果你之前其他网站已经执行过,并且新网站绑定的是同一个 D1 数据库,就不用再执行。

重复执行也不会删数据,因为这里用了:

IF NOT EXISTS

意思是:如果表不存在就创建,如果已经存在就不动。


4. 第二步:在 Astro 项目里创建 Function

在 Astro 项目根目录创建这个文件:

functions/f/[[path]].js

注意:是项目根目录,不是 src/ 里面。

目录结构应该像这样:

your-astro-site/
  src/
    pages/
      inquiry.astro
  functions/
    f/
      [[path]].js
  package.json

然后把下面代码放进去:

export async function onRequestPost(context) {
  const { request, env, params } = context;
  const url = new URL(request.url);

  const projectSlug = params.path && params.path.length > 0 ? params.path[0] : "contact";
  const vpsUrl = "https://api.oneuie.me" + url.pathname + url.search;

  let formData = null;
  let redirectTarget = request.headers.get("Referer") || "/";
  let backupSaved = false;

  try {
    formData = await request.formData();
    redirectTarget = formData.get("REDIRECT_URL") || redirectTarget;

    if (env.DB && typeof env.DB.prepare === "function") {
      try {
        const entry = {};
        for (const [k, v] of formData.entries()) {
          if (typeof v === "string") entry[k] = v;
        }

        entry.received_at = new Date().toISOString();
        entry.source_url = request.headers.get("Referer") || "";
        entry.cf_ray = request.headers.get("CF-Ray") || "";

        await env.DB.prepare("INSERT INTO submissions_backup (project, data) VALUES (?, ?)")
          .bind(projectSlug, JSON.stringify(entry))
          .run();

        backupSaved = true;
      } catch (e) {
        console.error("D1 backup failed:", e.message);
      }
    } else {
      console.error("D1 binding DB is missing.");
    }

    const headers = new Headers();
    ["accept", "user-agent", "referer", "origin"].forEach((h) => {
      const val = request.headers.get(h);
      if (val) headers.set(h, val);
    });
    headers.set("content-type", "application/x-www-form-urlencoded;charset=UTF-8");

    const forwardBody = new URLSearchParams();
    for (const [k, v] of formData.entries()) {
      if (typeof v === "string") forwardBody.append(k, v);
    }

    const turnstileToken = formData.get("cf-turnstile-response");
    if (typeof turnstileToken === "string" && turnstileToken) {
      forwardBody.set("turnstile_token", turnstileToken);
    }

    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), 6000);

    const vpsResponse = await fetch(vpsUrl, {
      method: "POST",
      body: forwardBody,
      headers,
      signal: controller.signal,
      redirect: "manual",
    });

    clearTimeout(timeout);

    if (vpsResponse.ok || (vpsResponse.status >= 300 && vpsResponse.status < 400)) {
      return vpsResponse;
    }

    throw new Error(`VPS returned ${vpsResponse.status}`);
  } catch (err) {
    console.error("Form proxy failed:", err.message);

    if (backupSaved) {
      return Response.redirect(new URL(redirectTarget, request.url).href, 302);
    }

    return new Response("Submission could not be saved. Please go back and try again.", {
      status: 503,
      headers: {
        "content-type": "text/plain; charset=utf-8",
        "x-onesend-backup": "failed",
      },
    });
  }
}

这段代码不用每个网站都改。通常只要 OneSend 域名还是:

https://api.oneuie.me

就可以直接复用。


5. 第三步:找到并改造现有询盘表单

网站做好以后,通常已经有询盘页或联系页。先找到实际收询盘的表单文件,常见位置有:

src/pages/inquiry.astro
src/pages/contact.astro
src/pages/contact-us.astro
src/pages/quote.astro
src/components/ContactForm.astro

你不需要重做页面,只需要检查和改造表单的关键属性。

需要改的第一件事:form action

假设 OneSend 项目名是 newsite,现有表单应该改成:

<form method="POST" action="/f/newsite">

不要写成:

<form method="POST" action="https://api.oneuie.me/f/newsite">

原因是:直接提交到 OneSend 会绕过 Cloudflare Function,D1 就没有备份。

需要加的第二件事:成功跳转字段

在表单里面加:

<input type="hidden" name="REDIRECT_URL" value="/thank-you" />

如果你的感谢页不是 /thank-you,就改成你自己的成功页路径。

需要加的第三件事:Turnstile

在表单提交按钮前加:

<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<div
  class="cf-turnstile"
  data-sitekey="你的 Turnstile site key"
  data-response-field-name="cf-turnstile-response"
></div>

需要保留或补齐的字段

OneSend 常用字段建议保留这些名字:

<input type="text" name="name" required />
<input type="email" name="email" required />
<input type="text" name="tel" />
<input type="text" name="add" />
<input type="text" name="product" />
<textarea name="message" required></textarea>

如果你原来的表单字段更多,可以保留。Function 会把字符串字段一起备份到 D1,并转发给 OneSend。

最小可用表单示例

下面不是要求你重新做页面,只是给你对照检查用:

---
const projectSlug = "newsite";
---

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Inquiry</title>
  </head>
  <body>
    <main>
      <h1>Request a Quote</h1>

      <form method="POST" action={`/f/${projectSlug}`}>
        <input type="hidden" name="REDIRECT_URL" value="/thank-you" />

        <div style="display:none;">
          <label>Keep blank</label>
          <input type="text" name="honeypot" />
        </div>

        <p>
          <label>
            Product
            <input type="text" name="product" />
          </label>
        </p>

        <p>
          <label>
            Name *
            <input type="text" name="name" required />
          </label>
        </p>

        <p>
          <label>
            Email *
            <input type="email" name="email" required />
          </label>
        </p>

        <p>
          <label>
            Phone
            <input type="text" name="tel" />
          </label>
        </p>

        <p>
          <label>
            Country
            <input type="text" name="add" />
          </label>
        </p>

        <p>
          <label>
            Requirements *
            <textarea
              name="message"
              required
              placeholder="Product type, capacity, liquid type, fittings, quantity, destination country..."
            ></textarea>
          </label>
        </p>

        <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
        <div
          class="cf-turnstile"
          data-sitekey="你的 Turnstile site key"
          data-response-field-name="cf-turnstile-response"
        ></div>

        <button type="submit">Submit Inquiry</button>
      </form>
    </main>
  </body>
</html>

如果你使用这个示例,把这里换成你自己的 OneSend 项目名:

const projectSlug = "newsite";

例如项目名是 bancy,就写:

const projectSlug = "bancy";

这样表单会提交到:

/f/bancy

Function 会自动转发到:

https://api.oneuie.me/f/bancy

6. 第四步:确认感谢页存在

确认网站里有成功提交后的感谢页,例如:

src/pages/thank-you.astro

如果已经有,就不用新建。只要确保表单里的:

<input type="hidden" name="REDIRECT_URL" value="/thank-you" />

和实际感谢页路径一致。

如果还没有感谢页,可以用下面这个最小版本:

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="robots" content="noindex" />
    <title>Thank You</title>
  </head>
  <body>
    <main>
      <h1>Thank you</h1>
      <p>We have received your inquiry and will get back to you soon.</p>
      <a href="/">Return to homepage</a>
    </main>
  </body>
</html>

为什么要有感谢页?

  • 告诉客户提交成功
  • 方便后续做转化统计
  • 避免用户重复提交

7. 第五步:配置 Cloudflare Pages D1 绑定

部署到 Cloudflare Pages 后,进入你的项目后台:

Cloudflare Pages
  -> 选择你的网站
  -> Settings
  -> Functions
  -> D1 database bindings

添加绑定:

Variable name: DB
D1 database: 选择你的询盘备份数据库

必须注意:

变量名必须叫 DB

因为 Function 代码里写的是:

env.DB

如果你写成 DATABASED1MY_DB,代码就找不到数据库。

还要注意生产环境和预览环境:

  • Production 生产环境要绑定
  • Preview 预览环境如果要测试,也要绑定

8. 第六步:配置 Turnstile

进入 Cloudflare Turnstile 后台:

Cloudflare
  -> Turnstile
  -> 选择你的 Widget
  -> Settings
  -> Permitted domains

把新网站域名加进去,例如:

example.com
www.example.com

然后把 Turnstile site key 放到表单里:

<div
  class="cf-turnstile"
  data-sitekey="你的 Turnstile site key"
  data-response-field-name="cf-turnstile-response"
></div>

注意:

  • site key 可以放在前端代码里
  • secret key 不要放在前端代码里
  • OneSend 后端如果已经配置好 secret,新网站一般只需要把域名加入允许列表

9. 第七步:部署前检查

在本地执行:

npm run build

确认没有报错。

然后检查这些东西是否存在:

functions/f/[[path]].js
现有询盘表单页面或表单组件
thank-you 成功页

检查表单:

<form method="POST" action="/f/项目名">

不要写成:

<form method="POST" action="https://api.oneuie.me/f/项目名">

直接提交到 OneSend 会绕过 D1 备份,不推荐。


10. 第八步:本地测试分两种情况

本地测试要分清楚你是在“看页面”,还是在“测试表单提交”。

情况 1:只是看页面

如果你只是想看页面有没有做好,例如:

/inquiry
/thank-you

可以用普通 Astro 命令:

npm run dev

或者先构建再预览:

npm run build
npm run preview

这种方式通常不需要绑定 Cloudflare D1。

原因是:

  • 你只是看静态页面和样式
  • functions/f/[[path]].js 不一定会按 Cloudflare Pages Function 的方式执行
  • env.DB 是 Cloudflare 环境里的绑定,普通 Astro 预览不会自动提供

情况 2:要本地测试表单提交

如果你想本地测试:

POST /f/项目名

也就是想让表单真的经过 Cloudflare Pages Function,可以用 Wrangler:

npm run build
npx wrangler pages dev dist

如果你的 wrangler.toml 里已经配置了 D1:

[[d1_databases]]
binding = "DB"
database_name = "你的数据库名称"
database_id = "你的数据库 ID"

wrangler pages dev dist 通常会读取这个绑定。

也可以显式传入 D1:

npx wrangler pages dev dist --d1 DB=你的数据库ID

注意:本地 D1 也需要有 submissions_backup 表。否则即使绑定了 DB,插入数据也会失败。

本地建表 SQL 仍然是:

CREATE TABLE IF NOT EXISTS submissions_backup (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  project TEXT NOT NULL,
  data TEXT NOT NULL,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

本地没有 D1 会怎样

当前 Function 逻辑是:

有 env.DB:先写 D1 备份,再转发 OneSend
没有 env.DB:打印 D1 binding DB is missing.,然后继续尝试转发 OneSend

所以 D1 是询盘备份保险,不是页面渲染必需品。

但是正式上线时,仍然建议绑定 D1。否则 OneSend 或网络短暂异常时,询盘没有备份,容易丢单。


11. 第九步:上线后测试

网站部署到 Cloudflare Pages 后,打开:

https://你的域名/inquiry

提交一条测试询盘,例如:

Name: Test
Email: test@example.com
Product: Test Product
Message: This is a test inquiry.

正常情况应该是:

  1. 页面跳转到 /thank-you
  2. D1 数据库有一条新记录
  3. OneSend 后台也有一条新询盘

这三个条件要同时满足。尤其是第 2 和第 3 条:

D1 有记录 + OneSend 有记录 = 双层保障成功

如果只有 D1 有记录,说明询盘没有丢,但正式业务系统可能没有收到,需要继续排查 OneSend。

如果只有 OneSend 有记录,说明询盘进了业务系统,但没有备份成功,D1 防丢单保险没有生效。


12. 查询 D1 是否收到询盘

在 Cloudflare D1 控制台执行:

SELECT id, project, data, created_at
FROM submissions_backup
ORDER BY id DESC
LIMIT 10;

你应该能看到:

project = 你的项目名
data = 一整段 JSON 表单数据

如果 projectnewsite,说明 /f/newsite 路径是对的。

如果 projectcontact,说明你的表单可能提交到了:

/f/contact

D1 查询只是第一层检查。看到 D1 有记录以后,还要继续去 OneSend / 网站询盘后台查同一条测试询盘。


13. OneSend 后台检查

确认 OneSend 后台也收到同一条测试询盘。

如果 D1 有记录,但 OneSend 没收到,说明:

  • 表单没有丢
  • Cloudflare 已经备份成功
  • 问题在 OneSend 项目路径、Turnstile 校验、API 转发或 OneSend 后台配置

这时可以先从 D1 的 data 字段里手动补单。

如果 D1 和 OneSend 都有记录,才算正式通过测试。

建议测试时在 message 里写一个明显的标记,例如:

TEST 2026-07-02 newsite inquiry

这样你可以在 D1 的 data 里查到,也可以在 OneSend / 网站后台里查到同一条记录。


14. 推荐表单字段

基础字段:

字段名 含义 是否建议必填
name 客户姓名
email 客户邮箱
tel 电话或 WhatsApp
add 国家或地区
product 感兴趣产品
message 需求说明
REDIRECT_URL 成功跳转地址
honeypot 反垃圾字段
cf-turnstile-response Turnstile token 自动生成

工业品 B2B 网站建议在提示语里引导客户提供:

  • 产品类型
  • 容量
  • 尺寸或可用安装空间
  • 液体类型
  • 应用场景
  • 材料要求
  • 进出口尺寸
  • 阀门类型
  • 接头类型
  • 数量
  • 目的国家
  • 包装要求
  • 图纸、照片或项目现场条件

这样销售收到的不是模糊的 “price?”,而是可以直接报价的项目需求。


15. 常见问题

问题 1:提交后显示 503

可能原因:

  • D1 没有绑定
  • D1 binding 名字不是 DB
  • D1 表没有创建
  • Cloudflare Function 没有生效

优先检查:

Cloudflare Pages -> Settings -> Functions -> D1 database bindings

确认变量名是:

DB

问题 2:D1 有记录,但 OneSend 没有

说明表单已经被 Cloudflare 收到并备份了。

继续检查:

  • OneSend 是否配置了这个项目名
  • 表单 action 是否是 /f/正确项目名
  • Turnstile 域名是否加入允许列表
  • OneSend 后端是否正常

问题 3:OneSend 有记录,但 D1 没有

通常说明表单直接提交到了 OneSend,没有经过 Cloudflare Function。

检查表单 action 是否写错:

错误写法:

action="https://api.oneuie.me/f/newsite"

正确写法:

action="/f/newsite"

问题 4:Turnstile 显示异常

检查:

  • 表单里是否加载了 Turnstile 脚本
  • data-sitekey 是否正确
  • Cloudflare Turnstile 后台是否添加了当前域名
  • 本地测试时域名可能不在允许列表,建议以上线域名测试

问题 5:提交成功但没有跳转感谢页

检查表单里是否有:

<input type="hidden" name="REDIRECT_URL" value="/thank-you" />

还要确认:

src/pages/thank-you.astro

这个页面真实存在。


16. 新网站接入清单

每次新网站接入 OneSend,可以照这个清单做:

  • 添加 functions/f/[[path]].js
  • 找到网站现有 inquiry / contact / quote 表单
  • 确认 /thank-you 或其他成功页存在
  • 表单 method="POST"
  • 表单 action="/f/项目名"
  • 表单包含 REDIRECT_URL
  • 表单包含 Turnstile 组件
  • Cloudflare Pages 绑定 D1,变量名为 DB
  • D1 里有 submissions_backup
  • Turnstile 后台加入新域名
  • OneSend 后台确认项目名可用
  • 上线后提交测试询盘
  • D1 有记录
  • OneSend 有记录
  • 页面能跳转 thank-you

17. 最简版本

记住这个就够了:

1. 表单提交到 /f/项目名
2. functions/f/[[path]].js 接收
3. env.DB 写入 D1 备份
4. 转发到 https://api.oneuie.me/f/项目名
5. 成功后跳转 /thank-you

核心检查点:

表单 action 不能是 OneSend 完整域名
D1 binding 必须叫 DB
Turnstile 域名必须加白
OneSend 项目名必须对应