action 和 function 有什么区别

Oinone(开源低代码 / 企业应用开发平台) 里,ActionFunction 都是“可被调用的逻辑单元”,但它们的定位和使用场景不同。可以简单理解为:

  • Function = 纯逻辑函数(偏后端能力)
  • Action = 面向业务操作的动作(偏应用行为 / UI触发)

下面给你详细对比一下。


1️⃣ Function:函数(逻辑能力)

Function 更像是一个 可复用的服务方法

特点

  • 通常是 纯逻辑处理
  • 不直接绑定 UI
  • 可以被 Action / Service / 其他 Function 调用
  • 用来封装 业务计算或工具逻辑

常见用途

比如:

  • 价格计算
  • 数据校验
  • 数据转换
  • 调用第三方 API
  • 复杂业务规则

示例

@Function(openLevel = FunctionOpenEnum.API)
@Function.Advanced(type = FunctionTypeEnum.QUERY)
public TradeOrder computePrice(TradeOrder data) {
    return data;
}

用途:

订单金额计算逻辑

然后可能被多个地方调用:

Action -> 调用 Function
Service -> 调用 Function
Workflow -> 调用 Function

📌 核心:可复用业务逻辑


2️⃣ Action:动作(业务操作)

Action 是一个 业务动作,通常是 用户触发的行为

特点

  • 通常绑定 UI
  • 可以在 按钮 / 菜单 / API / 工作流 中触发
  • 通常操作 模型数据
  • 可以调用 Function

常见用途

例如:

  • 创建订单
  • 提交审批
  • 发布文章
  • 批量删除
  • 导入数据

示例

@Action
public void submitOrder(Order order){
    order.setStatus("SUBMITTED");
}

UI 可能是:

订单详情页
[提交订单] 按钮

点击按钮 → 调用 Action。

📌 核心:业务行为入口


3️⃣ 核心区别总结

维度 Action Function
定位 业务动作 逻辑函数
是否绑定 UI 通常是
是否直接给用户操作
是否可复用 一般 很高
是否操作模型 常见 不一定
调用关系 可调用 Function 不调用 Action

4️⃣ 调用关系(典型架构)

通常推荐的结构:

UI按钮
   ↓
Action(业务入口)
   ↓
Function(业务逻辑)
   ↓
DAO / Repository

例如:

提交订单按钮
   ↓
submitOrderAction
   ↓
checkInventoryFunction
calcPriceFunction
createOrderFunction

这样:

  • Action 只负责 流程
  • Function 负责 逻辑

代码会更清晰。


5️⃣ 一句话理解

Action = 用户触发的业务操作
Function = 可复用的业务逻辑函数


💡 如果你在 Oinone 做模块开发,我可以再给你讲一个更实用的:

“什么时候写 Action,什么时候写 Function” 的最佳实践(很多人会写反)。

他们在权限上有什么区别

Oinone 里,Action 和 Function 在权限控制上的区别其实很明显:

Action 是有权限控制的业务入口,而 Function 通常没有独立权限控制。

可以从几个层面理解。


1️⃣ Action:有权限控制(面向用户)

Action 通常直接参与权限体系。

因为 Action 往往对应:

  • 页面按钮
  • 菜单操作
  • API接口
  • 工作流节点动作

所以平台会对 Action 做权限控制

常见控制方式

Action 可以被:

  • 角色权限控制
  • 菜单权限控制
  • 数据权限控制
  • 按钮可见性控制

例如:

角色:销售
   ✔ 提交订单 Action
   ✔ 查看订单
   ✘ 删除订单 Action

如果用户没有权限:

  • 按钮不会显示
  • 或调用 API 时被拒绝

📌 Action 是权限体系的主要控制点。


2️⃣ Function:通常没有独立权限

Function 只是逻辑方法,不直接暴露给用户。

它通常:

  • 只能被 Action / Service / Workflow 调用
  • 不直接作为 UI 操作入口

因此 Function 一般不做权限控制

例如:

submitOrderAction
    ↓
calcPriceFunction
checkInventoryFunction
createOrderFunction

权限只在:

submitOrderAction

检查一次即可。


3️⃣ 为什么设计成这样

这是典型的 业务入口权限模型

用户
 ↓
Action(权限控制)
 ↓
Function(内部逻辑)
 ↓
数据

好处:

1️⃣ 权限集中
2️⃣ 避免重复校验
3️⃣ Function 可复用

否则:

10个 Action
调用同一个 Function

每个 Function 再做权限判断会非常混乱。


4️⃣ 一个真实场景

删除订单

deleteOrderAction

权限:

角色:管理员 ✔
角色:销售 ✘

