Skip to content

Backend Function 编写指南

本文档旨在指导开发者编写适用于 lovrabet-runtime 平台的动态脚本。这些脚本用于实现数据权限过滤、动态脱敏、数据增强以及复杂的独立业务逻辑。


1. 核心约定 (Core Conventions)

为了支持高性能的异步操作(如数据库访问),所有业务脚本必须严格遵循以下函数签名规范。

1.1 入口函数签名

所有脚本必须导出一个 异步函数 (Async Function),并接收两个固定参数:paramscontext

JavaScript
/**
 * 标准入口函数
 * @param {Object} params - 业务数据对象(如请求参数、查询结果),可原地修改。
 * @param {Object} context - 执行上下文,包含用户信息、应用信息和数据库操作能力。
 * @returns {Promise<Object>} - 返回修改后的 params 对象(对于 HOOK 脚本)或业务结果(对于 ENDPOINT 脚本)。
 */
export default async function functionName(params, context) {
    // 业务逻辑...
    return params;
}

1.2 参数详解

params (业务数据)

  • Before 阶段 (前置脚本): 对应 API 的请求参数(requestBody)。例如查询条件、表单提交数据。
  • After 阶段 (后置脚本): 对应 API 的返回结果(responseBody.data)。例如查询出的列表数据。
  • Endpoint 脚本: 对应 HTTP 请求体中的 JSON 数据。

context (执行上下文)

提供脚本运行所需的辅助信息和能力:

属性名类型说明
context.userInfoObject当前登录用户信息(如 id, username, tenantCode)。
context.appCodeString当前应用编码。
context.tenantCodeString当前租户编码。
context.clientObject数据库操作入口。提供 models 方法以访问数据集。

2. 脚本类型与示例 (Script Types & Examples)

2.1 HOOK 脚本 (Before/After)

HOOK 脚本依附于标准数据接口(如 getList, create),用于拦截和修改数据。作用域在 http 接口层。

示例 1: 列表查询前置 - 权限过滤 (Before)

场景: 强制只允许查询当前用户创建的数据。

JavaScript
export default async function before(params, context) {
  // params 是 API 请求参数(如查询条件)
  
  // 强制注入过滤条件:create_by 必须等于当前用户 ID
  params.create_by = context.userInfo.id;
  
  // 必须返回修改后的 params
  return params;
}

示例 2: 根据用户角色过滤-权限过滤(before)

场景: 非管理员用户只能看到自己创建的数据。

JavaScript
export default async function(params, context) {
  // 非管理员只能看自己的数据
  if (context.userInfo.role !== "admin") {
    params.created_by = context.userInfo.id;
  }
  return params;
}

示例 3: 数据创建前置 - 业务校验 (Before)

场景: 校验订单金额不能超过限制。

JavaScript
export default async function before(params, context) {
  // params 是提交的表单数据
  
  if (params.amount > 500) {
      throw new Error("无法创建超过 500 元的订单");;
  }
  
  // 补充默认值
  params.source = "WEB_APP";
  
  return params;
}

示例 4: 自动填充字段 (Before)

JavaScript
export default async function(params, context) {
  params.created_by = context.userInfo.id;
  params.created_at = new Date().toISOString();
  params.tenant_code = context.tenantCode;
  return params;
}

示例 5: 数据脱敏 (After)

场景: 对返回列表中的手机号进行脱敏。

JavaScript
export default async function after(params, context) {
    // params 对应 response.data (通常包含 tableData 列表)
    const list = params.tableData;
    
    if (list && list.length > 0) {
        list.forEach(record => {
            if (record.phone) {
                // 正则替换
                record.phone = record.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
            }
        });
    }

    return params;
}

示例 6: 动态追加计算字段 (After)

JavaScript
export default async function(params, context) {
  // params 为接口返回的数据
  params.tableData?.forEach(record => {
    record.total_amount = record.price * record.quantity;
    record.is_vip = record.order_count > 10;
  });

  // 追加列定义(前端自动展示)
  const extraColumns = [
    { dataIndex: "total_amount", title: "总金额", type: "NUMBER" },
    { dataIndex: "is_vip", title: "VIP客户", type: "BOOLEAN" }
  ];
  extraColumns.forEach(col => params.tableColumns?.push(col));

  return params;
}

示例 7:数据权限过滤 (After)

JavaScript
export default async function(params, context) {
  // params 为接口返回的数据
  if (context.userInfo.role !== "admin") {
    params.tableColumns = params.tableColumns?.filter(col =>
      !["salary", "commission", "bank_account"].includes(col.dataIndex)
    );

    params.tableData?.forEach(record => {
      delete record.salary;
      delete record.commission;
      delete record.bank_account;
    });
  }
  return params;
}

示例8 :执行自定义SQL

执行自定义SQL的案例:BF支持自定义SQL示例

2.2 ENDPOINT 脚本 (独立端点)

INFO

需要独立部署的场景下:。。。。

