Backend Function(一):先验函数 - 数据校验与拦截
Backend Function 是 Lovrabet 平台的核心能力之一,它允许你在后端执行自定义业务逻辑。从本节开始,我们将通过三篇文章系统学习 Backend Function 体系:
- 本文(一):先验函数 - 在数据操作前自动校验和拦截
- 下一篇(二):后验函数 - 在数据操作后自动处理和转换
- 下一篇(三):独立接口 - 自定义 API 端点和复杂 SQL 调用
为什么需要先验函数?
前面几篇实现了列表、详情和新建功能,前端也加了基础校验。但有些校验必须在后端做——比如手机号唯一性、复杂业务规则、数据权限控制,这些前端绕不过去的。
这篇介绍 先验函数(Before 脚本):在数据操作执行前自动触发,校验不通过就拦截,通过了才写入数据库。
- 什么是先验函数(Before 脚本)
- 如何配置先验函数拦截数据操作
- 常见的校验场景实现
- 先验函数和前端校验的区别
需求
在数据写入数据库之前,自动执行业务规则校验:
- 防止重复数据(手机号、邮箱唯一性)
- 业务规则校验(如年龄限制)
- 数据权限控制(如只能操作本部门数据)
- 自动补充数据(如设置所属部门)
整个流程大致是:
用户提交表单
↓
前端校验(基础格式)
↓
先验函数(业务规则校验)
↓
校验通过 → 写入数据库
校验失败 → 返回错误信息
核心概念
先验函数也叫 Before 脚本或 HOOK 前置脚本,在数据操作执行前自动触发。在函数里抛出异常就能阻止操作执行,适合做数据校验、权限控制、数据补充。
| 特性 | 说明 |
|---|---|
| 触发时机 | create、update、delete、filter 等操作前 |
| 执行位置 | 后端(Lovrabet 服务端环境) |
| 阻止能力 | 抛出异常即阻止操作执行 |
| 函数命名 | beforeCreate、beforeUpdate、beforeFilter 等 |
实现步骤
步骤 1:在平台上创建先验函数
在 Lovrabet 平台上创建拦截器函数:
函数类型:RequestInception(先验函数/Before 脚本)
函数名:beforeCreate
平台配置地址:https://app.lovrabet.com/app/{appCode}/data/dataset/{datasetId}#api-list
/**
* 客户创建先验函数 - 数据唯一性校验和业务规则验证
*
* [接口路径] POST /api/{appCode}/customers/create
* 规则:/api/{应用编码}/{数据集编码}/create
*
* [平台配置] https://app.lovrabet.com/app/{appCode}/data/dataset/{datasetId}#api-list
* 说明:appCode 为应用编码,datasetId 为数据集 ID(整型数字,从 MCP get_dataset_detail 获取)
*
* [HTTP 请求体参数]
* { "name": "客户名称", "phone": "手机号", "email": "邮箱" }
*
* [返回数据结构]
* HOOK: 返回修改后的 params 对象
*
* @param {Object} params - 请求参数
* @param {Object} context - 执行上下文(平台自动注入,调用时无需传递)
* @param {Object} context.userInfo - 当前用户信息
* @param {Object} context.client - 数据库操作入口
* @returns {Object} 返回修改后的 params
*/
export default async function beforeCreate(params, context) {
// 数据集编码映射表
// 注释格式:数据集: {数据集名称} | 数据表: {数据表名称}
const TABLES = {
customers: "dataset_XXXXXXXXXX", // 数据集: 客户 | 数据表: customers(需替换为实际32位编码)
};
const models = context.client.models;
console.log("[先验函数] 开始校验客户创建:", params);
// ========== 校验 1:手机号唯一性 ==========
if (params.phone) {
// 数据集: 客户 | 数据表: customers
const existingPhone = await models[TABLES.customers].filter({
where: {
phone: { $eq: params.phone },
...(params.id ? { id: { $ne: params.id } } : {}), // 排除当前记录(更新时)
},
});
if (existingPhone.tableData && existingPhone.tableData.length > 0) {
throw new Error("手机号已被使用,请使用其他手机号");
}
}
// ========== 校验 2:邮箱唯一性 ==========
if (params.email) {
// 数据集: 客户 | 数据表: customers
const existingEmail = await models[TABLES.customers].filter({
where: {
email: { $eq: params.email },
...(params.id ? { id: { $ne: params.id } } : {}),
},
});
if (existingEmail.tableData && existingEmail.tableData.length > 0) {
throw new Error("邮箱已被使用,请使用其他邮箱");
}
}
// ========== 校验 3:年龄必须满18岁 ==========
if (params.birthday) {
const today = new Date();
const birthDate = new Date(params.birthday);
const age = today.getFullYear() - birthDate.getFullYear();
const monthDiff = today.getMonth() - birthDate.getMonth();
if (
monthDiff < 0 ||
(monthDiff === 0 && today.getDate() < birthDate.getDate())
) {
age--;
}
if (age < 18) {
throw new Error("客户年龄必须满18岁");
}
}
// ========== 自动补充:设置所属部门 ==========
if (!params.department && context.userInfo?.department) {
params.department = context.userInfo.department;
}
// 系统字段(create_by, create_time 等)由平台自动维护,无需手动设置
return params;
}
关键修改点:
- 使用
models[TABLES.customers]访问数据集 context.userInfo替代context.currentUserfilter返回{ tableData, paging, tableColumns }结构- 系统字段(create_by、create_time)由平台自动维护
步骤 2:配置先验函数绑定
在 Lovrabet 平台上配置 Before 脚本:
平台配置地址:https://app.lovrabet.com/app/{appCode}/data/dataset/{datasetId}#api-list