Action:

deleteOrderAction
   ↓
deleteOrderFunction

如果销售没有权限:

  • 按钮不显示
  • API 不允许调用

deleteOrderFunction 仍然存在,只是不会被调用。


5️⃣ 特殊情况(高级开发)

有些团队会在 Function 内再做安全校验,例如:

  • 防止被错误调用
  • 防止内部接口滥用

例如:

checkPermissionFunction()

但这属于 业务安全设计,不是 Oinone 的默认权限模型。


6️⃣ 一句话总结

对比 Action Function
是否有权限控制 ✅ 有 ❌ 通常没有
是否直接暴露给用户 ✅ 是 ❌ 否
是否是权限入口 ✅ 是 ❌ 不是
是否可被 UI 按钮触发 ✅ 可以 ❌ 不可以

核心理解:

权限控制在 Action 层,Function 是内部逻辑。


如果你在 Oinone 做企业系统开发,还有一个非常关键的点很多人会搞错:

Action 的权限 ≠ 数据权限(Data Permission)

区别到这就介绍完了,使用规范参考:Function、Action函数使用规范

Oinone社区 作者:nation原创文章,如若转载,请注明出处:https://doc.oinone.top/frontend/25520.html

访问Oinone官网:https://www.oinone.top获取数式Oinone低代码应用平台体验

Like (1)
nation's avatarnation数式员工
Previous 2026年2月10日 pm3:38
Next 2026年3月31日 pm11:21

