后端:如何自定义表达式实现特殊需求?扩展内置函数表达式

平台提供了很多的表达式,如果这些表达式不满足场景?那我们应该如何新增表达式去满足项目的需求?
目前平台支持的表达式内置函数,参考


1. 扩展表达式的场景

注解@Validation的rule字段支持配置表达式校验
如果需要判断入参List类型字段中的某一个参数进行NULL校验,发现平台的内置函数不支持该场景的配置,这里就可以通过平台的机制,对内置函数进行扩展。

常见的一些代码场景,如下:

package pro.shushi.pamirs.demo.core.action;

……引用类

@Model.model(PetShopProxy.MODEL_MODEL)
@Component
public class PetShopProxyAction extends DataStatusBehavior<PetShopProxy> {

    @Override
    protected PetShopProxy fetchData(PetShopProxy data) {
        return data.queryById();
    }
    @Validation(ruleWithTips = {
            @Validation.Rule(value = "!IS_BLANK(data.code)", error = "编码为必填项"),
            @Validation.Rule(value = "LEN(data.name) < 128", error = "名称过长,不能超过128位"),
    })
    @Action(displayName = "启用")
    @Action.Advanced(invisible="!(activeRecord.code !== undefined && !IS_BLANK(activeRecord.code))")
    public PetShopProxy dataStatusEnable(PetShopProxy data){
        data = super.dataStatusEnable(data);
        data.updateById();
        return data;
    }

……其他代码

}

2. 新建一个自定义表达式的函数

校验入参如果是个集合对象的情况下,单个对象的某个字段如果为空,返回false的函数。

例子:
新建一个CustomCollectionFunctions类

package xxx.xxx.xxx;

import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Component;
import pro.shushi.pamirs.meta.annotation.Fun;
import pro.shushi.pamirs.meta.annotation.Function;
import pro.shushi.pamirs.meta.common.constants.NamespaceConstants;
import pro.shushi.pamirs.meta.util.FieldUtils;

import java.util.List;

import static pro.shushi.pamirs.meta.enmu.FunctionCategoryEnum.COLLECTION;
import static pro.shushi.pamirs.meta.enmu.FunctionLanguageEnum.JAVA;
import static pro.shushi.pamirs.meta.enmu.FunctionOpenEnum.LOCAL;
import static pro.shushi.pamirs.meta.enmu.FunctionSceneEnum.EXPRESSION;

/**
 * 自定义内置函数
 */
@Fun(NamespaceConstants.expression)
@Component
public class CustomCollectionFunctions {

    /**
     * LIST_FIELD_NULL 就是我们自定义的表达式,不能与已经存在的表达式重复!!!
     *
     * @param list
     * @param field
     * @return
     */
    @Function.Advanced(
            displayName = "校验集成的参数是否为null", language = JAVA,
            builtin = true, category = COLLECTION
    )
    @Function.fun("LIST_FIELD_NULL")
    @Function(name = "LIST_FIELD_NULL", scene = {EXPRESSION}, openLevel = LOCAL,
            summary = "函数示例: LIST_FIELD_NULL(list,field),函数说明: 传入一个对象集合,校验集合的字段是否为空"
    )
    public Boolean listFieldNull(List list, String field) {
        if (null == list) {
            return false;
        }
        if (CollectionUtils.isEmpty(list)) {
            return false;
        }
        for (Object data : list) {
            Object value = FieldUtils.getFieldValue(data, field);
            if (value == null) {
                return false;
            }
        }
        return true;
    }

}

3. 将自定义的表达式类,注册到平台的白名单

新建CustomFaasScriptAllowListApi类,@Order优先级要高于平台默认的优先级才会生效。

package xxx.xxx.xxx;

import org.apache.commons.collections4.SetUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import pro.shushi.pamirs.framework.faas.fun.builtin.*;
import pro.shushi.pamirs.framework.faas.spi.api.guard.FaasScriptAllowListApi;
import pro.shushi.pamirs.meta.common.constants.NamespaceConstants;
import pro.shushi.pamirs.meta.common.spi.SPI;

import java.util.Set;

