BPMAXBPMAX
  • 快速入门
  • 核心概念
  • 管理员手册
  • 仿真和回放
  • 流程相关脚本
  • 表单相关脚本
  • 数据集相关脚本
  • 界面相关脚本
  • 系统相关脚本
  • 流程集成
  • 数据集
  • 接口集成
  • 实体映射
  • OpenAPI
  • 实体列表
  • 插件开发
  • 日志排查
  • 飞书平台

    • 同步组织架构
    • 同步团队组织架构
    • 一键拉群
    • 高级卡片消息
    • 服务台能力
  • 实用功能

    • 系统公告
    • 项目日历
    • 超时自动化
    • 报告自动生成
    • 流程资源档案
  • 文档更新记录
  • 系统更新说明
  • 快速入门
  • 核心概念
  • 管理员手册
  • 仿真和回放
  • 流程相关脚本
  • 表单相关脚本
  • 数据集相关脚本
  • 界面相关脚本
  • 系统相关脚本
  • 流程集成
  • 数据集
  • 接口集成
  • 实体映射
  • OpenAPI
  • 实体列表
  • 插件开发
  • 日志排查
  • 飞书平台

    • 同步组织架构
    • 同步团队组织架构
    • 一键拉群
    • 高级卡片消息
    • 服务台能力
  • 实用功能

    • 系统公告
    • 项目日历
    • 超时自动化
    • 报告自动生成
    • 流程资源档案
  • 文档更新记录
  • 系统更新说明
  • 插件开发入门

    • 插件开发
    • 插件架构与加载机制
    • 环境准备与开发模式
    • 第一个最小插件
  • 插件能力开发

    • 前端扩展点实战
    • 后端扩展点实战
    • 前后端联动完整案例:任务集成插件
  • 插件运行机制

    • 配置、安装、升级与发布
    • 定时任务与异步处理
    • Hook 机制与平台事件接入
    • 外部系统集成模式
  • 进阶与参考

    • 调试与排错
    • 设计规范与最佳实践
    • 能力类型索引与选型

后端扩展点实战

本文介绍 BPMAX 插件后端的基础结构和开发方式,目标是让你能编写插件接口、服务逻辑、安装脚本和简单数据模型。

学习目标

  • 理解插件后端目录的职责分层
  • 学会新增控制器、服务和模型
  • 学会使用安装脚本和卸载脚本

插件后端目录结构

一个典型插件后端通常包含以下目录:

server/src/
├── controller/
│   └── plugin_example.js
├── service/
│   └── plugin_example.js
├── model/
├── bootstrap/
│   └── hooks.js
├── install/
│   ├── plugin_example_install.js
│   └── plugin_example_uninstall.js
└── config/
    └── crontab_job.js

接口层

用于暴露插件接口,对应平台 API 路由入口。

控制器建议只负责:

  • 接收参数
  • 基础校验
  • 调用服务
  • 返回响应

service

用于承载真正的业务逻辑,例如:

  • 调用第三方接口
  • 组装数据
  • 执行同步或异步流程
  • 处理缓存和重试

model

适合处理:

  • 插件自有表
  • 插件队列数据
  • 插件配置持久化

bootstrap/hooks

用于注册 Hook,把插件逻辑挂到平台事件上。

install

用于安装和卸载时执行初始化逻辑。

config/crontab_job

用于定义插件定时任务。

编写一个最小接口

控制器入口

最简单的后端接口通常从接口层开始。例如:

import BaseRest from '../rest.js';

@think.RestController()
export default class extends BaseRest {
  async pingAction() {
    return this.success({
      plugin: 'plugin_example',
      message: 'pong',
    });
  }
}

访问路径可以先约定为:

/api/plugin_example/ping

这类控制器可以先作为最小后端入口,后续再补具体 action。

请求与响应约定

插件控制器通常沿用平台已有控制器风格:

  • 继承 BaseRest
  • 使用 this.success(...)
  • 使用 this.fail(...)

这样能和平台其他接口保持一致。

如果需要参数校验,可以先从最简单的写法开始:

