跳到主要内容

Backend Function(三):独立接口 - SQL 复杂查询

上一篇讲了后验函数。本篇是 Backend Function 系列的最后一篇,介绍如何在 Backend Function 中执行自定义 SQL。

前端调用 SQL 适合做数据展示类的报表查询,但有些场景需要在后端处理——比如批量更新数据、把 SQL 结果作为后续业务逻辑的输入、在事务中混合使用 SQL 和数据集 API。

本节你将学到
  • Backend Function 中调用 SQL 的方法
  • 前端 SDK 和 BFF 中 SQL 返回值的区别
  • 在事务中使用 SQL
  • SQL 结果与数据集 API 配合使用

需求

在 Backend Function 中使用自定义 SQL,覆盖这些场景:

  • 复杂报表统计(多表关联、分组聚合)
  • 批量数据更新(按条件批量修改状态)
  • 特殊查询(使用数据库特定函数)
  • SQL 结果 + 数据集 API 配合使用

核心概念

前端 vs Backend Function 调用 SQL

特性前端 SDKBackend Function
调用方式client.sql.execute()context.client.sql.execute()
返回结构{ execSuccess, execResult }直接返回数组
错误处理检查 execSuccesstry-catch 捕获异常
用途查询数据展示业务逻辑处理

实现步骤

步骤 1:准备自定义 SQL

在 Lovrabet 平台上创建自定义 SQL:

SQL 名称getUserStats

SELECT
u.id,
u.username,
u.email,
COUNT(o.id) as order_count,
COALESCE(SUM(o.total_amount), 0) as total_amount
FROM dataset_users u
LEFT JOIN dataset_orders o ON u.id = o.user_id
WHERE u.status = 'active'
<if test="userId">
AND u.id = #{userId}
</if>
GROUP BY u.id, u.username, u.email

步骤 2:在 Backend Function 中调用 SQL

查询统计数据

/**
* 获取用户统计信息 - Backend Function 独立端点
*
* [接口路径] POST /api/{appCode}/endpoint/getUserStats
*
* [平台配置] https://app.lovrabet.com/app/{appCode}/data/backend-function
* 说明:appCode 为应用编码,创建后可在平台上配置此独立端点
*
* [HTTP 请求体参数]
* { "userId": "用户ID(可选)" }
*
* [返回数据结构]
* ENDPOINT: 返回业务数据对象
* {
* "success": true,
* "data": [...], // 用户统计数组
* "total": 10 // 总数
* }
*
* @param {Object} params - 请求参数
* @param {string} params.userId - 用户ID(可选)
* @param {Object} context - 执行上下文(平台自动注入,调用时无需传递)
* @param {Object} context.userInfo - 当前用户信息
* @param {Object} context.client - 数据库操作入口
* @returns {Object} 返回统计结果
*/
export default async function getUserStats(params, context) {
const { userId } = params;

// 直接返回数组(BFF 中 sql.execute 直接返回结果数组)
const rows = await context.client.sql.execute({
sqlCode: "getUserStats",
params: { userId },
});

// 如果没有指定 userId,返回所有用户的统计
// 如果指定了 userId,返回单个用户的统计

if (userId) {
// 单个用户:返回对象或 null
return rows.length > 0 ? rows[0] : null;
} else {
// 所有用户:返回数组
return {
success: true,
data: rows,
total: rows.length,
};
}
}

前端调用

import { lovrabetClient } from "./api/client";

// 获取所有用户统计
const result = await lovrabetClient.bff.execute({
scriptName: "getUserStats",
params: {},
});

console.log(result.data); // 用户统计数组

// 获取单个用户统计
const userStats = await lovrabetClient.bff.execute({
scriptName: "getUserStats",
params: { userId: "123" },
});

console.log(userStats.orderCount); // 订单数量

批量更新数据

SQL 名称batchUpdateStatus

UPDATE dataset_orders
SET status = #{newStatus},
update_time = NOW()
WHERE status = #{oldStatus}
<if test="startDate">
AND create_time >= #{startDate}
</if>
<if test="endDate">
AND create_time &lt;= #{endDate}
</if>

Backend Function