/**
 * 自定义:支持表达式调用的函数白名单与黑名单SPI实现
 */
@Order(1) //此处把自定义的类优先级调高
@Component
@SPI.Service
public class CustomFaasScriptAllowListApi implements FaasScriptAllowListApi {

    //白名单
    public static final Set<String> DEFAULT_SET = SetUtils.hashSet(

            //白名单,直接复制默认实现,pro.shushi.pamirs.framework.faas.spi.service.DefaultFaasScriptAllowListApi
            CollectionFunctions.class.getName(),
            ContextFunctions.class.getName(),
            DateFunctions.class.getName(),
            LogicFunctions.class.getName(),
            MapFunctions.class.getName(),
            MathFunctions.class.getName(),
            ObjectFunctions.class.getName(),
            RegexFunctions.class.getName(),
            TextFunctions.class.getName(),

            //下面添加自己的白名单类
            CustomCollectionFunctions.class.getName()

    );

    @Override
    public Set<String> classWhiteList() {
        return DEFAULT_SET;
    }

    @Override
    public Set<String> namespaceWhiteList() {
        return SetUtils.hashSet(NamespaceConstants.expression);
    }

}

4. 使用自定义的表达式

使用场景demo:

    /**
     * 注意点:自定义函数的 [field]字段是个文本,一定要加个引号代表参数是文本,不然无法解析到数据,其他场景类似
     *
     * @param data
     * @return
     */
    @Action.Advanced(name = FunctionConstants.create, managed = true)
    @Action(displayName = "确定", summary = "创建", bindingType = ViewTypeEnum.FORM)
    @Function(name = FunctionConstants.create)
    @Function.fun(FunctionConstants.create)
    @Validation(ruleWithTips = {
            @Validation.Rule(value = "LIST_FIELD_NULL(data.itemAttributes,'itemId')", error = "字段不能为空"),
    })
    public DemoItem create(DemoItem data) {
        return demoItemService.create(data);
    }

结语:可以通过平台的机制,去沉淀一套满足自己场景需求的表达式。

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

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

Like (0)
oinone's avataroinone
Previous 2024年5月30日 pm4:26
Next 2024年5月30日 pm10:03