- 进入数据集的 API 列表页面
- 选择要配置的操作(如
create创建操作) - 在"业务逻辑扩展"区域,找到"先验函数"卡片
- 点击"编写"按钮,打开脚本编辑器
- 编写 Before 脚本代码并保存
说明:
- 每个操作(create/update/delete/filter)都可以独立配置 Before 脚本
- Before 脚本在操作执行前触发,可用于入参校验、权限过滤等
- 状态显示"暂无"表示未配置,配置后会显示函数名
步骤 3:前端代码无需修改
前端代码保持不变,SDK 会自动触发先验函数:
// 前端提交表单
const result = await lovrabetClient.models.dataset_customers.create({
name: "张三",
phone: "13800138000",
email: "zhangsan@example.com",
birthday: "1990-01-01",
});
// 如果先验函数校验失败,会抛出异常
try {
const result = await lovrabetClient.models.dataset_customers.create(data);
console.log("创建成功:", result);
} catch (error) {
console.error("创建失败:", error.message);
// 输出:手机号已被使用,请使用其他手机号
}
高级场景
多租户数据隔离
/**
* 多租户数据隔离先验函数(Before Create)
* 确保用户只能操作自己租户的数据
*
* [接口路径] POST /api/{appCode}/customers/create
* 规则:/api/{应用编码}/{数据集编码}/create
*
* [平台配置] https://app.lovrabet.com/app/{appCode}/data/dataset/{datasetId}#api-list
* 说明:appCode 为应用编码,datasetId 为数据集 ID(整型数字,从 MCP get_dataset_detail 获取)
*
* [HTTP 请求体参数]
* { "name": "客户名称", ... }
*
* [返回数据结构]
* HOOK: 返回修改后的 params 对象
*
* @param {Object} params - 请求参数
* @param {Object} context - 执行上下文(平台自动注入,调用时无需传递)
* @param {Object} context.userInfo - 当前用户信息
* @returns {Object} 返回修改后的 params
*/
export default async function beforeCreate(params, context) {
const TABLES = {
customers: "dataset_XXXXXXXXXX", // 数据集: 客户 | 数据表: customers(需替换为实际32位编码)
};
const models = context.client.models;
// 获取用户所属租户(从 userInfo 获取)
const userTenantId = context.userInfo?.tenantCode;
if (!userTenantId) {
throw new Error("用户未分配租户,无法操作");
}
// 创建操作:自动设置租户ID
params.tenant_id = userTenantId;
return params;
}
数据状态流转校验
/**
* 客户状态流转校验(Before Update)
* 限制状态只能按指定流程变更
*
* [接口路径] POST /api/{appCode}/customers/update
* 规则:/api/{应用编码}/{数据集编码}/update
*
* [平台配置] https://app.lovrabet.com/app/{appCode}/data/dataset/{datasetId}#api-list
* 说明:appCode 为应用编码,datasetId 为数据集 ID(整型数字,从 MCP get_dataset_detail 获取)
*
* [HTTP 请求体参数]
* { "id": 123, "status": "active" }
*
* [返回数据结构]
* HOOK: 返回修改后的 params 对象
*
* @param {Object} params - 请求参数
* @param {string} params.id - 记录ID
* @param {string} params.status - 新状态
* @param {Object} context - 执行上下文(平台自动注入,调用时无需传递)
* @returns {Object} 返回修改后的 params
*/
export default async function beforeUpdate(params, context) {
const TABLES = {
customers: "dataset_XXXXXXXXXX", // 数据集: 客户 | 数据表: customers(需替换为实际32位编码)
};
const models = context.client.models;
if (!params.status || !params.id) {
return params;
}
// 获取当前记录
// 数据集: 客户 | 数据表: customers
const current = await models[TABLES.customers].findOne({ id: params.id });
if (!current) {
throw new Error("记录不存在");
}
const currentStatus = current.status;
const newStatus = params.status;
// 定义允许的状态流转
const allowedTransitions = {
potential: ["dealed", "lost"], // 潜在 → 成交/流失
dealed: ["active", "inactive"], // 成交 → 活跃/非活跃
lost: ["potential"], // 流失 → 潜在(重新激活)
active: ["inactive", "completed"], // 活跃 → 非活跃/完成
inactive: ["active"], // 非活跃 → 活跃
};
const allowed = allowedTransitions[currentStatus] || [];
if (!allowed.includes(newStatus)) {
throw new Error(`状态不允许从"${currentStatus}"变更为"${newStatus}"`);
}
return params;
}
function getStatusName(status) {
const names = {
potential: "潜在",
dealed: "成交",
lost: "流失",
active: "活跃",
inactive: "非活跃",
completed: "完成",
};
return names[status] || status;
}
动态获取外部数据进行校验
/**
* 校验客户信用额度
* 通过外部 API 获取客户信用信息
*/
export default async function validateCreditLimit(params, context) {
const { data } = params;
if (!data.customerId) {
return { data };
}
// 调用外部服务查询客户信用
const creditInfo = await fetchCreditInfo(data.customerId);
if (creditInfo.blacklisted) {
throw new Error("该客户已列入黑名单,无法创建订单");
}
const orderAmount = data.totalAmount || 0;
const availableCredit = creditInfo.creditLimit - creditInfo.usedCredit;
if (orderAmount > availableCredit) {
throw new Error(
`超出信用额度。可用额度:¥${availableCredit},订单金额:¥${orderAmount}`
);
}
return { data };
}
/**
* 模拟调用外部信用服务
* 实际使用时替换为真实的 API 调用
*/
async function fetchCreditInfo(customerId) {
// 这里可以是 HTTP 请求或其他方式调用外部服务
// 例如:fetch('https://api.credit-service.com/check/' + customerId)
return {
blacklisted: false,
creditLimit: 100000,
usedCredit: 20000,
};
}
知识点整理
Before 脚本函数签名
export default async function beforeCreate(params, context) {
// params - 请求参数(根据接口类型不同)
// context - 执行上下文(平台自动注入)
// 数据库操作
const models = context.client.models;
// 返回修改后的 params
return params;
}
params 参数(根据操作类型不同)
| 操作类型 | params 内容 |
|---|---|
create | 提交的表单数据 { name: "xxx", phone: "xxx" } |
update | 更新的数据 { id: 123, name: "new" } |
delete | 删除条件 { id: 123 } |
filter | 查询参数 { where: {...}, select: [...] } |
context 对象
| 字段 | 类型 | 说明 |
|---|---|---|
userInfo | Object | 当前登录用户信息(id、username、tenantCode) |
client | Object | 数据库操作入口,通过 models 访问数据集 |
appCode | String | 当前应用编码 |
数据集操作方式
// 定义数据集映射(函数开头定义)
const TABLES = {
customers: "dataset_XXXXXXXXXX", // 32位数据集编码
};
const models = context.client.models;
// 查询单条
const record = await models[TABLES.customers].findOne({ id: 123 });
// 查询列表
const result = await models[TABLES.customers].filter({
where: { status: { $eq: "active" } },
});
// result.tableData 是数据数组
// 创建
const id = await models[TABLES.customers].create({
name: "张三",
phone: "13800138000",
});
// 更新
await models[TABLES.customers].update({
id: 123,
name: "新名字",
});
阻止操作的方式
// 抛出异常会阻止操作并返回错误消息
throw new Error('错误消息');
// 正常返回会继续执行
return params;
### 阻止操作的方式
```javascript
// 抛出异常会阻止操作并返回错误消息
throw new Error('错误消息');
// 返回值可以是空对象或修改后的 data
return {}; // 不修改数据
return { data: newData }; // 返回修改后的数据
常见问题
Q: 先验函数和前端校验有什么区别?
A:两者都应该有,各司其职:
| 校验位置 | 作用 | 优势 |
|---|---|---|
| 前端校验 | 用户体验 | 立即反馈,减少无效请求 |
| 先验函数 | 数据安全 | 必须执行,无法绕过 |
推荐做法:前端做基础格式校验(如手机号格式),先验函数做业务规则校验(如手机号唯一性)。
Q: 先验函数可以修改数据吗?
A:可以。返回 { data: newData } 会影响后续操作:
export default async function validate(params, context) {
const { data } = params;
// 自动补充字段
if (!data.createTime) {
data.createTime = new Date().toISOString();
}
// 返回修改后的数据
return { data };
}
Q: 如何调试先验函数?
A:使用 console.log 输出日志:
export default async function beforeCreate(params, context) {
console.log("[先验函数] 输入参数:", JSON.stringify(params));
console.log("[先验函数] 当前用户:", context.userInfo?.username);
// 业务逻辑...
console.log("[先验函数] 执行完成");
return params;
}
然后在平台的 Backend Function 日志中查看输出。
Q: 先验函数会影响性能吗?
A:每次数据操作都会执行先验函数,所以:
- 避免耗时操作:不要在先验函数中调用慢速外部 API
- 减少数据库查询:合并多个查询,使用索引
- 使用缓存:对于不常变化的数据(如配置信息)可以使用缓存
// ❌ 不推荐:多次查询
const TABLES = {
customers: "dataset_XXXXXXXXXX",
};
const models = context.client.models;
const existing1 = await models[TABLES.customers].filter({
where: { phone: { $eq: "13800138000" } },
});
const existing2 = await models[TABLES.customers].filter({
where: { email: { $eq: "test@example.com" } },
});
// ✅ 推荐:一次查询
const existing = await models[TABLES.customers].filter({
where: {
$or: [
{ phone: { $eq: "13800138000" } },
{ email: { $eq: "test@example.com" } },
],
},
});
本节小结
恭喜你学会了先验函数!关键知识点:
| 知识点 | 说明 |
|---|---|
| Before 脚本 | 数据操作前自动执行的校验函数 |
throw Error() | 抛出异常阻止操作执行 |
context.userInfo | 获取当前登录用户信息 |
models[TABLES.xxx] | 在 BF 中访问数据集 |
- 前端校验做格式检查,先验函数做业务规则
- 避免在先验函数中执行耗时操作
- 使用批量查询优化性能
- 添加日志便于调试
下一步
- 数据保存后处理 — 学习数据脱敏和格式转换
相关阅读
核心文档
- BFF API 参考 — Backend Function 完整使用指南
- SDK 配置 — 环境配置和认证设置
- 语法糖 — safe、sqlSafe 等便捷函数
进阶主题
- 数据脱敏:后验函数 — 后端统一处理数据格式
- 主子表单:事务处理 — BF 独立端点处理复杂业务
- 销售报表:自定义 SQL — 自定义 SQL 查询
难度等级:L2 | 预计耗时:35 分钟