Oinone开发实践-业务实现多租户方案

总体方案

具体实现方式

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 org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import pro.shushi.pamirs.demo.core.session.DemoSession;
import pro.shushi.pamirs.framework.connectors.data.mapper.context.MapperContext;
import pro.shushi.pamirs.meta.annotation.fun.extern.Slf4j;

import java.util.Map;
import java.util.Properties;

// https://www.jianshu.com/p/8ad3b4d6bd43
@Slf4j
@SuppressWarnings("unused")
@Intercepts({
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class,
                RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
@Component
@Order(99)
@ConditionalOnProperty(value = "pamirs.demo.isolation.enable", havingValue = "true")
public class IsolationCheckInterceptor implements Interceptor {

    private static final String[] BOUND_SQL_CLONE_FIELDS = new String[]{"additionalParameters", "metaParameters"};

    @Autowired
    private DemoIsolationConfiguration demoIsolationConfiguration;

    @Override
    @SuppressWarnings({"unchecked", "rawtypes"})
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        if (SqlCommandType.FLUSH == ms.getSqlCommandType() || SqlCommandType.UNKNOWN == ms.getSqlCommandType()) {
            return invocation.proceed();
        }
        // sql type: UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH
        String sqlCommandType = ms.getSqlCommandType().toString();
        // update 直接返回
        if ("UPDATE".equals(sqlCommandType)) {
            return invocation.proceed();
        }

        Object param = args[1];
        if (param instanceof Map) {
            Map map = (Map) param;
            // 获取配置信息
            String model = MapperContext.model(map);
            if (StringUtils.isBlank(model)) {
                return invocation.proceed();
            }

            if (!demoIsolationConfiguration.needIsolation(model)) {
                return invocation.proceed();
            }
            BoundSql boundSql = ms.getBoundSql(param);
            String sql = boundSql.getSql();
            // 通过jsqlparser解析SQL,此处的statement是封装过后的Insert/Update/Query等SQL语句
            Statement statement = CCJSqlParserUtil.parse(sql);

            switch (sqlCommandType) {
                case "INSERT":
                    Statement insert = prepareInsertSql(statement);
                    BoundSql insertBoundSql = new BoundSql(ms.getConfiguration(), insert.toString(), boundSql.getParameterMappings(), boundSql.getParameterObject());
                    cloneBoundSqlParameters(boundSql, insertBoundSql);
                    MappedStatement insertMs = buildMappedStatement(ms, new BoundSqlSqlSource(insertBoundSql));
                    // 更新 MappedStatement 对象
                    args[0] = insertMs;
                    break;
                case "SELECT":
                    Statement select = prepareSelectSql(statement);
                    BoundSql selectBoundSql = new BoundSql(ms.getConfiguration(), select.toString(), boundSql.getParameterMappings(), boundSql.getParameterObject());
                    cloneBoundSqlParameters(boundSql, selectBoundSql);
                    MappedStatement selectMs = buildMappedStatement(ms, new BoundSqlSqlSource(selectBoundSql));
                    // 更新 MappedStatement 对象
                    args[0] = selectMs;
                    break;
                default:
                    break;
            }
        }

        return invocation.proceed();
    }

    private void cloneBoundSqlParameters(BoundSql boundSql, BoundSql targetBoundSql) {
        MetaObject boundSqlObject = SystemMetaObject.forObject(boundSql);
        MetaObject targetBoundSqlObject = SystemMetaObject.forObject(targetBoundSql);
        for (String field : BOUND_SQL_CLONE_FIELDS) {
            targetBoundSqlObject.setValue(field, boundSqlObject.getValue(field));
        }
    }

    @Override
    public Object plugin(Object target) {
        if (target instanceof Executor) {
            return Plugin.wrap(target, this);
        }
        return target;
    }

    @Override
    public void setProperties(Properties properties) {
        // to do nothing
    }

    private MappedStatement buildMappedStatement(MappedStatement ms, SqlSource newSqlSource) {
        MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), newSqlSource, ms.getSqlCommandType());
        builder.resource(ms.getResource());
        builder.fetchSize(ms.getFetchSize());
        builder.statementType(ms.getStatementType());
        builder.keyGenerator(ms.getKeyGenerator());
        if (ms.getKeyProperties() != null && ms.getKeyProperties().length > 0) {
            builder.keyProperty(ms.getKeyProperties()[0]);
        }
        //禁止用缓存(重要)
        builder.flushCacheRequired(false);
        builder.timeout(ms.getTimeout());
        builder.parameterMap(ms.getParameterMap());
        builder.resultMaps(ms.getResultMaps());
        builder.cache(ms.getCache());
        builder.useCache(ms.isUseCache());
        return builder.build();
    }

    private static class BoundSqlSqlSource implements SqlSource {
        BoundSql boundSql;

        public BoundSqlSqlSource(BoundSql boundSql) {
            this.boundSql = boundSql;
        }

        @Override
        public BoundSql getBoundSql(Object parameterObject) {
            return boundSql;
        }
    }

    private Statement prepareInsertSql(Statement statement) {
        Insert insert = (Insert) statement;

        boolean isContainsIsolationColumn = false;
        int createDateColumnIndex = 0;
        for (int i = 0; i < insert.getColumns().size(); i++) {
            Column column = insert.getColumns().get(i);
            if (clearQuote(column.getColumnName()).equals(demoIsolationConfiguration.getColumn())) {
                // sql中包含了设置的列名,则只需要设置值
                isContainsIsolationColumn = true;
                createDateColumnIndex = i;
                break;
            }
        }

        if (!isContainsIsolationColumn) {
            intoValue("" + demoIsolationConfiguration.getColumn() + "", DemoSession.getCompany().getCode(), insert);
        } else {
            intoValueWithIndex(createDateColumnIndex, DemoSession.getCompany().getCode(), insert);
        }

        log.debug("intercept insert sql is : {}", insert);
        return insert;
    }

    private Statement prepareSelectSql(Statement statement) throws JSQLParserException {
        Select select = (Select) statement;
        PlainSelect plain = (PlainSelect) select.getSelectBody();
        FromItem fromItem = plain.getFromItem();

        StringBuffer whereSql = new StringBuffer();
        //增加sql语句的逻辑部分处理
        if (fromItem.getAlias() != null) {
            whereSql.append(fromItem.getAlias().getName()).append("." + demoIsolationConfiguration.getColumn() + " = ").append("'").append(DemoSession.getCompany().getCode()).append("'");
        } else {
            whereSql.append("" + demoIsolationConfiguration.getColumn() + " = ").append("'").append(DemoSession.getCompany().getCode()).append("'");
        }
        Expression where = plain.getWhere();
        if (where == null) {
            if (whereSql.length() > 0) {
                Expression expression = CCJSqlParserUtil.parseCondExpression(whereSql.toString());
                Expression whereExpression = (Expression) expression;
                plain.setWhere(whereExpression);
            }
        } else {
            if (whereSql.length() > 0) {
                //where条件之前存在,需要重新进行拼接
                whereSql.append(" and ( " + where.toString() + " )");
            } else {
                //新增片段不存在,使用之前的sql
                whereSql.append(where.toString());
            }
            Expression expression = CCJSqlParserUtil.parseCondExpression(whereSql.toString());
            plain.setWhere(expression);
        }

        return select;
    }

    private Statement prepareUpdateSql(Statement statement) throws JSQLParserException {
        Update update = (Update) statement;
        PlainSelect plain = (PlainSelect) update.getSelect().getSelectBody();
        FromItem fromItem = plain.getFromItem();

        StringBuffer whereSql = new StringBuffer();
        //增加sql语句的逻辑部分处理
        if (fromItem.getAlias() != null) {
            whereSql.append(fromItem.getAlias().getName()).append("." + demoIsolationConfiguration.getColumn() + " = ").append("'").append(DemoSession.getCompany().getCode()).append("'");
        } else {
            whereSql.append("" + demoIsolationConfiguration.getColumn() + " = ").append("'").append(DemoSession.getCompany().getCode()).append("'");
        }
        Expression where = plain.getWhere();
        if (where == null) {
            if (whereSql.length() > 0) {
                Expression expression = CCJSqlParserUtil.parseCondExpression(whereSql.toString());
                Expression whereExpression = expression;
                plain.setWhere(whereExpression);
            }
        } else {
            if (whereSql.length() > 0) {
                //where条件之前存在,需要重新进行拼接
                whereSql.append(" and ( " + where + " )");
            } else {
                //新增片段不存在,使用之前的sql
                whereSql.append(where);
            }
            Expression expression = CCJSqlParserUtil.parseCondExpression(whereSql.toString());
            plain.setWhere(expression);
        }

        return update;
    }

    /**
     * insert sql update column value
     *
     * @param index
     * @param columnValue
     * @param insert
     */
    private void intoValueWithIndex(final int index, final Object columnValue, Insert insert) {
        // 通过visitor设置对应的值
        if (insert.getItemsList() == null) {
            insert.getSelect().getSelectBody().accept(new PlainSelectVisitor(index, columnValue));
        } else {
            insert.getItemsList().accept(new ItemsListVisitor() {
                @Override
                public void visit(SubSelect subSelect) {
                    throw new UnsupportedOperationException("Not supported yet.");
                }

                @Override
                public void visit(NamedExpressionList namedExpressionList) {

                }

                @Override
                public void visit(ExpressionList expressionList) {
                    if (columnValue instanceof String) {
                        expressionList.getExpressions().set(index, new StringValue((String) columnValue));
                    } else if (columnValue instanceof Long) {
                        expressionList.getExpressions().set(index, new LongValue((Long) columnValue));
                    } else {
                        // if you need to add other type data, add more if branch
                        expressionList.getExpressions().set(index, new StringValue((String) columnValue));
                    }
                }

                @Override
                public void visit(MultiExpressionList multiExpressionList) {
                    for (ExpressionList expressionList : multiExpressionList.getExprList()) {
                        if (columnValue instanceof String) {
                            expressionList.getExpressions().set(index, new StringValue((String) columnValue));
                        } else if (columnValue instanceof Long) {
                            expressionList.getExpressions().set(index, new LongValue((Long) columnValue));
                        } else {
                            // if you need to add other type data, add more if branch
                            expressionList.getExpressions().set(index, new StringValue((String) columnValue));
                        }
                    }
                }
            });
        }
    }

    /**
     * insert sql add column
     *
     * @param columnName
     * @param columnValue
     * @param insert
     */
    private void intoValue(String columnName, final Object columnValue, Insert insert) {
        // 添加列
        insert.getColumns().add(new Column(columnName));
        // 通过visitor设置对应的值
        if (insert.getItemsList() == null) {
            insert.getSelect().getSelectBody().accept(new PlainSelectVisitor(-1, columnValue));
        } else {
            insert.getItemsList().accept(new ItemsListVisitor() {
                @Override
                public void visit(SubSelect subSelect) {
                    throw new UnsupportedOperationException("Not supported yet.");
                }

                @Override
                public void visit(ExpressionList expressionList) {
                    // 这里表示添加列时。列值在数据库中的数据类型, 目前只用到了Long和String,需要的自行扩展
                    if (columnValue instanceof String) {
                        expressionList.getExpressions().add(new StringValue((String) columnValue));
                    } else if (columnValue instanceof Long) {
                        expressionList.getExpressions().add(new LongValue((Long) columnValue));
                    } else {
                        // if you need to add other type data, add more if branch
                        expressionList.getExpressions().add(new StringValue((String) columnValue));
                    }
                }

                @Override
                public void visit(NamedExpressionList namedExpressionList) {
                }

                @Override
                public void visit(MultiExpressionList multiExpressionList) {
                    for (ExpressionList expressionList : multiExpressionList.getExprList()) {
                        if (columnValue instanceof String) {
                            expressionList.getExpressions().add(new StringValue((String) columnValue));
                        } else if (columnValue instanceof Long) {
                            expressionList.getExpressions().add(new LongValue((Long) columnValue));
                        } else {
                            // if you need to add other type data, add more if branch
                            expressionList.getExpressions().add(new StringValue((String) columnValue));
                        }
                    }
                }
            });
        }
    }

    /**
     * 支持INSERT INTO SELECT 语句
     */
    private class PlainSelectVisitor implements SelectVisitor {
        int index;
        Object columnValue;

        public PlainSelectVisitor(int index, Object columnValue) {
            this.index = index;
            this.columnValue = columnValue;
        }

        @Override
        public void visit(PlainSelect plainSelect) {
            if (index != -1) {
                if (columnValue instanceof String) {
                    plainSelect.getSelectItems().set(index, new SelectExpressionItem(new StringValue((String) columnValue)));
                } else if (columnValue instanceof Long) {
                    plainSelect.getSelectItems().set(index, new SelectExpressionItem(new LongValue((Long) columnValue)));
                } else {
                    // if you need to add other type data, add more if branch
                    plainSelect.getSelectItems().set(index, new SelectExpressionItem(new StringValue((String) columnValue)));
                }
            } else {
                if (columnValue instanceof String) {
                    plainSelect.getSelectItems().add(new SelectExpressionItem(new StringValue((String) columnValue)));
                } else if (columnValue instanceof Long) {
                    plainSelect.getSelectItems().add(new SelectExpressionItem(new LongValue((Long) columnValue)));
                } else {
                    // if you need to add other type data, add more if branch
                    plainSelect.getSelectItems().add(new SelectExpressionItem(new StringValue((String) columnValue)));
                }
            }
        }

        @Override
        public void visit(SetOperationList setOperationList) {
            throw new UnsupportedOperationException("Not supported yet.");
        }

        @Override
        public void visit(WithItem withItem) {
            if (index != -1) {
                if (columnValue instanceof String) {
                    withItem.getWithItemList().set(index, new SelectExpressionItem(new StringValue((String) columnValue)));
                } else if (columnValue instanceof Long) {
                    withItem.getWithItemList().set(index, new SelectExpressionItem(new LongValue((Long) columnValue)));
                } else {
                    // if you need to add other type data, add more if branch
                    withItem.getWithItemList().set(index, new SelectExpressionItem(new StringValue((String) columnValue)));
                }
            } else {
                if (columnValue instanceof String) {
                    withItem.getWithItemList().add(new SelectExpressionItem(new StringValue((String) columnValue)));
                } else if (columnValue instanceof Long) {
                    withItem.getWithItemList().add(new SelectExpressionItem(new LongValue((Long) columnValue)));
                } else {
                    // if you need to add other type data, add more if branch
                    withItem.getWithItemList().add(new SelectExpressionItem(new StringValue((String) columnValue)));
                }
            }
        }

        @Override
        public void visit(ValuesStatement valuesStatement) {

        }
    }

    /**
     * 去除''号
     *
     * @param value
     * @return {@link String}
     */
    private String clearQuote(String value) {
        if (value.startsWith("") && value.endsWith("")) {
            value = value.substring(1, value.length() - 1);
        }
        return value;
    }

}

