【后端】使用 Easy Rules 构建灵活的业务规则引擎 — Spring Boot 集成实践 - 实践

news/2025/12/6 13:08:01/文章来源:https://www.cnblogs.com/gccbuaa/p/19315528

目录

  • 引言
  • 一、Easy Rules 基础知识
    • 1.1 Easy Rules 是什么?
    • 1.2 核心概念介绍
    • 1.3 解耦业务逻辑的优势
  • 二、源码结构与示例分析
    • 示例一:基础用法展示
      • 核心代码实现
    • 示例二:高级集成模式
      • 关键组件说明
      • 核心实现代码
  • 三、关键代码解析
    • 3.1 AutoCreateFactory 工厂类详解
    • 3.2 核心功能
      • 3.2.1 Sign 标签分类机制
      • 3.2.2 工作原理
    • 3.3 使用工厂类创建规则的优势
    • 3.4 关键代码
  • 四、运行效果
  • 五、总结
  • 六、福利资源


引言

在现代软件开发过程中,业务规则往往复杂且多变。为了应对这些挑战,规则引擎成为了一种有效的解决方案。本文将介绍如何使用 Easy Rules 框架,在 Spring Boot 项目中实现一个可扩展、易维护的业务规则管理系统。


一、Easy Rules 基础知识

1.1 Easy Rules 是什么?

Easy Rules 是一个简单而强大的 Java 规则引擎,它允许你定义和执行一系列业务规则。其核心思想是将复杂的业务逻辑从代码中分离出来,使得规则可以独立于应用程序代码进行管理和修改。

1.2 核心概念介绍

1.3 解耦业务逻辑的优势

通过引入规则引擎,我们可以实现以下目标:

  • 将业务规则与程序代码解耦
  • 提高系统的灵活性和可维护性
  • 支持动态加载和更新规则
  • 易于测试和调试

二、源码结构与示例分析

本项目提供了两个示例来演示 Easy Rules 的不同应用场景:

示例一:基础用法展示

第一个示例是一个简单的规则应用案例,展示了如何使用 Easy Rules 定义基本的规则、设置条件和指定优先级。

核心代码实现

// Priority100RequestService.java
@Rule(name = "执行STEP_1", description = "执行第一步处理")
public class Priority100RequestService {
@Condition
public boolean condition() {
return true;
}
@Action
public void action(Facts facts) {
System.out.println("规则引擎执行STEP_1:开始处理");
String step1Input = (String) facts.get("STEP_1_INPUT");
System.out.println("STEP_1入参:" + step1Input);
// 设置STEP_2的输入参数
facts.put("STEP_2_INPUT", "来自STEP_1的数据");
System.out.println("规则引擎执行STEP_1:结束处理");
}
@Priority
public int priority() {
return 100;
}
}
// RuleController.java
@RestController
@RequestMapping("/api/rules")
public class RuleController {
@GetMapping("/execute")
public String executeRules() {
// 创建规则引擎参数
RulesEngineParameters parameters = new RulesEngineParameters()
.skipOnFirstAppliedRule(false);
// 创建规则引擎
RulesEngine rulesEngine = new DefaultRulesEngine(parameters);
// 创建规则集
Rules rules = new Rules();
rules.register(new Priority100RequestService());
rules.register(new Priority101RequestService());
// 创建事实
Facts facts = new Facts();
facts.put("STEP_1_INPUT", "初始输入数据");
// 执行规则
rulesEngine.fire(rules, facts);
// 返回最终结果
return facts.get("FINAL_OUTPUT");
}
}

示例二:高级集成模式

第二个示例更加贴近实际生产环境的需求,采用了工厂方法设计模式来创建规则实例,并结合 Spring 进行依赖注入管理。

关键组件说明

