当前位置: 首页 > news >正文

【RuleUtil】适用于全业务场景的规则匹配快速开发工具

一、RuleUtil 开发背景

1.1 越来越多,越来越复杂的业务规则

1、规则的应用场景多
2、规则配置的参数类型多(ID、数值、文本、日期等等)
3、规则的参数条件多(大于、小于、等于、包含、不包含、区间等等)
4、规则的结构复杂(父规则、子规则、子场景、与或、优先级、命中数量限制)

1.2 目前规则匹配实现方式,以及对应痛点

在这里插入图片描述

现有实现方式使用了许多 if-else 对匹配参数和条件运算符进行判断,结构复杂,可读性较差

痛点一:规则条件组合方式多样,普通实现代码结构容易混乱;
痛点二:同一规则在多个应用场景的复用性较差;
痛点三:每次需求迭代都要改动核心匹配逻辑,效率低,测试成本高。

二、RuleUtil 如何解决痛点

2.1 核心思想

1、通过设计 Rule 结构和四个字段类型,对业务规则进行高度抽象和统一,提高代码可复用性

2、研发仅需实现从业务规则到 Rule 的转换方法,以及定义匹配参数结构即可,无需再编写复杂的匹配逻辑,交给 match 方法通通搞定

RuleUtil 核心规则类(Rule)

/*** 核心规则类*/
public class RuleV2 {// 主规则idprivate String id;// 子规则idprivate String subId;// 规则参数private Object param;// 规则字段private List<Field> fields;public static class Field {// 字段名private String name;// 字段键private String key;// 字段类型private RuleFieldTypeEnum type;// 配置条件private RuleFieldConditionEnum condition;// 字段值private Object value;}
}

RuleUtil 核心匹配方法(match)

