自定义RSQL占位符(placeholder)及在权限中使用

1 自定义RSQL占位符常用场景

  • 统一的数据权限配置
  • 查询表达式的上下文变量扩展

2 自定义RSQL的模板

/**
 * 演示Placeholder占位符基本定义
 *
 * @author Adamancy Zhang at 13:53 on 2024-03-24
 */
@Component
public class DemoPlaceHolder extends AbstractPlaceHolderParser {

    private static final String PLACEHOLDER_KEY = "${thisPlaceholder}";

    /**
     * 占位符
     *
     * @return placeholder
     */
    @Override
    public String namespace() {
        return PLACEHOLDER_KEY;
    }

    /**
     * 占位符替换值
     *
     * @return the placeholder replace to the value
     */
    @Override
    protected String value() {
        return PamirsSession.getUserId().toString();
    }

    /**
     * 优先级
     *
     * @return execution order of placeholders, ascending order.
     */
    @Override
    public Integer priority() {
        return 0;
    }

    /**
     * 是否激活
     *
     * @return the placeholder is activated
     */
    @Override
    public Boolean active() {
        return true;
    }
}

注意事项

  • 在一些旧版本中,priorityactive可能不起作用,为保证升级时不受影响,请保证该属性配置正确。

  • PLACEHOLDER_KEY变量表示自定义占位符使用的关键字,需按照所需业务场景的具体功能并根据上下文语义正确定义。

  • 为保证占位符可以被正确替换并执行,所有占位符都不应该出现重复,尤其是不能与系统内置的重复。

3 占位符使用时的优先级问题

多个占位符在进行替换时,会根据优先级按升序顺序执行,如需要指定替换顺序,可使用SpringOrder注解对其进行排序。

import org.springframework.core.annotation.Order;

@Order(0)

4 Oinone平台内置的占位符

占位符 数据类型 含义 备注
${currentUser} String 当前用户ID 未登录时无法使用
${currentRoles} Set<String> 当前用户的角色ID集合 未登录时无法使用

5 如何覆盖平台内置的占位符?

通过指定占位符的优先级,并定义相同的namespace可优先替换。

6 如何定义会话级别的上下文变量?

在上述模板中,我们使用的是Oinone平台内置的上下文变量进行演示,通常情况下,我们需要根据实际业务场景增加上下文变量,以此来实现所需功能。

下面,我们将根据当前用户获取当前员工ID定义该上下文变量进行演示。

/**
 * 员工Session
 *
 * @author Adamancy Zhang at 14:33 on 2024-03-24
 */
@Component
public class EmployeeSession implements HookBefore {

    private static final String SESSION_KEY = "CUSTOM_EMPLOYEE_ID";

    @Autowired
    private DemoEmployeeService demoEmployeeService;

    public static String getEmployeeId() {
        return PamirsSession.getTransmittableExtend().get(SESSION_KEY);
    }

    @Override
    @Hook(priority = 1)
    public Object run(Function function, Object... args) {
        Long userId = PamirsSession.getUserId();
        if (userId == null) {
            return function;
        }
        if (StringUtils.isBlank(EmployeeSession.getEmployeeId())) {
            PamirsSession.getTransmittableExtend().put(SESSION_KEY, getCurrentEmployeeId());
        }
        return function;
    }

    private String getCurrentEmployeeId() {
        String employeeId = getEmployeeIdByCache();
        if (employeeId == null) {
            employeeId = demoEmployeeService.getCurrentEmployeeId();
        }
        return employeeId;
    }

    private String getEmployeeIdByCache() {
        // do something.
        return null;
    }
}

注意事项

  • 使用HookBefore在请求发起时向上下文中设置employeeId,使用EmployeeSession.getEmployeeId()获取即可。

  • HookBefore的优先级设置为priority = 1,需要该Hook在平台内置的UserHook之后执行,以确保PamirsSession.getUserId()中的值已经被正确设置。

  • DemoEmployeeService服务应使用平台@Fun@Function注解进行实现,以确保该Session可在分布式环境中正确运行。

  • getEmployeeIdByCache方法需自行实现,在运行时使用缓存可有效提高性能。

