目录
一、前言
需求:动态添加优惠券以及购物车优惠结算
解析需求:
优惠券:优惠券金额、优惠券名称、优惠券编码;
购物车优惠结算结果=购物车金额-优惠券;
解决思路:
我们把购物车优惠结果作为一个fact对象,通过匹配规则文件,来修改其购物金额;但这个规则文件是不固定的,因此我们可以通过模板引擎,传入优惠券相关参数,生成规则内容存放在数据库或生成规则文件,如图所示:
开发环境及工具:
windows10、jdk11、Spring Boot 2.5.2、Drools 7.54.0、模板引擎、MySql 8.0.25、MyBatis;
idea 2021.1.3
二、项目文件结构以及数据库表结构
创建一个Spring Boot项目,项目的文件结构如下:
创建一个数据库表:promote,sql如下:
/*
Navicat Premium Data Transfer
Source Server : localhost
Source Server Type : MySQL
Source Server Version : 80026
Source Host : localhost:3306
Source Schema : drools_test
Target Server Type : MySQL
Target Server Version : 80026
File Encoding : 65001
Date: 28/07/2021 14:31:23
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for promote
-- ----------------------------
DROP TABLE IF EXISTS `promote`;
CREATE TABLE `promote` (
`id` int(0) NOT NULL AUTO_INCREMENT,
`promote_code` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '优惠券编码',
`promote_rule` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '优惠规则',
`promote_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '优惠券名称',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 45 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
三、主要项目文件说明
1、pom文件
- drools相关:drools-compiler、drools-mvel(Drools7.44.0以下版本可以不添加)
- Spring Boot相关:spring-boot-starter-web、spring-boot-starter-log4j2、spring-boot-starter-test
- mybatis:mybatis-spring-boot-starter
- mysql:mysql-connector-java
- 模板引擎:ST4
- 其他:thymeleaf、lombok、fastjson、 gson
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.2</version>
<relativePath/>
</parent>
<groupId>com.qyl</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
<drools.version>7.54.0.Final</drools.version>
</properties>
<dependencies>
<!-- drools begin-->
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-compiler</artifactId>
<version>${drools.version}</version>
</dependency>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-mvel</artifactId>
<version>${drools.version}</version>
</dependency>
<!-- drools end -->
<!-- spring boot begin -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<!-- use log4j2 remove logging-->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<!-- mysql 链接-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<!--<scope>runtime</scope>-->
<version>8.0.25</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- lombok begin -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
<scope>provided</scope>
</dependency>
<!-- lombok end -->
<!-- json -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
<!-- Gson -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.7</version>
</dependency>
<dependency>
<groupId>org.antlr</groupId>
<artifactId>ST4</artifactId>
<version>4.0.8</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.5.2</version>
</plugin>
</plugins>
</build>
</project>
2、配置文件
application-dev.yml:
server:
port: 8080
# mysql配置
spring:
jpa:
show-sql: true
datasource:
url: jdbc:mysql://localhost:3306/drools_test
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver # 高版本用这个驱动 低版本用com.mysql.jdbc.Driver
#thymeleaf配置
thymeleaf:
prefix: classpath:/templates/
# mybatis配置
mybatis:
mapper-locations: classpath*:mapper/*.xml
type-aliases-package: com.qyl.demo.dao
3、实体类
promote.java:优惠券实体类,包括优惠券名称、优惠券编码以及模板引擎生成的规则内容
package com.qyl.demo.model;
import lombok.Data;
/**
* @author qiuyelin
* @version 1.0
* @Description: TODO 存放规则编码、规则姓名、规则内容等
* @date 2021/7/20 13:49
*/
@Data
public class Promote {
// 促销编号
private String promoteCode;
// 促销名称
private String promoteName;
// 规则内容
private String promoteRule;
}
RuleResultVo.java:优惠后实体类,包括优惠前价格、优惠后价格以及参加的优惠活动名称
package com.qyl.demo.model;
import lombok.Data;
/**
* @author qiuyelin
* @version 1.0
* @Description: TODO 返回值实体
* @date 2021/7/20 14:35
*/
@Data
public class RuleResultVo {
// 参加活动商品优惠后的价格
private double finallyMoney;
// 参加优惠前价格
private double beforeMoney;
// 参加活动的名称
private String promoteName;
}
4、添加优惠券业务实现
在PromoteService.java中添加如下方法:
/*
* 生成优惠券
* */
public String editPromote(String money, String ruleName) {
Promote promote = new Promote();
double v = Double.parseDouble(money);
// 根据模板引擎生成规则内容
String rule = UUIDUtil.rule(ruleWorkMap(ruleName, v));
// 生成编码
String promoteCode = UUIDUtil.typeJoinTime();
promote.setPromoteCode(promoteCode);
promote.setPromoteName(ruleName);
promote.setPromoteRule(rule);
// 优惠券保存到数据库
int i = promoteMapper.insertPromote(promote);
return i > 0 ? "success" : "fail";
}
根据优惠券名称及金额,组合业务规则Json:在PromoteService.java中添加如下方法:
/*
* 组合业务规则Json方法
* */
private String ruleWorkMap(String name, Double money) {
Map<String, Object> map = new HashMap<>();
// Rule部分
Map<String, Object> rule = new HashMap<>();
rule.put("name", name);
// When部分
Map<String, Object> when = new HashMap<>();
// then部分
Map<String, Object> then = new HashMap<>();
then.put("money", money);
//组合规则 rule when and then
map.put("rule", rule);
map.put("condition", when);
map.put("action", then);
return JSONObject.toJSONString(map);
}
通过模板引擎生成规则业务内容:关于模板引擎是一个新的技术,可以定义一个模板组文件,有兴趣的可以查找一些相关资料。 在UUIDUtil.java工具类中添加生成规则业务内容的方法,注意workMoneyST的语法格式。
/*
* 生成规则业务内容
* */
private static String ruleWordExchangeST(String json) {
STGroup group = new STGroupString(workMoneyST);
ST stFile = group.getInstanceOf("wordImport");
ST stRule = group.getInstanceOf("ruleValue");
JSONObject jsonObject = JSONObject.parseObject(json);
JSONObject condition = jsonObject.getJSONObject("condition");
JSONObject action = jsonObject.getJSONObject("action");
JSONObject rule = jsonObject.getJSONObject("rule");
stRule.add("condition", condition);
stRule.add("action", action);
stRule.add("rule", rule);
stFile.add("ruleStr", stRule);
return stFile.render();
}
//通过传参数 即可生成规则内容,将规则内容放在数据库或征程规则文件,为规则库的构建设定基础
private static String workMoneyST="wordImport(ruleStr)::=<<\n" +
" package rules\n" +
" import com.qyl.demo.model.RuleResultVo;\n" +
" <ruleStr;separator=\"\\n\">\n" +
">>\n" +
"ruleValue(condition,action,rule)::=<<\n" +
" rule \"<rule.name>\"\n" +
" no-loop true\n" +
" when\n" +
" $r:RuleResultVo(true)\n" +
" then\n" +
" modify($r){\n" +
" setPromoteName(\"<rule.name>\")\n" +
" <if(action)>,\n" +
" setFinallyMoney($r.getBeforeMoney()-<action.money> <endif>)\n" +
" }\n" +
" end\n" +
">>";
5、购物车结算业务实现
在PromoteService.java中添加如下方法:
/*
* 购物车计算
* */
public Map<String, Object> calcPromote(String moneySum) {
Map<String, Object> map = new HashMap<>();
// 优惠后价格
double afterMoney = Double.parseDouble(moneySum);
// 查询优惠券
List<Promote> promoteList = queryPromote();
// 优惠后结果
List<RuleResultVo> resultVoList = new ArrayList<>();
if (promoteList.size() > 0) {
// 说明有优惠券
for (Promote promote : promoteList
) {
// 匹配优惠券规则
RuleResultVo ruleResultVo = DrlExecute.rulePromote(promote, afterMoney);
afterMoney = ruleResultVo.getFinallyMoney();
resultVoList.add(ruleResultVo);
}
}
map.put("before", moneySum);
map.put("after", afterMoney);
map.put("list", resultVoList);
return map;
}
匹配优惠券规则工具类:DrlExecute.java
package com.qyl.demo.util;
import com.qyl.demo.model.Promote;
import com.qyl.demo.model.RuleResultVo;
import org.kie.api.KieBase;
import org.kie.api.runtime.StatelessKieSession;
import org.kie.internal.command.CommandFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
/**
* @author qiuyelin
* @version 1.0
* @Description: TODO 规则工具类
* @date 2021/7/20 14:41
*/
public class DrlExecute {
protected static Logger logger = LoggerFactory.getLogger(DrlExecute.class);
/*
* 匹配优惠券规则
* */
public static RuleResultVo rulePromote(Promote promote, Double beforeMoney) {
RuleResultVo resultVo = new RuleResultVo();
resultVo.setBeforeMoney(beforeMoney);
logger.info("优惠前价格" + beforeMoney);
// 命令执行对象
List cmdCondition = new ArrayList<>();
cmdCondition.add(CommandFactory.newInsert(resultVo));
// 初始化规则库
KieBase KieBase = KieUtil.ruleKieBase(promote.getPromoteRule(), promote.getPromoteCode());
// 无状态KieSession
StatelessKieSession kieSession = KieBase.newStatelessKieSession();
// 调用规则
kieSession.execute(CommandFactory.newBatchExecution(cmdCondition));
logger.info("优惠后的价格" + resultVo.getFinallyMoney());
return resultVo;
}
}
初始化规则库工具类:KieUtil.java
package com.qyl.demo.util;
import org.kie.api.KieBase;
import org.kie.api.io.Resource;
import org.kie.api.io.ResourceType;
import org.kie.internal.io.ResourceFactory;
import org.kie.internal.utils.KieHelper;
import java.io.StringReader;
/**
* @author qiuyelin
* @version 1.0
* @Description: TODO 初始化规则库
* @date 2021/7/20 14:25
*/
public class KieUtil {
// 初始化规则库 是Kie的一个工具类
public static KieBase ruleKieBase(String rule, String name) {
KieHelper kieHelper = new KieHelper();
try {
kieHelper.kfs.write(generateResourceName(ResourceType.DRL, name), rule);
return kieHelper.build();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(" drools init failed!");
}
}
private static String generateResourceName(ResourceType type, String name) {
// 这里的路径对应模板引擎中的package包路径
return "src/main/resources/rules/" + name + "." + type.getDefaultExtension();
}
}
四、实现效果
1、添加优惠券
在添加优惠券页面,填写信息
点击提交
如果结果为success,说明添加成功,数据库中也有相应数据
promote_rule的内容为:
package rules
import com.qyl.demo.model.RuleResultVo;
rule "优惠券30元"
no-loop true
when
$r:RuleResultVo(true)
then
modify($r){
setPromoteName("优惠券30元")
,
setFinallyMoney($r.getBeforeMoney()-30.0 )
}
end
2、购物车结算
在购物车界面输入购物车当前总金额
点击提交,即可查看优惠后的金额:
我添加多个优惠券
计算后的效果为:
五、结语
最近在学习Drools,其中有一个示例是优惠券的动态添加,感觉挺有意思的,就稍加更改,分享给大家,如果有哪里不对的,请大家帮忙提出,避免误人子弟╰( ̄▽ ̄)╭
完整的项目代码在https://gitlab.com/yeliner/drools_springboot_demo
参考资料:Drools规则引擎技术指南_第8章