销售提成计算引擎实现

1. 模块概述

该模块是销售提成计算系统的具体实现层,基于模板方法模式设计,实现了不同业务线(BPO、HRO)的提成计算逻辑。

image-20250709140808203

2. 目录结构

1
2
3
4
5
CopyInsertcalculation/engine/
├── AbstractCalculationEngine.java # 抽象计算引擎基类
├── CalculationEngine.java # 计算引擎接口
├── BpoCalculationEngine.java # BPO业务线计算引擎实现
└── HroCalculationEngine.java # HRO业务线计算引擎实现

3. 核心组件说明

3.1 计算引擎接口

CalculationEngine

  • 定义了计算引擎的基本契约

  • 核心方法:

    • calculation(CalculationDto): 执行业务数据核算

    • supportedProductLine(): 声明支持的产品线类型

3.2 抽象计算引擎

AbstractCalculationEngine

  • 实现了计算引擎的通用逻辑

  • 核心功能:

    1. 计算流程控制
    • calculation(CalculationDto): 总体计算流程控制
      • processCalculation(): 执行具体计算过程
      • validateCalculationDto(): 验证计算参数
    1. 数据处理
      • getDetailData(): 获取业务数据
      • getBasicSubjectValues(): 获取基础科目值
      • processTableDataBatch(): 处理单个表数据(in查询批量处理)
      • setDetailInfoBasicSubjectValues(): 设置明细数据基础科目值
    2. 结果处理
      • processCalculationResults(): 处理计算结果
      • writeCalculationDetail(): 写入计算明细
      • handleCalculationSuccess(): 处理计算成功
      • handleCalculationFailure(): 处理计算失败
      • handleErrorResults(): 处理错误结果

3.3 具体业务实现

3.3.1 BPO业务计算引擎

BpoCalculationEngine

  • 特点:

    • 继承AbstractCalculationEngine

    • 实现BPO业务特有的数据获取和处理逻辑

  • 核心方法:

    • getDetailData(): 获取BPO业务数据

    • writeDetailInfo(): 写入BPO计算结果

    • supportedProductLine(): 返回BPO产品线标识

3.3.2 HRO业务计算引擎

HroCalculationEngine

  • 特点:

    • 继承AbstractCalculationEngine

    • 实现HRO业务特有的数据获取和处理逻辑

  • 核心方法:

    • getDetailData(): 获取HRO业务数据

    • writeDetailInfo(): 写入HRO计算结果

    • supportedProductLine(): 返回HRO产品线标识

4. 核心流程说明

4.1 计算流程

  1. 参数验证

    1
    validateCalculationDto(calculationDto)
  2. 获取分布式锁

    1
    2
    3
    4
    RLock lock = redissonClient.getLock(RedisConstant.CALCULATION_LOCK_PREFIX + batch.getId());
    if (!lock.tryLock()) {
    throw new BusinessException("核算进行中,请稍后再试");
    }
  3. 创建计算任务

1
2
SysCalculationProcess process = calculationProcessService
.saveProcess(batch.getId(), calculationDto.getProductLineCode());
  1. 用户登录信息存入上下文

因计算过程是异步的,且区分于定时任务调用、用户手动调用,需在核算开始前添加用户登录信息至上下文,核算结束后生成的错误文件明细数据是用户数据权限下的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 优先从 上下文 获取用户,如果没有再从 ServletUtils 获取
LoginUser currentUser = UserContext.getUser();
if (currentUser == null) {
currentUser = ServletUtils.getLoginUser();
}
// 使用装饰器包装任务
taskExecutor.execute(UserContextDecorator.decorate(() -> {
try {
processCalculation(calculationDto, batch, process, subjects);
} catch (BusinessException e) {
handleCalculationFailure(batch, process, e);
} catch (Exception e) {
log.error("计算过程发生异常:{}", e.getMessage(), e);
handleCalculationFailure(batch, process, e);
} finally {
lock.forceUnlock();
}
}, currentUser));
}

用户装饰器类,这里使用了装饰器模式,不修改原有的代码结构的前提下,动态地在异步任务开始前设置用户上下文,异步任务结束后清理用户的上下文。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Slf4j
public class UserContextDecorator implements Runnable {
private final Runnable delegate;
private final LoginUser user;
private final String taskId;

public UserContextDecorator(Runnable delegate, LoginUser user) {
this.delegate = delegate;
this.user = user;
// 生成唯一任务ID
this.taskId = UUID.randomUUID().toString();
}

@Override
public void run() {
String threadName = Thread.currentThread().getName();
try {
log.debug("Task[{}] setting user context in thread: {}", taskId, threadName);
// 前置处理:设置用户上下文
UserContext.setUser(user);
// 执行原始任务
delegate.run();
} finally {
log.debug("Task[{}] clearing user context in thread: {}", taskId, threadName);
// 后置处理:清理用户上下文
UserContext.clear();
// 额外验证确保清理成功
if (UserContext.getUser() != null) {
log.error("Task[{}] failed to clear user context in thread: {}", taskId, threadName);
// 再次尝试清理
UserContext.clear();
}
}
}

public static Runnable decorate(Runnable task, LoginUser user) {
return new UserContextDecorator(task, user);
}
}
  1. 获取业务数据
1
List<DetailData> details = getDetailData(calculationDto)
  1. 获取基础科目值

    1
    Map<Long, Map<String, Object>> basicSubjectValues = getBasicSubjectValues(subjects, details)
  2. 执行计算

1
2
3
// 执行计算
Calculator calculator = new Calculator(subjects, details);
List<CalculationResult> results = calculator.calculate();

为了提升每次核算的速度,将每条明细数据的核算过程并行处理

