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

2. 目录结构
1 2 3 4 5
| CopyInsertcalculation/engine/ ├── AbstractCalculationEngine.java # 抽象计算引擎基类 ├── CalculationEngine.java # 计算引擎接口 ├── BpoCalculationEngine.java # BPO业务线计算引擎实现 └── HroCalculationEngine.java # HRO业务线计算引擎实现
|
3. 核心组件说明
3.1 计算引擎接口
CalculationEngine
3.2 抽象计算引擎
AbstractCalculationEngine
实现了计算引擎的通用逻辑
核心功能:
- 计算流程控制
calculation(CalculationDto)
: 总体计算流程控制
processCalculation()
: 执行具体计算过程
validateCalculationDto()
: 验证计算参数
- 数据处理
getDetailData()
: 获取业务数据
getBasicSubjectValues()
: 获取基础科目值
processTableDataBatch()
: 处理单个表数据(in查询批量处理)
setDetailInfoBasicSubjectValues()
: 设置明细数据基础科目值
- 结果处理
processCalculationResults()
: 处理计算结果
writeCalculationDetail()
: 写入计算明细
handleCalculationSuccess()
: 处理计算成功
handleCalculationFailure()
: 处理计算失败
handleErrorResults()
: 处理错误结果
3.3 具体业务实现
3.3.1 BPO业务计算引擎
BpoCalculationEngine
特点:
核心方法:
getDetailData()
: 获取BPO业务数据
writeDetailInfo()
: 写入BPO计算结果
supportedProductLine()
: 返回BPO产品线标识
3.3.2 HRO业务计算引擎
HroCalculationEngine
特点:
核心方法:
getDetailData()
: 获取HRO业务数据
writeDetailInfo()
: 写入HRO计算结果
supportedProductLine()
: 返回HRO产品线标识
4. 核心流程说明
4.1 计算流程
参数验证
1
| validateCalculationDto(calculationDto)
|
获取分布式锁
1 2 3 4
| RLock lock = redissonClient.getLock(RedisConstant.CALCULATION_LOCK_PREFIX + batch.getId()); if (!lock.tryLock()) { throw new BusinessException("核算进行中,请稍后再试"); }
|
创建计算任务
1 2
| SysCalculationProcess process = calculationProcessService .saveProcess(batch.getId(), calculationDto.getProductLineCode());
|
- 用户登录信息存入上下文
因计算过程是异步的,且区分于定时任务调用、用户手动调用,需在核算开始前添加用户登录信息至上下文,核算结束后生成的错误文件明细数据是用户数据权限下的数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 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; 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
| List<DetailData> details = getDetailData(calculationDto)
|
获取基础科目值
1
| Map<Long, Map<String, Object>> basicSubjectValues = getBasicSubjectValues(subjects, details)
|
执行计算
1 2 3
| Calculator calculator = new Calculator(subjects, details); List<CalculationResult> results = calculator.calculate();
|
为了提升每次核算的速度,将每条明细数据的核算过程并行处理
1 2 3 4 5 6 7 8 9 10
|
public List<CalculationResult> calculate() { validateAllMethods(); 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) { 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; } } } }
|
- 处理结果
将批次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 添加新的业务线
创建新的业务线计算引擎类,继承AbstractCalculationEngine
实现必要的抽象方法:
getDetailData()
writeDetailInfo()
supportedProductLine()
根据业务需求,可能需要重写其他方法
5.2 修改计算逻辑
- 核心计算逻辑在
processCalculation()
方法中
- 基础数据处理逻辑在
getBasicSubjectValues()
方法中
- 结果处理逻辑在
processCalculationResults()
方法中