使用json配置动态区间及动态执行公式
背景
有时候可能线上一直需要调整公式或者区间以及参数等等,如果使用代码方式,将会变得比较麻烦,可以在redis或者数据库配置一份动态配置,让代码进行解析并执行,可以对公式以及参数等进行动态调节
需求
x 是估值,n是区间 所属值
得分最大值:x-n=批发价得100分
得分最小值:批发价小于x-4n,大于x+n得0分
批发价在区间[x-4n,x-n]使用公式:100-(批发价-(x-n) )的绝对值/0.03n (注意区间的开闭)
批发价在区间(x-n,x+n]使用公式:100-(批发价-(x-n) )的绝对值/0.02n
批发价区间划分:(划分逻辑,中位数*0.2/4(满足偏差的最大值等于中位数的20%))
● 10000(批发价)以下 300 (n值)
● 10000-19999 750
● 20000-29999 1250
● 30000-49999 2000
● 50000-79999 3250
● 80000-99999 4500
● 100000-149999 6500
● 150000-199999 9000
● 200000-299999 15000
● 300000-499999 20000
● 500000以上 25000
逻辑1.2:批发价为空/批发等于零售价/批发价大于零售价=0分
实现
json配置
{
"realWholesaleScoringSystem": {
"description": "评分系统",
"totalScore": 100,
"coefficient": 0.00218727,
"scoringCriteria": [
{
"condition": "批发价小于零售价,在指定区间,动态配置",
"code": "wholesale_price_lt_retail_price_in_dynamic_range",
"ranges": [
{
"min": "x - 4 * n",
"max": "x - n",
"inclusiveMin": "closed",
"inclusiveMinDesc": "区间最小值开闭值",
"inclusiveMax": "closed",
"inclusiveMaxDesc": "区间最大值开闭值",
"scoreFormula": "100 - Math.abs(wholesalePrice - (x - n)) / (0.03 * n)"
},
{
"min": "x - n",
"max": "x + n",
"inclusiveMin": "open",
"inclusiveMinDesc": "区间最小值开闭值",
"inclusiveMax": "closed",
"inclusiveMaxDesc": "区间最大值开闭值",
"scoreFormula": "100 - Math.abs(wholesalePrice - (x - n)) / (0.02 * n)"
}
],
"notInRangeScore": 0,
"formulaDescription": "批发价小于零售价在不同区间的得分计算"
}, {
"condition": "有批发价,等于零售价",
"code": "wholesale_price_eq_retail_price",
"score": 60
},
{
"condition": "无批发价或批发价大于零售价",
"code": "wholesale_price_is_null_or_gt_retail_price",
"score": 0
},
{
"condition": "有批发价,无零售价",
"code": "wholesale_price_exists_no_retail_price",
"score": 100
}
],
"priceRangeSettings-n_value": [
{
"priceRange": {
"minPrice": 0,
"maxPrice": 9999,
"nValue": 300
}
},
{
"priceRange": {
"minPrice": 10000,
"maxPrice": 19999,
"nValue": 750
}
},
{
"priceRange": {
"minPrice": 20000,
"maxPrice": 29999,
"nValue": 1250
}
},
{
"priceRange": {
"minPrice": 30000,
"maxPrice": 49999,
"nValue": 2000
}
},
{
"priceRange": {
"minPrice": 50000,
"maxPrice": 79999,
"nValue": 3250
}
},
{
"priceRange": {
"minPrice": 80000,
"maxPrice": 99999,
"nValue": 4500
}
},
{
"priceRange": {
"minPrice": 100000,
"maxPrice": 149999,
"nValue": 6500
}
},
{
"priceRange": {
"minPrice": 150000,
"maxPrice": 199999,
"nValue": 9000
}
},
{
"priceRange": {
"minPrice": 200000,
"maxPrice": 299999,
"nValue": 15000
}
},
{
"priceRange": {
"minPrice": 300000,
"maxPrice": 499999,
"nValue": 20000
}
},
{
"priceRange": {
"minPrice": 500000,
"maxPrice": null,
"nValue": 25000
}
}
]
}
}
代码实现
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
public class WholesaleScoringSystem {
// JavaScript 引擎,用于动态计算公式
// 动态执行脚本
// 使用 ThreadLocal 为每个线程提供独立的 ScriptEngine 实例
private static final ThreadLocal<ScriptEngine> engine = ThreadLocal.withInitial(() ->
new ScriptEngineManager().getEngineByName("JavaScript")
);
// 获取当前线程的 ScriptEngine 实例
public static ScriptEngine getScriptEngine() {
return engine.get();
}
// 移除ScriptEngine 实例
public static void removeScriptEngine() {
engine.remove();
}
public static void main(String[] args) {
// 加载 JSON 配置(可从文件或其他来源获取)
String jsonConfig = jsonStr;
JSONObject wholesaleScoring = JSONObject.parseObject(jsonConfig).getJSONObject("realWholesaleScoringSystem");
// 测试不同场景的评分计算
testScenarios(wholesaleScoring);
}
/**
* 测试不同批发价情况下的评分计算
*
* @param wholesaleScoring 真批发评分系统的配置信息
*/
private static void testScenarios(JSONObject wholesaleScoring) {
// 设置零售价和估价值 x
Long retailPrice = 50000L;
Long x = 50000L;
// 预设不同的批发价用于测试
// Long[] wholesalePrices = {46000L, 48000L, 52000L, 50000L, 55000L, null, 49000L};
Long[] wholesalePrices = {1000L, 15000L, 25000L, 45000L, 120000L, 500000L, 46000L, 48000L, 52000L, 50000L, 55000L, null, 49000L};
// 遍历每个批发价并计算评分
for (Long wholesalePrice : wholesalePrices) {
BigDecimal score = computeWholesaleScore(wholesaleScoring, wholesalePrice, retailPrice, x);
System.out.println("批发价: " + wholesalePrice + ", 评分: " + score);
}
}
/**
* 计算批发评分
*
* @param wholesaleScoring 真批发评分系统的配置信息
* @param wholesalePrice 批发价
* @param retailPrice 零售价
* @param x 估价值
* @return 计算后的评分
*/
private static BigDecimal computeWholesaleScore(JSONObject wholesaleScoring, Long wholesalePrice, Long retailPrice, Long x) {
// 获取评分标准的配置信息
JSONArray scoringCriteria = wholesaleScoring.getJSONArray("scoringCriteria");
// 获取评分系数
BigDecimal sourceCoefficient = wholesaleScoring.getBigDecimal("coefficient");
// 遍历每个评分标准条件
for (Object obj : scoringCriteria) {
JSONObject criterion = (JSONObject) obj;
String code = criterion.getString("code");
switch (code) {
case "wholesale_price_lt_retail_price_in_dynamic_range":
// 判断批发价是否存在且低于零售价
if (wholesalePrice != null && wholesalePrice < retailPrice) {
// 根据批发价获取对应的 n 值
Long nValue = getNValue(wholesaleScoring.getJSONArray("priceRangeSettings-n_value"), wholesalePrice);
if (nValue == null) continue; // 如果没有找到合适的 n 值,跳过该条件
// 获取范围设置
JSONArray ranges = criterion.getJSONArray("ranges");
boolean isInRange = false;
// 遍历所有范围,判断批发价是否在范围内
for (Object rangeObj : ranges) {
JSONObject range = (JSONObject) rangeObj;
// 动态计算范围的 min 和 max
BigDecimal min = evaluateExpression(range.getString("min"), new HashMap<String, Object>() {{
put("n", nValue);
put("x", x);
}});
BigDecimal max = evaluateExpression(range.getString("max"), new HashMap<String, Object>() {{
put("n", nValue);
put("x", x);
}});
// 解析区间的开闭属性
String inclusiveMin = range.getString("inclusiveMin");
String inclusiveMax = range.getString("inclusiveMax");
// 判断批发价是否在当前区间内
boolean withinMinBound = "closed".equals(inclusiveMin) ? BigDecimal.valueOf(wholesalePrice).compareTo(min) >= 0 : BigDecimal.valueOf(wholesalePrice).compareTo(min) > 0;
boolean withinMaxBound = "closed".equals(inclusiveMax) ? BigDecimal.valueOf(wholesalePrice).compareTo(max) <= 0 : BigDecimal.valueOf(wholesalePrice).compareTo(max) < 0;
if (withinMinBound && withinMaxBound) {
isInRange = true;
// 根据配置的公式计算得分
String formula = range.getString("scoreFormula");
BigDecimal score = evaluateExpression(formula, new HashMap<String, Object>() {{
put("wholesalePrice", wholesalePrice);
put("x", x);
put("n", nValue);
}});
return score.multiply(sourceCoefficient); // 返回乘以系数后的得分
}
}
// 如果批发价不在任何指定的范围内,使用 notInRangeScore
if (!isInRange) {
return BigDecimal.valueOf(criterion.getDouble("notInRangeScore")).multiply(sourceCoefficient);
}
}
break;
case "wholesale_price_eq_retail_price":
// 判断批发价是否等于零售价
if (wholesalePrice != null && retailPrice != null && wholesalePrice.equals(retailPrice)) {
return BigDecimal.valueOf(criterion.getDouble("score")).multiply(sourceCoefficient);
}
break;
case "wholesale_price_is_null_or_gt_retail_price":
// 判断批发价是否为空或大于零售价
if (wholesalePrice == null || (wholesalePrice != null && retailPrice != null && wholesalePrice > retailPrice)) {
return BigDecimal.valueOf(criterion.getDouble("score")).multiply(sourceCoefficient);
}
break;
case "wholesale_price_exists_no_retail_price":
// 判断是否有批发价但无零售价
if (wholesalePrice != null && retailPrice == null) {
return BigDecimal.valueOf(criterion.getDouble("score")).multiply(sourceCoefficient);
}
break;
default:
break;
}
}
// 如果没有匹配到任何条件,返回 0 分
return BigDecimal.ZERO;
}
/**
* 动态计算表达式的值
*
* @param expression 要计算的表达式
* @param variables 表达式中使用的变量,以键值对的形式传入
* @return 计算结果
*/
private static BigDecimal evaluateExpression(String expression, Map<String, Object> variables) {
try {
ScriptEngine scriptEngine = getScriptEngine();
// 将所有变量放入引擎上下文
for (Map.Entry<String, Object> entry : variables.entrySet()) {
scriptEngine.put(entry.getKey(), entry.getValue());
}
// 计算表达式并返回结果
BigDecimal result = BigDecimal.valueOf(((Number) scriptEngine.eval(expression)).doubleValue());
removeScriptEngine();
return result;
} catch (ScriptException e) {
e.printStackTrace();
return BigDecimal.ZERO;
}
}
/**
* 根据批发价在价格范围设置中获取对应的 n 值
*
* @param priceRangeSettings 价格范围设置
* @param wholesalePrice 批发价
* @return 对应的 n 值
*/
private static Long getNValue(JSONArray priceRangeSettings, Long wholesalePrice) {
// 遍历价格范围设置,以确定 n 值
for (Object obj : priceRangeSettings) {
JSONObject rangeSetting = (JSONObject) obj;
JSONObject priceRange = rangeSetting.getJSONObject("priceRange");
Long minPrice = priceRange.getLong("minPrice");
Long maxPrice = priceRange.containsKey("maxPrice") ? priceRange.getLong("maxPrice") : Long.MAX_VALUE;
// 如果批发价在范围内,则返回相应的 n 值
if (wholesalePrice >= minPrice && wholesalePrice <= maxPrice) {
return priceRange.getLong("nValue");
}
}
return null; // 若没有找到匹配的范围,则返回 null
}
private static final String jsonStr = "{\n" +
" \"realWholesaleScoringSystem\": {\n" +
" \"description\": \"评分系统\",\n" +
" \"totalScore\": 100,\n" +
" \"coefficient\": 0.00218727,\n" +
" \"scoringCriteria\": [\n" +
" {\n" +
" \"condition\": \"批发价小于零售价,在指定区间,动态配置\",\n" +
" \"code\": \"wholesale_price_lt_retail_price_in_dynamic_range\",\n" +
" \"ranges\": [\n" +
" {\n" +
" \"min\": \"x - 4 * n\",\n" +
" \"max\": \"x - n\",\n" +
" \"inclusiveMin\": \"closed\",\n" +
" \"inclusiveMinDesc\": \"区间最小值开闭值\",\n" +
" \"inclusiveMax\": \"closed\",\n" +
" \"inclusiveMaxDesc\": \"区间最大值开闭值\",\n" +
" \"scoreFormula\": \"100 - Math.abs(wholesalePrice - (x - n)) / (0.03 * n)\"\n" +
" },\n" +
" {\n" +
" \"min\": \"x - n\",\n" +
" \"max\": \"x + n\",\n" +
" \"inclusiveMin\": \"open\",\n" +
" \"inclusiveMinDesc\": \"区间最小值开闭值\",\n" +
" \"inclusiveMax\": \"closed\",\n" +
" \"inclusiveMaxDesc\": \"区间最大值开闭值\",\n" +
" \"scoreFormula\": \"100 - Math.abs(wholesalePrice - (x - n)) / (0.02 * n)\"\n" +
" }\n" +
" ],\n" +
" \"notInRangeScore\": 0,\n" +
" \"formulaDescription\": \"批发价小于零售价在不同区间的得分计算\"\n" +
" }, {\n" +
" \"condition\": \"有批发价,等于零售价\",\n" +
" \"code\": \"wholesale_price_eq_retail_price\",\n" +
" \"score\": 60\n" +
" },\n" +
" {\n" +
" \"condition\": \"无批发价或批发价大于零售价\",\n" +
" \"code\": \"wholesale_price_is_null_or_gt_retail_price\",\n" +
" \"score\": 0\n" +
" },\n" +
" {\n" +
" \"condition\": \"有批发价,无零售价\",\n" +
" \"code\": \"wholesale_price_exists_no_retail_price\",\n" +
" \"score\": 100\n" +
" }\n" +
" ],\n" +
" \"priceRangeSettings-n_value\": [\n" +
" {\n" +
" \"priceRange\": {\n" +
" \"minPrice\": 0,\n" +
" \"maxPrice\": 9999,\n" +
" \"nValue\": 300\n" +
" }\n" +
" },\n" +
" {\n" +
" \"priceRange\": {\n" +
" \"minPrice\": 10000,\n" +
" \"maxPrice\": 19999,\n" +
" \"nValue\": 750\n" +
" }\n" +
" },\n" +
" {\n" +
" \"priceRange\": {\n" +
" \"minPrice\": 20000,\n" +
" \"maxPrice\": 29999,\n" +
" \"nValue\": 1250\n" +
" }\n" +
" },\n" +
" {\n" +
" \"priceRange\": {\n" +
" \"minPrice\": 30000,\n" +
" \"maxPrice\": 49999,\n" +
" \"nValue\": 2000\n" +
" }\n" +
" },\n" +
" {\n" +
" \"priceRange\": {\n" +
" \"minPrice\": 50000,\n" +
" \"maxPrice\": 79999,\n" +
" \"nValue\": 3250\n" +
" }\n" +
" },\n" +
" {\n" +
" \"priceRange\": {\n" +
" \"minPrice\": 80000,\n" +
" \"maxPrice\": 99999,\n" +
" \"nValue\": 4500\n" +
" }\n" +
" },\n" +
" {\n" +
" \"priceRange\": {\n" +
" \"minPrice\": 100000,\n" +
" \"maxPrice\": 149999,\n" +
" \"nValue\": 6500\n" +
" }\n" +
" },\n" +
" {\n" +
" \"priceRange\": {\n" +
" \"minPrice\": 150000,\n" +
" \"maxPrice\": 199999,\n" +
" \"nValue\": 9000\n" +
" }\n" +
" },\n" +
" {\n" +
" \"priceRange\": {\n" +
" \"minPrice\": 200000,\n" +
" \"maxPrice\": 299999,\n" +
" \"nValue\": 15000\n" +
" }\n" +
" },\n" +
" {\n" +
" \"priceRange\": {\n" +
" \"minPrice\": 300000,\n" +
" \"maxPrice\": 499999,\n" +
" \"nValue\": 20000\n" +
" }\n" +
" },\n" +
" {\n" +
" \"priceRange\": {\n" +
" \"minPrice\": 500000,\n" +
" \"maxPrice\": null,\n" +
" \"nValue\": 25000\n" +
" }\n" +
" }\n" +
" ]\n" +
" }\n" +
"}\n";
}