/**
* 批量更新订单状态 - Backend Function 独立端点
*
* [接口路径] POST /api/{appCode}/endpoint/batchUpdateOrderStatus
*
* [平台配置] https://app.lovrabet.com/app/{appCode}/data/backend-function
*
* [HTTP 请求体参数]
* {
* "oldStatus": "原状态",
* "newStatus": "新状态",
* "startDate": "开始日期(可选)",
* "endDate": "结束日期(可选)"
* }
*
* [返回数据结构]
* { "success": true, "message": "...", "affectedRows": 10 }
*
* @param {Object} params - 请求参数
* @param {string} params.oldStatus - 原状态
* @param {string} params.newStatus - 新状态
* @param {string} params.startDate - 开始日期(可选)
* @param {string} params.endDate - 结束日期(可选)
* @param {Object} context - 执行上下文(平台自动注入)
* @returns {Object} 返回更新结果
*/
export default async function batchUpdateOrderStatus(params, context) {
const { oldStatus, newStatus, startDate, endDate } = params;

// 参数校验
if (!oldStatus || !newStatus) {
throw new Error("oldStatus 和 newStatus 不能为空");
}

if (oldStatus === newStatus) {
throw new Error("新状态不能与原状态相同");
}

// 执行批量更新 SQL
const affectedRows = await context.client.sql.execute({
sqlCode: "batchUpdateStatus",
params: {
oldStatus,
newStatus,
startDate,
endDate,
},
});

// 返回更新结果(affectedRows 通常是一个数组,包含受影响的行数)
const rowCount = affectedRows[0]?.affected_rows || 0;

return {
success: true,
message: `成功更新 ${rowCount} 条订单`,
affectedRows: rowCount,
};
}

SQL 结果 + 数据集操作

SQL 名称getPendingOrders

SELECT
o.id,
o.order_no,
o.total_amount,
c.customer_name,
c.phone
FROM dataset_orders o
JOIN dataset_customers c ON o.customer_id = c.id
WHERE o.status = 'pending'
ORDER BY o.create_time DESC
LIMIT 50

Backend Function

/**
* 获取待处理订单并计算优惠 - Backend Function 独立端点
*
* [接口路径] POST /api/{appCode}/endpoint/getPendingOrdersWithDiscount
*
* [平台配置] https://app.lovrabet.com/app/{appCode}/data/backend-function
*
* [HTTP 请求体参数]
* {}
*
* [返回数据结构]
* { "success": true, "orders": [...], "summary": {...} }
*
* @param {Object} params - 请求参数
* @param {Object} context - 执行上下文(平台自动注入)
* @returns {Object} 返回订单列表及汇总
*/
export default async function getPendingOrdersWithDiscount(params, context) {
// 数据集编码映射表
const TABLES = {
customers: "dataset_XXXXXXXXXX", // 数据集: 客户 | 数据表: customers(需替换为实际32位编码)
};
const models = context.client.models;

// 1. 调用自定义 SQL 获取待处理订单
const orders = await context.client.sql.execute({
sqlCode: "getPendingOrders",
params: {},
});

// 收集所有客户ID,批量查询
const customerIds = [
...new Set(orders.map((o) => o.customer_id).filter(Boolean)),
];

// 批量查询客户信息
let customerMap = new Map();
if (customerIds.length > 0) {
const customers = await models[TABLES.customers].filter({
where: { id: { $in: customerIds } },
});
customerMap = new Map(customers.tableData.map((c) => [c.id, c]));
}

// 2. 处理每笔订单,计算折扣
const result = orders.map((order) => {
// 获取客户的会员等级(从批量查询结果中获取)
const customer = customerMap.get(order.customer_id);

let discount = 0;
if (customer?.level === "VIP") {
discount = 0.1; // VIP 9折
} else if (customer?.level === "SVIP") {
discount = 0.15; // SVIP 85折
}

const discountAmount = order.total_amount * discount;
const finalAmount = order.total_amount - discountAmount;

return {
...order,
discount: discount * 100, // 转换为百分比
discountAmount,
finalAmount,
};
});

// 3. 计算汇总信息
const totalOriginal = result.reduce((sum, o) => sum + o.total_amount, 0);
const totalDiscount = result.reduce((sum, o) => sum + o.discountAmount, 0);
const totalFinal = result.reduce((sum, o) => sum + o.finalAmount, 0);

return {
success: true,
orders: result,
summary: {
totalOriginal,
totalDiscount,
totalFinal,
},
};
}