组件描述
AppCenter应用中心,负责协调整个规则执行流程,初始化和管理
AutoCreateFactory工厂类,根据配置自动创建规则对象实例,支持注解扫描
RobotAdapter机器人适配器,封装多个流程步骤,作为规则执行的入口点
ProcessAdapter流程适配器,组织多个具体规则,管理规则引擎和规则集
AdapterRule所有自定义规则需实现此接口
AutoCreateFactory工厂类,基于@AutoCreate注解自动扫描和创建规则实例,支持按类型和标记分类进行实例化管理,实现规则的动态加载和配置
RefUtils反射工具类,负责包扫描和类加载,支持从指定包路径下查找带有特定注解的类,为AutoCreateFactory提供底层类扫描支持

核心实现代码

规则接口:

public interface AdapterRule {
}

规则1:

// Priority100RequestService.java - 带注解的规则实现
@AutoCreate(value = "MyAppCode", sign = {"STEP_1", "DEFAULT"}, isSingleton = true)
@Rule(name = "执行STEP_1", description = "执行STEP_1")
public class Priority100RequestService implements AdapterRule {
@Condition
public boolean condition() {
return true;
}
@Action
public void action(Facts facts) {
System.out.println("执行STEP_1:开始");
String step1Input = (String) facts.get("STEP_1_INPUT");
System.out.println("STEP_1入参:" + step1Input);
facts.put("STEP_2_INPUT", "STEP_1出参=STEP_2入参");
System.out.println("执行STEP_1:结束");
}
@Priority
public int priority() {
return 100;
}
}

规则2:

@AutoCreate(value = "MyAppCode", sign = {"STEP_1", "DEFAULT"}, isSingleton = true)
@Rule(name = "执行STEP_1_1", description = "执行STEP_1_1")
public class Priority101RequestService implements AdapterRule {
@Condition
public boolean condition() {
return true;
}
@Action
public void action(Facts facts) {
System.out.println("执行STEP_1_1:开始");
String step2Input = (String) facts.get("STEP_2_INPUT");
System.out.println("STEP_2入参:" + step2Input);
facts.put("FINAL_OUTPUT", "STEP_2出参");
System.out.println("执行STEP_1_1:结束");
}
@Priority
public int priority() {
return 101;
}
}

规则应用中心:

// AppCenter.java - 应用中心
@Service
public class AppCenter {
private static final Map<String, RobotAdapter> APP_MAP = new ConcurrentHashMap<>();@PostConstructpublic void init() {APP_MAP.put("MyAppCode", refresh());}private static final RulesEngineParameters rulesEngineParameters = new RulesEngineParameters().skipOnFirstAppliedRule(false).skipOnFirstFailedRule(true).skipOnFirstNonTriggeredRule(true);private static final RulesEngine rulesEngine = new DefaultRulesEngine(rulesEngineParameters);public RobotAdapter refresh() {RobotAdapter robotAdapter = new RobotAdapter();for(ProcessAdapterEnum adapterEnum : ProcessAdapterEnum.values()) {Rules rules = new Rules();String robotAdapterTemplateSign = adapterEnum.getCode();String methodSign = "DEFAULT";List<AdapterRule> adapterRules = AutoCreateFactory.getInstanceList(AdapterRule.class, "MyAppCode", methodSign, robotAdapterTemplateSign);rules.register(adapterRules.toArray());ProcessAdapter processAdapter = ProcessAdapter.builder().appCode("appCode").rulesEngine(rulesEngine).rules(rules).build();robotAdapter.getAdapters().put(adapterEnum, processAdapter);}return robotAdapter;}}

规则适配器:

@Service
@Getter
public class RobotAdapter {@Autowiredprivate AppCenter appCenter;private final Map adapters = new EnumMap<>(ProcessAdapterEnum.class);public  U adapter(ProcessAdapterEnum processAdapterService, T param) {ProcessAdapter processAdapter = this.getAdapters().get(ProcessAdapterEnum.STEP_1);if(processAdapter == null) {throw new IllegalArgumentException("未注册的ProcessAdapterEnum:" + processAdapterService);}return processAdapter.adapter(param);}public  U adapter(ProcessAdapterEnum processAdapterService, T param, U returnType) {U processAdapter = adapter(processAdapterService, param);return processAdapter == null ?  returnType : processAdapter;}@Overridepublic String toString() {return JSON.toJSONString(this);}
}