1
2
3
4
5
6
7
8
9
10
/**
* 执行计算过程
* @return 计算结果列表,每个明细一条结果记录
*/
public List<CalculationResult> calculate() {
// 1. 预先验证所有公式
validateAllMethods();
// 2. 使用并行流处理明细
return details.parallelStream().map(this::calculateDetail).collect(Collectors.toList());
}

每次计算都创建一次计算上下文,计算完成后将计算结果存进内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private CalculationResult calculateDetail(DetailData detail) {
DefaultContext<String, Object> context = createCalculationContext(detail);
CalculationResult result = new CalculationResult();
result.setDetailInfoId(detail.getDetailInfoId());
result.setIsCommissionPaidThisMonth(detail.getIsCommissionPaidThisMonth());

for (SysSubject subject : subjects) {
String calculationMethod = subject.getCalculationMethod();
if (StringUtils.isBlank(calculationMethod)) {
log.warn("科目计算的公式为空: {}", subject.getSubjectName());
continue;
}

try {
// 使用缓存的验证结果
if (!methodValidationCache.getOrDefault(calculationMethod, false)) {
throw new BusinessException(String.format("科目[%s]的计算公式配置错误", subject.getSubjectName()));
}
Object computed = calculateSubject(detail, subject, context);
saveCalculateResult(result, subject, computed);
} catch (BusinessException e) {
throw e;
} catch (Exception e) {
handleCalculationError(detail, subject, result, e);
}
}

return result;
}

使用规则引擎进行计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 执行单个科目的计算
*/
private Object calculateSubject(DetailData detail, SysSubject subject,
DefaultContext<String, Object> context) {
String method = subject.getCalculationMethod();
log.debug("计算明细表 detail ID: {}, 公式: {}", detail.getDetailInfoId(), method);
Object computed = QlExpressUtils.computer(method, context);

// 保留小数位
if (subject.getDecimalPlaces() != null && computed != null) {
BigDecimal bigDecimal = Convert.toBigDecimal(computed, BigDecimal.ZERO);
computed = bigDecimal.setScale(subject.getDecimalPlaces(), RoundingMode.HALF_UP);
}

context.put(subject.getSubjectCode(), computed);
log.debug("计算后的上下文: {}", context);
return computed;
}

使用懒加载模式,只在首次使用时初始化,避免了类加载时的初始化开销,保持了线程安全性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static Object computer(String express, DefaultContext<String, Object> context) {
if (StringUtils.isBlank(express) || Objects.isNull(context)) {
return null;
}
initializeIfNeeded();
try {
// 为每个计算创建独立的上下文
DefaultContext<String, Object> localContext = new DefaultContext<>();
localContext.putAll(context);
return expressRunner.execute(express, localContext, null, true, false);
} catch (QLBizException | QLException e) {
Throwable cause = e.getCause();
if (cause != null) {
throw new RuntimeException(cause.getMessage());
} else {
throw new RuntimeException(e.getMessage());
}
} catch (Exception e) {
log.error("计算运算公式:{} 失败,参数为:{}", express, JSONObject.toJSONString(context));
log.error("计算运算失败", e);
throw new RuntimeException(e.getMessage());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* 使用懒加载单例模式
*/
private static void initializeIfNeeded() {
if (expressRunner == null) {
synchronized (LOCK) {
if (expressRunner == null) {
// 优先使用Spring容器中的bean
ExpressRunner runner = SpringUtils.getBean(ExpressRunner.class);
if (runner == null) {
runner = new QlExpressConfig().expressRunner();
}

Map<String, String> defineClass = new QlExpressConfig().selfDefineClass(runner);

ExpressParse parse = SpringUtils.getBean(ExpressParse.class);
if (parse == null) {
parse = new QlExpressConfig().expressParse(runner);
}

// 所有初始化完成后才赋值给静态变量
expressRunner = runner;
selfDefineClass = defineClass;
expressParse = parse;
}
}
}
}
  1. 处理结果
  • 将批次id写入提成明细

  • 写入科目计算结果

  • 写入销售提成金额

  • 处理错误信息,生成错误文件

    1
    2
    3
    4
    5
    6
    7
    // 第一个事务:处理核心计算结果
    transactionTemplate.executeWithoutResult(status -> {
    writeDetailInfo(results, batch);
    writeCalculationDetail(results, batch);
    writeSaleCommissionDetail(results, batch);
    handleErrorResults(results, process, calculationDto.getSalaryMonth());
    });
  • 实时生成提成统计,并更改任务状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
try {
// 第二个事务:处理统计数据
transactionTemplate.executeWithoutResult(status -> {
generateStatisticData(calculationDto);
});
// 第三个事务:更新状态
transactionTemplate.executeWithoutResult(status -> {
handleCalculationSuccess(batch, process, subjects);
});
} catch (Exception e) {
log.error("生成统计数据或更新状态失败", e);
// 更新为部分完成状态
handlePartialSuccess(batch, process, subjects, e);
}

4.2 错误处理

  • 计算失败处理
    1
    handleCalculationFailure(batch, process, exception)
  • 结果错误处理
    1
    handleErrorResults(results, process)

5. 扩展指南

5.1 添加新的业务线

  1. 创建新的业务线计算引擎类,继承AbstractCalculationEngine

  2. 实现必要的抽象方法:

    • getDetailData()

    • writeDetailInfo()

    • supportedProductLine()

  3. 根据业务需求,可能需要重写其他方法

5.2 修改计算逻辑

  1. 核心计算逻辑在processCalculation()方法中
  2. 基础数据处理逻辑在getBasicSubjectValues()方法中
  3. 结果处理逻辑在processCalculationResults()方法中