跳到主内容

Astro Actions(内部 API)

ShopFaaS 内部业务 API 机制——Astro Actions,供贡献者参考。

本文面向贡献者(参与 ShopFaaS 核心代码贡献的开发者)。如果你是应用扩展开发者或第三方集成方,请参考 REST APIWebhook

Astro Actions 是 ShopFaaS 内部业务 API 的首选方式,提供类型安全 RPC、Zod 校验、统一错误处理。

何时使用 Actions

场景推荐
表单提交✅ Actions(defineFormAction,零 JS 即可工作)
客户端异步调用✅ Actions(defineJsonAction
对外公开 REST API❌ 用 Astro API Routes(src/pages/**/*.ts
二进制文件上传❌ 用 Astro API Routes(request.formData()
Webhook 回调❌ 用 Astro API Routes

基类

使用 src/boots/actions 基类(defineFormAction / defineJsonAction),禁止直接使用 astro:actionsdefineAction

基类用途accepthandler 收到
defineFormAction(schema, handler)表单提交"form"校验后的对象
defineJsonAction(schema, handler)客户端 fetch 异步调用"json"校验后的对象

基类统一提供:

  1. 输入校验:Zod schema 内联中文错误消息
  2. 调试日志:自动记录原始输入、校验失败、执行异常
  3. 错误处理:未知异常转为 ActionError 不泄漏堆栈

文件组织

_actions/ 采用子目录模式组织:

src/pages/admin/_actions/
├── _shared.ts                    # 公共工具
├── orders/                       # 订单模块
│   ├── index.ts                  # Action handlers
│   └── schema.ts                 # Zod schema(前后端共享)
├── products/                     # 商品模块
│   ├── index.ts
│   └── schema.ts
└── settings/                     # 设置模块
    ├── index.ts
    └── schema.ts

schema.ts 规则

  • 必须为纯 Zod 定义
  • 禁止 import 任何 #/boots/* 或含 Node 内置模块的工具(避免污染客户端 bundle)
  • 前端 .tsx 直接 import { xxxInputSchema } 复用,禁止内联重写

index.ts 规则

  • ./schema import schema,不得内联定义 Zod schema
  • 使用 defineFormAction / defineJsonAction(来自 #/boots/actions

示例

1. 定义 schema

// src/pages/admin/_actions/orders/schema.ts
import { z } from "astro/zod";

export const refundInputSchema = z.object({
  orderId: z.uuid({ message: "订单 ID 格式错误" }),
  amount: z.number().positive({ message: "退款金额必须大于 0" }),
  reason: z.string().min(1, { message: "退款原因不能为空" }),
});

export type RefundInput = z.infer<typeof refundInputSchema>;

2. 定义 Action

// src/pages/admin/_actions/orders/index.ts
import { defineJsonAction } from "#/boots/actions";
import { refundInputSchema } from "./schema";
import { assertPermission } from "#/pages/admin/_actions/_shared";
import { authenticateRequestFlexible } from "@shopfaas/boots-core/auth";
import { NotFoundError, BusinessError } from "@shopfaas/boots-core/errors";

export const refund = defineJsonAction(refundInputSchema, async (input, ctx) => {
  const authCtx = await authenticateRequestFlexible(ctx);
  await assertPermission(authCtx, input.shopId, "order:refund");

  const order = await db.selectFrom("orderDetail").selectAll().where("id", "=", input.orderId).executeTakeFirst();

  if (!order) {
    throw new NotFoundError("订单", input.orderId);
  }

  if (order.status !== "PAID") {
    throw new BusinessError("仅已支付订单可退款");
  }

  // 退款业务逻辑...

  return { success: true, refundId: "uuid-1" };
});

3. 挂载到 server

// src/actions/index.ts
import * as ordersActions from "#/pages/admin/_actions/orders";

export const actions = {
  orders: ordersActions,
};

4. 前端调用

表单提交(PRG 模式)

---
import { actions } from "astro:actions";
const result = Astro.getActionResult(actions.orders.refund);
---

<form method="POST" action={actions.orders.refund}>
  <input name="orderId" type="hidden" value={order.id} />
  <input name="amount" type="number" step="0.01" />
  <textarea name="reason"></textarea>
  <button type="submit">提交退款</button>
</form>
{result && <p>{result.data?.success ? "退款成功" : "退款失败"}</p>}

客户端异步调用

import { actions } from "astro:actions";

async function handleRefund() {
  const { data, error } = await actions.orders.refund({
    orderId: "uuid-1",
    amount: 99.5,
    reason: "商品损坏",
  });

  if (error) {
    toast.error(error.message);
    return;
  }

  toast.success("退款成功");
}

Action 认证

src/middleware.tsenforceActionAuth()next() 之前执行:

  • 白名单:仅 auth.login 是公开 Action
  • 其余所有 Action 必须携带有效 Bearer Token

新增公开 Action

若新增公开 Action(如注册、找回密码),必须将 action 名称加入 src/middleware.tsPUBLIC_ACTIONS 白名单。

Zod v4 注意事项

ShopFaaS 使用 Astro 内置的 Zod v4(import { z } from "astro/zod"):

  • 必须用 v4 写法z.email() / z.url() / z.uuid() / z.ip() 顶层 API
  • 禁止新代码用 v3 写法(z.string().email() 等)
  • 错误消息:统一用 { message: "..." }

相关文档

此页面有帮助吗?

在 GitHub 上编辑此页