async saveConfigAction() {
  const { enabled, platform_id } = this.post();

  if (!platform_id) {
    return this.fail('platform_id is required');
  }

  const result = await this.service('plugin_example').saveConfig({
    enabled,
    platform_id,
  });

  return this.success(result);
}

路由命名约定

建议接口名与插件英文名保持一致,例如:

  • /api/plugin_example/...
  • /api/plugin_task_integration/...

建议保持:

  • 插件前缀明确
  • 含义稳定
  • 尽量避免缩写混乱

编写服务层逻辑

服务层职责

服务层应该承接核心业务,而不是把逻辑堆在控制器里。

例如任务集成类插件的服务层可以负责:

  • 获取第三方访问令牌
  • 创建外部任务
  • 更新或完成外部任务

一个最小服务示例如下:

export default class PluginExampleService extends think.Service {
  async saveConfig(data) {
    const payload = {
      enabled: Boolean(data.enabled),
      platform_id: data.platform_id,
      updated_at: Date.now(),
    };

    return payload;
  }
}

如果要调用第三方接口,可以进一步拆成:

export default class PluginExampleService extends think.Service {
  async getAccessToken(appId, secret) {
    const res = await this.http.post('/oauth/token', {
      app_id: appId,
      secret,
    });

    return res.data.access_token;
  }

  async createTask(platformConfig, payload) {
    const token = await this.getAccessToken(
      platformConfig.app_id,
      platformConfig.secret
    );

    return this.http.post('/tasks', payload, {
      headers: {
        Authorization: `Bearer ${token}`,
      },
    });
  }
}

何时调用平台服务

插件后端可以调用平台已有服务,例如:

  • Redis
  • OSS
  • 消息
  • 平台配置

调用前建议先确认平台中是否已有成熟能力,能复用时尽量复用。

日志与异常处理

后端插件建议至少做到:

  • 关键分支打日志
  • 第三方调用错误带上下文
  • 对外响应尽量稳定

特别是集成类插件,失败时一定要能判断是:

  • 参数问题
  • 配置问题
  • 第三方网络问题
  • 权限或鉴权问题

何时需要模型层

插件独立数据表

如果插件需要存储自己的业务记录、缓存队列或绑定关系,就需要模型层。

例如可以为任务绑定关系建立一个简单模型:

export default class PluginTaskMapModel extends think.Model {
  get tableName() {
    return 'plugin_task_map';
  }
}

插件自有表怎么设计

先判断是否真的需要自有表

不是所有插件都要建表。通常只有下面几类场景才建议维护插件自有表:

  • 需要保存插件自己的业务状态
  • 需要记录平台对象与第三方对象的绑定关系
  • 需要实现失败重试、补偿、队列或审计
  • 需要高频读取且不适合每次都现场计算

如果只是一次性调用第三方接口,且没有后续状态跟踪,很多时候服务层加配置就够了。

常见表类型

插件自有表通常可以分成四类:

  1. 绑定关系表
  2. 任务状态表
  3. 日志或审计表
  4. 队列或重试表

例如任务集成类插件,常见会拆成:

plugin_task_map
plugin_task_retry
plugin_task_audit_log

绑定关系表怎么设计

绑定关系表用于记录“平台对象”和“第三方对象”的对应关系,字段一般要包含:

  • 平台主键
  • 第三方主键
  • 当前状态
  • 创建时间
  • 更新时间

一个表结构示例如下:

CREATE TABLE plugin_task_map (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  project_guid VARCHAR(64) NOT NULL,
  step_guid VARCHAR(64) NOT NULL,
  external_task_id VARCHAR(128) NOT NULL,
  status VARCHAR(32) NOT NULL DEFAULT 'pending',
  created_at BIGINT NOT NULL,
  updated_at BIGINT NOT NULL,
  UNIQUE KEY uk_project_step (project_guid, step_guid)
);

这类表最关键的是唯一键设计。要先回答:

  • 一个流程实例是否只允许绑定一个外部对象
  • 一个流程环节是否允许重复创建任务

只有先明确业务唯一性,唯一索引才能定得准。

重试表怎么设计

如果插件存在失败补偿或异步重试,建议单独拆一张重试表,不要把所有字段都堆在主业务表里。

例如:

CREATE TABLE plugin_task_retry (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  task_map_id BIGINT NOT NULL,
  retry_count INT NOT NULL DEFAULT 0,
  next_retry_at BIGINT NOT NULL,
  last_error_message TEXT,
  created_at BIGINT NOT NULL,
  updated_at BIGINT NOT NULL
);

这样做有两个好处:

  • 主表保持稳定,不被临时状态污染
  • 重试策略可以独立演进

审计日志表怎么设计

只要插件对接外部系统,通常都值得留一张最小审计表:

CREATE TABLE plugin_task_audit_log (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  project_guid VARCHAR(64) NOT NULL,
  action VARCHAR(64) NOT NULL,
  request_id VARCHAR(128),
  result VARCHAR(32) NOT NULL,
  message TEXT,
  created_at BIGINT NOT NULL
);

这类表不要追求保存完整请求体,重点是留下可定位的摘要字段:

  • 对象是谁
  • 做了什么动作
  • 成功还是失败
  • 哪个请求触发的

模型层示例

对应到插件模型,可以拆成下面这样:

export class PluginTaskMapModel extends think.Model {
  get tableName() {
    return 'plugin_task_map';
  }
}

export class PluginTaskRetryModel extends think.Model {
  get tableName() {
    return 'plugin_task_retry';
  }
}

服务层调用时建议保持边界清晰:

async bindExternalTask(data) {
  return this.model('plugin_task_map').add({
    project_guid: data.project_guid,
    step_guid: data.step_guid,
    external_task_id: data.external_task_id,
    status: 'pending',
    created_at: Date.now(),
    updated_at: Date.now(),
  });
}

字段设计原则

设计插件自有表时,建议优先遵循:

  1. 字段名直接表达业务含义,不用过度缩写。
  2. 状态字段统一枚举值,不要不同表各写一套含义相近的状态。
  3. 所有业务关键表都带 created_at 和 updated_at。
  4. 外部对象 id 使用足够宽的字符串字段,避免后续第三方 id 长度不够。
  5. 尽量显式设计唯一键,而不是依赖代码层“应该不会重复”。

什么时候拆表,什么时候加字段

判断标准可以很简单:

  • 如果字段描述的是“主体的稳定属性”,优先加在主表
  • 如果字段描述的是“重试、日志、事件、历史”,优先拆表
  • 如果某类数据增长快、查询模式不同,也优先拆表

例如:

  • external_task_id 适合放主表
  • retry_count 可以放重试表
  • last_error_message 更适合放重试表或日志表
  • 回调原始报文更适合单独日志表

常见建模错误

最常见的问题有:

  • 一个表既存主业务状态,又存日志、重试、回调、缓存
  • 没有唯一键,导致重复创建外部对象后无法修复
  • 直接把第三方原始响应整包塞进主表
  • 没有时间字段,后续排障只能靠日志猜测

插件配置缓存

如果配置读取频繁,也可以由模型层或服务层做缓存封装。

与平台模型协作

很多插件并不维护完整的独立数据表,而是通过服务层调用平台模型完成业务联动。

常见平台 Model 能力

这一节总结的是实际插件中最常见、复用率最高的平台模型能力。
对于外部插件开发,优先理解这些能力,比一开始就自建整套业务表更重要。

project 模型

最常见用途:

  • 查询流程实例
  • 创建子流程或关联流程
  • 更新项目扩展字段
  • 按业务条件筛选项目

最常见写法:

const projectModel = this.model('project');

const project = await projectModel
  .field(['id', 'name', 'ext_config'])
  .where({
    id: projectId,
    is_del: 0,
  })
  .find();

在现有插件里,project 最常被复用的能力之一是 createProject:

const result = await this.model('project').createProject({
  flow_type: 'default',
  key: 'flow_student_signup',
  user_id: studentId,
  project_id: parentProjectId,
  step_id: 0,
  workspace: 'default',
  projectForm: {
    key_to_value: {
      studentId,
      studentName,
      signUpStatus: 0,
    },
  },
});

适用场景:

  • 根据主流程自动创建子流程
  • 批量创建待处理业务实例
  • 从插件动作触发新流程

form 模型

最常见用途:

  • 回写表单字段
  • 批量补充插件计算结果
  • 把第三方返回值落回流程表单

