跳到主要内容

4. 主从详情

主从表:订单与订单明细的事务处理

前面几篇讲的都是单表操作。但实际业务中经常需要同时操作多张表——比如提交订单时,要同时创建订单主表和多条订单明细,而且必须保证要么全部成功,要么全部回滚。这篇以订单提交为例,介绍如何用 Backend Function 的独立端点实现主子表单的事务性提交。

本节你将学到

  • 什么是 Backend Function 独立端点

  • 如何使用事务保证数据一致性

  • 主子表单的数据提交模式

  • 前端如何调用 Backend Function

需求

实现一个订单提交页面,包含主表(客户信息、订单日期、备注)和明细表(多个商品及数量单价)。一次性提交,保证数据一致性,成功后返回订单号。最终效果:

新建订单页面

为什么需要 Backend Function?

普通的 create API 只能操作单张表,无法满足:

  1. 跨表操作 - 需要同时写入主表和明细表

  2. 事务保证 - 要么全部成功,要么全部回滚

  3. 业务逻辑 - 计算金额、校验库存等复杂逻辑Backend Function 独立端点 可以:

  • 在后端一次性完成多表操作

  • 使用事务保证数据一致性

  • 返回自定义的响应格式


AI 辅助开发(推荐)

**为什么 AI 能帮你做主从表事务?**没有 rabetbase,提交订单 = 写一个后端服务。后端同学选框架(Laravel?Spring Boot?Express?)、连数据库、写事务:BEGIN → INSERT 主表 → SELECT LAST\_INSERT\_ID\(\) → 批量 INSERT 明细 → COMMIT → ROLLBACK。事务逻辑写了一天。部署服务又一天:买服务器、装环境、配 nginx 反向代理、加 SSL。前端调用还得处理跨域、Token 转发。联调:前端发 JSON 后端取不到(content-type 没对),明细数组解析报错(格式不对),),事务超时回滚但前端没收到错误信息。一个"提交订单"按钮,从写后端到上线用了两周。有了 rabetbase:不用写后端服务、不用部署服务器。BaaS 的逆向引擎已分析出主从关系,BFF 运行时原生支持事务。CLI 的 bff new \-\-type ENDPOINT 创建脚手架(事务 API 就绪),Skill 指导 AI 实现正确的事务模式。bff push 推送到平台直接生效。不用买服务器、不用配 nginx、不用联调。1 小时搞定。

在 Claude Code 中输入:

用 rabetbase CLI 帮我创建一个订单提交的 Backend Function(ENDPOINT 类型),需要事务处理同时创建订单主表和明细表。客户信息包含客户名称、电话、订单日期、备注,明细包含商品名称、数量、单价。计算总金额,返回订单号和总金额。

AI 会做什么

