内容
# minshop
一个小型的、完全基于Cloudflare的电子商务商店:前台 + 后台 + Stripe支付,运行在Cloudflare Workers上。没有预设品牌 - 商店名称是一个配置值,因此可以作为模板克隆。
它的设计目的是轻量级和低成本运行:Cloudflare的免费层可以覆盖直到实际流量产生,只有Stripe的每次销售费用是硬成本。
## 功能
- **前台** — 服务器渲染的产品列表 + 产品详情,近乎零客户端JS
- **购物车** — 基于cookie(无客户端JS),具有“添加到购物车”和一键“立即购买”功能
- **搜索** — FTS5全文搜索(bm25,前缀匹配, typo-correct),无JS — 或通过Workers AI + Vectorize的**语义搜索**,只需一个配置开关(参见 [搜索](#搜索))
- **分类** — 嵌套(任意深度树),与产品多对多;前台分类页面带有面包屑 + 子分类下钻(递归后代查询)
- **后台** — 产品CRUD(创建/编辑/删除)与图像上传,以及最近订单视图
- **支付** — Stripe Checkout(托管,单项或多项)与签名验证的webhook,记录订单 + 明细项目 — 或 **比特币闪电网络**(phoenixd / LNbits自行托管,或OpenNode托管),只需一个配置开关(参见 [支付](#支付))
- **运输** — Stripe Checkout收集运输地址 + 提供可配置的费率(固定费率 + 免费超过阈值);地址和费用被记录到订单上
- **折扣代码** — 结账时的促销代码字段(配置中切换);代码在Stripe仪表板中创建/管理,应用的折扣被记录到订单上
- **税** — 通过Stripe Tax(默认为关闭 — **在仪表板中激活Stripe Tax**)销售税/增值税;根据客户地址计算并记录到订单上
- **订单邮件** — 在`EmailProvider`接口后面带有两个适配器:**Resend**(HTTPS API,在Workers免费计划上工作)或 **Cloudflare Email**(绑定,付费计划);通过配置门控(配置前关闭,配置后是一个标志)
- **图像** — 上传到R2,通过应用程序提供(无需公共存储桶)
- **可交换的接口** — 支付和存储位于接口后面(参见 [架构](#架构))
## 技术栈
| 组件 | 选择 |
|---|---|
| 框架 | [Astro](https://astro.build)(SSR)通过`@astrojs/cloudflare`在Cloudflare Workers上 |
| 样式 | Tailwind CSS v4(`@tailwindcss/vite`) |
| 数据 | Cloudflare D1(SQLite)— 产品,订单 |
| 图像 | Cloudflare R2 — 零出站流量 |
| 支付 | Stripe Checkout,或比特币闪电网络(phoenixd / LNbits / OpenNode) |
## 快速开始(本地)
> **需要Node ≥ 22.12** — 使用Node 22,这是测试/支持的发布线。
```sh
npm install
# 一次性:构建,迁移 + 播种本地数据库,并生成两个本地
# 机密(SECRETS_KEK + AUTH_SECRET)到.dev.vars
npm run provision:local -- --seed
npm run dev # http://localhost:4321
```
前台位于 `/`,后台位于 `/admin` — 第一次访问会进入 **设置向导**。无需手动填写提供商机密:支付/电子邮件/Turnstile密钥在 **后台 → 设置** 中粘贴,并存储在D1中加密(参见 [设置](#设置))。还存在细粒度的脚本(`db:migrate`,`db:seed`),`npm run preview` 在生产模式下运行构建的Worker(后台门控活动),而 `npm run destroy:local` 重置本地商店。
### 测试
```sh
npm test # vitest运行(纯逻辑单元测试)
npm run test:watch
npm run test:d1 # 刷新迁移 + 播种 + 构建的Worker对抗隔离的D1
```
涵盖纯函数 — `slugify`,FTS搜索净化器 + 编辑距离,`parseProductForm`,图像验证,购物车计数,预订目标聚合,订单编号方案,Access-JWT验证器,分页限制,以及白名单的`orderByClause`排序构建器(可排序表的SQL注入边界)。`npm run test:d1` 添加了预订并发/释放/结算/旧兼容性的干净室D1门控,然后启动生产Worker对抗隔离数据库,并运行演示结账到付费订单结算和确认。`npm run verify` 运行两个套件,完整的Astro诊断,生产构建和MCP类型检查/部署干运行。
### 本地测试支付
在 **后台 → 设置 → 支付 → 卡(Stripe)** 中粘贴您的 `sk_test_…` 密钥,然后使用 [Stripe CLI](https://docs.stripe.com/stripe-cli) 将Stripe事件转发到您的开发服务器:
```sh
stripe listen --forward-to localhost:4321/api/webhook
# 将打印的whsec_…粘贴到设置 → 支付 → 卡(Stripe)→ webhook签名密钥
stripe trigger checkout.session.completed
```
订单显示在 `/admin` 中(和D1中:`npx wrangler d1 execute minshop-db --local --command "SELECT * FROM orders"`)。
## 部署
### 一键部署(部署到Cloudflare)
[](https://deploy.workers.cloudflare.com/?url=https://github.com/ddyy/minshop)
分叉仓库,配置D1(`minshop-db`)+ R2(`minshop-images`),应用迁移,并部署 — 仅限免费计划资源。然后在 `/admin/setup` 完成上线(如下)。可选的付费附加组件(图像,语义搜索,Cloudflare Email)在 `wrangler.jsonc` 中注释 — 取消注释 + 重新部署。
**部署表单显示的字段:
- **`SECRETS_KEK`,`AUTH_SECRET`** (掩码,必填)— 您必须设置的两个。 在每个字段中粘贴一个新的随机值(它们必须不同):运行 `openssl rand -base64 32` 两次。 按钮无法生成它们,由于字段被掩码,因此它们的提示被隐藏 — 因此在它们上面有一个前导标签字段, **`USE_A_LONG_RANDOM_STRING_FOR_SECRETS_KEK_AND_AUTH_SECRET`** ,带有上面的指令(其自己的值是未使用的填充)。 留空,商店仍然运行:未设置的 `SECRETS_KEK` 保持支付密钥库休眠(仅演示结账),未设置的 `AUTH_SECRET` 禁用客户帐户 — 两者都可以在以后配置。
- **位置提示** — Cloudflare自己的字段,用于指定D1数据库的位置;选择最近的购物者区域。
- **无商店名称/时区/搜索字段** — 这些是运行时设置,您在设置向导和 **后台 → 设置** 中配置(存储在D1中),而不是在部署时。 默认值直到然后:`我的商店` / `UTC` / 关键字(FTS)搜索。
### CLI(一键部署)
**一键部署** — 配置一个全新的,完全独立的实例(它自己的D1,R2,Vectorize索引,Worker)并设置两个Worker机密:
```sh
npx wrangler login
npm run provision:cf my-store # scripts/provision-cf.sh <slug>
```
或 **手动**:
```sh
npx wrangler login
# 配置真实资源。 提交的wrangler.jsonc声明D1/R2按名称
# 没有id(因此一键部署/ Workers构建自动配置它们);对于
# 手动部署,添加打印的database_id到“DB”条目 — 或让
# `wrangler deploy` 创建它。
npx wrangler d1 create minshop-db
npx wrangler r2 bucket create minshop-images
npm run db:migrate:remote # 将迁移/应用到生产数据库
# 仅限两个Worker机密。 所有其他内容(Stripe,OpenNode,Lightning,
# Resend,Turnstile密钥及其配置)都在后台 → 设置中输入
# 并存储在D1下加密的SECRETS_KEK。
openssl rand -base64 32 | npx wrangler secret put AUTH_SECRET # 签署会话
openssl rand -base64 32 | npx wrangler secret put SECRETS_KEK # 加密密钥库
npm run deploy # astro build && wrangler deploy
```
然后打开网站 — 它会进入 **设置向导**:设置管理员密码(需要完成;在此之前 `/admin/setup` 是开放的,因此请立即执行或在 `/admin` 前面加上Access),然后在 **设置 → 支付** 中粘贴您的支付密钥。 最后,将 **生产Stripe webhook** 指向 `https://<your-host>/api/webhook/stripe` (事件:`checkout.session.completed`,`checkout.session.async_payment_succeeded`,`checkout.session.async_payment_failed` 和 `checkout.session.expired`;异步成功实现延迟方法,而失败/过期释放库存)并在同一卡中粘贴其 `whsec_…` 签名密钥。 使用 `npm run destroy:cf <slug>` 拆除实例;使用 `npm run reset:remote` 在原地重置其数据。
## 后台认证
`/admin` **和** `/api/admin/*` 都由 [`src/middleware.ts`](src/middleware.ts) 保护。 您可以通过以下两种方式之一进行身份验证:
- **设置向导密码(内置)。** 在新商店中,**`/admin/setup`** 上的设置向导可访问,因此您可以创建管理员密码;它存储为D1中的PBKDF2哈希(**没有 `ADMIN_PASSWORD` 环境变量**)。 保存后,门控激活:`/admin` + `/api/admin` 需要在 **`/admin/login`** 登录(发布带有HMAC签名,HttpOnly会话cookie的表单;`/api/admin` 返回401而不是重定向),保存后立即登录。 **首次运行是开放的** — 直到存在密码,`/admin/setup` 是公开可访问的,因此请在公共部署上立即设置(或在 `/admin` 前面加上Access)。
- **后台登录表单上的可选Turnstile**:在 **后台 → 设置 → 机器人保护** 中启用它(切换 + sitekey,带有存储在加密保险库中的密钥)以在检查密码之前添加Cloudflare的机器人挑战。 相同的切换也控制客户账户登录。 Cloudflare记录的始终通过测试密钥适用于本地试验。
- **Cloudflare Access(推荐用于生产)。** 边缘SSO/MFA,无应用密码,它也保护首次设置。 在Zero Trust仪表板中添加一个 **自托管应用程序**,涵盖两个路径 `your-host/admin` *和* `your-host/api/admin` (API是一个单独的路径 — 仅保护 `/admin` 会使突变端点开放),添加身份提供者(一次性PIN码就足够),和允许您的电子邮件的Allow策略。 在`wrangler.jsonc`变量中设置 `CF_ACCESS_TEAM_DOMAIN` 和 `CF_ACCESS_AUD`;它们是 **Access模式所必需的**,因此中间件可以验证每个JWT签名(零依赖,Web Crypto — 参见 [`src/features/auth/access.ts`](src/features/auth/access.ts))。 只要存在任一变量,除非设置了两个变量并提供了有效的断言,否则请求将关闭 — 包括直接的 `*.workers.dev` 请求,没有头部。 (使用Access *或* 向导密码;如果设置了密码,则密码登录优先。)
**忘记管理员密码?** 哈希存储在D1数据库中,因此商店所有者可以始终重置它 — 清除它会使商店恢复到开放的设置向导:
```bash
npm run admin:reset:remote # 部署商店(或:npm run admin:reset 本地)
```
然后重新加载 `/admin/setup` 并设置一个新的。 (与运行 `DELETE FROM settings WHERE key='admin_password_hash';` 在Cloudflare仪表板 → D1控制台相同。) 它需要Cloudflare账户访问,因此它不是公共重置 — 如果您运行Cloudflare Access,则永远不会被锁定。
本地开发(`astro dev`)绕过门控,因此您永远不会被锁定。 不要将 `/admin` 设为“秘密”路径 — 那是安全通过模糊性实现的安全性;门控保护它。
Worker还对匿名登录POST(每分钟10次,限于每个商店,每个路由和连接的客户端)和结账/发票POST(每分钟20次)应用本地边缘速率限制。 Webhooks和身份验证的管理员API被故意排除;提供者签名和管理员门控保护这些路径而不破坏合法的重试或批量管理。 速率限制在配置模板中声明为 `AUTH_RATE_LIMITER` 和 `CHECKOUT_RATE_LIMITER` 绑定;现有的手动维护的Wrangler配置需要相同的 `ratelimits` 块。
## 支付
结账和webhook依赖于 `PaymentProvider` 接口(`src/features/payments`)。 每个轨道都在 **后台 → 设置 → 支付** 中配置 — 其密钥进入加密保险库,其配置(默认轨道,节点URL)在D1设置中;一旦设置了其密钥,轨道的结账按钮将自动出现,而始终开启的 **演示** 结账功能无需密钥即可工作。 默认轨道选择器选择首先提供的轨道:
| 轨道 | 它是什么 | 设置(设置 → 支付) |
|---|---|---|
| `stripe` *(默认)* | 主机卡结账 | 粘贴密钥 + webhook签名密钥 |
| `lightning` | 通过 **自托管** 节点比特币闪电网络 — minshop铸造BOLT11发票并渲染其自己的 `/pay` 页面(QR + `lightning:` 链接),然后确认结算 | 运行 **phoenixd** 或 **LNbits** (如下);输入其URL + 密钥 |
| `opennode` | **托管** 闪电结账(托管处理器) — 重定向 + webhook,像Stripe一样 | 粘贴OpenNode API密钥 |
两个端口,嵌套:外部 `PaymentProvider` (stripe / lightning / opennode)和内部 `LightningBackend` (phoenixd / lnbits)由自渲染流程共享。 添加另一个节点是一个适配器文件。
**如何信任结算。** 闪电网络webhook被视为不可信的 *提示* — 接收后,minshop **重新轮询节点** 以获取支付(权威),因此伪造的webhook无法伪造销售。 `/pay` 页面也通过轮询结算,因此即使没有公共webhook,它也适用于本地开发。 订单保持“仅付费”:未支付的发票存储在 `pending_payments` 表中,从不存储在 `orders` 中。
**运输(闪电网络)。** 当 `shipping.enabled` 时,闪电网络购物车通过minshop自己的 `/checkout` 页面路由 — 服务器渲染的地址 + 运输选项步骤(基于区域的费率来自 `cfg.shipping`,参见 [运输](#运输--订单邮件配置))— 因此发票总金额包括运输费,地址 + 电子邮件登录订单,与Stripe相同。 (Stripe保留其自己的托管地址/运输收集,不变。)
**限制(闪电网络)。** **税** 和促销代码仍然是Stripe Checkout功能,在闪电网络路径上跳过 — 要对非Stripe轨道收取税,您需要自己计算(例如Stripe的Tax *计算* API)并将其添加到总金额中。 没有自动退款(闪电网络无法原路返回)。 发票以厘为单位,从结账时获取的BTC现货汇率(`payments.lightning.rateUrl`,默认为Coinbase — 无密钥)。
### 运行phoenixd(推荐自托管后端)
[phoenixd](https://phoenix.acinq.co/server) 是ACINQ的自托管无头守护程序 — 无完整比特币节点,无手动通道管理(ACINQ是LSP),主网(真实厘)。
1. 在始终在线的主机(小型VPS / Pi)上运行 `phoenixd`。 首次启动写入 **种子** 到 `~/.phoenix/` — 备份它。
2. 通过HTTPS将API(`127.0.0.1:9740`)暴露给Worker — **Cloudflare Tunnel** (`cloudflared`)是最干净的,无需开放端口。
3. 在 **设置 → 支付 → 闪电网络** 中,选择phoenixd节点,输入其公共URL,并粘贴密码 — 使用 **`http-password-limited-access`** 值(`~/.phoenix/phoenix.conf`):仅接收,不能消费。
4. 将phoenixd的 `webhook-url` 指向 `https://<your-host>/api/webhook/lightning`。
**LNbits** 也是如此:选择LNbits并输入其URL + 钱包的*发票/读取密钥*。 **OpenNode**:在卡中粘贴API密钥并将默认轨道设置为OpenNode。
## 架构
组织为 **功能文件夹** (垂直切片) — 每个拥有自己的数据访问,类型和组件。 删除文件夹仍然可以构建。
```
src/
config.ts 商店名称 + 功能开关
layouts/Layout.astro
features/
products/ 数据库 · 表单 · 图像 · ProductForm.astro
orders/ 数据库
payments/ 提供者(端口)· stripe(适配器)· 索引(工厂)
storage/ 提供者(端口)· r2(适配器)· 索引(工厂)
pages/
index.astro 前台
product/[id].astro
admin/ 后台UI(CRUD)
images/[...key].ts 提供R2对象
api/ 结账 · webhook · 后台产品端点
```
两个 **端口和适配器缝隙** 将供应商代码放在边缘:
- **`PaymentProvider`** — `checkout.ts` / `webhook.ts` 依赖于接口,而不是Stripe。 换成Paddle / Lemon Squeezy是一个新的适配器文件。
- **`StorageProvider`** — 应用程序按密钥存储和提供图像;R2是一个适配器。
故意 **没有运行时插件系统** — Workers在构建时捆绑,因此组合只是导入 + 工厂。
## 数据库迁移
模式位于 `migrations/` 中,作为编号的附加SQL文件(D1跟踪哪些已运行),因此部署仅应用新的文件。要演化模式,请添加一个新文件 — 永远不要编辑已应用的文件:
```sh
npx wrangler d1 migrations create minshop-db add_product_sku # 编写migrations/0002_...
# 编辑它(ALTER TABLE / CREATE TABLE — 附加的,无破坏性的DROP)
npm run db:migrate # 本地
npm run db:migrate:remote # 生产环境
```
`seed.sql` 是仅用于开发的示例数据(可重新运行),与模式分开保存。
## 设置
大多数设置都是**运行时**的,可以在 **`/admin/settings`** 中修改,存储在 D1 的 `settings` 表中,立即生效,无需重新部署。密钥存储在同一张表中,使用 `SECRETS_KEK` Worker 密钥(仅限从 UI 端写入,绝不会回显),这使得实例可以从仪表盘完全配置和克隆:
- **常规** - 存储名称,购物车/结账开关,"立即购买"开关。
- **支付** - 每个支付渠道(Stripe / Lightning / OpenNode / 示例):启用/禁用开关,配置(节点 URL,默认渠道)和密钥。
- **电子邮件** - 启用/禁用,服务提供商(Resend / Cloudflare),发件地址,密钥,发送测试按钮。
- **机器人保护** - Turnstile 开关 + 站点密钥 + 密钥。
- **搜索** - 关键词(FTS5)与语义(Workers AI + Vectorize)+ 索引。
少数**数据耦合**的设置在编译时生效(更改 + `npm run deploy`):
| 设置 | 位置 | 效果 |
|---|---|---|
| 货币 | `src/config.ts` 中的 `currency` | 全局货币 - 价格存储为与之绑定的整数小单位,因此无法在运行时安全地更改 |
| 图片优化 | `src/config.ts` 中的 `images.optimizeOnUpload` / `images.maxWidth` | 默认关闭($0)。开启时,将上传的图片缩放到 `maxWidth` 像素 + WebP(通过 Cloudflare Images,**付费**功能;本地开发时不转换 - 恢复为原始图片) |
| 订单号 | `src/config.ts` 中的 `orderNumber.{offset,step,randomStep}` | 用户友好的订单编号,基于内部 ID(例如 `#1000`)。`step` 间隔订单编号;`randomStep` 添加随机偏移以隐藏订单数量(保持 `step > randomStep`)。URL/安全使用随机 `public_id,而非此编号 |
| 图标 | 替换 `public/favicon.svg` | 浏览器标签页图标 |
更改一个值,然后运行 `npm run deploy`。货币使用 `Intl.NumberFormat`,因此 `usd → $`,`eur → €`,`gbp → £`,`jpy → ¥`(每种货币处理小数)。
### 运费(配置)& 订单电子邮件(仪表盘)
- **`shipping`** - 存放在 `src/config.ts` 中:`enabled` + 目的地 **`zones`** (与服务提供商无关,参见 `features/shipping/calculator`)。每个区域有 `countries`(ISO alpha-2,或 `['*']` 通配符,按从上到下匹配),`rates`(标签 + 金额),和 `freeOverCents`(一个 "$0" "免费运费" 选项,当订单金额达到指定阈值时启用;`null` 禁用)。同一个引擎为 **Stripe Checkout** 选项和 **Lightning** `/checkout` 总金额提供支持,因此两个渠道收取相同的运费。(Stripe 显示一个静态列表 - 第一个区域的费率 - 因为它在收集地址后;Lightning `/checkout` 页面根据输入的国家/地区准确显示区域费率。)
- **电子邮件** - 在 **管理后台 → 设置 → 电子邮件** 中配置(启用/禁用,提供商,发件地址;密钥存储在加密保险库中)。如果未配置,系统会安全地静默失败 - 结账仍然成功。**发送测试电子邮件** 按钮验证实际投递。两个提供商:
- **Resend** (默认,**在 Workers 免费计划中可用** - 一个纯 HTTPS 调用):在 [resend.com](https://resend.com) 获取一个免费密钥,将其粘贴到设置 → 电子邮件中,并设置发件地址(一个 Resend 验证的域名,或 `onboarding@resend.dev` 用于测试发送到您自己的地址)。
- **Cloudflare(Workers 付费计划)**:`wrangler email sending enable <yourdomain.com>`(`EMAIL` 绑定已经声明),然后在设置 → 电子邮件中选择 Cloudflare,并设置发件地址为该域的子域名。该部分标志指示绑定是否已连接。
## 主题
品牌颜色、强调色、字体和圆角半径是 `src/styles/global.css` 中的**设计令牌**(一个 Tailwind v4 `@theme` 块)- 通过编辑几个值来重新品牌化克隆,无需更改组件:
```css
@theme {
--color-brand: #111827; /* 实心按钮、聚焦环 */
--color-brand-hover: #374151; /* 其悬停色 */
--color-accent: #2563eb; /* 文本链接 */
--font-sans: ...;
}
```
这些生成整个 UI 中使用的 `bg-brand` / `border-brand` / `text-accent` 实用程序,因此更改一个值会一次性重新设计所有按钮和链接。没有运行时模板或主题引擎:组件是模板,令牌是旋钮。
## 注意事项
在构建过程中收集的一些经验(可能会花费一个下午):
- **Node ≥ 22.12 需要** - 使用 Node 22,经过测试/支持的发布线。
- **绑定通过 `import { env } from 'cloudflare:workers'`** - `Astro.locals.runtime.env` 已在 Astro v6 中删除。
- **`wrangler.jsonc` 中没有 `main` / `assets`** - Cloudflare 适配器提供工作线程入口本身;设置 `main` 会破坏干净的构建。
- **默认启用 CSRF** - Astro 拒绝跨源表单 POST(403)。真正的浏览器发送 `Origin` 并通过;仅限脚本请求需要设置它。
- **Webhook 需要 `constructEventAsync`** - 同步验证器使用 Node crypto,在 Workers 上不可用。
- **一次只能有一个本地开发服务器** - 两个 `astro dev` 实例共享同一个 `.wrangler` 本地 D1 状态并竞争,表现为短暂的 `no such table` 错误。使用一个服务器,或传递一个单独的持久化目录。
- **`wrangler d1 export` 失败,带有 FTS5 虚拟表** - D1 无法导出具有虚拟表的数据库。要备份:删除 `products_fts`,导出,然后重新创建它(重新运行迁移 `0003`)。
- **FTS5 `MATCH` 在原始输入上抛出** - 特殊字符(`-`,`"`, `:`,`*`)导致 `fts5: syntax error`。在查询前将令牌清理为字母数字前缀(参见 `features/products/search.ts`)。
## 搜索
产品搜索位于 `SearchProvider` 接口(`features/search`)后面,具有一个单行开关,`search.provider` 在 `src/config.ts` 中(或 `SEARCH_PROVIDER` 变量):
- **`fts`** (**默认**)- SQLite **FTS5** 关键词搜索:$0,完全本地,精确匹配 + 错误纠正("did you mean")。无需设置。
- **`vector`** - 通过 **Workers AI** 嵌入 + **Vectorize** 进行**语义**搜索:按**含义**匹配("something to drink coffee from" → 杯子),并支持相似度。需要 AI + Vectorize 绑定,因此:
```sh
# 1. 创建索引(维度必须与模型匹配 - bge-base = 768)
wrangler vectorize create minshop-products --dimensions=768 --metric=cosine
# 2. 在 wrangler.jsonc 中取消注释 "ai" + "vectorize" 绑定
# 3. 打开它:SEARCH_PROVIDER=vector (变量)- 或 store.config.ts 中的 search.provider
# 4. 回填现有产品:POST /api/admin/search/reindex (/admin/settings 中的按钮)
```
相同的嵌入也支持产品页面上的 **"您可能还喜欢"** - 语义相似度(产品在 Vectorize 中的最近邻居),如果关闭向量搜索,则降级为基于类别的相关产品。当启用时,产品在创建/更新时会自动嵌入(删除时也会自动删除)。如果选择 `vector` 但绑定丢失,它会**回退到 FTS** 而不是崩溃。无论如何,FTS 仍然可用 - 保留它用于精确/SKU 查找。成本:Workers AI 按推理付费(便宜的嵌入,免费每日分配)和 Vectorize 按使用付费 - 因此语义搜索用严格的 $0 换取基于含义的匹配。
## 账户(无密码客户登录)
通过**魔术链接**登录提供可选的客户账户 - 无密码,因此无需哈希/重置/泄露责任。默认关闭(`features.accounts`);访客结账不受影响(登录仅用于订单历史)。启用时,需要 `AUTH_SECRET` 密钥(签名登录令牌 + 会话cookie)和配置的电子邮件(发送链接)。
它的工作原理:`/account/login` 会发送一个一次性、15 分钟的链接到 `/account/verify`,后者会设置一个 30 天的签名会话cookie,然后 `/account` 会列出客户的订单(按其验证的电子邮件查询 - 已经存储订单,因此几乎没有新的架构)。重用 Web-Crypto HMAC 令牌基元和 `EmailProvider` 接口;换成 OAuth 需要替换 `features/auth/customer.ts`。
```sh
# 启用:store.config.ts 中的 features.accounts: true
# 需要:AUTH_SECRET Worker 密钥(设置脚本会自动设置)
# + 在管理后台 → 设置 → 电子邮件中配置电子邮件(以发送链接)
```
在本地开发过程中,魔术链接也会记录到服务器控制台,因此您可以测试电子邮件投递。
## MCP 服务器(从助手操作商店)
`mcp/` 是一个**独立的 Cloudflare Worker** (Cloudflare Agents SDK / `McpAgent`),它将商店操作作为 MCP 工具公开,以便像 Claude 这样的助手可以对话式地运行商店 - "本周收入是多少?", "标记订单 1142 已发货", "创建一个产品"。它绑定与商店前端相同的 **D1**,并重用 `features/*/db.ts`(无重复逻辑)。
**为什么是单独的 Worker** (自己的 `mcp/package.json`,自己的 `node_modules`):Astro 适配器拥有商店前端 Worker 的入口,Agents SDK 拉取 workerd/miniflare 依赖项,这些依赖项会干扰 Astro 的构建,如果提升到根目录。保持它为 sibling Worker 可以隔离两者。
**工具:** `list_products`,`get_product`,`list_orders`,`get_order`,`order_stats`,`daily_totals`(读取)+ `create_product`,`update_product`,`fulfill_order`(写入)。
**身份验证:** 一个 bearer token。设置 `MCP_TOKEN`;客户端发送 `Authorization: Bearer <token>`。如果未设置,服务器返回 503(失败关闭)。(Cloudflare 的 Workers OAuth 提供者是用于范围/多用户访问的升级版。)
```sh
cd mcp && npm install && cd ..
cp mcp/.dev.vars.example mcp/.dev.vars # 设置 MCP_TOKEN
npm run mcp:dev # 本地,与商店前端共享本地 D1
npm run mcp:check # 构建的干运行
# 部署:wrangler secret put MCP_TOKEN --config mcp/wrangler.jsonc ; npm run mcp:deploy
```
在生产环境中,两个 Worker 绑定相同的 D1(按 `database_id`),因此 MCP 服务器自动操作**真实**的商店。连接任何 MCP 客户端到 `https://<your-mcp-host>/mcp`(可流式传输的 HTTP)并带有 bearer 头。注意:写入工具(创建/履行)会更改实时数据 - 像密码一样保护令牌。
## 代理 API(目录 + 结账)
一个小型、**公开**、机器可读的 API,以便代理可以**浏览和购买**,无需刮擦 HTML 或驱动浏览器 - 代理"购物商店"的对应部分,用于 MCP 服务器的"代理操作商店"。干净的 JSON,绝对 URL,开放 CORS。
| 方法 & 路径 | 目的 |
|---|---|
| `GET /api/products` | 活动目录。`?q=` (使用活动**搜索**后端 - 语义开启时),`?limit=` (1-100,默认 24),`?offset=` |
| `GET /api/products/:slug` | 一个产品作为 JSON(如果丢失/非活动状态,则 404) |
| `POST /api/checkout` | 程序化结账。 正文 `{ "items": [{ "slug", "quantity" }] }` → `{ checkout_url, … }` |
每个产品都是自我描述的,价格以主要 + 小单位表示:
```json
{ "slug": "merino-wool-beanie", "name": "Merino Wool Beanie",
"price": { "amount": 32, "cents": 3200, "currency": "USD" },
"in_stock": true, "categories": ["Apparel"],
"image": "https://…/images/products/merino-wool-beanie.webp",
"url": "https://…/product/merino-wool-beanie" }
```
`POST /api/checkout` 重用与商店前端相同的 Stripe/OpenNode 会话(适用运费/税/折扣),并返回一个 `checkout_url`,代理将该 URL 提供给人类进行支付 - minshop 仅限于 *"生成支付链接"*, 而非 *"代表用户转移资金"*,因为代理支付标准尚未确定。浏览器表单结账功能保持不变;JSON 路径仅在 `Content-Type: application/json` 时触发。
### 演示 - 代理购物商店
```sh
node scripts/agent-demo.mjs https://<your-host> "warm hat" 40
# 搜索 "warm hat" 低于 40:3 个库存候选(s)
# USD 32 Merino Wool Beanie [merino-wool-beanie] ← 选择(在预算内最相关)
# → 打印完整的 Stripe 结账 URL
```
或者直接询问任何使用工具的 LLM:
> "使用 `<host>/api/products`,查找一个暖帽,价格低于 $40,然后使用 `{items:[{slug,quantity}]}` 调用 `POST /api/checkout`,并给我结账 URL."
使用 Stripe **测试**卡 `4242 4242 4242 4242` 完成支付(任意未来到期日期/CVC/邮政编码)- 生产环境运行 Stripe 测试密钥,因此**不会产生任何费用**;订单随后出现在 `/admin → Orders` 中。
> **注意事项:** 结账 URL 的 `#` 片段携带会话令牌 - 复制/打印它 **整个** 链接。截断的链接会产生 Stripe 的 "出了点问题"。
## 成本
Cloudflare 免费层(Workers 100k 请求/天,D1 5 GB,R2 10 GB)可以轻松覆盖一个小型商店 - 有效地 **$0** 直到扩展。Stripe 仅按交易收费。
MCP 配置
以下是此 MCP Server 的配置信息,可直接复制到 Cursor 或其他 MCP 客户端中使用。
mcp.json
连接信息
猜你喜欢
暂无推荐服务器