流程适配器:

@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ProcessAdapter {private String appCode;private String businessLineId;private String businessLineName;private String robotId;private RulesEngine rulesEngine;private Rules rules;public  U adapter(T param) {Facts facts = new Facts();facts.put("STEP_1_INPUT", "STEP_1入参");rulesEngine.fire(rules, facts);System.out.println("执行结果:" + facts.get("FINAL_OUTPUT"));return facts.get("FINAL_OUTPUT");}
}

控制器:

// AdapterController.java - 控制器调用
@RestController
@RequestMapping("/demos/adapter")
public class AdapterController {
@Autowired
private AppCenter appCenter;
@RequestMapping("/execute")
public String execute() {
RobotAdapter robotAdapter = appCenter.getRobotAdapter("MyAppCode");
robotAdapter.adapter(ProcessAdapterEnum.STEP_1, "入参");
return "";
}
}

三、关键代码解析

3.1 AutoCreateFactory 工厂类详解

AutoCreateFactory 是一个通用的自动创建工厂类,它提供了一种灵活的机制来动态创建和管理带有 @AutoCreate 注解的类实例。

3.2 核心功能

  • 动态类扫描: 利用 RefUtils 工具类扫描指定包路径下所有带有 @AutoCreate 注解的类
  • 实例创建管理: 根据类型 value 来创建和管理类实例
  • 灵活的分类机制: 支持通过 sign 关键词对实例进行分类和筛选

3.2.1 Sign 标签分类机制

@AutoCreate 属性提供了强大的分类功能,可以理解为给类实例"打标签":

@AutoCreate(value = "MyAppCode", sign = {"STEP_1", "DEFAULT"}, isSingleton = true)

在上面的例子中,** Priority100RequestService** 类被打上了两个标签:"STEP_1""DEFAULT"