AI 会使用 rabetbase CLI 自动完成以下工作:

  1. 查询订单和订单明细数据集结构(rabetbase dataset detail

  2. 创建 BFF 独立端点脚手架(rabetbase bff new \-\-type ENDPOINT

  3. 编写完整的事务处理逻辑(参数校验、金额计算、主子表创建)

  4. 推送到平台生效(rabetbase bff push)完成后前端即可通过 bff\.execute\(\) 调用。下方是 AI 生成的完整代码。


🔧 手动操作(备选)

方式一:手动 CLI

# 1. 创建端点脚手架
rabetbase bff new --type ENDPOINT --name submitOrder

# 2. 用编辑器打开生成的文件
code .rabetbase/bff/<appCode>/ENDPOINT/submitOrder.js

# 3. 推送
rabetbase bff push --yes --type ENDPOINT --name submitOrder


方式二:平台 UI

  1. 打开 Backend Function 管理页面:https://app\.lovrabet\.com/app/\{appCode\}/data/backend\-function

  2. 点击「新建」创建独立端点

  3. 粘贴代码并保存


步骤 1:完整代码参考

无论用 AI 还是手动,最终创建的 Backend Function 代码如下:函数名submitOrder

/**
* 提交订单 - Backend Function 独立端点
* 创建订单主表和明细表,使用事务保证数据一致性
*
* [接口路径] POST /api/{appCode}/endpoint/submitOrder
*
* [平台配置] https://app.lovrabet.com/app/{appCode}/data/backend-function
* 说明:appCode 为应用编码,创建后可在平台上配置此独立端点
*
* [HTTP 请求体参数]
* {
* "customerName": "客户名称",
* "customerPhone": "客户电话",
* "orderDate": "订单日期",
* "remark": "备注",
* "items": [
* { "productName": "商品A", "quantity": 2, "price": 100 }
* ]
* }
*
* [返回数据结构]
* ENDPOINT: 返回业务数据对象
* {
* "success": true,
* "orderId": "订单ID",
* "totalAmount": "订单总金额",
* "message": "订单创建成功"
* }
*
* @param {Object} params - 请求参数
* @param {string} params.customerName - 客户名称
* @param {string} params.customerPhone - 客户电话
* @param {string} params.orderDate - 订单日期
* @param {string} params.remark - 备注
* @param {Array} params.items - 订单明细
* @param {Object} context - 执行上下文(平台自动注入,调用时无需传递)
* @param {Object} context.userInfo - 当前用户信息
* @param {Object} context.client - 数据库操作入口
* @returns {Object} 返回提交结果
*/
export default async function submitOrder(params, context) {
const { customerName, customerPhone, orderDate, remark, items } = params;

// 数据集编码映射表
const TABLES = {
orders: "dataset_XXXXXXXXXX", // 数据集: 订单 | 数据表: orders(需替换为实际32位编码)
orderItems: "dataset_YYYYYYYYYY", // 数据集: 订单明细 | 数据表: order_items
};
const models = context.client.models;

// 1. 参数校验
if (!items || items.length === 0) {
throw new Error("订单明细不能为空");
}

// 2. 计算订单金额
let totalAmount = 0;
items.forEach((item) => {
const subtotal = item.quantity * item.price;
totalAmount += subtotal;
});

// 3. 使用事务创建订单(主表 + 明细表)
const result = await context.client.db.transaction(async (tx) => {
// 创建主订单(使用 context.client.models 而非 tx.models)
const orderId = await models[TABLES.orders].create({
customerName,
customerPhone,
orderDate,
remark,
totalAmount,
status: "pending",
});

// 创建订单明细
for (const item of items) {
await models[TABLES.orderItems].create({
orderId,
productName: item.productName,
quantity: item.quantity,
price: item.price,
subtotal: item.quantity * item.price,
});
}

return { orderId, totalAmount };
});

return {
success: true,
orderId: result.orderId,
totalAmount: result.totalAmount,
message: "订单创建成功",
};
}


代码写好后,推送到平台使其生效。如果是 AI 辅助开发,这一步已自动完成。手动推送:

# 检查状态
rabetbase bff status --format json

# 预览并推送
rabetbase bff push --dry-run --type ENDPOINT --name submitOrder --format json
rabetbase bff push --yes --type ENDPOINT --name submitOrder


关键点

  • context\.client\.db\.transaction\(\) 开启事务

  • 事务中使用 models\[TABLES\.xxx\] 而非 tx\.models\[TABLES\.xxx\]

  • 事务中任何异常都会回滚

  • 返回自定义的结果格式


步骤 2:创建前端表单组件

// src/pages/order-form.tsx

/**
* Title: 新建订单
*/
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { lovrabetClient } from "../api/client";

interface OrderItem {
productName: string;
quantity: number;
price: number;
}

interface OrderForm {
customerName: string;
customerPhone: string;
orderDate: string;
remark: string;
items: OrderItem[];
}

export default function OrderForm() {
const navigate = useNavigate();
const [form, setForm] = useState<OrderForm>({
customerName: "",
customerPhone: "",
orderDate: new Date().toISOString().split("T")[0],
remark: "",
items: [{ productName: "", quantity: 1, price: 0 }],
});
const [submitting, setSubmitting] = useState(false);

// 添加商品
const addItem = () => {
setForm({
...form,
items: [...form.items, { productName: "", quantity: 1, price: 0 }],
});
};

// 删除商品
const removeItem = (index: number) => {
if (form.items.length <= 1) {
alert("至少需要一条商品明细");
return;
}
setForm({
...form,
items: form.items.filter((_, i) => i !== index),
});
};

// 更新商品
const updateItem = (
index: number,
field: keyof OrderItem,
value: string | number
) => {
const newItems = [...form.items];
newItems[index] = { ...newItems[index], [field]: value };
setForm({ ...form, items: newItems });
};

// 计算合计
const totalAmount = form.items.reduce(
(sum, item) => sum + item.quantity * item.price,
0
);

// 表单校验
const validate = (): boolean => {
if (!form.customerName.trim()) {
alert("请输入客户名称");
return false;
}
if (!form.customerPhone.trim()) {
alert("请输入客户电话");
return false;
}
if (!form.orderDate) {
alert("请选择订单日期");
return false;
}

for (let i = 0; i < form.items.length; i++) {
const item = form.items[i];
if (!item.productName.trim()) {
alert(`请输入第 ${i + 1} 行的商品名称`);
return false;
}
if (item.quantity <= 0) {
alert(`第 ${i + 1} 行的商品数量必须大于0`);
return false;
}
if (item.price <= 0) {
alert(`第 ${i + 1} 行的商品单价必须大于0`);
return false;
}
}

return true;
};

// 提交订单
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();

if (!validate()) {
return;
}

setSubmitting(true);
try {
// 调用 Backend Function
const result = await lovrabetClient.bff.execute({
scriptName: "submitOrder",
params: form,
});

alert(`订单创建成功!订单号:${result.orderId}`);
navigate("/orders");
} catch (error) {
console.error("创建订单失败:", error);
alert("创建订单失败,请重试");
} finally {
setSubmitting(false);
}
};

return (
<div className="order-form">
<div className="header">
<h1>新建订单</h1>
</div>

<form onSubmit={handleSubmit}>
{/* 主表信息 */}
<div className="section">
<h3>订单信息</h3>

<div className="form-group">
<label>
客户名称 <span className="required">*</span>
</label>
<input
type="text"
value={form.customerName}
onChange={(e) =>
setForm({ ...form, customerName: e.target.value })
}
disabled={submitting}
/>
</div>

<div className="form-group">
<label>
客户电话 <span className="required">*</span>
</label>
<input
type="text"
value={form.customerPhone}
onChange={(e) =>
setForm({ ...form, customerPhone: e.target.value })
}
disabled={submitting}
/>
</div>

<div className="form-group">
<label>
订单日期 <span className="required">*</span>
</label>
<input
type="date"
value={form.orderDate}
onChange={(e) => setForm({ ...form, orderDate: e.target.value })}
disabled={submitting}
/>
</div>

<div className="form-group">
<label>备注</label>
setForm({ ...form, remark: e.target.value })}
rows={3}
disabled={submitting}
/>
</div>
</div>

{/* 明细表 */}
<div className="section">
<h3>商品明细</h3>

<table className="items-table">
<thead>
<tr>
<th>商品名称</th>
<th>数量</th>
<th>单价</th>
<th>小计</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{form.items.map((item, index) => (
<tr key={index}>
<td>
<input
type="text"
value={item.productName}
onChange={(e) =>
updateItem(index, "productName", e.target.value)
}
disabled={submitting}
/>
</td>
<td>
<input
type="number"
value={item.quantity}
onChange={(e) =>
updateItem(index, "quantity", Number(e.target.value))
}
min="1"
disabled={submitting}
/>
</td>
<td>
<input
type="number"
value={item.price}
onChange={(e) =>
updateItem(index, "price", Number(e.target.value))
}
min="0"
step="0.01"
disabled={submitting}
/>
</td>
<td>¥{(item.quantity * item.price).toFixed(2)}</td>
<td>
<button
type="button"
onClick={() => removeItem(index)}
disabled={submitting || form.items.length === 1}
>
删除
</button>
</td>
</tr>
))}
</tbody>
</table>

<button type="button" onClick={addItem} disabled={submitting}>
+ 添加商品
</button>
</div>

{/* 合计 */}
<div className="summary">
<strong>合计:¥{totalAmount.toFixed(2)}</strong>
</div>

{/* 操作按钮 */}
<div className="actions">
<button
type="button"
onClick={() => navigate("/orders")}
disabled={submitting}
>
取消
</button>
<button type="submit" disabled={submitting}>
{submitting ? "提交中..." : "提交订单"}
</button>
</div>
</form>
</div>
);
}