/*** 规则工具类V2*/
@Slf4j
public class RuleUtilV2 {/*** 规则匹配** @param param 待匹配参数* @param rules 待匹配规则池* @return 命中规则池*/public static List<RuleV2> match(Object param, List<RuleV2> rules) {return match(param, rules, rules.size());}/*** 规则匹配** @param param      待匹配参数* @param rules      待匹配规则池* @param matchLimit 命中规则数量限制* @return 命中规则池*/public static List<RuleV2> match(Object param, List<RuleV2> rules, Integer matchLimit) {List<RuleV2> matchRules = new ArrayList<>();if (Objects.isNull(param) || CollUtil.isEmpty(rules) || Objects.isNull(matchLimit) || matchLimit <= NumberConst.ZERO) {return matchRules;}for (RuleV2 rule : rules) {if (CollUtil.isEmpty(rule.getFields())) {continue;}boolean isMatch = true;for (RuleV2.Field field : rule.getFields()) {if (StrUtil.isBlank(field.getName()) || Objects.isNull(field.getType()) || Objects.isNull(field.getCondition()) || Objects.isNull(field.getValue())) {isMatch = false;log.info("RuleUtil::match 规则匹配存在空字段 规则id:{} 规则字段:{}", rule.getId(), field);break;}// 根据规则字段名获取参数字段值Object paramFieldValueObj = ReflectUtil.getFieldValue(param, field.getName());// 获取规则字段值Object ruleFieldValueObj = field.getValue();if (RuleFieldTypeEnum.NORMAL.equals(field.getType())) {// 普通属性Set<String> paramFieldValues = collConvertToSet(paramFieldValueObj);Set<String> ruleFieldValues = collConvertToSet(ruleFieldValueObj);if (Set.of(RuleFieldConditionEnum.LT, RuleFieldConditionEnum.LE, RuleFieldConditionEnum.GT, RuleFieldConditionEnum.GE).contains(field.getCondition())) {isMatch = false;break;}if (RuleFieldConditionEnum.EQ.equals(field.getCondition()) && !Objects.equals(paramFieldValues, ruleFieldValues)) {// 等于isMatch = false;break;}if (RuleFieldConditionEnum.NE.equals(field.getCondition()) && Objects.equals(paramFieldValues, ruleFieldValues)) {// 不等于isMatch = false;break;}if (RuleFieldConditionEnum.IN.equals(field.getCondition()) && !CollUtil.containsAny(paramFieldValues, ruleFieldValues)) {// 包含isMatch = false;break;}if (RuleFieldConditionEnum.NIN.equals(field.getCondition()) && CollUtil.containsAny(paramFieldValues, ruleFieldValues)) {// 不包含isMatch = false;break;}}if (RuleFieldTypeEnum.NUMBER.equals(field.getType())) {// 数值属性if (Set.of(RuleFieldConditionEnum.IN, RuleFieldConditionEnum.NIN).contains(field.getCondition())) {Set<String> paramFieldValues = collConvertToSet(paramFieldValueObj);Set<String> ruleFieldValues = collConvertToSet(ruleFieldValueObj);if (RuleFieldConditionEnum.IN.equals(field.getCondition()) && !CollUtil.containsAny(paramFieldValues, ruleFieldValues)) {// 包含isMatch = false;break;}if (RuleFieldConditionEnum.NIN.equals(field.getCondition()) && CollUtil.containsAny(paramFieldValues, ruleFieldValues)) {// 不包含isMatch = false;break;}continue;}String paramFieldValue = String.valueOf(paramFieldValueObj);if (!NumberUtil.isNumber(paramFieldValue) || !NumberUtil.isNumber(String.valueOf(ruleFieldValueObj))) {isMatch = false;log.info("RuleUtil::match 规则匹配存在非数值字段 规则id:{} 规则字段值:{} 参数字段值:{}", rule.getId(), field.getValue(), paramFieldValue);break;}double paramFieldDouble = NumberUtil.parseDouble(paramFieldValue);double ruleFieldDouble = NumberUtil.parseDouble(String.valueOf(ruleFieldValueObj));// 数值属性if (RuleFieldConditionEnum.EQ.equals(field.getCondition()) && paramFieldDouble != ruleFieldDouble) {// 等于isMatch = false;break;}if (RuleFieldConditionEnum.NE.equals(field.getCondition()) && paramFieldDouble == ruleFieldDouble) {// 不等于isMatch = false;break;}if (RuleFieldConditionEnum.LT.equals(field.getCondition()) && paramFieldDouble >= ruleFieldDouble) {// 小于isMatch = false;break;}if (RuleFieldConditionEnum.LE.equals(field.getCondition()) && paramFieldDouble > ruleFieldDouble) {// 小于等于isMatch = false;break;}if (RuleFieldConditionEnum.GT.equals(field.getCondition()) && paramFieldDouble <= ruleFieldDouble) {// 大于isMatch = false;break;}if (RuleFieldConditionEnum.GE.equals(field.getCondition()) && paramFieldDouble < ruleFieldDouble) {// 大于等于isMatch = false;break;}}if (RuleFieldTypeEnum.NORMAL_KV.equals(field.getType()) && StrUtil.isNotBlank(field.getKey())) {// 普通键值对属性if (Set.of(RuleFieldConditionEnum.LT, RuleFieldConditionEnum.LE, RuleFieldConditionEnum.GT, RuleFieldConditionEnum.GE).contains(field.getCondition())) {isMatch = false;break;}Set<String> paramFieldValues = mapConvertToSet(paramFieldValueObj, field.getKey());Set<String> ruleFieldValues = collConvertToSet(ruleFieldValueObj);if (RuleFieldConditionEnum.EQ.equals(field.getCondition()) && !Objects.equals(paramFieldValues, ruleFieldValues)) {// 等于isMatch = false;break;}if (RuleFieldConditionEnum.NE.equals(field.getCondition()) && Objects.equals(paramFieldValues, ruleFieldValues)) {// 不等于isMatch = false;break;}if (RuleFieldConditionEnum.IN.equals(field.getCondition()) && !CollUtil.containsAny(paramFieldValues, ruleFieldValues)) {// 包含isMatch = false;break;}if (RuleFieldConditionEnum.NIN.equals(field.getCondition()) && CollUtil.containsAny(paramFieldValues, ruleFieldValues)) {// 不包含isMatch = false;break;}}if (RuleFieldTypeEnum.NUMBER_KV.equals(field.getType()) && StrUtil.isNotBlank(field.getKey())) {// 数值键值对属性if (Set.of(RuleFieldConditionEnum.IN, RuleFieldConditionEnum.NIN).contains(field.getCondition())) {Set<Double> paramFieldDoubles = mapConvertToSet(paramFieldValueObj, field.getKey()).stream().filter(NumberUtil::isNumber).map(NumberUtil::parseDouble).collect(Collectors.toSet());Set<Double> ruleFieldDoubles = collConvertToSet(ruleFieldValueObj).stream().filter(NumberUtil::isNumber).map(NumberUtil::parseDouble).collect(Collectors.toSet());if (RuleFieldConditionEnum.IN.equals(field.getCondition()) && !CollUtil.containsAny(paramFieldDoubles, ruleFieldDoubles)) {// 包含isMatch = false;break;}if (RuleFieldConditionEnum.NIN.equals(field.getCondition()) && CollUtil.containsAny(paramFieldDoubles, ruleFieldDoubles)) {// 不包含isMatch = false;break;}continue;}String paramFieldValue = IterUtil.getFirst(mapConvertToSet(paramFieldValueObj, field.getKey()));if (!NumberUtil.isNumber(paramFieldValue) || !NumberUtil.isNumber(String.valueOf(ruleFieldValueObj))) {isMatch = false;log.info("RuleUtil::match 规则匹配存在非数值字段 规则id:{} 规则字段值:{} 参数字段值:{}", rule.getId(), field.getValue(), paramFieldValue);break;}double paramFieldDouble = NumberUtil.parseDouble(paramFieldValue);double ruleFieldDouble = NumberUtil.parseDouble(String.valueOf(ruleFieldValueObj));// 数值属性if (RuleFieldConditionEnum.EQ.equals(field.getCondition()) && paramFieldDouble != ruleFieldDouble) {// 等于isMatch = false;break;}if (RuleFieldConditionEnum.NE.equals(field.getCondition()) && paramFieldDouble == ruleFieldDouble) {// 不等于isMatch = false;break;}if (RuleFieldConditionEnum.LT.equals(field.getCondition()) && paramFieldDouble >= ruleFieldDouble) {// 小于isMatch = false;break;}if (RuleFieldConditionEnum.LE.equals(field.getCondition()) && paramFieldDouble > ruleFieldDouble) {// 小于等于isMatch = false;break;}if (RuleFieldConditionEnum.GT.equals(field.getCondition()) && paramFieldDouble <= ruleFieldDouble) {// 大于isMatch = false;break;}if (RuleFieldConditionEnum.GE.equals(field.getCondition()) && paramFieldDouble < ruleFieldDouble) {// 大于等于isMatch = false;break;}}}if (isMatch) {matchRules.add(rule);if (matchRules.size() >= matchLimit) {return matchRules;}}}return matchRules;}/*** 把集合类型对象转换为set** @param obj 对象* @return set*/private static Set<String> collConvertToSet(Object obj) {Set<String> set = new HashSet<>();if (Objects.isNull(obj)) {return set;}if (obj instanceof Collection<?> paramFieldColl) {for (Object subObj : paramFieldColl) {set.add(String.valueOf(subObj));}} else {set.add(String.valueOf(obj));}return set;}/*** 把map类型对象转换为set** @param obj 对象* @param key 键* @return set*/private static Set<String> mapConvertToSet(Object obj, String key) {Set<String> set = new HashSet<>();if (Objects.isNull(obj) || StrUtil.isBlank(key) || !(obj instanceof Map<?, ?> map)) {return set;}for (Map.Entry<?, ?> entry : map.entrySet()) {if (key.equals(String.valueOf(entry.getKey()))) {if (entry.getValue() instanceof Collection<?> paramFieldColl) {for (Object subObj : paramFieldColl) {set.add(String.valueOf(subObj));}} else {set.add(String.valueOf(entry.getValue()));}break;}}return set;}
}

RuleUtil 把业务规则配置的字段类型,归纳为以下四种(代码枚举:RuleFieldTypeEnum),各个类型支持的场景如下:

/*** 规则项字段类型枚举*/
@Getter
@AllArgsConstructor
public enum RuleFieldTypeEnum {UNKNOWN(0, ""),NORMAL(1, "普通"),NUMBER(2, "数值"),NORMAL_KV(3, "普通键值对"),NUMBER_KV(4, "数值键值对"),;private final Integer code;private final String desc;
}

在这里插入图片描述

2.2 应用场景

应用场景一:列举以下四个业务规则示例,分别对应上面四种字段类型:

在这里插入图片描述
第一步:实现 convert 方法,将业务规则转换为 Rule 实体列表

根据示例转换后的 Rule 实体 json 结构如下:

在这里插入图片描述

第二步:根据【字段名-Field.name】【字段类型-Field.type】自行定义匹配 Rule 所需要的参数实体类。根据示例定义的 Param 实体结构如下:

/*** 规则匹配参数*/
class Param {// 品类组合id(普通)private List<Long> combIds;// 库存数量(数值)private Long stock;// 属性id-属性值id(普通键值对)private Map<Long, List<Long>> attrIdToAttrValIdMap;// 成分属性值id-属性数值(数值键值对)private Map<Long, Double> componentAttrValIdToValMap;
}

第三步:调用 RuleUtile.match 方法,得到 Param 命中的 Rule 列表。

根据示例 Param 创建一个待匹配对象

{"combIds": ["1-男装","3-上装"],"stock": 15,"attrIdToAttrValIdMap": {"10-适合类型": ["11-宽松","13-修身"],"20-织造方式": ["21-牛仔"]},"componentAttrValIdToValMap": {"10-棉": 15.0,"20-尼龙": 85.0}
}

调用 RuleUtile.match 方法如下,方法返回了能够命中的 Rule

在这里插入图片描述

match 方法使用反射,根据【Rule.name】获取 Param 对应字段的值进行条件匹配

应用场景二:假设业务规则发生变化,迭代为嵌套【且】【或】关系的业务规则

在这里插入图片描述
第一步:更新 convert 方法,将新的业务规则转换为 Rule 实体列表

根据示例转换后的 Rule 实体 json 结构如下:

在这里插入图片描述

⚠️注意:多个【Rule】之间是【或】关系,多个【Rule.Field】之间是【且】关系

第二步:定义的 Param 实体结构不变

第三步:调用 RuleUtile.match 方法如下,方法返回了能够命中的 Rule

在这里插入图片描述
场景一、场景二的完整单元测试用例代码:

public class RuleUtilTest {@Testpublic void demo01() {// 规则匹配参数TestParam param = new TestParam();param.setCombIds(List.of(1L, 3L));param.setStock(15L);param.setAttrIdToAttrValIdMap(Map.of(10L, List.of(11L, 13L), 20L, List.of(21L)));param.setComponentAttrValIdToValMap(Map.of(10L, 15.0, 20L, 85.0));// 品类组合RuleV2.Field fieldCombIds = new RuleV2.Field();fieldCombIds.setName(LambdaUtil.getFieldName(TestParam::getCombIds));fieldCombIds.setType(RuleFieldTypeEnum.NORMAL); // 普通fieldCombIds.setCondition(RuleFieldConditionEnum.IN); // 包含fieldCombIds.setValue(List.of(1L, 2L));RuleV2 rule01 = new RuleV2();rule01.setId("rule01");rule01.setFields(List.of(fieldCombIds));// 库存数量RuleV2.Field fieldStockGt = new RuleV2.Field();fieldStockGt.setName(LambdaUtil.getFieldName(TestParam::getStock));fieldStockGt.setType(RuleFieldTypeEnum.NUMBER); // 数值fieldStockGt.setCondition(RuleFieldConditionEnum.GT); // 大于fieldStockGt.setValue("10");RuleV2.Field fieldStockLt = new RuleV2.Field();fieldStockLt.setName(LambdaUtil.getFieldName(TestParam::getStock));fieldStockLt.setType(RuleFieldTypeEnum.NUMBER);fieldStockLt.setCondition(RuleFieldConditionEnum.LT); // 小于fieldStockLt.setValue("20");RuleV2 rule02 = new RuleV2();rule02.setId("rule02");rule02.setFields(List.of(fieldStockGt, fieldStockLt));// 属性id-属性值id(普通键值对)RuleV2.Field fieldAttrIdToAttrValIdMap = new RuleV2.Field();fieldAttrIdToAttrValIdMap.setName(LambdaUtil.getFieldName(TestParam::getAttrIdToAttrValIdMap));fieldAttrIdToAttrValIdMap.setKey("10");fieldAttrIdToAttrValIdMap.setType(RuleFieldTypeEnum.NORMAL_KV);fieldAttrIdToAttrValIdMap.setCondition(RuleFieldConditionEnum.IN); // 包含fieldAttrIdToAttrValIdMap.setValue(List.of(11L, 12L));RuleV2 rule03 = new RuleV2();rule03.setId("rule03");rule03.setFields(List.of(fieldAttrIdToAttrValIdMap));// 成分属性值id-属性数值RuleV2.Field fieldComponentAttrValIdToValMapGt = new RuleV2.Field();fieldComponentAttrValIdToValMapGt.setName(LambdaUtil.getFieldName(TestParam::getComponentAttrValIdToValMap));fieldComponentAttrValIdToValMapGt.setKey("10");fieldComponentAttrValIdToValMapGt.setType(RuleFieldTypeEnum.NUMBER_KV); // 数值键值对fieldComponentAttrValIdToValMapGt.setCondition(RuleFieldConditionEnum.GT); // 大于fieldComponentAttrValIdToValMapGt.setValue("10");RuleV2.Field fieldComponentAttrValIdToValMapLt = new RuleV2.Field();fieldComponentAttrValIdToValMapLt.setName(LambdaUtil.getFieldName(TestParam::getComponentAttrValIdToValMap));fieldComponentAttrValIdToValMapLt.setKey("10");fieldComponentAttrValIdToValMapLt.setType(RuleFieldTypeEnum.NUMBER_KV);fieldComponentAttrValIdToValMapLt.setCondition(RuleFieldConditionEnum.LT); // 小于fieldComponentAttrValIdToValMapLt.setValue("20");RuleV2 rule04 = new RuleV2();rule04.setId("rule04");rule04.setFields(List.of(fieldComponentAttrValIdToValMapGt, fieldComponentAttrValIdToValMapLt));// 调用 RuleUtil 规则匹配方法List<RuleV2> matchResults = RuleUtilV2.match(param, List.of(rule01, rule02, rule03, rule04));// 命中四种字段类型的规则assertEquals(List.of(rule01, rule02, rule03, rule04), matchResults);}@Testpublic void demo02() {// 规则匹配参数TestParam param = new TestParam();param.setCombIds(List.of(1L, 3L));param.setStock(15L);param.setAttrIdToAttrValIdMap(Map.of(10L, List.of(11L, 13L), 20L, List.of(21L)));param.setComponentAttrValIdToValMap(Map.of(10L, 15.0, 20L, 85.0));// 品类组合RuleV2.Field fieldCombIds = new RuleV2.Field();fieldCombIds.setName(LambdaUtil.getFieldName(TestParam::getCombIds));fieldCombIds.setType(RuleFieldTypeEnum.NORMAL); // 普通fieldCombIds.setCondition(RuleFieldConditionEnum.IN); // 包含fieldCombIds.setValue(List.of(1L, 2L));// 库存数量RuleV2.Field fieldStockGt = new RuleV2.Field();fieldStockGt.setName(LambdaUtil.getFieldName(TestParam::getStock));fieldStockGt.setType(RuleFieldTypeEnum.NUMBER); // 数值fieldStockGt.setCondition(RuleFieldConditionEnum.GT); // 大于fieldStockGt.setValue("10");RuleV2.Field fieldStockLt = new RuleV2.Field();fieldStockLt.setName(LambdaUtil.getFieldName(TestParam::getStock));fieldStockLt.setType(RuleFieldTypeEnum.NUMBER);fieldStockLt.setCondition(RuleFieldConditionEnum.LT); // 小于fieldStockLt.setValue("20");RuleV2 prule01_rule01 = new RuleV2();prule01_rule01.setId("prule01");prule01_rule01.setSubId("rule01");prule01_rule01.setFields(List.of(fieldCombIds, fieldStockGt, fieldStockLt));// 属性id-属性值id(普通键值对)RuleV2.Field fieldAttrIdToAttrValIdMap = new RuleV2.Field();fieldAttrIdToAttrValIdMap.setName(LambdaUtil.getFieldName(TestParam::getAttrIdToAttrValIdMap));fieldAttrIdToAttrValIdMap.setKey("10");fieldAttrIdToAttrValIdMap.setType(RuleFieldTypeEnum.NORMAL_KV);fieldAttrIdToAttrValIdMap.setCondition(RuleFieldConditionEnum.IN); // 包含fieldAttrIdToAttrValIdMap.setValue(List.of(11L, 12L));// 成分属性值id-属性数值RuleV2.Field fieldComponentAttrValIdToValMapGt = new RuleV2.Field();fieldComponentAttrValIdToValMapGt.setName(LambdaUtil.getFieldName(TestParam::getComponentAttrValIdToValMap));fieldComponentAttrValIdToValMapGt.setKey("10");fieldComponentAttrValIdToValMapGt.setType(RuleFieldTypeEnum.NUMBER_KV); // 数值键值对fieldComponentAttrValIdToValMapGt.setCondition(RuleFieldConditionEnum.GT); // 大于fieldComponentAttrValIdToValMapGt.setValue("10");RuleV2.Field fieldComponentAttrValIdToValMapLt = new RuleV2.Field();fieldComponentAttrValIdToValMapLt.setName(LambdaUtil.getFieldName(TestParam::getComponentAttrValIdToValMap));fieldComponentAttrValIdToValMapLt.setKey("10");fieldComponentAttrValIdToValMapLt.setType(RuleFieldTypeEnum.NUMBER_KV);fieldComponentAttrValIdToValMapLt.setCondition(RuleFieldConditionEnum.LT); // 小于fieldComponentAttrValIdToValMapLt.setValue("20");RuleV2 prule01_rule02 = new RuleV2();prule01_rule02.setId("prule01");prule01_rule02.setSubId("rule02");prule01_rule02.setFields(List.of(fieldAttrIdToAttrValIdMap, fieldComponentAttrValIdToValMapGt, fieldComponentAttrValIdToValMapLt));// 调用 RuleUtil 规则匹配方法List<RuleV2> matchResults = RuleUtilV2.match(param, List.of(prule01_rule01, prule01_rule02));// 命中组合字段的规则assertEquals(List.of(prule01_rule01, prule01_rule02), matchResults);}
}

三、RuleUtil 实践技巧

1、Rule 类的 param 字段可以 set 任何规则信息(如子规则配置的图片 id、枚举值等等);

2、若业务规则存在不同优先级,可以通过对 Rule 列表进行排序实现优先级匹配,排序靠前的优先匹配;

3、若存在命中规则的数量限制(比如优先或随机选择命中的第一个规则),可以通过入参 matchLimit 进行控制;

4、若业务场景对业务规则的更新不要求实时,可以对转换后的 Rule 数组进行缓存,降低数据库查询和转换次数,提高性能;

5、后续将逐步完善 RuleUtil 单元测试用例,保证核心匹配逻辑准确无误。

使用 VuePress + Github 30分钟搭建属于自己的AI网站

相关文章:

