LiteFlow条件组件的设计组件标签|组件参数,EL与或非表达式正反解析,元数据管理

news2024/11/15 23:36:41

个人博客:无奈何杨(wnhyang)

个人语雀:wnhyang

共享语雀:在线知识共享

Github:wnhyang - Overview


参考

https://juejin.cn/post/7005869798483558431

🍉组件参数 | LiteFlow

🍍组件标签 | LiteFlow

https://gitee.com/freshday/radar/wikis/home

image

条件配置

在需要灵活配置规则的业务系统中,如上图这样的条件配置非常常见。

风控系统之普通规则条件,使用LiteFlow实现

风控系统之通用规则条件设计,算术单元/逻辑单元/函数式接口

不管是在第一个参考链接(https://juejin.cn/post/7005869798483558431)里或是从自己的分析中都可以知道条件配置就是对同一逻辑单元的与或非编排。对于LiteFlow相同组件的编排有组件标签和组件参数两种解决方法。

组件标签实现

通过组件标签实现,就是在编排LiteFlow时加上组件tag

那么就将条件存储在数据库中,在组件运行时通过tag再到数据库中去取。

idtypevaluelogic_typeexpect_typeexpect_value
1normalN_S_appNameeqinputphone
2normalN_S_payerAccounteqinput123456
3normalN_F_transAmountgtinput15
4normalN_S_appNameeqinputphone
5normalN_F_transAmountltinput100
6normalN_F_transAmountgteinput20.0
7normalN_S_appNameeqcontextN_S_ipCity
8zb1gtinput20

如:IF(AND(c_cn.tag("4"),c_cn.tag("5")),r_tcn.tag("2"),r_fcn);

就是对条件组件c_cn.tag("id")进行与或非编排,就能几乎适用于所有条件。

create table de_condition
(
  id           bigint auto_increment comment '主键' primary key,
  type         varchar(20)                 default 'normal'          not null comment '类型',
  value        varchar(32)                 default ''                not null comment '操作对象',
  logic_type   varchar(32)                 default 'null'            not null comment '逻辑类型',
  expect_type  varchar(32)                 default 'input'           not null comment '期望值类型',
  expect_value varchar(32)                 default ''                not null comment '期望值',
  description  varchar(64) charset utf8mb4 default ''                null comment '描述',
  creator      varchar(64) charset utf8mb4 default ''                null comment '创建者',
  create_time  datetime                    default CURRENT_TIMESTAMP not null comment '创建时间',
  updater      varchar(64) charset utf8mb4 default ''                null comment '更新者',
  update_time  datetime                    default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
  deleted      bit                         default b'0'              not null comment '是否删除'
)comment '规则条件表';

忽略公共字段,实际上有用的只有typevaluelogicTypeexpect_typeexpect_value

/**
 * @author wnhyang
 * @date 2024/6/18
 **/
public interface ConditionType {

    /**
     * 普通条件
     */
    String NORMAL = "normal";

    /**
     * 指标条件
     */
    String ZB = "zb";

    /**
     * 正则条件
     */
    String REGULAR = "regular";

    /**
     * 名单条件
     */
    String LIST = "list";

    /**
     * 脚本条件
     */
    String SCRIPT = "script";

}

type相对,value代表的意义不同,typenormal时,value是系统字段名;为zb时,value是指标id,等等,可参考以下组件示例代码。

/**
 * @author wnhyang
 * @date 2024/4/3
 **/
@AllArgsConstructor
@Getter
public enum LogicType {

    NULL("null"),
    NOT_NULL("not_null"),
    EQ("eq"),
    NOT_EQ("not_eq"),
    GT("gt"),
    GTE("gte"),
    LT("lt"),
    LTE("lte"),
    CONTAINS("contains"),
    NOT_CONTAINS("not_contains"),
    PREFIX("prefix"),
    NOT_PREFIX("not_prefix"),
    SUFFIX("suffix"),
    NOT_SUFFIX("not_suffix"),

    /**
     * 适用正则、名单条件
     */
    MATCH("match"),

    /**
     * 适用正则、名单条件
     */
    NOT_MATCH("not_match"),

    /**
     * 适用正则条件
     */
    MATCH_IGNORE_CASE("match_ignore_case"),

    /**
     * 适用正则条件
     */
    NOT_MATCH_IGNORE_CASE("not_match_ignore_case");

    private final String type;

    public static LogicType getByType(String type) {
        for (LogicType logicType : LogicType.values()) {
            if (logicType.getType().equals(type)) {
                return logicType;
            }
        }
        return null;
    }
}

logicType取值如上,同样,根据typevalue的不同,逻辑适用的也不同,具体不同参考以下普通字段类型和逻辑函数式接口的实现。

/**
 * @author wnhyang
 * @date 2024/5/8
 **/
public interface ExpectType {

    /**
     * 常量
     */
    String INPUT = "input";

    /**
     * 上下文
     */
    String CONTEXT = "context";

    /**
     * 名单集
     */
    String LIST_SET = "listSet";
}

expectType有上,同样适用于不同条件。

字段

字段命名规则为[N/D]_[S/N/F/D/E/B]_{name}

N普通字段,D动态字段;S/N/F/D/E/B字段类型;name字段名。

如:N_S_appName表示普通字符字段appNameN_D_transTime表示普通日期字段transTime

/**
 * @author wnhyang
 * @date 2024/3/13
 **/
@AllArgsConstructor
@Getter
public enum FieldType {

    /**
     * 字符型,支持【等于、不等于、包含、不包含、前缀、非前缀、后缀、非后缀、为空、不为空、存在于、不存在于】
     */
    STRING("S"),

    /**
     * 整数型,支持【等于、不等于、大于、小于、大于等于、小于等于、为空、不为空、存在于,不存在于】
     */
    NUMBER("N"),

    /**
     * 小数型,支持【等于、不等于、大于、小于、大于等于、小于等于、为空、不为空】
     */
    FLOAT("F"),

    /**
     * 日期型,支持【等于、不等于、大于、小于、大于等于、小于等于、为空、不为空】
     */
    DATE("D"),

    /**
     * 枚举型,支持【等于、不等于、为空、不为空】
     */
    ENUM("E"),

    /**
     * 布尔型,支持【等于、不等于、为空、不为空】
     */
    BOOLEAN("B");

    private final String type;

    public static FieldType getByType(String type) {
        for (FieldType value : values()) {
            if (value.getType().equals(type)) {
                return value;
            }
        }
        return null;
    }

    public static FieldType getByFieldName(String fieldName) {
        if (fieldName.length() >= 3) {
            String sub = StrUtil.sub(fieldName, 2, 3);
            return getByType(sub);
        }
        return null;
    }
}

逻辑运算

/**
 * @author wnhyang
 * @date 2024/6/14
 **/
@FunctionalInterface
public interface LogicOp<T> {

    boolean apply(T a, LogicType logicType, T b);
}

逻辑运算实现有下。

/**
 * @author wnhyang
 * @date 2024/5/16
 **/
@Slf4j
public class FunUtil {

    private FunUtil() {
    }

    public static final FunUtil INSTANCE = new FunUtil();

    public LogicOp<String> stringLogicOp = (a, logicType, b) -> switch (Objects.requireNonNull(logicType)) {
        case NULL -> StrUtil.isBlank(a);
        case NOT_NULL -> !StrUtil.isBlank(a);
        case EQ -> a.equals(b);
        case NOT_EQ -> !a.equals(b);
        case CONTAINS -> a.contains(b);
        case NOT_CONTAINS -> !a.contains(b);
        case PREFIX -> a.startsWith(b);
        case NOT_PREFIX -> !a.startsWith(b);
        case SUFFIX -> a.endsWith(b);
        case NOT_SUFFIX -> !a.endsWith(b);
        default -> false;
    };

    public LogicOp<Integer> integerLogicOp = (a, logicType, b) -> switch (Objects.requireNonNull(logicType)) {
        case NULL -> a == null;
        case NOT_NULL -> a != null;
        case EQ -> a.equals(b);
        case NOT_EQ -> !a.equals(b);
        case LT -> a < b;
        case LTE -> a <= b;
        case GT -> a > b;
        case GTE -> a >= b;
        default -> false;
    };

    public LogicOp<Double> doubleLogicOp = (a, logicType, b) -> switch (Objects.requireNonNull(logicType)) {
        case NULL -> a == null;
        case NOT_NULL -> a != null;
        case EQ -> a.equals(b);
        case NOT_EQ -> !a.equals(b);
        case LT -> a < b;
        case LTE -> a <= b;
        case GT -> a > b;
        case GTE -> a >= b;
        default -> false;
    };

    public LogicOp<LocalDateTime> dateLogicOp = (a, logicType, b) -> switch (Objects.requireNonNull(logicType)) {
        case NULL -> a == null;
        case NOT_NULL -> a != null;
        case EQ -> a.equals(b);
        case NOT_EQ -> !a.equals(b);
        case LT -> a.isBefore(b);
        case LTE -> a.isBefore(b) || a.equals(b);
        case GT -> a.isAfter(b);
        case GTE -> a.isAfter(b) || a.equals(b);
        default -> false;
    };

    public LogicOp<String> enumLogicOp = (a, logicType, b) -> switch (Objects.requireNonNull(logicType)) {
        case NULL -> a == null;
        case NOT_NULL -> a != null;
        case EQ -> a.equals(b);
        case NOT_EQ -> !a.equals(b);
        default -> false;
    };

    public LogicOp<Boolean> booleanLogicOp = (a, logicType, b) -> switch (Objects.requireNonNull(logicType)) {
        case NULL -> a == null;
        case NOT_NULL -> a != null;
        case EQ -> a.equals(b);
        case NOT_EQ -> !a.equals(b);
        default -> false;
    };

    public LogicOp<String> regularLogicOp = (a, logicType, b) -> switch (Objects.requireNonNull(logicType)) {
        case MATCH -> ReUtil.isMatch(b, a);
        case MATCH_IGNORE_CASE -> ReUtil.isMatch("(?i)" + b, a);
        case NOT_MATCH -> !ReUtil.isMatch(b, a);
        case NOT_MATCH_IGNORE_CASE -> !ReUtil.isMatch("(?i)" + b, a);
        default -> false;
    };


}

这样做是为了简化条件组件代码,抽象一些实现。

条件组件|组件标签

@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS_BOOLEAN, nodeId = LFUtil.CONDITION_COMMON_NODE, nodeType = NodeTypeEnum.BOOLEAN)
public boolean cond(NodeComponent bindCmp) {

    // 获取当前tag
    String tag = bindCmp.getTag();

    // 获取当前tag对应的条件
    Condition condition = conditionMapper.selectById(tag);

    // 获取上下文
    AccessRequest accessRequest = bindCmp.getContextBean(AccessRequest.class);

    String type = condition.getType();
    boolean cond = false;

    LogicType byType = LogicType.getByType(condition.getLogicType());
    try {
        // 普通条件,适用指标、规则
        if (ConditionType.NORMAL.equals(type)) {

            // 获取条件字段
            String fieldName = condition.getValue();
            FieldType fieldType = FieldType.getByFieldName(fieldName);

            String expectValue = condition.getExpectValue();
            if (ExpectType.CONTEXT.equals(condition.getExpectType())) {
                expectValue = accessRequest.getStringData(expectValue);
            }

            if (fieldType == null || byType == null) {
                return false;
            }

            switch (fieldType) {
                case STRING:
                    String stringData = accessRequest.getStringData(fieldName);
                    log.debug("字段值:{}, 操作:{}, 期望值:{}", stringData, byType, expectValue);
                    cond = FunUtil.INSTANCE.stringLogicOp.apply(stringData, byType, expectValue);
                    break;
                case NUMBER:
                    Integer numberData = accessRequest.getNumberData(fieldName);
                    Integer expectInteger = Integer.parseInt(expectValue);
                    log.debug("字段值:{}, 操作:{}, 期望值:{}", numberData, byType, expectInteger);
                    cond = FunUtil.INSTANCE.integerLogicOp.apply(numberData, byType, expectInteger);
                    break;
                case FLOAT:
                    Double floatData = accessRequest.getFloatData(fieldName);
                    Double expectDouble = Double.parseDouble(expectValue);
                    log.debug("字段值:{}, 操作:{}, 期望值:{}", floatData, byType, expectDouble);
                    cond = FunUtil.INSTANCE.doubleLogicOp.apply(floatData, byType, expectDouble);
                    break;
                case DATE:
                    LocalDateTime dateData = accessRequest.getDateData(fieldName);
                    LocalDateTime expectDateTime = LocalDateTimeUtil.parse(expectValue, DatePattern.NORM_DATETIME_FORMATTER);
                    log.debug("字段值:{}, 操作:{}, 期望值:{}", dateData, byType, expectDateTime);
                    cond = FunUtil.INSTANCE.dateLogicOp.apply(dateData, byType, expectDateTime);
                    break;
                case ENUM:
                    String enumData = accessRequest.getEnumData(fieldName);
                    log.debug("字段值:{}, 操作:{}, 期望值:{}", enumData, byType, expectValue);
                    cond = FunUtil.INSTANCE.enumLogicOp.apply(enumData, byType, expectValue);
                    break;
                case BOOLEAN:
                    Boolean booleanData = accessRequest.getBooleanData(fieldName);
                    log.debug("字段值:{}", booleanData);
                    cond = FunUtil.INSTANCE.booleanLogicOp.apply(booleanData, byType, Boolean.parseBoolean(expectValue));
                    break;
            }

        } else if (ConditionType.ZB.equals(type)) {
            log.info("指标条件");
            String indicatorId = condition.getValue();
            IndicatorContext indicatorContext = bindCmp.getContextBean(IndicatorContext.class);
            String indicatorValue = indicatorContext.getIndicatorValue(Long.valueOf(indicatorId));
            String expectValue = condition.getExpectValue();
            if (ExpectType.CONTEXT.equals(condition.getExpectType())) {
                expectValue = accessRequest.getStringData(expectValue);
            }
            cond = FunUtil.INSTANCE.doubleLogicOp.apply(Double.parseDouble(indicatorValue), byType, Double.valueOf(expectValue));
        } else if (ConditionType.REGULAR.equals(type)) {
            log.info("正则条件");

            String fieldName = condition.getValue();

            String stringData = accessRequest.getStringData(fieldName);
            cond = FunUtil.INSTANCE.regularLogicOp.apply(stringData, byType, condition.getExpectValue());
        } else if (ConditionType.LIST.equals(type)) {
            log.info("名单条件");
            String fieldName = condition.getValue();

            String stringData = accessRequest.getStringData(fieldName);
            // 查名单集做匹配
            cond = listDataService.hasListData(Long.valueOf(condition.getExpectValue()), stringData);
        } else if (ConditionType.SCRIPT.equals(type)) {
            // TODO 脚本条件
            log.info("脚本条件");
        } else {
            log.error("未知条件类型:{}", type);
        }
    } catch (Exception e) {
        log.error("条件:{}, 运行异常:{}", condition, e.getMessage());
    }
    return cond;
}