5、租户配置信息代码实现

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

import com.google.common.collect.Lists;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import pro.shushi.pamirs.meta.annotation.fun.Data;
import pro.shushi.pamirs.meta.api.dto.config.ModelConfig;
import pro.shushi.pamirs.meta.api.dto.config.ModelFieldConfig;
import pro.shushi.pamirs.meta.api.session.PamirsSession;
import pro.shushi.pamirs.meta.common.constants.ModuleConstants;

import java.util.List;
import java.util.Optional;

@Data
@Configuration
@ConfigurationProperties(prefix = "pamirs.demo.isolation")
public class DemoIsolationConfiguration {

    private Boolean enable = Boolean.FALSE;
    /**隔离字段对应的数据表column*/
    private String column;
    /**隔离字段对应的模型field*/
    private String field;
    /**即使包含隔离字段field,也不需要隔离的模型列表。 实际项目中也可采用白名单方式*/
    private List ignoreModels = Lists.newArrayList("business.PamirsEmployee", "demo.PetEmployee");

    public boolean needIsolation(String model) {
        // 1、在忽略列表中的不需要进行隔离
        if (matchesIgnoreModel(model)) {
            return false;
        }

        // 2、模块为空或者为base模块的不需要进行隔离
        String module = Optional.ofNullable(PamirsSession.getContext())
                .map(v -> v.getModelConfig(model)).map(ModelConfig::getModule).orElse(null);
        if (StringUtils.isBlank(module) || module.equals(ModuleConstants.MODULE_BASE)) {
            return false;
        }

        // 3、超级管理员进入不需要进行隔离(根据实际情况确定是否开启)
        /**
        if (PamirsSession.getAdminTag()==null || PamirsSession.getAdminTag()) {
            return false;
        }**/

        // 4、模型中无隔离字段的不需要进行隔离
        ModelFieldConfig modelField = PamirsSession.getContext().getModelField(model, field);
        if (modelField == null) {
            return false;
        }

        return true;
    }