在事务中使用 SQL

SQL 名称decreaseStock

UPDATE dataset_products
SET stock = stock - #{quantity},
update_time = NOW(),
version = version + 1
WHERE id = #{productId}
AND stock >= #{quantity}
AND version = #{version}

Backend Function

/**
* 提交订单(扣减库存)- Backend Function 独立端点
*
* [接口路径] POST /api/{appCode}/endpoint/submitOrderWithStock
*
* [平台配置] https://app.lovrabet.com/app/{appCode}/data/backend-function
*
* [HTTP 请求体参数]
* {
* "customerName": "客户名称",
* "customerPhone": "客户电话",
* "items": [
* { "productId": "商品ID", "quantity": 1, "price": 100 }
* ]
* }
*
* [返回数据结构]
* { "success": true, "message": "...", "orderId": "...", "totalAmount": 0 }
*
* @param {Object} params - 请求参数
* @param {string} params.customerName - 客户名称
* @param {string} params.customerPhone - 客户电话
* @param {Array} params.items - 订单明细
* @param {Object} context - 执行上下文(平台自动注入)
* @returns {Object} 返回提交结果
*/
export default async function submitOrderWithStock(params, context) {
const { items } = params; // items = [{ productId, quantity, price }, ...]

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

// 使用事务确保库存扣减和订单创建的原子性
const result = await context.client.db.transaction(async (tx) => {
let totalAmount = 0;

// 1. 检查库存并扣减(使用 SQL)
for (const item of items) {
// 获取当前库存和版本号
const products = await tx.sql.execute({
sqlCode: "getProductStock",
params: { productId: item.productId },
});

if (products.length === 0) {
throw new Error(`商品 ${item.productId} 不存在`);
}

const product = products[0];
const currentStock = product.stock;
const currentVersion = product.version;

if (currentStock < item.quantity) {
throw new Error(
`商品 ${product.name} 库存不足,当前库存:${currentStock}`
);
}

// 扣减库存(使用 version 实现乐观锁)
await tx.sql.execute({
sqlCode: "decreaseStock",
params: {
productId: item.productId,
quantity: item.quantity,
version: currentVersion,
},
});

totalAmount += product.price * item.quantity;
}

// 2. 创建订单(使用 models 而非 tx.models)
const orderId = await models[TABLES.orders].create({
customerName: params.customerName,
customerPhone: params.customerPhone,
totalAmount,
status: "paid",
});

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

return { orderId, totalAmount };
});

return {
success: true,
message: "订单提交成功",
...result,
};
}

知识点整理

Backend Function 中 SQL 执行语法

// 直接返回数组,无需检查 execSuccess
const rows = await context.client.sql.execute({
sqlCode: "yourSqlCode",
params: {
/* 参数 */
},
});

// rows 就是结果数组
rows.forEach((row) => console.log(row));

与前端 SDK 的区别

环境调用方式返回值数据获取
前端client.sql.execute(){ execSuccess, execResult }result.execResult
BFFcontext.client.sql.execute()直接返回数组直接使用 rows
// ❌ 错误:BFF 中没有 execResult
const result = await context.client.sql.execute({ sqlCode: "xxx" });
const rows = result.execResult; // undefined!

// ✅ 正确:BFF 返回值直接就是数组
const rows = await context.client.sql.execute({ sqlCode: "xxx" });
const firstRow = rows[0];

在事务中调用 SQL

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