独立端点脚本是一种特殊的 JS 脚本,其类型为 ENDPOINT

  • API 路径: /api/``endpoint/``{appCode}/{scriptName}
  • 请求方式: POST
  • 请求体: JSON 对象,将直接作为 params 传递给脚本。

示例 1: 复杂业务逻辑 (库存校验与下单)

这是一个独立端点脚本,模拟“下单”事务。

JavaScript
export default async function createOrder(params, context) {
  // 1. 校验库存
  const product = await context.client.models.dataset_XXXXXXXXXX.getOne({
    id: params.productId
  });

  if (!product || product.stock < params.quantity) {
    throw new Error(`库存不足,当前库存: ${product ? product.stock : 0}`);
  }

  // 2. 计算价格 (根据用户等级打折)                                        
  const user = await context.client.models.dataset_XXXXXXXXXX.getOne({
    id: context.userInfo.id
  });

  const discount = (user.vip_level >= 3) ? 0.8 : 1.0;
  const finalPrice = product.price * params.quantity * discount;

  // 3. 创建订单
  const orderId = await context.client.models.dataset_XXXXXXXXXX.create({
    user_id: context.userInfo.id,
    product_id: params.productId,
    quantity: params.quantity,
    amount: finalPrice,
    status: "pending",
    create_time: new Date()
  });

  // 4. 扣减库存 (更新操作)
  await context.client.models.dataset_XXXXXXXXXX.update({
      id: product.id,
      stock: product.stock - params.quantity
  });

  return {
    success: true,
    orderId: orderId,
    payAmount: finalPrice,
    discountRate: discount
  };
}

3. 数据集模型操作能力 (Dataset Model Access)

通过 context.client.models,脚本可以安全、异步地访问应用内的数据集。支持 await 语法。

3.1 基础 API

所有操作均基于数据集编码 (datasetCode)。api 依赖于java 应用内部的接口实现。

JavaScript
// 获取数据集访问器
const ds = context.client.models.dataset_XXXXXXXXXX // "XXXXXXXXXX" 是数据集编码
方法说明参数示例返回值
getOne(params)查询单条数据{ id: 1 }对象 Object
getList(params)查询列表(分页){ page: 1, size: 20 }列表 List
filter(params)高级过滤查询{

ytWhere: {

age: 18

}


}
列表 List
create(data)创建数据{ name: "John", age: 20 }
update(data)更新数据{ id: 1, name: "New Name" }成功(true)/失败(false)
delete(params)删除数据{ id: 1 }成功(true)/失败(false)

3.2 关联查询示例 (Join-like)

在查询订单列表后,根据 user_id 查询用户详情并拼接到订单数据中。

JavaScript
export default async function before(params, context) {
    const list = params.tableData;
    
    // 遍历列表(注意:为了性能,建议使用 Promise.all 并行查询)
    for (let i = 0; i < list.length; i++) {
        const order = list[i];
        if (order.user_id) {
            // 异步查询用户信息
            const user = await context.client.models.dataset_XXXXXXXXXX.getOne({
                id: order.user_id
            });
            // 数据增强
            if (user) {
                order.user_name = user.username;
                order.user_level = user.vip_level;
            }
        }
    }
    
    return params;
}

4. 注意事项 (Best Practices)

  1. 返回值: 务必在函数末尾 return params(对于 HOOK)或业务结果对象(对于 ENDPOINT)。如果忘记返回,后续流程可能会收到 null 数据。
  2. 异步等待: 所有数据库操作 (context.client.models) 都是异步的,必须使用 await 等待结果,否则脚本会继续执行而无法拿到数据。
  3. 异常处理: 脚本中抛出的 Error 会被系统捕获并返回给前端,状态码通常为 500 (Failure)。请确保错误信息对用户友好。
  4. 限流: 为了防止死循环,单个脚本执行过程中允许的数据库调用次数有限制(默认 50 次)。超出限制会抛出异常。

5. 脚本生效与缓存机制 (Caching)

为了保证高性能,系统对动态脚本实施了多级缓存机制。在修改数据库中的脚本内容后,请注意以下生效逻辑:

5.1 缓存级别

  1. 元数据缓存 (Metadata Cache):系统切面会缓存脚本的查询结果(包括脚本代码),缓存有效期为 5 分钟。这意味着在数据库修改脚本后,最长可能需要 5 分钟才会触发更新。
  2. 编译缓存 (Execution Cache):脚本引擎会缓存已编译的 JavaScript 对象以提高执行效率,该缓存遵循“最近最少使用”原则(LRU),默认有效期为 10 分钟

5.2 调试建议

  • 开发测试:在开发环境下,如果您希望修改立即生效,建议临时重启应用或通过管理接口清理缓存。
  • 发布生产:脚本发布到生产环境后,请预留至少 5-10 分钟的观察窗口,确保所有集群节点的缓存都已刷新。

基于飞书知识库同步生成,内容以飞书源文档为准