相关推荐

  • Dubbo配置详解

    概述 Dubbo是一款高性能、轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。 Oinone平台默认使用dubbo-v2.7.22版本,本文以该版本为例进行描述。 基本概念 Dubbo在注册provider/consumer时使用Netty作为RPC调用的核心服务,其具备客户端/服务端(C/S)的基本特性。即:provider作为服务端,consumer作为客户端。 客户端通过服务中心发现有服务可被调用时,将通过服务中心提供的服务端调用信息,连接服务端并发起请求,从而实现远程调用。 服务注册(绑定Host/Port) JAVA程序启动时,需要将provider的信息注册到服务中心,并在当前环境为Netty服务开启Host/Port监听,以实现服务注册功能。 在下文中,我们通过绑定Host/Port表示Netty服务的访问地址,通过注册Host/Port表示客户端的访问地址。 使用yaml配置绑定Host/Port PS:该配置可在多种环境中通用,改变部署方式无需修改此配置。 dubbo: protocol: name: dubbo # host: 0.0.0.0 port: -1 假设当前环境的可用IP为192.168.1.100 以上配置将使得Netty服务默认绑定在0.0.0.0:20880地址,服务注册地址为192.168.1.100:20880 客户端将通过192.168.1.100:20880调用服务端服务 若发生20880端口占用,则自动向后查找可用端口。如20881、20882等等 若当前可用端口为20881,则以上配置将使得Netty服务默认绑定在0.0.0.0:20881地址,服务注册地址为192.168.1.100:20881 使用环境变量配置注册Host/Port 当服务端被放置在容器环境中时,由于容器环境的特殊性,其内部的网络配置相对于宿主机而言是独立的。因此为保证客户端可以正常调用服务端,还需在容器中配置环境变量,以确保客户端可以通过指定的注册Host/Port进行访问。 以下示例为体现无法使用20880端口的情况,将宿主机可访问端口从20880改为20881。 DUBBO_IP_TO_REGISTRY=192.168.1.100 DUBBO_PORT_TO_REGISTRY=20881 假设当前宿主机环境的可用IP为192.168.1.100 以上配置将使得Netty服务默认绑定在0.0.0.0:20881地址,服务注册地址为192.168.1.100:20881 客户端将通过192.168.1.100:20881调用服务端服务 使用docker/docker-compose启动 需添加端口映射,将20881端口映射至宿主机20881端口。(此处容器内的端口发生变化,若需要了解具体原因,可参考题外话章节) docker-run IP=192.168.1.100 docker run -d –name designer-allinone-full \ -e DUBBO_IP_TO_REGISTRY=$IP \ -e DUBBO_PORT_TO_REGISTRY=20881 \ -p 20881:20881 \ docker-compose services: backend: container_name: designer-backend image: harbor.oinone.top/oinone/designer-backend-v5.0 restart: always environment: DUBBO_IP_TO_REGISTRY: 192.168.1.100 DUBBO_PORT_TO_REGISTRY: 20881 ports: – 20881:20881 # dubbo端口 使用kubernetes启动 工作负载(Deployment) kind: Deployment apiVersion: apps/v1 spec: replicas: 1 template: spec: containers: – name: designer-backend image: harbor.oinone.top/oinone/designer-backend-v5.0 ports: – name: dubbo containerPort: 20881 protocol: TCP env: – name: DUBBO_IP_TO_REGISTRY value: "192.168.1.100" – name: DUBBO_PORT_TO_REGISTRY value: "20881" 服务(Services) kind: Service apiVersion: v1 spec: type: NodePort ports: – name: dubbo protocol: TCP port: 20881 targetPort: dubbo nodePort: 20881 PS:此处的targetPort为对应Deployment#spec. template.spec.containers.ports.name配置的端口名称。若未配置,可使用20881直接指定对应容器的端口号。 使用kubernetes其他暴露服务方式 在Kubernetes中部署服务,有多种配置方式均可用暴露服务。上述配置仅用于通过Service/NodePort将20881端口暴露至宿主机,其他服务可用通过任意Kubernetes节点IP进行调用。 若其他服务也在Kubernetes中进行部署,则可以通过Service/Service方式进行调用。将DUBBO_IP_TO_REGISTRY配置为${serviceName}.${namespace}即可。 若其他服务无法直接访问Kubernetes的master服务,则可以通过Ingress/Service方式进行调用。将DUBBO_IP_TO_REGISTRY配置为Ingress可解析域名即可。 Dubbo调用链路图解 PS: Consumer的绑定Host/Port是其作为Provider使用的,下面所有图解仅演示单向的调用链路。 名词解释 Provider: 服务提供者(JVM) Physical Machine Provider: 服务提供者所在物理机 Provider Container: 服务提供者所在容器 Kubernetes Service: Kubernetes Service资源类型 Consumer: 服务消费者(JVM) Registration Center: 注册中心;可以是zookeeper、nacos等。 bind: 服务绑定Host/Port到指定ip:port。 registry: 服务注册;注册Host/Port到注册中心的信息。 discovery: 服务发现;注册Host/Port到消费者的信息。 invoke: 服务调用;消费者通过注册中心提供的提供者信息向提供者发起服务调用。 forward: 网络转发;通常在容器环境需要进行必要的网络转发,以使得服务调用可以到达服务提供者。 物理机/物理机调用链路 “` mermaidsequenceDiagram participant p as Provider<br>(bind 0.0.0.0:20880)participant m as Physical Machine Provider<br>(bind 192.168.1.100:20881)participant…

    2024年8月10日
    2.8K00
  • 分库分表与自定义分表规则

    总体介绍 Oinone的分库分表方案是基于Sharding-JDBC的整合方案,要先具备一些Sharding-JDBC的知识。[Sharding-JDBC]https://shardingsphere.apache.org/document/current/cn/overview/ 做分库分表前,大家要有一个明确注意的点就是分表字段(也叫均衡字段)的选择,它是非常重要的,与业务场景非常相关。在明确了分库分表字段以后,甚至在功能上都要做一些妥协。比如分库分表字段在查询管理中做为查询条件是必须带上的,不然效率只会更低。 分表字段不允许更新,所以代码里更新策略设置类永不更新,并在设置了在页面修改的时候为readonly 配置分表策略 配置ShardingModel模型走分库分表的数据源pamirsSharding 为pamirsSharding配置数据源以及sharding规则 a. pamirs.sharding.define用于oinone的数据库表创建用 b. pamirs.sharding.rule用于分表规则配置 为pamirsSharding配置数据源以及sharding规则 1)指定模型对应数据源 pamirs: framework: system: system-ds-key: base system-models: – base.WorkerNode data: default-ds-key: pamirs ds-map: base: base modelDsMap: "[demo.ShardingModel]": pamirsSharding #配置模型对应的库 2)分库分表规则配置 pamirs: sharding: define: data-sources: ds: pamirs pamirsSharding: pamirs #申明pamirsSharding库对应的pamirs数据源 models: "[trigger.PamirsSchedule]": tables: 0..13 "[demo.ShardingModel]": tables: 0..7 table-separator: _ rule: pamirsSharding: #配置pamirsSharding库的分库分表规则 actual-ds: – pamirs #申明pamirsSharding库对应的pamirs数据源 sharding-rules: # Configure sharding rule ,以下配置跟sharding-jdbc配置一致 – tables: demo_core_sharding_model: #demo_core_sharding_model表规则配置 actualDataNodes: pamirs.demo_core_sharding_model_${0..7} tableStrategy: standard: shardingColumn: user_id shardingAlgorithmName: table_inline shardingAlgorithms: table_inline: type: INLINE props: algorithm-expression: demo_core_sharding_model_${(Long.valueOf(user_id) % 8)} props: sql.show: true 自定义规则 默认规则即通用的分库分表策略,如按照数据量、哈希等方式进行分库分表;通常默认规则是可以的。 但在一些复杂的业务场景下,使用默认规则可能无法满足需求,需要根据实际情况进行自定义。例如,某些业务可能有特定的数据分布模式或者查询特点,需要定制化的分库分表规则来优化数据访问性能或者满足业务需求。在这种情况下,使用自定义规则可以更好地适应业务的需求。 自定义分表规则示例 示例1:按月份分表(DATE_MONTH ) package pro.shushi.pamirs.demo.core.sharding; import cn.hutool.core.date.DateUtil; import com.google.common.collect.Range; import org.apache.shardingsphere.sharding.api.sharding.standard.PreciseShardingValue; import org.apache.shardingsphere.sharding.api.sharding.standard.RangeShardingValue; import org.apache.shardingsphere.sharding.api.sharding.standard.StandardShardingAlgorithm; import org.springframework.stereotype.Component; import pro.shushi.pamirs.meta.annotation.fun.extern.Slf4j; import java.util.*; /** * @author wangxian * @version 1.0 * @description */ @Component @Slf4j public class DateMonthShardingAlgorithm implements StandardShardingAlgorithm<Date> { private Properties props; @Override public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Date> preciseShardingValue) { Date date = preciseShardingValue.getValue(); String suffix = "_" + (DateUtil.month(date) + 1); for (String tableName : availableTargetNames) { if (tableName.endsWith(suffix)) { return tableName; } } throw new IllegalArgumentException("未找到匹配的数据表"); } @Override public Collection<String> doSharding(Collection<String> availableTargetNames, RangeShardingValue<Date> rangeShardingValue) { List<String> list =…

    2024年5月11日
    1.8K00
  • 如何自定义覆盖内置模块的页面

    1.首先通过sql查询找到我们需要的页面,从其中的template字段复制出原视图的配置 通过模型编码model在base_view查找需要修改的视图 select * from base_view where model='workflow.WorkflowUserTask' and is_deleted = 0; 2.将base_view的template内容复制到java的core工程的resources目录下新建一个xml文件,修改里面的动作名称 <view widget="WorkFlowImplement"> <template slot="actions"> <action name="$$internal_GotoListTableRouter" priority="1" model="workflow.WorkflowUserTask" tag="contextFreeAction"/> <action name="approveStaging" widget="FlowTaskCommonAction" invisible="!(activeRecord.allowStaging)" priority="2" label="新暂存" model="workflow.WorkflowUserTask" displayName="新暂存"/> <action name="workflow_agree" invisible="!(activeRecord.allowAgree && activeRecord.status == &apos;ACTIVE&apos;)" priority="3" label="新同意" model="workflow.WorkflowUserTask" load="fetchDetailReadOnly" displayName="新同意" goBack="true" validateForm="true" loadRootData="true"/> <action name="workflow_rejust" invisible="!(activeRecord.allowReject && activeRecord.status == &apos;ACTIVE&apos;)" priority="4" label="新拒绝" model="workflow.WorkflowUserTask" displayName="新拒绝" goBack="true" loadRootData="true" tag="contextFreeAction"/> <action name="workflow_turnon" invisible="!(activeRecord.taskType == &apos;APPROVE&apos; && activeRecord.allowTransfer && activeRecord.status == &apos;ACTIVE&apos;)" priority="5" label="新转交" model="workflow.WorkflowUserTask" load="fetchDetailReadOnly" displayName="新转交" goBack="true" loadRootData="true" tag="contextFreeAction"/> <action name="workflow_addsign" invisible="!(activeRecord.taskType == &apos;APPROVE&apos; && activeRecord.allowAddSign && activeRecord.status == &apos;ACTIVE&apos;)" priority="6" label="新加签" model="workflow.WorkflowUserTask" load="fetchDetailReadOnly" displayName="新加签" goBack="true" loadRootData="true" tag="contextFreeAction"/> <action name="workflow_write_fallback" invisible="!(activeRecord.taskType == &apos;WRITE&apos; && activeRecord.allowFallback && activeRecord.status == &apos;ACTIVE&apos;)" priority="7" label="新回退" model="workflow.WorkflowUserTask" displayName="新回退" goBack="true" loadRootData="true" tag="contextFreeAction"/> <action name="workflow_sharing" priority="8" label="新分享" model="workflow.WorkflowUserTask" displayName="新分享" goBack="true" loadRootData="true" tag="contextFreeAction"/> </template> <template slot="fields"> <field name="remark" widget="TextArea" invisible="activeRecord.taskType != &apos;APPROVE&apos;" priority="8" model="workflow.WorkflowUserTask" data="remark" displayName="意见备注"/> </template> </view> 3.在生命周期的元数据编辑方法内覆盖视图 package pro.shushi.pamirs.demo.core.init; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import pro.shushi.pamirs.boot.common.api.command.AppLifecycleCommand; import pro.shushi.pamirs.boot.common.extend.MetaDataEditor; import pro.shushi.pamirs.core.common.InitializationUtil; import pro.shushi.pamirs.demo.api.DemoModule; import pro.shushi.pamirs.meta.annotation.fun.extern.Slf4j; import pro.shushi.pamirs.meta.api.dto.meta.Meta; import pro.shushi.pamirs.meta.enmu.ViewTypeEnum; import pro.shushi.pamirs.workflow.app.api.model.WorkflowUserTask; import java.util.Map; @Slf4j @Component @Order(Ordered.LOWEST_PRECEDENCE) public class DemoModuleMetaInstall implements MetaDataEditor { @Override public void edit(AppLifecycleCommand command,…

    2024年7月2日
    1.9K00
  • Oinone开发实践-业务实现多租户方案

    总体方案 业务项目中,需要隔离的模型自定义增加租户字段进行数据隔离; 参考了Mybatis-Plus插件的TenantSqlParser进行的JPA实现,使用jsqlparser解析并修改SQL; 实现获取当前用户租户ID,SQL增删改查时处理租户字段,实现租户数据的隔离 参考项目: https://github.com/baomidou/mybatis-plus https://github.com/JSQLParser/JSqlParser 具体实现方式 1、业务上定义两个基础抽象模型包含租户字段 定义包含ID的基础抽象模型,且包含租户字段(如:公司编码, 用其他字段作为租户字段也可以,根据实际业务情况灵活修改)。 @Model.model(XXIdModel.MODEL_MODEL) @Model.Advanced(type = ModelTypeEnum.ABSTRACT) @Model(displayName = “带公司CODE的基础ID抽象模型”, summary = “带公司Code的Id模型”) public abstract class XXIdModel extends IdModel { public static final String MODEL_MODEL = “demo.biz.XXIdModel”; @Field.String @Field(displayName = “所属公司编码”, invisible = true, index = true) private String companyCode; } 定义包含Code的基础抽象模型,且包含租户字段(如:公司编码, 用其他字段作为租户字段也可以,根据实际业务情况灵活修改)。 @Model.model(XXCodeModel.MODEL_MODEL) @Model.Advanced(type = ModelTypeEnum.ABSTRACT) @Model(displayName = “带公司CODE的基础Code抽象模型”, summary = “带公司CODE的Code模型”) public abstract class XXCodeModel extends CodeModel { public static final String MODEL_MODEL = “demo.biz.XXCodeModel”; @Field.String @Field(displayName = “所属公司编码”, invisible = true, index = true) private String companyCode; } 2、业务模块的模型需租户隔离的都是继承上面这两个模型; @Model.model(PetPetCompany.MODEL_MODEL) @Model(displayName = “宠物公司”, labelFields = “name”) public class PetPetCompany extends AbstractCompanyCodeModel { public static final String MODEL_MODEL = “demo.PetPetCompany”; @Field.String @Field(displayName = “名称”) private String name; @Field.Text @Field(displayName = “简介”) private String introduction; } 3、自定义扩展Session,Session中设置租户信息 每次请求多把登录用户所属公司编码(companyCode)放到Session中;Session扩展参考:https://doc.oinone.top/oio4/9295.html 4、定义拦截器Interceptor进行数据隔离 数据创建和查询通过拦截器把Session中的中的公司编码(companyCode)设置到隔离字段中;拦截器的java示例代码参考: package pro.shushi.pamirs.demo.core.interceptor; import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.LongValue; import net.sf.jsqlparser.expression.StringValue; import net.sf.jsqlparser.expression.operators.relational.ExpressionList; import net.sf.jsqlparser.expression.operators.relational.ItemsListVisitor; import net.sf.jsqlparser.expression.operators.relational.MultiExpressionList; import net.sf.jsqlparser.expression.operators.relational.NamedExpressionList; import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.insert.Insert; import net.sf.jsqlparser.statement.select.*; import net.sf.jsqlparser.statement.update.Update; import net.sf.jsqlparser.statement.values.ValuesStatement; import org.apache.commons.lang3.StringUtils; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.SqlCommandType; import org.apache.ibatis.mapping.SqlSource; import org.apache.ibatis.plugin.*; import org.apache.ibatis.reflection.MetaObject; import org.apache.ibatis.reflection.SystemMetaObject; import…

    2024年4月6日
    1.4K00
  • 「前端」 获取当前用户信息以及语言

    概述 TopBarService 是用于管理用户信息和多语言配置的工具类,提供以下核心功能: 用户信息查询(含缓存机制) 当前语言状态获取 可用语言列表查询 语言切换操作 快速开始 // 基础使用示例 import { TopBarService } from '@kunlun/dependencies'; // 获取用户信息(自动缓存) const userInfo = await TopBarService.getUserInfo(); // 获取当前语言配置 const currentLang = await TopBarService.getCurrentLang(); // 查询支持的语言列表 const languages = await TopBarService.queryLanguageList(); // 激活指定语言 await TopBarService.activeLang('zh_CN'); API 详解 方法功能说明 方法名称 描述 参数 返回值 ​getUserInfo 获取当前用户信息(自动缓存) 无 用户信息对象 ​queryUserInfo 强制获取最新用户信息 无 最新用户信息对象 ​getCurrentLang 获取当前激活的语言配置 无 语言配置对象 / undefined ​queryLanguageList 获取所有可用语言列表 无 语言列表数组 ​activeLang 激活指定语言 语言 ID (string) 更新后的语言配置对象

    2025年3月21日
    1.9K00

Leave a Reply

Please Login to Comment