3.2.2 工作原理

  1. 注册阶段: AutoCreateFactory 扫描所有带有 @AutoCreate 注解的类,并根据 valuesign 属性建立映射关系
  2. 查询阶段: 当调用 getInstanceList 方法时,可以通过指定 typesigns 参数来筛选符合条件的实例
   List adapterRules = AutoCreateFactory.getInstanceList(AdapterRule.class,"MyAppCode",           // 对应 @AutoCreate 的 value"DEFAULT",             // 对应 @AutoCreate 的 signrobotAdapterTemplateSign  // 另一个维度的 sign 筛选);
  1. 匹配逻辑: 工厂会查找同时满足以下条件的类:
    • value 匹配指定的类型
    • sign 包含所有指定的标签

3.3 使用工厂类创建规则的优势

  • 解耦: 业务逻辑与具体实现类解耦,通过配置即可改变行为
  • 可扩展: 新增规则只需添加带注解的类,无需修改现有代码
  • 灵活筛选: 多维度的标签系统支持复杂的实例筛选需求
  • 性能优化: 支持单例模式,避免重复创建实例

3.4 关键代码

注解,加到每个规则类的上方

// 自动注册到工厂
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface AutoCreate {/*** 标记类型名称 支持多个* 简单工厂[必须]指定创建类型名称自动创建实例*/String[] value();/*** 标记分类名称 支持多个* 简单工厂[可以]指定标记分类名称自动创建实例*/String[] sign() default {};boolean isSingleton() default false;
}

工厂类,自动扫描和创建规则服务的实例

/*** 简单的自动创建工厂* 线程安全* 支持自动扫描@AutoCreate注解的实现类生成通用工具* 不需要在new对象时写繁琐的if else 判断语句*/
@Slf4j
public class AutoCreateFactory {private AutoCreateFactory() {}/*** Map>>>*/private static final Map>>>> classzzCostructorMap = new ConcurrentHashMap<>();/*** 创建单个实例* 默认扫描实例父类class所在的包*/public static  T getInstance(Class clazz, String type, String... signs) {List list = getInstanceList(clazz, type, signs);if(CollectionUtils.isEmpty(list)) {return null;}return list.get(0);}public static  T getInstanceInPackage(Package scanPackage, Class clazz, String type, String... signs) {List list = getInstanceListInPackage(scanPackage, clazz, type, signs);if(CollectionUtils.isEmpty(list)) {return null;}return list.get(0);}public static  List getInstanceList(Class clazz, String type, String... signs) {return getInstanceListInPackage(clazz.getPackage(), clazz, type, signs);}private static  List getInstanceListInPackage(Package scanPackage, Class clazz, String type, String... signs) {Assert.notNull(clazz, "auto create instance class can not be null");Assert.notNull(type, "auto create instance type can not be null");String scanPackageName = scanPackage.getName();String key = scanPackageName + "-" + clazz.getSimpleName();Map>>> constructorMap = classzzCostructorMap.computeIfAbsent(key, k -> getConstructorMap(scanPackageName, clazz));Map>> signConstructorMap = constructorMap.get(type);if(signConstructorMap == null) {return new ArrayList<>();}List signList;if(ArrayUtils.isEmpty(signs)) {signList = new ArrayList<>();signList.add(null);} else {signList = Arrays.asList(signs);}Set> signFitSet = Collections.emptySet();for(int i = 0; i < signList.size(); i++) {String sign = signList.get(i);Set> supplierSet = signConstructorMap.computeIfAbsent(sign, k -> new HashSet<>());if(i == 0) {signFitSet = supplierSet;} else {signFitSet = Sets.intersection(signFitSet, supplierSet);}}return signFitSet.stream().map(supplier ->  (T) supplier.get()).collect(Collectors.toList());}/*** 扫描所有标记了@AutoCreate注解的实践类并自动注册到工厂*/private static  Map>>> getConstructorMap(String scanPackage, Class parentClazz) {Map>>> constructorMap = new HashMap<>();// 获取制定包路径下的所有实现类@AutoCreate注解的class子类Set> allClazzSet = RefUtils.loadClasses(scanPackage, parentClazz, AutoCreate.class);for(Class clazz : allClazzSet) {try {initConstructorMap(constructorMap, clazz);} catch (Exception e) {log.error("", e);}}return Collections.unmodifiableMap(constructorMap);}private static  void initConstructorMap(Map>>> constructorMap, Class clazz) throws Exception {AutoCreate autoCreate = clazz.getAnnotation(AutoCreate.class);Constructor constructor = clazz.getDeclaredConstructor () ;if(!constructor.isAccessible()) {constructor.setAccessible(true);}Supplier consumer = getSupplier(autoCreate, constructor);String[] types = autoCreate.value();String[] signs = autoCreate.sign();for(String type: types) {boolean isInitNulSign = true;for(String sign : signs) {Map>> signMap = constructorMap.computeIfAbsent(type, k -> new HashMap<>());signMap.computeIfAbsent(sign, k -> new HashSet<>()).add(consumer);if (sign == null) {isInitNulSign = false;}}if(isInitNulSign) {Map>> signMap = constructorMap.computeIfAbsent(type, k -> new HashMap<>());signMap.computeIfAbsent(null, key -> new HashSet<>()).add(consumer);}}}// 获取父类private static  Supplier getSupplier(AutoCreate autoCreate, Constructor constructor) throws  InstantiationException, IllegalAccessException, InvocationTargetException {Supplier consumer;if(autoCreate.isSingleton()) {final T singletonInstance = constructor.newInstance();// 单例consumer = () -> singletonInstance;} else {// 原型consumer = () -> {try {//T instance = constructor.newInstance();//autowiredProcess(instance, instance.getClass());//return instance;return constructor.newInstance();} catch (Exception e) {log.warn(e.getMessage());}return null;};}return consumer;}
}

反射工具类,负责包扫描和类加载

public class RefUtils {public static  Set> loadClasses(String pack, Class parentClazz, Class annotationClass) {Set> clazzSet = new LinkedHashSet<>();Set> allClazzSet = loadClasses(pack);for (Class clazz : allClazzSet) {try {if(parentClazz != null && !parentClazz.isAssignableFrom(clazz)) {continue;}if(annotationClass != null && clazz.getAnnotation(annotationClass) == null) {continue;}clazzSet.add(clazz);} catch (Exception e) {e.printStackTrace();}}return clazzSet;}public static  Set> loadClasses(String pack) {Set> classes = new LinkedHashSet<>();boolean recursive = true;String packageDirName = pack.replace('.', '/');Enumeration dirs;try {dirs = Thread.currentThread().getContextClassLoader().getResources(packageDirName);while (dirs.hasMoreElements()) {URL url = dirs.nextElement();String protocol = url.getProtocol();if ("file".equals(protocol)) {String filePath = URLDecoder.decode(url.getFile(), "UTF-8");findClassesInPackageByFile(pack, filePath, recursive, classes);}if("jar".equals(protocol)) {findClassesInPackageByJar(url, pack, packageDirName, recursive, classes);}}} catch (Exception e) {e.printStackTrace();}return classes;}private static  void findClassesInPackageByFile(String packageName, final String packagePath, boolean recursive, Set> classes) {java.io.File dir = new java.io.File(packagePath);if (!dir.exists() || !dir.isDirectory()) {return;}java.io.File[] dirfiles = dir.listFiles(file -> (recursive && file.isDirectory()) || (file.getName().endsWith(".class")));for(File file : dirfiles) {if (file.isDirectory()) {findClassesInPackageByFile(packageName + "." + file.getName(), file.getAbsolutePath(), recursive, classes);} else {String className = file.getName().substring(0, file.getName().length() - 6);try {String classPath = packageName + '.' + className;classes.add(loadClass(classPath));} catch (Exception e) {e.printStackTrace();}}}}private static  void findClassesInPackageByJar(URL url, String packageName, String packageDirName, final boolean recursive, Set> classes) {try {java.util.jar.JarFile jar = ((java.net.JarURLConnection) url.openConnection()).getJarFile();java.util.Enumeration entries = jar.entries();while(entries.hasMoreElements()) {packageDirName = findClassesByOneJar(packageName, packageDirName, recursive, classes, entries.nextElement());}} catch (Exception e) {e.printStackTrace();}}private static  String findClassesByOneJar(String packageName, String packageDirName, boolean recursive, Set> classes, JarEntry jarEntry) {String className = jarEntry.getName();if (className.charAt(0) == '/') {className = className.substring(1);}if(className.startsWith(packageDirName)) {int idx = className.lastIndexOf('/');if (idx != -1) {packageName = className.substring(0, idx).replace('/', '.');}if(((idx != -1) && recursive) && className.endsWith(".class") && !jarEntry.isDirectory()) {String classNameNew = className.substring(packageName.length() + 1, className.length() - 6);try {String classPath = packageName + '.' + classNameNew;classes.add(loadClass(classPath));} catch (Exception e) {e.printStackTrace();}}}return packageName;}public static  Class loadClass(String classPath) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {ClassLoader classLoader = Thread.currentThread().getContextClassLoader();Method forNameMethod = Class.class.getDeclaredMethod("forName", String.class, boolean.class, ClassLoader.class);Class clazz = (Class) forNameMethod.invoke(null, classPath, false, classLoader);return clazz;}
}

四、运行效果

通过访问对应的接口:

  • 示例一:http://localhost:8080/api/rules/execute
  • 示例二:http://localhost:8080/demos/adapter/execute

可以观察到规则按照预设的优先级顺序执行,并且能够正确传递参数和处理业务逻辑。


五、总结

本文通过对 Easy Rules 在 Spring Boot 中的应用进行了深入探讨,不仅介绍了其基本概念及工作原理,还给出了两种典型的应用场景及其对应的实现方案。希望读者能够借此掌握规则引擎的设计思路和技术要点,在今后的实际工作中加以运用。

六、福利资源

完整的代码已上传为附件,可在Intellij IDEA中导入并直接运行,欢迎交流讨论。如果你对该项目感兴趣或者有任何疑问,或者有更好的实现方案,欢迎留言交流!

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

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

相关文章

2025 年 12 月北京管道疏通服务权威推荐榜:朝阳区马桶疏通、下水道快速清淤,专业高效解决堵塞难题

2025 年 12 月北京管道疏通服务权威推荐榜:朝阳区马桶疏通、下水道快速清淤,专业高效解决堵塞难题 在北京这座超大型城市,市政排水管网与建筑内部排水系统的健康运行,是保障城市功能正常运转和居民生活质量的基础。…

TinyMCE:功能丰富且可定制的开源HTML编辑器 - 教程

TinyMCE:功能丰富且可定制的开源HTML编辑器 - 教程pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas"…

2025 年 12 月模胚模架厂家权威推荐榜:精密制造与稳定耐用口碑之选,解析核心优势与选购指南

2025 年 12 月模胚模架厂家权威推荐榜:精密制造与稳定耐用口碑之选,解析核心优势与选购指南 在精密模具制造产业链中,模胚(也称模架)作为模具的“骨骼”与“基座”,其质量直接决定了最终模具的精度、寿命与生产效…

2025英文网站建设推荐TOP5权威榜单:企业/商城网站建设

数字化时代,企业官网与商城网站已成为链接全球用户的核心入口。2024年数据显示,具备多语言适配能力的企业网站转化率较单语种网站高62%,但38%的企业因选择不当遭遇语言适配漏洞、支付流程卡顿、品牌形象割裂等问题:…

2025年度浙江实力强的复读学校TOP5权威推荐:看哪家口碑

高复赛道竞争激烈,2024年浙江高复市场规模超12亿元,年增速18%,但32%的投诉集中在教学质量不稳定、售后跟进敷衍、心理疏导缺失三大问题。高复学子常踩坑:部分机构承诺名师授课却临时换兼职老师,导致知识点衔接断层…

2025 年 12 月上海保洁服务权威推荐榜:专业开荒、精细养护与全场景保洁口碑之选

2025 年 12 月上海保洁服务权威推荐榜:专业开荒、精细养护与全场景保洁口碑之选 随着上海城市建设的不断深化与居民生活品质的持续提升,专业保洁服务已从单纯的环境维护,演变为提升空间价值、保障健康安全、优化运营…

部分克隆 + 稀疏检出

部分克隆 + 稀疏检出部分克隆 + 稀疏检出 一、需求背景 在git使用过程中,为了解决只对目标文件或者目标文件夹以及灵活组合的配置下去维护对应的文件需求,避免工作目录+.git仓库过大的问题 二、脚本实例 # ====== 配…

重练算法(代码随想录版) day32 - 动态规划part1

今日刷题量:3 当前刷题总量:130 Easy: 59 Mid: 64 Hard: 7 Day32 基础理论 如果题目大致符合这些特征,可以优先往dp上想:求最值/计数/可行性:最大值、最小值、方案数、是否能做到…… 问题可以拆成子问题:整题答…

2025 年 12 月大回旋切断机厂家权威推荐榜:高效精准的金属加工利器,实力工厂与创新技术深度解析

2025 年 12 月大回旋切断机厂家权威推荐榜:高效精准的金属加工利器,实力工厂与创新技术深度解析 在金属管材、棒材及型材的现代化加工领域,大回旋切断机凭借其高效、精准、低损耗的加工特性,已成为众多制造企业提升…

2025年辽宁省工商注册机构排行:知名的工商注册品牌企业有哪

本榜单依托辽宁省工商服务市场全维度调研与真实客户口碑反馈,从服务专业度、流程效率、风险控制、售后保障四大核心维度筛选标杆企业,为创业者与企业主提供客观选型依据,助力精准匹配靠谱的工商注册服务伙伴。 TOP1…

什么平台可以修改发布文章的时间?网页的时间可以修改吗?

以下是一些支持修改发布文章时间的平台及其操作方式: 1. WordPress功能:支持修改文章的发布时间。 操作方式:在文章编辑页面,找到“发布”模块,点击“编辑”按钮,修改日期和时间。 或通过“快速编辑”在文章列表…

2025 年 12 月二手压铸机厂家权威推荐榜:力劲/伊之密/锌合金/铝合金/热室/冷室压铸机买卖回收,精选耐用机型与高性价比之选

2025 年 12 月二手压铸机厂家权威推荐榜:力劲/伊之密/锌合金/铝合金/热室/冷室压铸机买卖回收,精选耐用机型与高性价比之选 在全球制造业持续追求降本增效与可持续发展的宏观背景下,二手压铸机市场正迎来前所未有的…

2025玻璃钢拉挤型材源头厂家TOP5权威推荐:甄选高性价比

工业领域对耐腐蚀、轻量化材料的需求持续攀升,2024年数据显示,玻璃钢拉挤型材市场规模突破120亿元,年增速达32%,但30%的采购投诉集中在品质不稳定、安装维护难、全生命周期成本高等问题。企业常遇选型坑:传统金属…

2025年辽宁省口碑不错的工商注册公司推荐:服务不错的工商注

本榜单依托辽宁省内企业服务市场调研与真实客户口碑,筛选出5家标杆企业,为辽企提供客观选型依据,助力精准匹配工商财税服务伙伴。 TOP1 推荐:大连方仪企业咨询服务有限公司 推荐指数:★★★★★ 口碑评分:辽宁…

Obsidian的Bases数据库入门教程,使用数据库实现Todo待办管理系统

Obsidian数据库 Obsidian发布 Bases 数据库已经有一段时间了,数据库的功能上线,将会重新定义Obsidian的玩法,一篇文章带你了解数据库的玩法,并实现开发任务待办管理功能,附数据库模板,导入即用,效果如下图。准备…

Proxmox Datacenter Manager 1.0 发布 - 集中管理 Proxmox 虚拟化环境

Proxmox Datacenter Manager 1.0 发布 - 集中管理 Proxmox 虚拟化环境Proxmox Datacenter Manager 1.0 - 集中管理 Proxmox 虚拟化环境 centralized management solution to oversee and manage multiple nodes and cl…

2025年12月AI智搜GEO服务商权威推荐榜:福州/莆田/三明地区智能搜索推广与排名优化专家深度解析

2025年12月AI智搜GEO服务商权威推荐榜:福州/莆田/三明地区智能搜索推广与排名优化专家深度解析 在数字经济浪潮持续奔涌的今天,企业线上获客的精准性与效率已成为决定市场竞争力的关键。传统的搜索引擎营销(SEM)模…

量化图像“概念相似性”的新方法

本文提出了一种利用视觉语言模型量化图像间概念相似性的新方法。该方法通过生成不同长度的图像描述,并计算这些描述指代图像的概率,将原本依赖人类判断的概念相似性比较形式化。在评估中,该方法相比现有技术能更准确…

PbootCMS网站修改CSS样式后自动更新缓存

修改 CSS 或 JS 文件后,由于浏览器缓存机制,用户可能无法立即看到最新样式。 手动添加版本号(如 ?v=1)可以解决缓存问题,但需要每次手动更新版本号,操作繁琐。解决方案 1. 添加自动时间版本号方法 通过 PHP 动态…

pbootcms模板获取指定栏目下面的所有单页内容(PbootCMS模板获取指定栏目下所有单页内容的方法)

1. 功能概述 在 PbootCMS 中,可以通过 {pboot:nav} 和 {pboot:content} 标签组合,获取指定栏目下的所有单页内容。此功能适用于展示某个栏目下的单页信息(如公司简介、联系我们等),并支持排除特定编号的内容。2. …