组件参数实现

组件参数与组件标签不同,数据就存储在EL表达式中。

与上面组件标签表达式IF(AND(c_cn.tag("4"),c_cn.tag("5")),r_tcn.tag("2"),r_fcn);对应的组件参数表达式是:

IF(
    AND(
        c_cn.tag('{"type":"normal","value":"N_S_appName","logic_type":"eq","expect_type":"input","expect_value":"Phone"}'),
        c_cn.tag('{"type":"normal","value":"N_F_transAmount","logic_type":"lt","expect_type":"input","expect_value":"100"}')
    ),
    r_tcn.tag("2"),
    r_fcn
);

这样在条件组件中就不用再通过tag再次查询数据库来获取数据了。

@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS_BOOLEAN, nodeId = LFUtil.CONDITION_COMMON_NODE, nodeType = NodeTypeEnum.BOOLEAN)
public boolean cond(NodeComponent bindCmp) {

    // 获取当前tag
    String tag = bindCmp.getTag();

    // 获取当前tag对应的条件
    Condition condition = conditionMapper.selectById(tag);

如上的组件标签组件实现替换为以下方法就好。

@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS_BOOLEAN, nodeId = LFUtil.CONDITION_COMMON_NODE, nodeType = NodeTypeEnum.BOOLEAN)
public boolean cond(NodeComponent bindCmp) {

    Condition cmpData = bindCmp.getCmpData(Condition.class);

而且对于这样的条件配置几乎不会复用,都是配置即存在删除即无,省去了删除条件配置同时要在数据库中删除多条条件数据的繁琐步骤。对于其缺点,可能就是在需要反向查找时,有点麻烦。因为绝大多数时候都是使用条件配置确认最终的条件,但是如果反向要查找某字段、指标、名单集被引用的地方,就比较麻烦了,这就需要从Chain解析到所有条件,然后再判断,不如数据库中直接能找到引用的。

EL与或非表达式与条件json的正反向解析

上面都是关于后端条件数据存储的一点想法,但其最终到前端都是要成为统一的结构化数据的。

配置的规则条件总要反显到前端吧,前端条件json总要送给后端用于组成EL表达式吧。

如果有这样的条件类,其中logicOp取值有AND|OR|NOT,其与children共生共存,与其他条件字段互斥,即logicOpchildren要么都有值要么都为空,当其有值时其他条件字段为空,当其为空时,其他一定有值,表示具体的逻辑条件。

还有一点要注意ANDOR是接受复合子条件的,而NOT仅支持单个子条件。

@Data
public class Cond implements Serializable {

    @Serial
    private static final long serialVersionUID = -1831587613757992692L;

    /**
     * AND|OR|NOT
     */
    private String logicOp;

    private List<Cond> children;

    /**
     * 条件类型
     */
    private String type;

    /**
     * 操作对象
     */
    private String value;

    /**
     * 操作类型
     */
    private String logicType;

    /**
     * 期望类型
     */
    private String expectType;

    /**
     * 期望值
     */
    private String expectValue;

    /**
     * 描述
     */
    private String description;
}

在以下示例中并不完善,使用仅是字符串的处理,大家可以尝试一下ElBus的构建,而且没有正确性校验,仅供参考。

其提供了条件类到EL表达式的构建和EL表达式反向解析到类的方法。

/**
 * @author wnhyang
 * @date 2024/7/18
 **/
public class LFUtil {

    private static final ObjectMapper objectMapper = new ObjectMapper();

    static {
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        objectMapper.registerModules(buildJavaTimeModule());
    }
    
    private static String[] splitExpressions(String expression) {
        List<String> parts = new ArrayList<>();
        int level = 0;
        int startIndex = 0;

        for (int i = 0; i < expression.length(); i++) {
            char c = expression.charAt(i);
            if (c == '(') {
                level++;
            } else if (c == ')') {
                level--;
            } else if (c == ',' && level == 0) {
                parts.add(expression.substring(startIndex, i));
                startIndex = i + 1;
            }
        }
        parts.add(expression.substring(startIndex));

        return parts.toArray(new String[0]);
    }

    @SneakyThrows
    private static String buildCondEl(Cond cond) {
        if (cond != null) {
            if (cond.getLogicOp() != null && cond.getChildren() != null && !cond.getChildren().isEmpty()) {
                List<String> expressions = cond.getChildren().stream()
                        .map(LFUtil::buildCondEl)
                        .collect(Collectors.toList());
                return cond.getLogicOp() + "(" + String.join(", ", expressions) + ")";
            } else {
                return "c_cn.data('" + objectMapper.writeValueAsString(cond) + "')";
            }
        }
        return "";
    }

    public static Cond parseToCond(String expression) throws Exception {
        expression = expression.replaceAll("\s+", "");
        return parseExpressionToCond(expression);
    }

    private static Cond parseExpressionToCond(String expression) throws Exception {
        if (expression.startsWith("AND(")) {
            return parseLogicExpression("AND", expression.substring(4, expression.length() - 1));
        } else if (expression.startsWith("OR(")) {
            return parseLogicExpression("OR", expression.substring(3, expression.length() - 1));
        } else if (expression.startsWith("NOT(")) {
            return parseLogicExpression("NOT", expression.substring(4, expression.length() - 1));
        } else {
            return parseVariable(expression);
        }
    }

    private static Cond parseVariable(String variableExpression) throws Exception {
        if (variableExpression.contains(".data('")) {
            int dataIndex = variableExpression.indexOf(".data('");
            String jsonData = variableExpression.substring(dataIndex + 7, variableExpression.length() - 2).trim();
            return objectMapper.readValue(jsonData, Cond.class);
        }
        return new Cond();
    }

    private static Cond parseLogicExpression(String operator, String subExpression) throws Exception {
        Cond cond = new Cond();
        cond.setLogicOp(operator);
        cond.setChildren(new ArrayList<>());
        String[] subExpressions = splitExpressions(subExpression);
        for (String subExp : subExpressions) {
            cond.getChildren().add(parseExpressionToCond(subExp.trim()));
        }
        return cond;
    }


    public static void main(String[] args) throws Exception {

        Cond cond = new Cond();
        cond.setLogicOp("AND");
        List<Cond> children = new ArrayList<>();
        children.add(new Cond().setType("normal").setValue("N_S_appName").setLogicType("eq").setExpectType("input").setExpectValue("Phone"));
        children.add(new Cond().setType("normal").setValue("N_F_transAmount").setLogicType("lt").setExpectType("input").setExpectValue("100"));
        cond.setChildren(children);

        String condEl = buildCondEl(cond);
        System.out.println(condEl);

        System.out.println(parseToCond(condEl));
    }

}

测试结果

AND(c_cn.data('{"type":"normal","value":"N_S_appName","logicType":"eq","expectType":"input","expectValue":"Phone"}'), c_cn.data('{"type":"normal","value":"N_F_transAmount","logicType":"lt","expectType":"input","expectValue":"100"}'))
Cond(logicOp=AND, children=[Cond(logicOp=null, children=null, type=normal, value=N_S_appName, logicType=eq, expectType=input, expectValue=Phone, description=null), Cond(logicOp=null, children=null, type=normal, value=N_F_transAmount, logicType=lt, expectType=input, expectValue=100, description=null)], type=null, value=null, logicType=null, expectType=null, expectValue=null, description=null)

元数据管理

🫔查看指定规则下的所有组件 | LiteFlow

v2.12.0中加入的元数据管理,可以通过FlowBus获取ChainNode的信息。

如下面这样的chain,可以通过FlowBus.getChainFlowBus.getNodesByChainId分别获取ChainChain对应的组件Node

<chain name="R_C#1">
  THEN(IF(
  AND(c_cn.data('{"id":"1","name":"wnhyang","phoneNumber":"123456789"}'),c_cn.data('{"id":"1","name":"wnhyang","phoneNumber":"123456789"}'),NOT(c_cn.data('{"id":"1","name":"wnhyang","phoneNumber":"123456789"}'))),a,b),IF(
  AND(c_cn.data('{"id":"1","name":"wnhyang","phoneNumber":"123456789"}'),c_cn.data('{"id":"1","name":"wnhyang","phoneNumber":"123456789"}')),a,b));
</chain>

测试Debug一下。

@Test
void test3() {

    Chain chain = FlowBus.getChain("R_C#1");
    log.info("chain: {}", chain);

    List<Node> nodesByChainId = FlowBus.getNodesByChainId("R_C#1");
    log.info("nodesByChainId: {}", nodesByChainId);

}

Chain的所有信息都是有的,不过这个就要求对于LiteFlowChainNode设计理解比较深了。

image

相比于直接获取Chain,获取Node看起来就简单多了,尤其对于我们在文档中常见的组件标签和组件参数,一览无遗。

image

总结

最后总结一下组件标签和组件参数在以上应用中所带来的差别,简称方式1和2了,下面👇

其实其带来的差别只有方式1多了一张条件表,所以差别就在这里,废话🤪

对于创建EL方式1需要多创建一条数据,方式2就避免这样的麻烦,直接放在EL就好了。

对于更新EL:如果当作全新的条件,方式1要先删除原有条件,再插入新的条件;如果要对比新老后再修改,那方式1就更麻烦了。而对于方式2,不管是怎样,直接覆盖就好了。

对于删除EL方式1需要删除所有的条件数据,方式2就不会有这些烦恼。

对于运行组件:方式1需要多查一步数据库,方式2更简单了使用获取组件方法就好,而且支持直接转类型,很方便。

从上面对比来看方式2爆杀啊!但还没对比完呢!

当需要反向查找时,方式2就有劣势了。反向查找指的是需要查询上面设计条件里的字段、指标、名单集的引用时,方式1可以方便的直接到数据库层查找引用的EL,而方式2就漫无目的了,不经其他设计的话,只能全部EL遍历了。

写在最后

拙作艰辛,字句心血,望诸君垂青,多予支持,不胜感激。


个人博客:无奈何杨(wnhyang)

个人语雀:wnhyang

共享语雀:在线知识共享

Github:wnhyang - Overview

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1956310.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

黑龙江等保测评如何做到既全面又高效?

在黑龙江省进行等保测评&#xff0c;必须在全面和高效之间寻求一个平衡点&#xff0c;以保证网络的安全性和可靠性。黑龙江等保测评怎样才能在二者之间发现黄金交汇点&#xff1f;下面&#xff0c;我们来揭开谜底。 精准定位&#xff0c;明确测评范围 首先&#xff0c;一个综…

LLaMA Factory添加新模型template的实战解析

大家好,我是herosunly。985院校硕士毕业,现担任算法研究员一职,热衷于大模型算法的研究与应用。曾担任百度千帆大模型比赛、BPAA算法大赛评委,编写微软OpenAI考试认证指导手册。曾获得阿里云天池比赛第一名,CCF比赛第二名,科大讯飞比赛第三名。授权多项发明专利。对机器学…

OpenCV facedetect 人脸检测官方示例项目配置

运行程序。该程序会自动打开摄像头&#xff0c;识别并定位摄像头前的人脸以及眼睛部位。 输入q或者Q&#xff0c;退出程序。 或进行文本中所包含的图片路径 或 单个图片进行检测&#xff0c;自行修改代码即可 配置环境项目&#xff0c;debug 解决error C4996: ‘fopen’: This…

python 可视化探索(一):基础图表

总结&#xff1a;本文为和鲸python 可视化探索训练营资料整理而来&#xff0c;加入了自己的理解&#xff08;by GPT4o&#xff09; 原作者&#xff1a;作者&#xff1a;大话数据分析&#xff0c;知乎、公众号【大话数据分析】主理人&#xff0c;5年数据分析经验&#xff0c;前…

六、Spring Boot - 上手篇(2)

&#x1f33b;&#x1f33b;目录 一、SpringBoot 构建RESTful API1.1 RESTful介绍1.2 RESTful接口设计1.3 用户实体bean创建 User1.4 创建Controller UserController1.5 Postman 测试RESTful 接口 二、SpringBoot 使用Swagger2 构建API文档2.1 Swagger2介绍2.2 SpringBoot 开启…

2020年 - 2022年 上市公司-劳动投资效率数据(原始数据、代码do文件、参考文献、最终结果)

劳动投资效率概述 劳动投资效率是衡量企业在人力资源管理方面效果和效率的关键指标。它关注企业在劳动力投资上的效益&#xff0c;即企业对于人力资源的投入与产出之间的比率。这一指标对于评估企业的人力资源管理策略及其对企业绩效的影响至关重要。 劳动投资效率的测度指标…

【Golang 面试 - 基础题】每日 5 题(八)

✍个人博客&#xff1a;Pandaconda-CSDN博客 &#x1f4e3;专栏地址&#xff1a;http://t.csdnimg.cn/UWz06 &#x1f4da;专栏简介&#xff1a;在这个专栏中&#xff0c;我将会分享 Golang 面试中常见的面试题给大家~ ❤️如果有收获的话&#xff0c;欢迎点赞&#x1f44d;收藏…

Redis快速入门基础

Redis入门 Redis是一个基于内存的 key-value 结构数据库。mysql是二维表的接口数据库 优点&#xff1a; 基于内存存储&#xff0c;读写性能高 适合存储热点数据(热点商品、资讯、新闻) 企业应用广泛 官网:https://redis.io 中文网:https://www.redis.net.cn/ Redis下载与…

带你学会Git必会操作

文章目录 带你学会Git必会操作1Git的安装2.Git基本操作2.1本地仓库的创建2.2配置本地仓库 3.认识一些Git的基本概念3.1操作流程&#xff1a; 4.一些使用场景4.1添加文件场景一4.2查看git文件4.3修改文件4.4Git版本回退4.5git撤销修改 5.分支管理5.1查看分支5.2创建本地分支5.3切…

功能实现——使用 OpenPDF 将 HTML 转换为 PDF,并将其上传到 FTP 服务器

目录 1.需求分析2.项目环境搭建3.将 HTML 转换为 PDF3.1.代码实现mail.htmlHtmlToPDFController.javaPDFConverterService.javaPDFConverterServiceImpl.java 3.2.测试3.3.注意事项 4.将生成的 PDF 上传到 FTP 服务器4.1.搭建 FTP 服务器4.2.配置文件4.3.代码实现FtpUtil.javaF…

PostgreSQL性能优化之体系结构

本文介绍 PostgreSQL 数据库的体系结构&#xff0c;包括实例结构&#xff08;进程与内存&#xff09;、存储结构&#xff08;物理与逻辑&#xff09;以及插件式存储引擎。 实例与数据库聚簇 PostgreSQL 使用典型的客户端/服务器&#xff08;Client/Server&#xff09;架构&am…

【Android】Fragment的添加

上一篇文章学到了碎片的创建与生命周期&#xff0c;接下来学习碎片的常用操作&#xff0c;其中会用到上一篇文章的三个碎片&#xff0c;就做一个简单的说明吧&#xff1a;LeftFragment&#xff08;包含一个按钮&#xff09;、RightFragment4&#xff08;以粉色为背景的文本&…

【人工智能】穿越科技迷雾:解锁人工智能、机器学习与深度学习的奥秘之旅

文章目录 前言一、人工智能1. 人工智能概述a.人工智能、机器学习和深度学习b.人工智能发展必备三要素c.小案例 2.人工智能发展历程a.人工智能的起源b.发展历程 3.人工智能的主要分支 二、机器学习1.机器学习工作流程a.什么是机器学习b.机器学习工作流程c.特征工程 2.机器学习算…

动手学深度学习V2每日笔记(模型选择+过拟合和欠拟合)

本文主要参考沐神的视频教程 https://www.bilibili.com/video/BV1K64y1Q7wu/?spm_id_from333.788.recommend_more_video.0&vd_sourcec7bfc6ce0ea0cbe43aa288ba2713e56d 文档教程 https://zh-v2.d2l.ai/ 本文的主要内容对沐神提供的代码中个人不太理解的内容进行笔记记录&a…

Java之归并排序

归并排序 归并排序(Merge Sort)算法&#xff0c;使用的是分治思想。分治&#xff0c;顾名思义&#xff0c;就是分而治之&#xff0c;将一个大问题分解成小的子问题来解决。小的子问题解决了&#xff0c;大问题也就解决了。 核心源码: mergeSort(m->n) merge(mergeSort(m-&g…

对于500强企业来说,有比FTP好用的传输工具吗?

500强企业在进行文件传输时&#xff0c;会根据其业务需求、数据安全性要求以及技术架构的不同&#xff0c;选择多种文件传输方式&#xff0c;最常见的便是FTP。然而FTP在使用却存在较多的问题&#xff1a; 1&#xff09;安全性问题 缺乏安全策略&#xff1a;FTP本身不提供加密…

「百年孤独」

引言 《百年孤独》是加西亚马尔克斯创作的魔幻现实主义经典小说&#xff0c;刻画了布恩迪亚家族七代人的跌宕起伏和马孔多小镇的兴衰。是拉丁美洲文学中一部不朽的杰作。 故事概述 小说从布恩迪亚家族的始祖荷塞阿卡迪奥布恩迪亚和妻子乌尔苏拉开始&#xff0c;讲述了七代人…

DeiT III(Meta)论文解读

paper&#xff1a;DeiT III: Revenge of the ViT official implementation&#xff1a;https://github.com/facebookresearch/deit 出发点 本研究旨在重新审视ViT的监督训练方法&#xff0c;并提出一种基于ResNet-50训练方法的简化版新训练策略。与现有的自动数据增强方法不…

C++从入门到起飞之——友元内部类匿名对象对象拷贝时的编译器优化 全方位剖析!

&#x1f308;个人主页&#xff1a;秋风起&#xff0c;再归来~&#x1f525;系列专栏&#xff1a;C从入门到起飞 &#x1f516;克心守己&#xff0c;律己则安 目录 1、友元 2、内部类 3、 匿名对象 4、对象拷⻉时的编译器优化 5、完结散花 1、友元 • 友元提供…

springAOP理解及事务

AOP&#xff1a; springAOP是什么&#xff1a; AOP&#xff1a;Aspect Oriented Programming&#xff08;面向切面编程、面向方面编程&#xff09;&#xff0c;其实就是面向特定方法编程。 使用场景&#xff1a; 比如你想统计业务中每个方法的执行耗时&#xff0c;那我们最…