    private boolean matchesIgnoreModel(String model) {
        if (CollectionUtils.isEmpty(ignoreModels)) {
            return false;
        }
        if (ignoreModels.contains(model)) {
            return true;
        }
        return false;
    }
}

6、对应的yml文件配置示例

  demo:
    isolation:
      enable: true
      field:  companyCode
      column: company_code
      # ignoreModels:

7、本文中的示例代码附件

拦截器实现租户隔离示例 interceptor
Session扩展示例 Session扩展示例

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

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

(0)
望闲的头像望闲数式管理员
上一篇 2024年4月3日 pm4:39
下一篇 2024年4月7日 pm7:48

相关推荐

  • 工作流-流程代办等页面自定义

    1. 审批/填写节点的视图页面 在界面设计器中创建对应模型的表单视图,可根据业务场景需要自定义所需流程待办的审批页面 2. 在审批/填写节点中选择刚创建的视图 在工作流待办数据权限可在节点数据权限中可对字段设置查看、编辑、隐藏

    2024年5月14日
    1.7K00
  • 首次登录修改密码和自定义密码规则等

    场景描述 在某些场景下,可能需要实现 用户首次登录强制修改密码的功能,或者存在修改平台默认密码等校验规则等需求;本文将讲解不改变平台代码的情况下,如何实现这些功能需求。 首次登录修改密码 方案概述 自定义User增加是否是第一次登录的属性,登录后执行一个扩展点。 判断是否是一次登录,如果是则返回对应的状态码,前端根据状态码重定向到修改密码的页面。修改完成则充值第一次登录的标识。 PS:首次登录的标识平台前端已默认实现 扩展PamirsUser(例如:DemoUser) /** * @author wangxian */ @Model.model(DemoUser.MODEL_MODEL) @Model(displayName = "用户", labelFields = {"nickname"}) @Model.Advanced(index = {"companyId"}) public class DemoUser extends PamirsUser { public static final String MODEL_MODEL = "demo.DemoUser"; @Field.Integer @Field.Advanced(columnDefinition = "bigint DEFAULT '0'") @Field(displayName = "公司ID", invisible = true) private Long companyId; /** * 默认true->1 */ @Field.Boolean @Field.Advanced(columnDefinition = "tinyint(1) DEFAULT '1'") @Field(displayName = "是否首次登录") private Boolean firstLogin; } 定义扩展点接口(实际项目按需要增加和删减接口的定义) import pro.shushi.pamirs.meta.annotation.Ext; import pro.shushi.pamirs.meta.annotation.ExtPoint; import pro.shushi.pamirs.user.api.model.tmodel.PamirsUserTransient; @Ext(PamirsUserTransient.class) public interface PamirsUserTransientExtPoint { @ExtPoint PamirsUserTransient loginAfter(PamirsUserTransient user); @ExtPoint PamirsUserTransient loginCustomAfter(PamirsUserTransient user); @ExtPoint PamirsUserTransient firstResetPasswordAfter(PamirsUserTransient user); @ExtPoint PamirsUserTransient firstResetPasswordBefore(PamirsUserTransient user); @ExtPoint PamirsUserTransient modifyCurrentUserPasswordAfter(PamirsUserTransient user); @ExtPoint PamirsUserTransient modifyCurrentUserPasswordBefore(PamirsUserTransient user); } 编写扩展点实现(例如:DemoUserLoginExtPoint) @Order(0) @Component @Ext(PamirsUserTransient.class) @Slf4j public class DemoUserLoginExtPoint implements PamirsUserTransientExtPoint { @Override @ExtPoint.Implement public PamirsUserTransient loginAfter(PamirsUserTransient user) { return checkFirstLogin(user); } private PamirsUserTransient checkFirstLogin(PamirsUserTransient user) { //首次登录需要修改密码 Long userId = PamirsSession.getUserId(); if (userId == null) { return user; } DemoUser companyUser = new DemoUser().queryById(userId); // 判断用户是否是第一次登录,如果是第一次登录,需要返回错误码,页面重新向登录 Boolean isFirst = companyUser.getFirstLogin(); if (isFirst) { //如果是第一次登录,返回一个标识给前端。 // 首次登录的标识平台已默认实现 user.setBroken(Boolean.TRUE); user.setErrorCode(UserExpEnumerate.USER_FIRST_LOGIN_ERROR.code()); return user; } return user; } @Override public PamirsUserTransient loginCustomAfter(PamirsUserTransient user) { return checkFirstLogin(user); } @Override…

    2024年5月25日
    5.4K00
  • 【KDB】后端部署使用Kingbase数据库(人大金仓/电科金仓)

    KDB数据库配置 驱动配置 Maven配置 点击查看官方驱动说明 PS:官方驱动说明中的9.0.0版本目前并未推送至公共仓库,因此使用8.6.0版本替代。 <kdb.version>8.6.0</kdb.version> <dependency> <groupId>cn.com.kingbase</groupId> <artifactId>kingbase8</artifactId> <version>${kdb.version}</version> </dependency> 离线驱动下载 kingbase8-8.6.0.jar JDBC连接配置 pamirs: datasource: base: type: com.alibaba.druid.pool.DruidDataSource driverClassName: com.kingbase8.Driver url: jdbc:kingbase8://127.0.0.1:4321/pamirs?currentSchema=base&autosave=always&cleanupSavepoints=true username: xxxxxx password: xxxxxx initialSize: 5 maxActive: 200 minIdle: 5 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true asyncInit: true validConnectionCheckerClassName: com.alibaba.druid.pool.vendor.PGValidConnectionChecker PS:validConnectionCheckerClassName配置非常重要,连接存活检查是连接池可以保持连接的重要配置。Druid连接池可以自动识别大多数的数据库类型,由于jdbc:kingbase8协议属于非内置识别的类型,因此需要手动配置。 连接url配置 点击查看官方JDBC连接配置说明 url格式 jdbc:kingbase8://${host}:${port}/${database}?currentSchema=${schema}&autosave=always&cleanupSavepoints=true 在jdbc连接配置时,${database}和${schema}必须配置,不可缺省。autosave=always、cleanupSavepoints=true属于必须配置的事务参数,否则事务回滚行为与其他数据库不一致,会导致部分操作失败。 其他连接参数如需配置,可自行查阅相关资料进行调优。 方言配置 pamirs方言配置 pamirs: dialect: ds: base: type: KDB version: 9 major-version: V009R001C001B0030 pamirs: type: KDB version: 9 major-version: V009R001C001B0030 数据库版本 type version majorVersion V009R001C001B0030 KDB 9 V009R001C001B0030 V008R006C008B0020 KDB 9 V009R001C001B0030 PS:由于方言开发环境为V009R001C001B0030版本,其他类似版本原则上不会出现太大差异,如出现其他版本无法正常支持的,可在文档下方留言。 schedule方言配置 pamirs: event: enabled: true schedule: enabled: true dialect: type: PostgreSQL version: 14 major-version: 14.3 type version majorVersion PostgreSQL 14 14.3 PS:由于schedule的方言与PostgreSQL数据库并无明显差异,Kingbase数据库可以直接使用PostgreSQL数据库方言。 其他配置 逻辑删除的值配置 pamirs: mapper: global: table-info: logic-delete-value: (EXTRACT(epoch FROM CURRENT_TIMESTAMP) * 1000000 + EXTRACT(MICROSECONDS FROM CURRENT_TIMESTAMP))::bigint KDB数据库关键参数检查 PS:以下参数为Oinone平台接入KDB时使用的数据库参数,参数不一致时可尝试启动。 数据库模式 推荐配置:DB_MODE=oracle 数据库安装/初始化时配置 是否大小写敏感 推荐配置:enable_ci=off 是否启用语句级回滚 推荐配置:ora_statement_level_rollback = off show ora_statement_level_rollback; set ora_statement_level_rollback=off; 此参数在Oinone平台接入时使用的版本中未体现出应有的效果。从官方提供的文档来看,此参数与数据库连接串上的autosave=always&cleanupSavepoints=true配置结果应该是一致的,由于此参数配置无效,因此在数据库连接串上必须指定这两个参数。 Oinone平台在最初开发时使用的是基于mysql数据库的事务特性,即不支持语句级回滚的事务行为。因此,为了保证Oinone平台功能正常,需要使得事务行为保持一致。 如不一致,则可能出现某些功能无法正常使用的情况。如:流程设计器首次发布定时触发的工作流时会出现报错;导入/导出任务出现异常无法正常更新任务状态等。 是否将空字符串视为NULL 推荐配置:ora_input_emptystr_isnull = off show ora_input_emptystr_isnull; set ora_input_emptystr_isnull=off; KDB数据库用户初始化及授权 — init root user (user name can be modified by oneself) CREATE USER root WITH PASSWORD 'password'; — if using automatic database and schema creation, this is…

    2024年10月29日
    1.6K00
  • 平台配置日志输出和推送到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.6K00
  • 使用Mapper方式进行联表查询

    有些业务场景需要查询两张表的数据,这时候就需要用到联表查询。下面将介绍两种方式进行联表查询。 场景:A模型页面,查询条件中包含B模型字段 模型A @Model.model(YesOne.MODEL_MODEL) @Model(displayName = "YesOne", summary = "YesOne") public class YesOne extends IdModel { public static final String MODEL_MODEL = "top.YesOne"; @Field.Integer @Field(displayName = "YesId") private Long yesId; @Field.String @Field(displayName = "名字") private String name; @Field.String @Field(displayName = "科目名字") private String professionalName; @Field(displayName = "关联YesTwo") @Field.many2one @Field.Relation(relationFields = {"yesId"},referenceFields = {"id"}) private YesTwo yesTwo; } 模型B @Model.model(YesTwo.MODEL_MODEL) @Model(displayName = "YesTwo", summary = "YesTwo") public class YesTwo extends IdModel { public static final String MODEL_MODEL = "top.YesTwo"; @Field.Integer @Field(displayName = "科目id") private Long professionalId; @Field.String @Field(displayName = "科目名字") private String professionalName; } 1. 使用in的方式查询 通过B模型的查询条件查询出符合条件的所有数据ID,再根据这个ID去A模型里面查询出所需的数据。 @Function.Advanced(displayName = "查询列表", type = FunctionTypeEnum.QUERY, category = FunctionCategoryEnum.QUERY_PAGE, managed = true) @Function(openLevel = {FunctionOpenEnum.LOCAL, FunctionOpenEnum.REMOTE, FunctionOpenEnum.API}) public Pagination<YesOne> queryPage(Pagination<YesOne> page, IWrapper<YesOne> queryWrapper) { String professionalName = (String) queryWrapper.getQueryData().get("professionalName"); if (StringUtils.isNotBlank(professionalName)) { List<Long> yesTwoId = new YesTwo().queryList(Pops.<YesTwo>lambdaQuery() .from(YesTwo.MODEL_MODEL) .eq(YesTwo::getProfessionalName, professionalName)) .stream().map(YesTwo::getId) .collect(Collectors.toList()); LambdaQueryWrapper<YesOne> wq = Pops.<YesOne>lambdaQuery().from(YesOne.MODEL_MODEL); if (CollectionUtils.isNotEmpty(yesTwoId)) { wq.in(YesOne::getYesId, yesTwoId); } return new YesOne().queryPage(page, wq); } return new YesOne().queryPage(page, queryWrapper); } 2. 使用mapper的方式查询 利用sql的方式去直接查询出结果。使用联表查询的方式查询 @Autowired private YesOneQueryMapper yesOneQueryMapper; @Function.Advanced(displayName = "查询列表", type = FunctionTypeEnum.QUERY, category = FunctionCategoryEnum.QUERY_PAGE, managed = true) @Function(openLevel = {FunctionOpenEnum.LOCAL,…

    2024年9月27日
    1.2K00

Leave a Reply

登录后才能评论