  • Post-Processing PropertySource instance详解 和 BeanFactoryPostProcessor详解
  • 信息系统项目管理师_第十三章 项目干系人管理
  • MySQL 双主复制架构入门
  • Sentinel数据S2_SR_HARMONIZED连续云掩膜+中位数合成
  • JDK安装超详细步骤
  • Java中实现单例模式的多种方法:原理、实践与优化
  • 【Git】fork 和 branch 的区别
  • 复盘2025北京副中心马拉松赛
  • 大模型面经 | 春招、秋招算法面试常考八股文附答案(四)
  • IDEA 创建Maven 工程(图文)
  • MCP Host、MCP Client、MCP Server全流程实战
  • 【安装部署】Linux下最简单的 pytorch3d 安装
  • 查看Spring Boot项目所有配置信息的几种方法,包括 Actuator端点、日志输出、代码级获取 等方式,附带详细步骤和示例
  • 2025年特种作业操作证考试题库及答案(登高架设作业)
  • Ubuntu数据连接访问崩溃问题
  • Electron主进程渲染进程间通信的方式
  • UWB与GPS技术融合的室内外无缝定位方案
  • 【MCP Node.js SDK 全栈进阶指南】利用TypeScript-SDK打造高效MCP应用
  • 程序生成随机数
  • 4.22学习总结
  • 上海市政府常务会议研究抓好稳就业稳企业稳市场稳预期工作,让企业感受温度
  • 商务部:入境消费增长潜力巨大,离境退税有助降低境外旅客购物成本
  • 我驻美使馆:中美并没有就关税问题磋商谈判,更谈不上达成协议
  • 从中央政治局会议看经济工作着力点:以高质量发展的确定性应对外部不确定性
  • 交警不在就闯红灯?上海公安用科技手段查处非机动车违法
  • 一季度公募管理规模出炉:44家实现增长,4家规模环比翻倍