跳到主要内容

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

前面几篇讲的都是单表操作。但实际业务中经常需要同时操作多张表——比如提交订单时,要同时创建订单主表和多条订单明细,而且必须保证要么全部成功,要么全部回滚

这篇以订单提交为例,介绍如何用 Backend Function独立端点实现主子表单的事务性提交。

本节你将学到
  • 什么是 Backend Function 独立端点
  • 如何使用事务保证数据一致性
  • 主子表单的数据提交模式
  • 前端如何调用 Backend Function

需求

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

最终效果:

新建订单页面


为什么需要 Backend Function?

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

  1. 跨表操作 - 需要同时写入主表和明细表
  2. 事务保证 - 要么全部成功,要么全部回滚
  3. 业务逻辑 - 计算金额、校验库存等复杂逻辑

Backend Function 独立端点 可以:

  • 在后端一次性完成多表操作
  • 使用事务保证数据一致性
  • 返回自定义的响应格式

实现步骤

步骤 1:创建 Backend Function

在 Lovrabet 平台上创建独立端点函数:

函数名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: "订单创建成功",
};
}

关键点

  • 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>
<textarea
value={form.remark}
onChange={(e) => 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);
参数类型必填说明
scriptNamestring后端函数名称(与平台配置一致)
paramsRecord<string, any>函数参数

返回值:直接返回业务数据(已从 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
  • 做好参数校验和错误处理
  • 返回友好的错误消息

下一步

相关阅读

核心文档

进阶主题


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