相关推荐

  • 【前端】登录页面扩展点

    登录页面扩展点 场景 1: 登录之前需要二次确认框2: 前端默认的错误提示允许被修改3: 后端返回的错误提示允许被修改4: 登录后跳转到自定义的页面 方案 前端默认错误可枚举 errorMessages: { loginEmpty: '用户名不能为空', passwordEmpty: '密码不能为空', picCodeEmpty: '图形验证码不能为空', phoneEmpty: '手机号不能为空', verificationCodeEmpty: '验证码不能为空', picCodeError: '图形验证码错误', inputVerificationCodeAlign: '请重新输入验证码' } 登录按钮添加拓展点beforeClick、afterClick 代码 新增一个ts文件,继承平台默认的LoginPageWidget @SPI.ClassFactory(RouterWidget.Token({ widget: 'Login' })) export class CustomLoginPageWidget extends LoginPageWidget { constructor() { super(); // 修改前端默认的错误文案 this.errorMessages.loginEmpty = '登录用户名不能为空'; } /** * 用来处理点击「登录」之前的事件,可以做二次确定或者其他的逻辑 * 只有return true,才会继续往下执行 */ public beforeClick(): Promise<Boolean | null | undefined> { return new Promise((resolve) => { Modal.confirm({ title: '提示', content: '是否登录?', onOk: () => { resolve(true); } }); }); } /** * * @param result 后端接口返回的数据 * * 用来处理「登录」接口调用后的逻辑,可以修改后端返回的错误文案,也可以自定义 * * 只有return true,才会执行默认的跳转事件 */ public afterClick(result): Promise<any | null | undefined> { // if(result.redirect) { // 自定义跳转 //return false //} if (result.errorCode === 20060023) { result.errorMsg = '手机号不对,请联系管理员'; } return result; } }

    2023年11月1日
    1.4K00
  • 自定义前端拦截器

    某种情况下,我们需要通过自定义请求拦截器来做自己的逻辑处理,平台内置了一些拦截器 登录拦截器LoginRedirectInterceptor 重定向到登录拦截器LoginRedirectInterceptor import { UrlHelper, IResponseErrorResult, LoginRedirectInterceptor } from '@kunlun/dependencies'; export class BizLoginRedirectInterceptor extends LoginRedirectInterceptor { /** * 重定向到登录页 * @param response 错误响应结果 * @return 是否重定向成功 */ public redirectToLogin(response: IResponseErrorResult): boolean { if (window.parent === window) { const redirect_url = location.pathname; // 可自定义跳转路径 location.href = `${UrlHelper.appendBasePath('login')}?redirect_url=${redirect_url}`; } else { // iframe页面的跳转 window.open(`${window.parent.location.origin}/#/login`, '_top'); } return true; } } 请求成功拦截器RequestSuccessInterceptor 请求失败拦截器 RequestErrorInterceptor 网络请求异常拦截器 NetworkErrorInterceptor 当我们需要重写某个拦截器的时候,只需要继承对应的拦截器,然后重写里面的方法即可 // 自定义登录拦截器 export class CustomLoginRedirectInterceptor extends LoginRedirectInterceptor{ public error(response: IResponseErrorResult) { // 自己的逻辑处理 return true // 必写 } } // 自定义请求成功拦截器 export class CustomRequestSuccessInterceptor extends RequestSuccessInterceptor{ public success(response: IResponseErrorResult) { // 自己的逻辑处理 return true // 必写 } } // 自定义请求失败拦截器 export class CustomRequestErrorInterceptor extends RequestErrorInterceptor{ public error(response: IResponseErrorResult) { const { errors } = response; if (errors && errors.length) { const notPermissionCodes = [ SystemErrorCode.NO_PERMISSION_ON_MODULE, SystemErrorCode.NO_PERMISSION_ON_VIEW, SystemErrorCode.NO_PERMISSION_ON_MODULE_ENTRY, SystemErrorCode.NO_PERMISSION_ON_HOMEPAGE ]; /** * 用来处理重复的错误提示 */ const executedMessages: string[] = []; for (const errorItem of errors) { const errorCode = errorItem.extensions?.errorCode; if (!notPermissionCodes.includes(errorCode as any)) { const errorMessage = errorItem.extensions?.messages?.[0]?.message || errorItem.message; if (!executedMessages.includes(errorMessage)) { // 自己的逻辑处理 } executedMessages.push(errorMessage); } } } return true; } } // 自定义网络请求异常拦截器…

    前端 2023年11月1日
    1.8K01
  • 【MSSQL】后端部署使用MSSQL数据库(SQLServer)

    MSSQL数据库配置 驱动配置 Maven配置(2017版本可用) <mssql.version>9.4.0.jre8</mssql.version> <dependency> <groupId>com.microsoft.sqlserver</groupId> <artifactId>mssql-jdbc</artifactId> <version>${mssql.version}</version> </dependency> 离线驱动下载 mssql-jdbc-7.4.1.jre8.jarmssql-jdbc-9.4.0.jre8.jarmssql-jdbc-12.2.0.jre8.jar JDBC连接配置 pamirs: datasource: base: type: com.alibaba.druid.pool.DruidDataSource driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=base username: xxxxxx password: xxxxxx initialSize: 5 maxActive: 200 minIdle: 5 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true asyncInit: true 连接url配置 暂无官方资料 url格式 jdbc:sqlserver://${host}:${port};DatabaseName=${database} 在jdbc连接配置时,${database}必须配置,不可缺省。 其他连接参数如需配置,可自行查阅相关资料进行调优。 方言配置 pamirs方言配置 pamirs: dialect: ds: base: type: MSSQL version: 2017 major-version: 2017 pamirs: type: MSSQL version: 2017 major-version: 2017 数据库版本 type version majorVersion 2017 MSSQL 2017 2017 PS:由于方言开发环境为2017版本,其他类似版本原则上不会出现太大差异,如出现其他版本无法正常支持的,可在文档下方留言。 schedule方言配置 pamirs: event: enabled: true schedule: enabled: true dialect: type: MSSQL version: 2017 major-version: 2017 type version majorVersion MSSQL 2017 2017 PS:由于schedule的方言在多个版本中并无明显差异,目前仅提供一种方言配置。 其他配置 逻辑删除的值配置 pamirs: mapper: global: table-info: logic-delete-value: CAST(DATEDIFF(S, CAST('1970-01-01 00:00:00' AS DATETIME), GETUTCDATE()) AS BIGINT) * 1000000 + DATEPART(NS, SYSUTCDATETIME()) / 100 MSSQL数据库用户初始化及授权 — init root user (user name can be modified by oneself) CREATE LOGIN [root] WITH PASSWORD = 'password'; — if using mssql database, this authorization is required. ALTER SERVER ROLE [sysadmin] ADD MEMBER [root];

    2024年10月18日
    1.4K00
  • 如何自定义 GraphQL 请求

    在开发过程中,有时需要自定义 GraphQL 请求来实现更灵活的数据查询和操作。本文将介绍两种主要的自定义 GraphQL 请求方式:手写 GraphQL 请求和调用平台 API。 方式一:手写 GraphQL 请求 手写 GraphQL 请求是一种直接编写查询或变更语句的方式,适用于更复杂或特定的业务需求。以下分别是 query 和 mutation 请求的示例。 1. 手写 Query 请求 以下是一个自定义 query 请求的示例,用于查询某个资源的语言信息列表。 const customQuery = async () => { const query = `{ resourceLangQuery { queryListByEntity(query: {active: ACTIVE, installState: true}) { id name active installState code isoCode } } }`; const result = await http.query('resource', query); this.list = result.data['resourceLangQuery']['queryListByEntity']; }; 说明: query 语句定义了一个请求,查询 resourceLangQuery 下的语言信息。 查询的条件是 active 和 installState,只返回符合条件的结果。 查询结果包括 id、name、active、installState 等字段。 2. 手写 Mutation 请求 以下是一个 mutation 请求的示例,用于创建新的资源分类信息。 const customMutation = async () => { const code = Date.now() const name = `测试${code}` const mutation = `mutation { resourceTaxKindMutation { create(data: {code: "${code}", name: "${name}"}) { id code name createDate writeDate createUid writeUid } } }`; const res = await http.mutate('resource', mutation); console.log(res); }; 说明: mutation 语句用于创建一个新的资源分类。 create 操作的参数是一个对象,包含 code 和 name 字段。 返回值包括 id、createDate 等字段。 方式二:调用平台的 API 平台 API 提供了简化的 GraphQL 调用方法,可以通过封装的函数来发送 query 和 mutation 请求。这种方式减少了手写 GraphQL 语句的复杂性,更加简洁和易于维护。 1. 调用平台的 Mutation API 使用平台的 customMutation 方法可以简化 Mutation 请求。 /** * 自定义请求方法 * @param modelModel 模型编码 * @param method 方法名或方法对象 * @param records 请求参数,可以是单体对象或者对象的列表 * @param requestFields…

    2024年9月21日
    2.1K00
  • 自定义组件之手动渲染弹出层(v4)

    阅读之前 你应该: 了解自定义组件相关内容。 自定义组件之手动渲染基础(v4) 弹出层组件 我们内置了两个弹出层组件,弹窗(Dialog)和抽屉(Drawer),以下所有内容全部围绕弹窗(Dialog)进行描述,抽屉相关内容与弹窗完全一致。 下面这个对照表格可以帮助你区分两个弹出层组件的异同: 弹出层相关功能 弹窗(Dialog) 抽屉(Drawer) API方法 Dialog#create Drawer#create 内置组件 DialogWidget DrawerWidget 内置插槽 header, default, footer header, default, footer 渲染弹出层的方式 我们提供了三种渲染弹出层组件的方式,根据不同的情况使用不同的方式可以让实现变得更简单。 使用Dialog#createAPI方法创建弹窗:一般用于简单场景,动作区无法进行自定义。 使用DSL渲染能力创建弹窗 调用ActionWidget#click方法打开弹窗(适用于自动渲染场景) 使用createWidget创建DialogWidget并打开弹窗(适用于手动渲染场景) 使用第三方组件创建弹窗:一般用于弹窗需要自定义的场景中,性能最优,可用于任何场景。 使用Dialog#createAPI方法创建弹窗 以下是一个自定义组件的完整示例,其使用ViewCache#compule方法获取视图。 view.ts export const template = `<view> <field data="id" invisible="true" /> <field data="code" label="编码" /> <field data="name" label="名称" /> </view>`; ManualDemoWidget.ts import { BaseElementWidget, createRuntimeContextForWidget, Dialog, FormWidget, MessageHub, RuntimeView, SPI, ViewCache, ViewType, Widget } from '@kunlun/dependencies'; import ManualDemo from './ManualDemo.vue'; import { template } from './view'; @SPI.ClassFactory(BaseElementWidget.Token({ widget: 'ManualDemo' })) export class ManualDemoWidget extends BaseElementWidget { public initialize(props) { super.initialize(props); this.setComponent(ManualDemo); return this; } @Widget.Method() public async openPopup() { // 获取运行时视图 const view = await this.fetchViewByCompile(); if (!view) { console.error('Invalid view'); return; } // 创建运行时上下文 const runtimeContext = createRuntimeContextForWidget(view); const runtimeContextHandle = runtimeContext.handle; // 获取初始化数据 const formData = await runtimeContext.getInitialValue(); // 创建弹窗 const dialogWidget = Dialog.create(); // 设置弹窗属性 dialogWidget.setTitle('这是一个演示弹窗'); // 创建所需组件 dialogWidget.createWidget(FormWidget, undefined, { metadataHandle: runtimeContextHandle, rootHandle: runtimeContextHandle, dataSource: formData, activeRecords: formData, template: runtimeContext.viewTemplate, inline: true }); dialogWidget.on('ok', () => { MessageHub.info('click ok'); // 关闭弹窗 dialogWidget.onVisibleChange(false); }); dialogWidget.on('cancel', () => { MessageHub.info('click cancel'); // 关闭弹窗 dialogWidget.onVisibleChange(false); }); // 打开弹窗…

    前端 2023年11月1日
    1.4K00

Leave a Reply

Please Login to Comment