await context.client.db.transaction(async (tx) => {
// 使用 tx.sql.execute() 确保在事务中执行
await tx.sql.execute({
sqlCode: "yourSqlCode",
params: {
/* ... */
},
});

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

SQL 参数传递

// SQL 中使用 #{paramName}
const rows = await context.client.sql.execute({
sqlCode: "yourSqlCode",
params: {
userId: 123,
startDate: "2024-01-01",
status: "active",
},
});

几个好的做法

参数校验

export default async function myFunction(params, context) {
const { userId, startDate, endDate } = params;

// 必填参数校验
if (!userId) {
throw new Error("userId 不能为空");
}

// 业务规则校验
if (startDate && endDate && startDate > endDate) {
throw new Error("开始日期不能大于结束日期");
}

// 业务逻辑...
}

错误处理

export default async function myFunction(params, context) {
try {
const rows = await context.client.sql.execute({
sqlCode: "yourSqlCode",
params: {},
});

return {
success: true,
data: rows,
};
} catch (error) {
// 记录日志
console.error("SQL 执行失败:", error);

return {
success: false,
message: error.message || "查询失败",
};
}
}

添加 JSDoc 注释

给 Backend Function 加完整的 JSDoc 注释,方便后续维护:

/**
* 获取用户统计信息 - Backend Function 独立端点
*
* [接口路径] POST /api/{appCode}/endpoint/getUserStats
*
* [平台配置] https://app.lovrabet.com/app/{appCode}/data/backend-function
*
* [HTTP 请求体参数]
* { "userId": "用户ID(可选)" }
*
* [返回数据结构]
* { "success": true, "data": [...], "total": 10 }
*
* @param {Object} params - 请求参数
* @param {string} params.userId - 用户ID(可选)
* @param {Object} context - 执行上下文(平台自动注入)
* @returns {Promise<Object>} 返回统计结果
*/
export default async function getUserStats(params, context) {
// ...
}

常见问题

Q: 如何调试 Backend Function 中的 SQL?

A: 使用 console.log 输出调试信息:

/**
* Backend Function 示例 - 调试日志
*
* [接口路径] POST /api/{appCode}/endpoint/myFunction
*
* [平台配置] https://app.lovrabet.com/app/{appCode}/data/backend-function
*/
export default async function myFunction(params, context) {
console.log("输入参数:", JSON.stringify(params));
console.log("当前用户:", context.userInfo?.username);

const rows = await context.client.sql.execute({
sqlCode: "yourSqlCode",
params: {},
});

console.log("查询结果行数:", rows.length);

return rows;
}

然后查看平台的 Backend Function 日志。

Q: SQL 执行超时怎么办?

A

  1. 优化 SQL 查询,添加索引
  2. 使用 LIMIT 限制返回行数
  3. 减少复杂的多表关联
  4. 检查是否存在死锁

Q: 如何获取受影响的行数?

A:这取决于具体的数据库:

const rows = await context.client.sql.execute({
sqlCode: "batchUpdate",
params: {},
});

// MySQL: 返回 { affected_rows: 数字 }
const affectedRows = rows[0]?.affected_rows || 0;

// PostgreSQL: 使用 RETURNING
// SQL: UPDATE ... RETURNING *
// rows 直接是更新的行数据
const affectedRows = rows.length;

Q: 事务中的 SQL 失败会回滚吗?

A:是的,事务中任何异常都会自动回滚:

await context.client.db.transaction(async (tx) => {
// 执行 SQL
await tx.sql.execute({ sqlCode: "update1" });

// 如果这里抛出异常,上面的 update1 也会回滚
if (someError) {
throw new Error("操作失败");
}

// 只有全部成功,事务才会提交
await tx.sql.execute({ sqlCode: "update2" });
});

本节小结

恭喜你完成了所有开发教程!关键知识点:

知识点说明
context.client.sql.execute()BFF 中执行 SQL
直接返回数组BFF 中 SQL 返回值不同于前端
tx.sql.execute()事务中执行 SQL
SQL + 数据集 API两种方式配合使用
前端 vs BFF 的区别
// 前端 SDK:返回 { execSuccess, execResult }
const data = await client.sql.execute({ sqlCode: "xxx" });
if (data.execSuccess) { data.execResult.forEach(...); }

// Backend Function:直接返回数组
const rows = await context.client.sql.execute({ sqlCode: "xxx" });
rows.forEach(...);

🎉 恭喜完成开发阶段!

你已经掌握了 Lovrabet 开发的所有核心技能。接下来是构建与部署,把你的成果上线!

相关阅读

核心文档

进阶主题


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