Astro Actions(内部 API)
ShopFaaS 内部业务 API 机制——Astro Actions,供贡献者参考。
本文面向贡献者(参与 ShopFaaS 核心代码贡献的开发者)。如果你是应用扩展开发者或第三方集成方,请参考 REST API 与 Webhook。
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:actions 的 defineAction:
| 基类 | 用途 | accept | handler 收到 |
|---|---|---|---|
defineFormAction(schema, handler) | 表单提交 | "form" | 校验后的对象 |
defineJsonAction(schema, handler) | 客户端 fetch 异步调用 | "json" | 校验后的对象 |
基类统一提供:
- 输入校验:Zod schema 内联中文错误消息
- 调试日志:自动记录原始输入、校验失败、执行异常
- 错误处理:未知异常转为 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 规则
- 从
./schemaimport 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.ts 的 enforceActionAuth() 在 next() 之前执行:
- 白名单:仅
auth.login是公开 Action - 其余所有 Action 必须携带有效 Bearer Token
新增公开 Action
若新增公开 Action(如注册、找回密码),必须将 action 名称加入 src/middleware.ts 的 PUBLIC_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: "..." }