知识点整理

BFF execute API

调用 Backend Function 独立端点:

// 基础用法
const result = await lovrabetClient.bff.execute({
scriptName: "submitOrder",
params: {
/* 参数 */
},
});

// 带类型提示
interface OrderResult {
success: boolean;
orderId: string;
totalAmount: number;
message: string;
}

const result = await lovrabetClient.bff.execute<OrderResult>({
scriptName: "submitOrder",
params: form,
});

console.log(result.orderId);


参数

类型

必填

说明

scriptName

string

后端函数名称(与平台配置一致)

params

Record\&lt;string, any\&gt;

函数参数

返回值:直接返回业务数据(已从 data 字段提取)。

Backend Function 事务语法

// 开启事务
await context.client.db.transaction(async (tx) => {
const models = context.client.models;

// 在这里执行数据库操作
// 正常结束自动提交,抛出异常自动回滚

// 使用 models[TABLES.xxx] 进行数据集操作
await models[TABLES.orders].create({
/* 数据 */
});

// 使用 tx.sql.execute() 执行自定义 SQL
await tx.sql.execute({
sqlCode: "yourSqlCode",
params: {
/* 参数 */
},
});

// 如果这里抛出异常,所有操作都会回滚
if (someError) {
throw new Error("操作失败");
}
});


