跳到主要内容

Backend Function(一):先验函数 - 数据校验与拦截

Backend Function 是 Lovrabet 平台的核心能力之一,它允许你在后端执行自定义业务逻辑。从本节开始,我们将通过三篇文章系统学习 Backend Function 体系:

  • 本文(一):先验函数 - 在数据操作前自动校验和拦截
  • 下一篇(二):后验函数 - 在数据操作后自动处理和转换
  • 下一篇(三):独立接口 - 自定义 API 端点和复杂 SQL 调用

为什么需要先验函数?

前面几篇实现了列表、详情和新建功能,前端也加了基础校验。但有些校验必须在后端做——比如手机号唯一性、复杂业务规则、数据权限控制,这些前端绕不过去的。

这篇介绍 先验函数(Before 脚本):在数据操作执行前自动触发,校验不通过就拦截,通过了才写入数据库。

本节你将学到
  • 什么是先验函数(Before 脚本)
  • 如何配置先验函数拦截数据操作
  • 常见的校验场景实现
  • 先验函数和前端校验的区别

需求

在数据写入数据库之前,自动执行业务规则校验:

  • 防止重复数据(手机号、邮箱唯一性)
  • 业务规则校验(如年龄限制)
  • 数据权限控制(如只能操作本部门数据)
  • 自动补充数据(如设置所属部门)

整个流程大致是:

用户提交表单

前端校验(基础格式)

先验函数(业务规则校验)

校验通过 → 写入数据库
校验失败 → 返回错误信息

核心概念

先验函数也叫 Before 脚本或 HOOK 前置脚本,在数据操作执行前自动触发。在函数里抛出异常就能阻止操作执行,适合做数据校验、权限控制、数据补充。

特性说明
触发时机createupdatedeletefilter 等操作前
执行位置后端(Lovrabet 服务端环境)
阻止能力抛出异常即阻止操作执行
函数命名beforeCreatebeforeUpdatebeforeFilter

实现步骤

步骤 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.currentUser
  • filter 返回 { tableData, paging, tableColumns } 结构
  • 系统字段(create_by、create_time)由平台自动维护

步骤 2:配置先验函数绑定

在 Lovrabet 平台上配置 Before 脚本:

平台配置地址https://app.lovrabet.com/app/{appCode}/data/dataset/{datasetId}#api-list

先验函数配置

  1. 进入数据集的 API 列表页面
  2. 选择要配置的操作(如 create 创建操作)
  3. 在"业务逻辑扩展"区域,找到"先验函数"卡片
  4. 点击"编写"按钮,打开脚本编辑器
  5. 编写 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 对象

字段类型说明
userInfoObject当前登录用户信息(idusernametenantCode
clientObject数据库操作入口,通过 models 访问数据集
appCodeString当前应用编码

数据集操作方式

// 定义数据集映射(函数开头定义)
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:每次数据操作都会执行先验函数,所以:

  1. 避免耗时操作:不要在先验函数中调用慢速外部 API
  2. 减少数据库查询:合并多个查询,使用索引
  3. 使用缓存:对于不常变化的数据(如配置信息)可以使用缓存
// ❌ 不推荐:多次查询
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 中访问数据集
最佳实践
  • 前端校验做格式检查,先验函数做业务规则
  • 避免在先验函数中执行耗时操作
  • 使用批量查询优化性能
  • 添加日志便于调试

下一步

相关阅读

核心文档

进阶主题


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