7 将员工Sessionplaceholder中使用

DemoPlaceHolder改写,使用${currentEmployeeId}获取员工Session中保存的employeeId

/**
 * 演示Placeholder占位符使用员工Session
 *
 * @author Adamancy Zhang at 15:02 on 2024-03-24
 */
@Component
public class DemoPlaceHolder extends AbstractPlaceHolderParser {

    private static final String PLACEHOLDER_KEY = "${currentEmployeeId}";

    /**
     * 占位符
     *
     * @return placeholder
     */
    @Override
    public String namespace() {
        return PLACEHOLDER_KEY;
    }

    /**
     * 占位符替换值
     *
     * @return the placeholder replace to the value
     */
    @Override
    protected String value() {
        return EmployeeSession.getEmployeeId();
    }

    /**
     * 优先级
     *
     * @return execution order of placeholders, ascending order.
     */
    @Override
    public Integer priority() {
        return 0;
    }

    /**
     * 是否激活
     *
     * @return the placeholder is activated
     */
    @Override
    public Boolean active() {
        return true;
    }
}

至此,我们已完成了一个员工ID占位符。

8 在权限配置时使用该占位符作为过滤条件

下面,我们将模拟一个简单业务场景,详细介绍该占位符如何在业务中使用。

8.1 场景描述

当前系统中包含部门员工两个模型,模型的基本定义如下所示:

部门

/**
 * 演示部门
 *
 * @author Adamancy Zhang at 15:18 on 2024-03-24
 */
@Model.model(DemoDepartment.MODEL_MODEL)
@Model(displayName = "演示部门", labelFields = "name")
public class DemoDepartment extends IdModel {

    private static final long serialVersionUID = -300189841334506668L;

    public static final String MODEL_MODEL = "demo.DemoDepartment";

    @Field(displayName = "部门名称")
    private String name;

    @Field(displayName = "管理员")
    private DemoEmployee manager;

}

员工

/**
 * 演示员工
 *
 * @author Adamancy Zhang at 15:17 on 2024-03-24
 */
@Model.model(DemoEmployee.MODEL_MODEL)
@Model.Advanced(unique = {"bindingUserId"})
@Model(displayName = "演示员工", labelFields = "name")
public class DemoEmployee extends IdModel {

    private static final long serialVersionUID = -6237083162460091500L;

    public static final String MODEL_MODEL = "demo.DemoEmployee";

    @Field(displayName = "员工名称")
    private String name;

    @Field.Relation(relationFields = {"bindingUserId"}, referenceFields = {"id"})
    @Field(displayName = "绑定用户")
    private PamirsUser bindingUser;

    @Field(displayName = "绑定用户ID")
    private Long bindingUserId;

}

我们要求当前登录用户仅能查看其作为管理员的所在部门,因此,我们需要使用managerId == ${currentEmployeeId}这一过滤条件,在权限中进行配置并使其生效。

此处忽略数据准备过程,仅展示关键页面的配置及最终效果。

8.2 权限项配置

从应用中切换至权限模块,并选择权限项列表,创建一个数据权限项,并按照下图内容进行配置。

权限项配置

8.3 角色权限配置

选择角色列表,并选择角色表格中指定角色的权限配置按钮,进入权限配置页面,并按照下图内容进行配置。

PS:这里省略了动作权限相关配置,配置的权限应保证该角色可以正确进入演示部门页面查看效果。

角色权限配置

8.4 为用户绑定指定角色(如已绑定可忽略该步骤)

从应用中切换至用户中心模块,选择指定用户,绑定指定角色。

8.5 在演示部门页面查看权限配置效果

未配置权限页面效果

未配置权限页面效果

权限配置后页面效果