实际插件中非常常见的方法是 patchFormField:

await this.model('form').patchFormField({
  project_id,
  form: {
    key_to_value: {
      externalTaskId: externalTaskId,
      syncStatus: 'success',
      syncTime: Date.now(),
    },
  },
});

适用场景:

  • 第三方单号、回执号回填
  • 自动计算字段回写
  • 执行动作后更新展示字段

step 模型

最常见用途:

  • 查询当前待办环节
  • 执行流程流转
  • 基于动作推进到目标环节

现有插件里最常用的是 solveStepByAction:

await this.model('step').solveStepByAction(
  'approve',
  stepId,
  {
    project_id,
    target_step: nextStepId,
  },
  this.ctx.userInfo
);

适用场景:

  • 插件自动审批或自动驳回
  • 定时任务到点自动推进流程
  • 第三方回调后驱动流程继续执行

写这类逻辑时要特别注意:

  • stepId 必须是当前有效环节
  • target_step 必须与流程定义一致
  • 执行人上下文要明确,不能随意传空对象

message 模型

最常见用途:

  • 发送站内消息
  • 构造带项目链接的通知
  • 在插件动作完成后通知相关人

常见模式如下:

const messageModel = this.model('message');

const messageData = messageModel.buildMessageDataForProjectLink(
  '任务处理完成',
  '您有一个新的处理结果,请点击查看详情。',
  project_id,
  step_id
);

await messageModel.sendMessage(
  fromUserId,
  [toUserId],
  messageData,
  [workspace],
  {
    project_id,
    step_id,
  }
);

适用场景:

  • 给流程负责人发处理提醒
  • 给发起人发完成通知
  • 给下一处理人发待办消息

其他常见基础模型

已有插件里也经常直接复用这些模型:

  • user:查询人员基础信息
  • group:查询组织、门店、部门等组织结构数据
  • role、user_role:按角色反查用户
  • pending_task:处理插件生成的待办记录
  • form_field:按字段维度做定向查询

推荐原则:

  • 能通过平台已有模型完成的业务联动,优先不要自己再维护一份镜像表
  • 只有当你需要插件专属状态、审计、重试机制时,再设计插件自有表

常见平台 Service 与底层能力

这一部分区分两类:

  • 平台核心服务:通常是插件联动流程时最常见的能力
  • 公共扩展服务:有些环境会提供,使用前应确认是否已安装

核心能力:流程同步与索引刷新

实际插件里频繁出现的是 bpmax_server 相关能力,例如:

  • activeSyncProjectData
  • forceSyncProject

常见写法:

await this.service('bpmax_server').activeSyncProjectData(project_id);

或:

await this.service('bpmax_server').forceSyncProject(project_id);

适用场景:

  • patchFormField 之后需要刷新项目展示数据
  • 自动流转后需要立刻让列表、详情、搜索结果同步
  • 批量修改流程数据后主动触发同步

经验建议:

  • 单条更新优先用 activeSyncProjectData
  • 强制重建或复杂补偿场景再考虑 forceSyncProject
  • 批量处理时优先合并 id,避免一条一条同步造成压力

公共能力:消息与群通知

在一些已安装消息能力的环境中,常见会复用这些服务:

  • plugin_common_feishu
  • plugin_feishu_group_service
  • plugin_feishu_message_service
  • plugin_common_widget

实际插件里出现过的典型能力包括:

  • getUserOpenId
  • addUserToChatGroup
  • sendMsgChatGroup
  • sendMessage

示例:

const openUser = await this.service('plugin_common_feishu').getUserOpenId({
  user_id: userId,
});

await this.service('plugin_common_feishu').sendMsgChatGroup({
  chat_id,
  msg_type: 'text',
  content: `流程 ${projectName} 已更新`,
});

这类服务适合:

  • 发送群通知
  • 把用户加入群聊
  • 给第三方协作平台推送消息

注意:

  • 这类能力往往依赖额外的集成插件或环境配置
  • 外部插件在接入前应先确认部署环境中是否已提供对应服务

公共能力:导出文件

部分环境会提供导出服务,例如 streamExportExcel 这一类能力:

const url = await this.service('plugin_export_file_service').streamExportExcel({
  fileName: 'certificate-list.xlsx',
  columns: [
    { header: '姓名', key: 'userName', width: 20 },
    { header: '状态', key: 'status', width: 16 },
  ],
  getBatchData: async (page, pageSize) => {
    return await queryPage(page, pageSize);
  },
});

适用场景:

  • 插件报表导出
  • 批量台账导出
  • 后台异步生成 Excel

公共能力:敏感字段加解密

在处理手机号、身份证号等字段时,已有插件中常见的做法是复用统一加密服务:

const encryptedPhone = this.service('plugin_common_crypto_service').encryptString(
  phone
);

或:

const phone = await this.service('plugin_common_crypto_service').decryptPhone(
  encryptedPhone
);

适用场景:

  • 敏感信息脱敏存储
  • 向第三方传输前统一加密
  • 导出或展示前按规则解密

什么时候可以直接用 SDK

如果插件只是调用一个外部 HTTP API,通常有两种实现路径:

  1. 直接在插件服务层中封装 axios 或 HTTP Client
  2. 先封装一个插件自己的 SDK / Client,再由 service 调用

更推荐第二种,当你满足以下条件时:

  • 第三方接口不止 1 个
  • 有统一鉴权、重试、签名逻辑
  • 后续多个 action / hook / cron 都会复用

示例:

import axios from 'axios';

export class ExternalTaskClient {
  constructor(baseURL: string, token: string) {
    this.client = axios.create({
      baseURL,
      headers: {
        Authorization: `Bearer ${token}`,
      },
      timeout: 10000,
    });
  }

  async createTask(payload: Record<string, any>) {
    return this.client.post('/tasks', payload);
  }
}

然后在插件 service 里调用:

export default class PluginTaskService extends think.Service {
  async createExternalTask(config, payload) {
    const client = new ExternalTaskClient(config.baseUrl, config.token);
    const res = await client.createTask(payload);
    return res.data;
  }
}

后端能力选择顺序

可以按下面顺序判断:

  1. 先看平台现有 model 是否已经能完成数据读写
  2. 再看平台核心 service 是否已经覆盖同步、消息、导出等通用能力
  3. 若环境中已安装公共集成服务,可以直接复用
  4. 若以上都不满足,再为插件自己封装 SDK 或新建数据表

这样做能明显减少:

  • 重复造轮子
  • 数据副本过多
  • 插件升级时的兼容成本

安装与卸载脚本

适用场景

以下场景建议增加安装脚本:

  • 初始化配置
  • 初始化基础数据
  • 创建必要绑定关系

以下场景建议增加卸载脚本:

  • 清理绑定关系
  • 释放外部注册
  • 回收插件初始化的系统资源

命名约定

通常放在 src/install/ 下,并遵循:

  • {plugin_en_name}_install.js
  • {plugin_en_name}_uninstall.js

平台安装机制会按这个约定查找对应文件。

安装脚本最小示例如下:

export default class extends think.Service {
  async run() {
    think.logger.info('[plugin_example] install start');
    return true;
  }
}

卸载脚本可以保持同样结构:

export default class extends think.Service {
  async run() {
    think.logger.info('[plugin_example] uninstall start');
    return true;
  }
}

执行时机

从平台实现看:

  • 首次安装时会尝试执行 install 脚本
  • 卸载时会尝试执行 uninstall 脚本

因此脚本要尽量做到可重复执行或至少可安全失败。

后端开发常见风险

文件命名冲突导致覆盖平台文件

当前插件安装机制会把文件写入平台运行环境,所以一定要避免插件文件名与平台已有文件重名,否则可能覆盖平台实现。

安装脚本不可重复执行

如果安装脚本假设“只会执行一次”,升级或重复安装时就容易出问题。建议脚本本身做好存在性检查。

插件升级时的兼容性问题

升级时最容易出问题的是:

  • 旧配置结构和新代码不兼容
  • 新字段没有默认值
  • 历史数据没有迁移

下一步

  • 前后端联动完整案例:任务集成插件
  • 配置、安装、升级与发布
Prev
前端扩展点实战
Next
前后端联动完整案例:任务集成插件