重要规则

  1. 必须使用 await - 事务是异步的

  2. 数据集操作使用 models[TABLES.xxx] - 使用 context\.client\.models 而非 tx\.models

  3. SQL 操作使用 tx.sql.execute() - 在事务中执行 SQL

  4. 异常自动回滚 - 抛出异常会回滚所有操作

  5. 不要在事务中执行耗时操作 - 如调用外部 API


几个好的做法

参数校验

export default async function myFunction(params, context) {
// 校验必填参数
if (!params.name) {
throw new Error("name 参数不能为空");
}

// 校验参数类型
if (typeof params.quantity !== "number") {
throw new Error("quantity 必须是数字");
}

// 校验业务规则
if (params.quantity <= 0) {
throw new Error("quantity 必须大于0");
}

// 业务逻辑...
}


返回标准格式

export default async function myFunction(params, context) {
try {
// 业务逻辑...
const result = doSomething();

return {
success: true,
data: result,
message: "操作成功",
};
} catch (error) {
return {
success: false,
message: error.message,
};
}
}


事务中的错误处理

/**
* 提交订单 - 带完整错误处理
*
* [接口路径] POST /api/{appCode}/endpoint/submitOrder
*
* [平台配置] https://app.lovrabet.com/app/{appCode}/data/backend-function
*/
export default async function submitOrder(params, context) {
const TABLES = {
orders: "dataset_XXXXXXXXXX", // 数据集: 订单 | 数据表: orders
orderItems: "dataset_YYYYYYYYYY", // 数据集: 订单明细 | 数据表: order_items
};
const models = context.client.models;

try {
const result = await context.client.db.transaction(async (tx) => {
// 1. 创建主表
const orderId = await models[TABLES.orders].create(params.order);

// 2. 校验明细
if (!params.items || params.items.length === 0) {
throw new Error("订单明细不能为空");
}

// 3. 创建明细
for (const item of params.items) {
await models[TABLES.orderItems].create({
orderId: orderId,
...item,
});
}

return { orderId };
});

return { success: true, orderId: result.orderId };
} catch (error) {
// 事务已自动回滚
return {
success: false,
message: error.message,
};
}
}



常见问题

Q: BFF 调用返回 404?

A: 检查以下几点:

  1. scriptName 是否与平台上配置的函数名完全一致

  2. 函数是否已发布

  3. appCode 是否有访问该函数的权限

Q: 事务执行失败但数据仍然被写入了?

A: 检查是否正确使用 tx

// ❌ 错误:使用了 tx.models,应该使用 context.client.models
await context.client.db.transaction(async (tx) => {
await tx.models.dataset_xxx.create(); // 错误!
});

// ✅ 正确:使用 context.client.models 和 TABLES 常量
const TABLES = {
orders: "dataset_XXXXXXXXXX",
};
const models = context.client.models;

await context.client.db.transaction(async (tx) => {
await models[TABLES.orders].create({
/* 数据 */
}); // 正确
});


Q: 如何在事务中调用自定义 SQL?

A: 使用 tx\.sql\.execute\(\)

const TABLES = {
orders: "dataset_XXXXXXXXXX", // 数据集: 订单 | 数据表: orders
};
const models = context.client.models;

await context.client.db.transaction(async (tx) => {
// 调用自定义 SQL
await tx.sql.execute({
sqlCode: "update-stock",
params: { productId: "xxx", quantity: 1 },
});

// 使用数据集 API
await models[TABLES.orders].create({
/* ... */
});
});


Q: 事务超时怎么办?

A:事务执行时间应控制在几秒内:

  • 减少循环中的操作次数

  • 避免在事务中调用外部 API

  • 大数据量操作使用批量 API 或后台任务


本节小结

恭喜你掌握了主子表单的事务处理!关键知识点:

知识点

说明

Backend Function

后端函数,处理复杂业务逻辑

独立端点

可被前端直接调用的 BFF 接口

事务

保证多表操作的原子性

bff\.execute\(\)

前端调用 Backend Function

最佳实践

  • 涉及多表操作时使用事务

  • 事务中使用 models\[TABLES\.xxx\] 而非 tx\.models

  • 做好参数校验和错误处理

  • 返回友好的错误消息

下一步

相关阅读

核心文档

  • BFF API 参考 — Backend Function 完整使用指南

  • API 使用指南 — CRUD 操作完整说明

  • 错误处理 — 事务错误处理和回滚

进阶主题


难度等级:L2 | 预计耗时:45 分钟