权限配置后页面效果

本文来自投稿,不代表Oinone社区立场,如若转载,请注明出处:https://doc.oinone.top/backend/6393.html

(1)
张博昊的头像张博昊数式管理员
上一篇 2024年3月20日 pm3:03
下一篇 2024年3月24日 pm1:45

相关推荐

  • 工作流引入流程概览与流程监控

    流程概览依赖说明 使用 流程概览 功能前,需要在项目中引入 pamirs-workflow-datavi-core 依赖: <dependency> <groupId>pro.shushi.pamirs.workflow</groupId> <artifactId>pamirs-workflow-datavi-core</artifactId> </dependency> 警告: 在 oinone 平台启用「流程概览」能力时,应用启动模块一旦引入 pamirs-workflow-api/core,必须同时引入 pamirs-workflow-datavi-api/core。在多启动模块架构下,严禁出现仅部分启动模块引入 pamirs-workflow-core 而未引入 pamirs-workflow-datavi-core 的情况,否则将导致流程概览相关元数据计算异常,出现删表等情况。 流程概览配置项 流程概览页面内置缓存机制,可通过配置项调整缓存刷新周期及图表展示的数据条数: pamirs: workflow: dashboard: cache-time: 10 # 流程概览缓存刷新时间(单位:分钟),默认 10 分钟 page-size: 10 # 流程运行分析中 4 个图表的展示数量,默认查询前 10 条数据 统计指标说明 引入 pamirs-workflow-datavi-core 依赖后,系统会按照以下规则进行数据同步: 当日数据同步:每小时同步一次当日数据; 昨日数据同步:次日凌晨同步前一日数据。 由于在引入依赖后才会开始执行数据同步,统计指标页提供了「同步」按钮,可用于对历史数据进行补采。即使不执行历史同步,也不会影响核心业务流程,仅会影响统计数据和图表的展示效果。 统计指标数据主要用于 支撑 流程概览 和 流程监控 中的统计图表展示; 为数据分析与可视化提供基础数据。 上述统计数据对工作流的审批、流转等核心业务无任何影响。如有需要,也可以基于流程监控的数据,配合数据可视化设计器,自定义构建符合业务需求的展示页面。

    2025年11月17日
    3100
  • 平台配置日志输出和推送到APM与LogStash

    场景描述 目前设计器镜像启动后日志文件为out.log,是启动脚本中定向输出了(>>)out.log文件。实际项目可能: 日志输出到特定目录的特定文件名中 指定以日志保留策略(单个文件大小和文件保留个数) 日志输出到APM工具中(如skywalking) 日志推送到LogStash 日志自定义输出 不定向输出,采用自己配置的方式,与标准的SpringBoot工程配置日志一样。两种方式(都是Spring提供的方式): 方式一 bootstrap.yml 里面可以按profiles指定logback的配置文件,具体文件名和文件输入在logback里面进行配置,跟通用的logback配置一致. 例如: logging: config: classpath:logback-pre.xml 方式二 resources的根目录,直接配置 logback-spring.xml, 启动会自动加载。 日志自定义场景 配置日志推送到LogStash <!–配置日志推送到LogStash–> <contextListener class="pro.shushi.pamirs.demo.core.config.DemoLogbackFiledConfig"/> <appender name="LogStash" class="net.logstash.logback.appender.LogstashTcpSocketAppender"> <destination>127.0.0.1:4560</destination> <!– encoder必须配置,有多种可选 –> <encoder charset="UTF-8" class="net.logstash.logback.encoder.LogstashEncoder"> <!– SkyWalking插件, log加tid–> <provider class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.logstash.TraceIdJsonProvider" /> <!–在生成的json中会加这些字段–> <customFields> {"app.name":"pamirs-demo", "app.type":"Microservice", "platform":"pamirs", "env":"dev"} </customFields> <timeZone>Asia/Shanghai</timeZone> <writeVersionAsInteger>true</writeVersionAsInteger> <providers> <pattern> <pattern> <!–动态的变量–> { "ip": "%{ip}", "server.name": "%{server.name}", "logger_name": "%logger" } </pattern> </pattern> </providers> </encoder> </appender> skywalking的日志rpc上传 <!– skywalking的日志rpc上传 –> <appender name="SkyWalkingLogs" class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.log.GRPCLogClientAppender"> <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder"> <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.mdc.TraceIdMDCPatternLogbackLayout"> <Pattern>${CONSOLE_LOG_PATTERN}</Pattern> </layout> </encoder> </appender> 完整的代码示例 Logback自定义字段 package pro.shushi.pamirs.demo.core.config; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.spi.LoggerContextListener; import ch.qos.logback.core.Context; import ch.qos.logback.core.spi.ContextAwareBase; import ch.qos.logback.core.spi.LifeCycle; import java.net.InetAddress; import java.net.UnknownHostException; /** * Logback自定义字段 * * @author wx@shushi.pro * @date 2024/4/17 */ public class DemoLogbackFiledConfig extends ContextAwareBase implements LoggerContextListener, LifeCycle { private boolean started = false; @Override public boolean isResetResistant() { return false; } @Override public void onStart(LoggerContext loggerContext) { } @Override public void onReset(LoggerContext loggerContext) { } @Override public void onStop(LoggerContext loggerContext) { } @Override public void onLevelChange(Logger logger, Level level) { } @Override public void start() { if (started) { return; } Context context = getContext();…

    2024年5月18日
    1.4K00
  • 函数之触发与定时配置和示例

    异步任务总体介绍 函数的触发和定时在很多场景中会用到,也是一个oinone的基础能力。比如我们的流程产品中在定义流程触发时就会让用户选择模型触发还是时间触发,就是用到了函数的触发与定时能力。 触发任务TriggerTaskAction 触发任务的创建,使用sql-record模块监听mysql的binlog事件,通过rocketmq发送变更数据消息,收到MQ消息后,创建TriggerAutoTask。 触发任务的执行,使用TBSchedule拉取触发任务后,执行相应函数。 项目中引入依赖 1、项目的API工程引入依赖pamirs-core-trigger模块 <dependency> <groupId>pro.shushi.pamirs.core</groupId> <artifactId>pamirs-trigger-api</artifactId> </dependency> 2、DemoModule在模块依赖定义中增加@Module(dependencies={TriggerModule.MODULE_MODULE}) @Component @Module( name = DemoModule.MODULE_NAME, displayName = "oinoneDemo工程", version = "1.0.0", dependencies = {ModuleConstants.MODULE_BASE, CommonModule.MODULE_MODULE, UserModule.MODULE_MODULE, TriggerModule.MODULE_MODULE} ) @Module.module(DemoModule.MODULE_MODULE) @Module.Advanced(selfBuilt = true, application = true) @UxHomepage(PetShopProxy.MODEL_MODEL) public class DemoModule implements PamirsModule { ……其他代码 } 3、项目的boot工程引入依赖 <dependency> <groupId>pro.shushi.pamirs.core</groupId> <artifactId>pamirs-trigger-core</artifactId> </dependency> <dependency> <groupId>pro.shushi.pamirs.core</groupId> <artifactId>pamirs-trigger-bridge-tbschedule</artifactId> </dependency> <dependency> <groupId>pro.shushi.pamirs.core</groupId> <artifactId>pamirs-sql-record-core</artifactId> </dependency> yml文件修改(applcation-xxx.yml) a. 修改pamris.event.enabled和pamris.event.schedule.enabled为trueb. pamirs_boot_modules增加启动模块:trigger、sql_record pamirs: record: sql: #改成自己路径 store: /opt/pamirs/logs … event: enabled: true schedule: enabled: true rocket-mq: namesrv-addr: 127.0.0.1:9876 boot: init: true sync: true modules: – base -…… – trigger – sql_record -…… 新建触发任务 新建PetTalentTrigger类,当PetTalent模型的数据记录被新建时触发系统做一些事情 package pro.shushi.pamirs.demo.core.trigger; import pro.shushi.pamirs.demo.api.model.PetTalent; import pro.shushi.pamirs.meta.annotation.Fun; import pro.shushi.pamirs.meta.annotation.Function; import pro.shushi.pamirs.meta.annotation.fun.extern.Slf4j; import pro.shushi.pamirs.trigger.annotation.Trigger; import pro.shushi.pamirs.trigger.enmu.TriggerConditionEnum; @Fun(PetTalent.MODEL_MODEL) @Slf4j public class PetTalentTrigger { @Function @Trigger(displayName = “PetTalent创建时触发”,name = “PetTalent#Trigger#onCreate”,condition = TriggerConditionEnum.ON_CREATE) public PetTalent onCreate(PetTalent data){ log.info(data.getName() + “,被创建”); //可以增加逻辑 return data; } } 定时任务 定时任务是一种非常常见的模式,这里就不介绍概念了,直接进入示例环节 新建PetTalentAutoTask实现ScheduleAction getInterfaceName()需要跟taskAction.setExecuteNamespace定义保持一致,都是函数的命名空间 taskAction.setExecuteFun("execute");跟执行函数名“execute”一致 TaskType需配置为CYCLE_SCHEDULE_NO_TRANSACTION_TASK,把定时任务的schedule线程分开,要不然有一个时间长的任务会导致普通异步或触发任务全部延时。 package pro.shushi.pamirs.demo.core.task; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import pro.shushi.pamirs.core.common.enmu.TimeUnitEnum; import pro.shushi.pamirs.demo.api.model.PetTalent; import pro.shushi.pamirs.meta.annotation.Fun; import pro.shushi.pamirs.meta.annotation.Function; import pro.shushi.pamirs.meta.annotation.fun.extern.Slf4j; import pro.shushi.pamirs.meta.domain.fun.FunctionDefinition; import pro.shushi.pamirs.middleware.schedule.api.ScheduleAction; import pro.shushi.pamirs.middleware.schedule.common.Result; import pro.shushi.pamirs.middleware.schedule.domain.ScheduleItem; import pro.shushi.pamirs.middleware.schedule.eunmeration.TaskType; import pro.shushi.pamirs.trigger.enmu.TriggerTimeAnchorEnum; import pro.shushi.pamirs.trigger.model.ScheduleTaskAction; import pro.shushi.pamirs.trigger.service.ScheduleTaskActionService; @Slf4j @Component @Fun(PetTalent.MODEL_MODEL) public class PetTalentAutoTask implements…

    2024年5月25日
    1.3K00
  • 工作流用户待办过滤站内信

    工作流用户待办过滤站内信 全局过滤 启动工程application.yml中配置: pamirs: workflow: notify: false 个性化过滤 实现pro.shushi.pamirs.workflow.app.api.service.WorkflowMailFilterApi接口 返回true表示需要发送站内信 返回false表示不需要发送站内信 示例: import org.apache.commons.lang3.StringUtils; import pro.shushi.pamirs.message.model.PamirsMessage; import pro.shushi.pamirs.meta.annotation.Fun; import pro.shushi.pamirs.meta.annotation.Function; import pro.shushi.pamirs.user.api.model.PamirsUser; import pro.shushi.pamirs.workflow.app.api.model.WorkflowUserTask; import pro.shushi.pamirs.workflow.app.api.service.WorkflowMailFilterApi; /** * MyWorkflowMailFilterImpl * * @author yakir on 2025/02/24 16:28. */ @Fun(WorkflowMailFilterApi.FUN_NAMESPACE) public class MyWorkflowMailFilterImpl implements WorkflowMailFilterApi { @Override @Function public Boolean filter(WorkflowUserTask workflowUserTask, PamirsUser user, PamirsMessage message) { // 按用户待办过滤 workflowUserTask if (10000L == workflowUserTask.getInitiatorUid()){ return true; } // 按用户过滤 user if (1000L == user.getId()){ return true; } // 按站内信消息过滤 message if (StringUtils.contains(message.getBody(), "你好")) { return true; } return false; } }

    2025年2月24日
    88200
  • IWrapper、QueryWrapper和LambdaQueryWrapper使用

    条件更新updateByWrapper 通常我们在更新的时候new一个对象出来在去更新,减少更新的字段 Integer update = new DemoUser().updateByWrapper(new DemoUser().setFirstLogin(Boolean.FALSE), Pops.<DemoUser>lambdaUpdate().from(DemoUser.MODEL_MODEL).eq(IdModel::getId, userId) 使用基础模型的updateById方法更新指定字段的方法: new 一下update对象出来,更新这个对象。 WorkflowUserTask userTaskUp = new WorkflowUserTask(); userTaskUp.setId(userTask.getId()); userTaskUp.setNodeContext(json); userTaskUp.updateById(); 条件删除updateByWrapper public List<T> delete(List<T> data) { List<Long> petTypeIdList = new ArrayList(); for(T item:data){ petTypeIdList.add(item.getId()); } Models.data().deleteByWrapper(Pops.<PetType>lambdaQuery().from(PetType.MODEL_MODEL).in(PetType::getId,petTypeIdList)); return data; } 构造条件查询数据 示例1: LambdaQueryWrapper拼接查询条件 private void queryPetShops() { LambdaQueryWrapper<PetShop> query = Pops.<PetShop>lambdaQuery(); query.from(PetShop.MODEL_MODEL); query.setSortable(Boolean.FALSE); query.orderBy(true, true, PetShop::getId); List<PetShop> petShops2 = new PetShop().queryList(query); System.out.printf(petShops2.size() + ""); } 示例2: IWrapper拼接查询条件 private void queryPetShops() { IWrapper<PetShop> wrapper = Pops.<PetShop>lambdaQuery() .from(PetShop.MODEL_MODEL).eq(PetShop::getId,1L); List<PetShop> petShops4 = new PetShop().queryList(wrapper); System.out.printf(petShops4.size() + ""); } 示例3: QueryWrapper拼接查询条件 private void queryPetShops() { //使用Lambda获取字段名,防止后面改字段名漏改 String nameField = LambdaUtil.fetchFieldName(PetTalent::getName); //使用Lambda获取Clumon名,防止后面改字段名漏改 String nameColumn = PStringUtils.fieldName2Column(nameField); QueryWrapper<PetShop> wrapper2 = new QueryWrapper<PetShop>().from(PetShop.MODEL_MODEL) .eq(nameColumn, "test"); List<PetShop> petShops5 = new PetShop().queryList(wrapper2); System.out.printf(petShops5.size() + ""); } IWrapper转为LambdaQueryWrapper @Function.Advanced(type= FunctionTypeEnum.QUERY) @Function.fun(FunctionConstants.queryPage) @Function(openLevel = {FunctionOpenEnum.API}) public Pagination<PetShopProxy> queryPage(Pagination<PetShopProxy> page, IWrapper<PetShopProxy> queryWrapper) { LambdaQueryWrapper<PetShopProxy> wrapper = ((QueryWrapper<PetShopProxy>) queryWrapper).lambda(); // 非存储字段从QueryData中获取 Map<String, Object> queryData = queryWrapper.getQueryData(); if (null != queryData && !queryData.isEmpty()) { String codes = (String) queryData.get("codes"); if (org.apache.commons.lang3.StringUtils.isNotBlank(codes)) { wrapper.in(PetShopProxy::getCode, codes.split(",")); } } return new PetShopProxy().queryPage(page, wrapper); }

    2024年5月25日
    1.6K00

Leave a Reply

登录后才能评论