Java + Groovy计费引擎详解:从入门到实战
前言
计费引擎是电信、金融、SaaS等行业的核心系统之一,需要处理复杂多变的计费规则。传统的Java硬编码方式难以应对频繁变化的业务规则,而Groovy作为JVM上的动态脚本语言,与Java无缝集成,可以实现规则的热加载和动态配置。本文将深入讲解如何使用Java + Groovy构建一个灵活、高性能的计费引擎。
一、计费引擎概述
1.1 什么是计费引擎
计费引擎(Billing Engine)是一个负责根据业务规则计算费用的核心组件,广泛应用于:
计费引擎应用场景
┌─────────────────────────────────────────┐
│ 电信行业 │
│ - 通话计费 │
│ - 流量计费 │
│ - 套餐计费 │
├─────────────────────────────────────────┤
│ 金融行业 │
│ - 手续费计算 │
│ - 利息计算 │
│ - 分期计费 │
├─────────────────────────────────────────┤
│ 云计算/SaaS │
│ - 按量计费 │
│ - 订阅计费 │
│ - 资源使用计费 │
├─────────────────────────────────────────┤
│ 电商行业 │
│ - 优惠计算 │
│ - 运费计算 │
│ - 会员折扣 │
└─────────────────────────────────────────┘
1.2 为什么选择Java + Groovy
Java + Groovy优势
┌─────────────────────────────────────────┐
│ Java优势 │
│ ✓ 高性能、稳定可靠 │
│ ✓ 丰富的生态系统 │
│ ✓ 强类型、编译期检查 │
│ ✓ 企业级框架支持 │
├─────────────────────────────────────────┤
│ Groovy优势 │
│ ✓ 动态语言、灵活简洁 │
│ ✓ 与Java无缝集成 │
│ ✓ 支持脚本热加载 │
│ ✓ DSL友好、易读易写 │
├─────────────────────────────────────────┤
│ 组合优势 │
│ ✓ Java负责框架和核心逻辑 │
│ ✓ Groovy负责动态规则和配置 │
│ ✓ 规则变更无需重启服务 │
│ ✓ 业务人员可直接编写规则 │
└─────────────────────────────────────────┘
1.3 计费引擎架构
markdown
计费引擎架构图
┌─────────────────────────────────────────┐
│ 业务层(API) │
│ - 计费请求接口 │
│ - 规则管理接口 │
└──────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────┐
│ 计费引擎核心(Java) │
│ ┌───────────────────────────────────┐ │
│ │ 规则加载器 │ │
│ │ - 从数据库加载规则 │ │
│ │ - Groovy脚本编译 │ │
│ │ - 规则缓存管理 │ │
│ └───────────────────────────────────┘ │
│ ┌───────────────────────────────────┐ │
│ │ 计费执行器 │ │
│ │ - 规则匹配 │ │
│ │ - 计费计算 │ │
│ │ - 结果聚合 │ │
│ └───────────────────────────────────┘ │
└──────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────┐
│ 规则层(Groovy脚本) │
│ - 基础规则 │
│ - 套餐规则 │
│ - 优惠规则 │
│ - 阶梯计费规则 │
└──────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────┐
│ 数据层 │
│ - 规则配置表 │
│ - 计费记录表 │
│ - 用户套餐表 │
└─────────────────────────────────────────┘
二、环境搭建
2.1 Maven依赖
xml
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Groovy -->
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>3.0.9</version>
<type>pom</type>
</dependency>
<!-- Groovy模板引擎 -->
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-templates</artifactId>
<version>3.0.9</version>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- MySQL -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
2.2 项目结构
bash
billing-engine/
├── src/main/java/
│ └── com/example/billing/
│ ├── BillingApplication.java
│ ├── controller/
│ │ ├── BillingController.java
│ │ └── RuleController.java
│ ├── service/
│ │ ├── BillingService.java
│ │ └── RuleService.java
│ ├── engine/
│ │ ├── BillingEngine.java
│ │ ├── RuleLoader.java
│ │ └── RuleExecutor.java
│ ├── model/
│ │ ├── BillingRequest.java
│ │ ├── BillingResult.java
│ │ └── Rule.java
│ └── config/
│ └── GroovyConfig.java
├── src/main/resources/
│ ├── application.yml
│ └── rules/ # Groovy规则脚本目录
│ ├── base_rule.groovy
│ ├── package_rule.groovy
│ └── discount_rule.groovy
└── pom.xml
2.3 配置文件
yaml
# application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/billing?useSSL=false&serverTimezone=UTC
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
host: localhost
port: 6379
# 计费引擎配置
billing:
rule:
path: classpath:rules/ # Groovy脚本路径
cache-enabled: true # 启用规则缓存
cache-ttl: 3600 # 缓存过期时间(秒)
hot-reload: true # 热加载开关
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
三、核心组件实现
3.1 数据模型
java
/**
* 计费请求
*/
@Data
@Builder
public class BillingRequest {
/** 用户ID */
private Long userId;
/** 业务类型(通话、流量、短信等) */
private String serviceType;
/** 使用量 */
private BigDecimal usage;
/** 使用时长(秒) */
private Long duration;
/** 计费时间 */
private LocalDateTime billingTime;
/** 扩展参数 */
private Map<String, Object> extraParams;
}
/**
* 计费结果
*/
@Data
@Builder
public class BillingResult {
/** 用户ID */
private Long userId;
/** 原始费用 */
private BigDecimal originalFee;
/** 折扣金额 */
private BigDecimal discountAmount;
/** 最终费用 */
private BigDecimal finalFee;
/** 应用的规则列表 */
private List<String> appliedRules;
/** 计费明细 */
private List<BillingDetail> details;
/** 计费时间 */
private LocalDateTime billingTime;
}
/**
* 计费明细
*/
@Data
@AllArgsConstructor
public class BillingDetail {
/** 规则名称 */
private String ruleName;
/** 描述 */
private String description;
/** 金额 */
private BigDecimal amount;
}
/**
* 计费规则实体
*/
@Data
@TableName("billing_rule")
public class Rule {
@TableId(type = IdType.AUTO)
private Long id;
/** 规则名称 */
private String ruleName;
/** 规则类型(BASE/PACKAGE/DISCOUNT/LADDER) */
private String ruleType;
/** 业务类型 */
private String serviceType;
/** 规则脚本(Groovy) */
private String ruleScript;
/** 优先级(数字越大优先级越高) */
private Integer priority;
/** 是否启用 */
private Boolean enabled;
/** 生效时间 */
private LocalDateTime effectiveTime;
/** 失效时间 */
private LocalDateTime expiryTime;
/** 创建时间 */
private LocalDateTime createdAt;
/** 更新时间 */
private LocalDateTime updatedAt;
}
/**
* 用户套餐
*/
@Data
@TableName("user_package")
public class UserPackage {
@TableId(type = IdType.AUTO)
private Long id;
/** 用户ID */
private Long userId;
/** 套餐类型 */
private String packageType;
/** 套餐名称 */
private String packageName;
/** 剩余量 */
private BigDecimal remainingQuota;
/** 套餐总量 */
private BigDecimal totalQuota;
/** 生效时间 */
private LocalDateTime effectiveTime;
/** 失效时间 */
private LocalDateTime expiryTime;
}
3.2 Groovy配置
java
/**
* Groovy配置类
*/
@Configuration
public class GroovyConfig {
/**
* 配置GroovyShell
*/
@Bean
public GroovyShell groovyShell() {
CompilerConfiguration config = new CompilerConfiguration();
config.setScriptBaseClass(DelegatingScript.class.getName());
return new GroovyShell(
Thread.currentThread().getContextClassLoader(),
new Binding(),
config
);
}
/**
* 配置GroovyClassLoader
*/
@Bean
public GroovyClassLoader groovyClassLoader() {
CompilerConfiguration config = new CompilerConfiguration();
config.setSourceEncoding("UTF-8");
return new GroovyClassLoader(
Thread.currentThread().getContextClassLoader(),
config
);
}
}
3.3 规则加载器
java
/**
* 规则加载器
*/
@Component
@Slf4j
public class RuleLoader {
@Autowired
private GroovyShell groovyShell;
@Autowired
private GroovyClassLoader groovyClassLoader;
@Autowired
private RuleMapper ruleMapper;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Value("${billing.rule.cache-enabled:true}")
private boolean cacheEnabled;
@Value("${billing.rule.cache-ttl:3600}")
private long cacheTtl;
private static final String RULE_CACHE_PREFIX = "billing:rule:";
/**
* 加载规则脚本
*/
public Script loadRule(Long ruleId) {
// 从缓存获取
if (cacheEnabled) {
Script cached = getCachedRule(ruleId);
if (cached != null) {
return cached;
}
}
// 从数据库加载
Rule rule = ruleMapper.selectById(ruleId);
if (rule == null || !rule.getEnabled()) {
throw new RuntimeException("规则不存在或已禁用: " + ruleId);
}
// 编译Groovy脚本
Script script = compileScript(rule.getRuleScript());
// 缓存规则
if (cacheEnabled) {
cacheRule(ruleId, script);
}
return script;
}
/**
* 加载指定类型的所有规则
*/
public List<Script> loadRulesByType(String serviceType, String ruleType) {
QueryWrapper<Rule> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("service_type", serviceType)
.eq("rule_type", ruleType)
.eq("enabled", true)
.le("effective_time", LocalDateTime.now())
.ge("expiry_time", LocalDateTime.now())
.orderByDesc("priority");
List<Rule> rules = ruleMapper.selectList(queryWrapper);
return rules.stream()
.map(rule -> compileScript(rule.getRuleScript()))
.collect(Collectors.toList());
}
/**
* 编译Groovy脚本
*/
private Script compileScript(String scriptText) {
try {
return groovyShell.parse(scriptText);
} catch (Exception e) {
log.error("Groovy脚本编译失败", e);
throw new RuntimeException("脚本编译失败: " + e.getMessage());
}
}
/**
* 从缓存获取规则
*/
private Script getCachedRule(Long ruleId) {
String cacheKey = RULE_CACHE_PREFIX + ruleId;
return (Script) redisTemplate.opsForValue().get(cacheKey);
}
/**
* 缓存规则
*/
private void cacheRule(Long ruleId, Script script) {
String cacheKey = RULE_CACHE_PREFIX + ruleId;
redisTemplate.opsForValue().set(
cacheKey,
script,
cacheTtl,
TimeUnit.SECONDS
);
}
/**
* 清除规则缓存
*/
public void clearRuleCache(Long ruleId) {
String cacheKey = RULE_CACHE_PREFIX + ruleId;
redisTemplate.delete(cacheKey);
}
/**
* 清除所有规则缓存
*/
public void clearAllRuleCache() {
Set<String> keys = redisTemplate.keys(RULE_CACHE_PREFIX + "*");
if (keys != null && !keys.isEmpty()) {
redisTemplate.delete(keys);
}
}
}
3.4 规则执行器
java
/**
* 规则执行器
*/
@Component
@Slf4j
public class RuleExecutor {
@Autowired
private RuleLoader ruleLoader;
/**
* 执行单个规则
*/
public Object executeRule(Script script, Map<String, Object> context) {
try {
// 设置上下文变量
Binding binding = new Binding(context);
script.setBinding(binding);
// 执行脚本
Object result = script.run();
log.debug("规则执行成功,结果: {}", result);
return result;
} catch (Exception e) {
log.error("规则执行失败", e);
throw new RuntimeException("规则执行失败: " + e.getMessage());
}
}
/**
* 执行多个规则并聚合结果
*/
public List<Object> executeRules(List<Script> scripts, Map<String, Object> context) {
return scripts.stream()
.map(script -> executeRule(script, context))
.collect(Collectors.toList());
}
/**
* 执行规则链(规则间有依赖)
*/
public Object executeRuleChain(List<Script> scripts, Map<String, Object> context) {
Object result = null;
for (Script script : scripts) {
// 将上一个规则的结果放入上下文
if (result != null) {
context.put("previousResult", result);
}
result = executeRule(script, context);
// 如果规则返回false,中断执行
if (result instanceof Boolean && !(Boolean) result) {
log.debug("规则链中断");
break;
}
}
return result;
}
}
3.5 计费引擎核心
java
/**
* 计费引擎核心
*/
@Service
@Slf4j
public class BillingEngine {
@Autowired
private RuleLoader ruleLoader;
@Autowired
private RuleExecutor ruleExecutor;
@Autowired
private UserPackageMapper userPackageMapper;
/**
* 执行计费
*/
public BillingResult calculate(BillingRequest request) {
log.info("开始计费,用户: {}, 业务类型: {}",
request.getUserId(), request.getServiceType());
// 构建计费上下文
Map<String, Object> context = buildContext(request);
// 加载规则
List<Script> rules = ruleLoader.loadRulesByType(
request.getServiceType(),
null
);
if (rules.isEmpty()) {
log.warn("未找到适用的计费规则");
return createDefaultResult(request);
}
// 初始化计费结果
BillingResult result = BillingResult.builder()
.userId(request.getUserId())
.originalFee(BigDecimal.ZERO)
.discountAmount(BigDecimal.ZERO)
.finalFee(BigDecimal.ZERO)
.appliedRules(new ArrayList<>())
.details(new ArrayList<>())
.billingTime(LocalDateTime.now())
.build();
// 执行规则
for (Script rule : rules) {
try {
context.put("result", result);
ruleExecutor.executeRule(rule, context);
} catch (Exception e) {
log.error("规则执行失败", e);
}
}
// 计算最终费用
result.setFinalFee(
result.getOriginalFee().subtract(result.getDiscountAmount())
);
log.info("计费完成,最终费用: {}", result.getFinalFee());
return result;
}
/**
* 构建计费上下文
*/
private Map<String, Object> buildContext(BillingRequest request) {
Map<String, Object> context = new HashMap<>();
// 基础信息
context.put("userId", request.getUserId());
context.put("serviceType", request.getServiceType());
context.put("usage", request.getUsage());
context.put("duration", request.getDuration());
context.put("billingTime", request.getBillingTime());
// 扩展参数
if (request.getExtraParams() != null) {
context.putAll(request.getExtraParams());
}
// 查询用户套餐
QueryWrapper<UserPackage> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_id", request.getUserId())
.eq("package_type", request.getServiceType())
.le("effective_time", LocalDateTime.now())
.ge("expiry_time", LocalDateTime.now());
UserPackage userPackage = userPackageMapper.selectOne(queryWrapper);
context.put("userPackage", userPackage);
// 工具方法
context.put("log", log);
context.put("Math", Math.class);
context.put("BigDecimal", BigDecimal.class);
return context;
}
/**
* 创建默认结果
*/
private BillingResult createDefaultResult(BillingRequest request) {
return BillingResult.builder()
.userId(request.getUserId())
.originalFee(BigDecimal.ZERO)
.discountAmount(BigDecimal.ZERO)
.finalFee(BigDecimal.ZERO)
.appliedRules(new ArrayList<>())
.details(new ArrayList<>())
.billingTime(LocalDateTime.now())
.build();
}
}
四、Groovy规则脚本
4.1 基础计费规则
groovy
/**
* 基础通话计费规则
* 文件: base_call_rule.groovy
*/
import java.math.BigDecimal
import java.math.RoundingMode
// 获取上下文变量
def duration = duration as Long
def result = result
// 计费单价(元/分钟)
def unitPrice = new BigDecimal("0.15")
// 计算通话分钟数(不足1分钟按1分钟计算)
def minutes = (duration + 59) / 60
// 计算费用
def fee = unitPrice.multiply(new BigDecimal(minutes))
.setScale(2, RoundingMode.HALF_UP)
// 更新结果
result.originalFee = result.originalFee.add(fee)
result.appliedRules.add("基础通话计费")
result.details.add(
new com.example.billing.model.BillingDetail(
"基础通话计费",
"${minutes}分钟 × ${unitPrice}元/分钟",
fee
)
)
log.info("基础计费: ${fee}元")
return true
4.2 套餐优惠规则
groovy
/**
* 套餐优惠规则
* 文件: package_discount_rule.groovy
*/
import java.math.BigDecimal
// 获取用户套餐
def userPackage = userPackage
if (userPackage == null) {
log.info("用户无套餐,跳过套餐优惠")
return true
}
// 检查套餐余量
def usage = usage as BigDecimal
def remaining = userPackage.remainingQuota
if (remaining.compareTo(BigDecimal.ZERO) <= 0) {
log.info("套餐余量已用完")
return true
}
// 计算套餐内使用量
def packageUsage = usage.min(remaining)
def overageUsage = usage.subtract(packageUsage)
// 套餐内免费
if (packageUsage.compareTo(BigDecimal.ZERO) > 0) {
result.appliedRules.add("套餐内免费")
result.details.add(
new com.example.billing.model.BillingDetail(
"套餐内免费",
"套餐内使用${packageUsage}分钟",
BigDecimal.ZERO
)
)
// 更新套餐余量
userPackage.remainingQuota = remaining.subtract(packageUsage)
}
// 超出套餐部分
if (overageUsage.compareTo(BigDecimal.ZERO) > 0) {
def overagePrice = new BigDecimal("0.20")
def overageFee = overageUsage.multiply(overagePrice)
result.originalFee = result.originalFee.add(overageFee)
result.details.add(
new com.example.billing.model.BillingDetail(
"套餐外计费",
"超出${overageUsage}分钟 × ${overagePrice}元/分钟",
overageFee
)
)
}
return true
4.3 阶梯计费规则
groovy
/**
* 阶梯计费规则
* 文件: ladder_billing_rule.groovy
*/
import java.math.BigDecimal
import java.math.RoundingMode
// 阶梯定义
def ladders = [
[limit: 100, price: 0.10], // 0-100分钟: 0.10元/分钟
[limit: 300, price: 0.08], // 101-300分钟: 0.08元/分钟
[limit: 500, price: 0.06], // 301-500分钟: 0.06元/分钟
[limit: null, price: 0.05] // 500分钟以上: 0.05元/分钟
]
def usage = usage as BigDecimal
def totalFee = BigDecimal.ZERO
def remainingUsage = usage
def usedInLadder = BigDecimal.ZERO
for (ladder in ladders) {
if (remainingUsage.compareTo(BigDecimal.ZERO) <= 0) {
break
}
def limit = ladder.limit ? new BigDecimal(ladder.limit) : null
def price = new BigDecimal(ladder.price as String)
// 计算当前阶梯的使用量
if (limit == null) {
// 最后一个阶梯,无上限
usedInLadder = remainingUsage
} else {
// 当前阶梯剩余量
def ladderRemaining = limit.subtract(
usage.subtract(remainingUsage)
)
usedInLadder = remainingUsage.min(ladderRemaining)
}
// 计算当前阶梯费用
def ladderFee = usedInLadder.multiply(price)
.setScale(2, RoundingMode.HALF_UP)
totalFee = totalFee.add(ladderFee)
remainingUsage = remainingUsage.subtract(usedInLadder)
result.details.add(
new com.example.billing.model.BillingDetail(
"阶梯${ladders.indexOf(ladder) + 1}",
"${usedInLadder}分钟 × ${price}元/分钟",
ladderFee
)
)
}
result.originalFee = result.originalFee.add(totalFee)
result.appliedRules.add("阶梯计费")
log.info("阶梯计费总额: ${totalFee}元")
return true
4.4 时段优惠规则
groovy
/**
* 时段优惠规则
* 文件: time_discount_rule.groovy
*/
import java.time.LocalTime
// 获取计费时间
def billingTime = billingTime
def hour = billingTime.hour
// 定义优惠时段和折扣
def timeRanges = [
[start: 0, end: 6, discount: 0.5, name: "深夜半价"],
[start: 22, end: 24, discount: 0.7, name: "夜间7折"]
]
// 检查当前时间是否在优惠时段
for (timeRange in timeRanges) {
if (hour >= timeRange.start && hour < timeRange.end) {
def discount = new BigDecimal(timeRange.discount as String)
def discountAmount = result.originalFee
.multiply(BigDecimal.ONE.subtract(discount))
result.discountAmount = result.discountAmount.add(discountAmount)
result.appliedRules.add(timeRange.name)
result.details.add(
new com.example.billing.model.BillingDetail(
timeRange.name,
"优惠${(1 - timeRange.discount) * 100}%",
discountAmount.negate()
)
)
log.info("应用时段优惠: ${timeRange.name}, 优惠${discountAmount}元")
break
}
}
return true
4.5 会员折扣规则
groovy
/**
* 会员折扣规则
* 文件: vip_discount_rule.groovy
*/
import java.math.BigDecimal
// 获取用户会员等级
def vipLevel = extraParams?.vipLevel ?: "NORMAL"
// 会员折扣配置
def vipDiscounts = [
NORMAL: 1.0, // 普通用户无折扣
SILVER: 0.95, // 银卡95折
GOLD: 0.90, // 金卡9折
PLATINUM: 0.85, // 白金卡85折
DIAMOND: 0.80 // 钻石卡8折
]
def discountRate = new BigDecimal(
vipDiscounts[vipLevel]?.toString() ?: "1.0"
)
// 计算折扣金额
if (discountRate.compareTo(BigDecimal.ONE) < 0) {
def discountAmount = result.originalFee
.multiply(BigDecimal.ONE.subtract(discountRate))
result.discountAmount = result.discountAmount.add(discountAmount)
result.appliedRules.add("${vipLevel}会员折扣")
result.details.add(
new com.example.billing.model.BillingDetail(
"${vipLevel}会员折扣",
"${(1 - discountRate) * 100}%优惠",
discountAmount.negate()
)
)
log.info("应用会员折扣: ${vipLevel}, 折扣率${discountRate}")
}
return true
五、服务层实现
5.1 计费服务
java
/**
* 计费服务
*/
@Service
@Slf4j
public class BillingService {
@Autowired
private BillingEngine billingEngine;
@Autowired
private BillingRecordMapper billingRecordMapper;
/**
* 执行计费
*/
@Transactional(rollbackFor = Exception.class)
public BillingResult doBilling(BillingRequest request) {
// 参数校验
validateRequest(request);
// 执行计费
BillingResult result = billingEngine.calculate(request);
// 保存计费记录
saveBillingRecord(request, result);
return result;
}
/**
* 批量计费
*/
public List<BillingResult> batchBilling(List<BillingRequest> requests) {
return requests.stream()
.map(this::doBilling)
.collect(Collectors.toList());
}
/**
* 异步计费
*/
@Async
public CompletableFuture<BillingResult> asyncBilling(BillingRequest request) {
return CompletableFuture.completedFuture(doBilling(request));
}
/**
* 试算(不保存记录)
*/
public BillingResult trialBilling(BillingRequest request) {
validateRequest(request);
return billingEngine.calculate(request);
}
/**
* 参数校验
*/
private void validateRequest(BillingRequest request) {
if (request.getUserId() == null) {
throw new IllegalArgumentException("用户ID不能为空");
}
if (request.getServiceType() == null) {
throw new IllegalArgumentException("业务类型不能为空");
}
if (request.getUsage() == null ||
request.getUsage().compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("使用量必须大于0");
}
}
/**
* 保存计费记录
*/
private void saveBillingRecord(BillingRequest request, BillingResult result) {
BillingRecord record = new BillingRecord();
record.setUserId(request.getUserId());
record.setServiceType(request.getServiceType());
record.setUsage(request.getUsage());
record.setDuration(request.getDuration());
record.setOriginalFee(result.getOriginalFee());
record.setDiscountAmount(result.getDiscountAmount());
record.setFinalFee(result.getFinalFee());
record.setAppliedRules(String.join(",", result.getAppliedRules()));
record.setBillingTime(result.getBillingTime());
record.setCreatedAt(LocalDateTime.now());
billingRecordMapper.insert(record);
}
/**
* 查询计费记录
*/
public Page<BillingRecord> queryBillingRecords(
Long userId,
String serviceType,
LocalDateTime startTime,
LocalDateTime endTime,
int page,
int size) {
Page<BillingRecord> pageParam = new Page<>(page, size);
QueryWrapper<BillingRecord> queryWrapper = new QueryWrapper<>();
if (userId != null) {
queryWrapper.eq("user_id", userId);
}
if (serviceType != null) {
queryWrapper.eq("service_type", serviceType);
}
if (startTime != null) {
queryWrapper.ge("billing_time", startTime);
}
if (endTime != null) {
queryWrapper.le("billing_time", endTime);
}
queryWrapper.orderByDesc("billing_time");
return billingRecordMapper.selectPage(pageParam, queryWrapper);
}
/**
* 统计用户消费
*/
public Map<String, Object> statisticsUserConsumption(
Long userId,
LocalDateTime startTime,
LocalDateTime endTime) {
QueryWrapper<BillingRecord> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_id", userId)
.ge("billing_time", startTime)
.le("billing_time", endTime);
List<BillingRecord> records = billingRecordMapper.selectList(queryWrapper);
BigDecimal totalFee = records.stream()
.map(BillingRecord::getFinalFee)
.reduce(BigDecimal.ZERO, BigDecimal::add);
Map<String, BigDecimal> feeByType = records.stream()
.collect(Collectors.groupingBy(
BillingRecord::getServiceType,
Collectors.reducing(
BigDecimal.ZERO,
BillingRecord::getFinalFee,
BigDecimal::add
)
));
Map<String, Object> result = new HashMap<>();
result.put("totalFee", totalFee);
result.put("totalCount", records.size());
result.put("feeByType", feeByType);
return result;
}
}
5.2 规则服务
java
/**
* 规则管理服务
*/
@Service
@Slf4j
public class RuleService {
@Autowired
private RuleMapper ruleMapper;
@Autowired
private RuleLoader ruleLoader;
/**
* 创建规则
*/
@Transactional(rollbackFor = Exception.class)
public Rule createRule(Rule rule) {
// 校验规则脚本
validateRuleScript(rule.getRuleScript());
rule.setCreatedAt(LocalDateTime.now());
rule.setUpdatedAt(LocalDateTime.now());
ruleMapper.insert(rule);
log.info("规则创建成功: {}", rule.getRuleName());
return rule;
}
/**
* 更新规则
*/
@Transactional(rollbackFor = Exception.class)
public Rule updateRule(Long ruleId, Rule rule) {
Rule existing = ruleMapper.selectById(ruleId);
if (existing == null) {
throw new RuntimeException("规则不存在: " + ruleId);
}
// 校验规则脚本
if (rule.getRuleScript() != null) {
validateRuleScript(rule.getRuleScript());
}
rule.setId(ruleId);
rule.setUpdatedAt(LocalDateTime.now());
ruleMapper.updateById(rule);
// 清除缓存
ruleLoader.clearRuleCache(ruleId);
log.info("规则更新成功: {}", ruleId);
return ruleMapper.selectById(ruleId);
}
/**
* 删除规则
*/
@Transactional(rollbackFor = Exception.class)
public void deleteRule(Long ruleId) {
ruleMapper.deleteById(ruleId);
ruleLoader.clearRuleCache(ruleId);
log.info("规则删除成功: {}", ruleId);
}
/**
* 启用/禁用规则
*/
@Transactional(rollbackFor = Exception.class)
public void toggleRule(Long ruleId, boolean enabled) {
Rule rule = new Rule();
rule.setId(ruleId);
rule.setEnabled(enabled);
rule.setUpdatedAt(LocalDateTime.now());
ruleMapper.updateById(rule);
ruleLoader.clearRuleCache(ruleId);
log.info("规则{}成功: {}", enabled ? "启用" : "禁用", ruleId);
}
/**
* 查询规则列表
*/
public Page<Rule> queryRules(
String serviceType,
String ruleType,
Boolean enabled,
int page,
int size) {
Page<Rule> pageParam = new Page<>(page, size);
QueryWrapper<Rule> queryWrapper = new QueryWrapper<>();
if (serviceType != null) {
queryWrapper.eq("service_type", serviceType);
}
if (ruleType != null) {
queryWrapper.eq("rule_type", ruleType);
}
if (enabled != null) {
queryWrapper.eq("enabled", enabled);
}
queryWrapper.orderByDesc("priority", "created_at");
return ruleMapper.selectPage(pageParam, queryWrapper);
}
/**
* 测试规则
*/
public Object testRule(String ruleScript, Map<String, Object> context) {
validateRuleScript(ruleScript);
try {
GroovyShell shell = new GroovyShell();
Script script = shell.parse(ruleScript);
Binding binding = new Binding(context);
script.setBinding(binding);
return script.run();
} catch (Exception e) {
log.error("规则测试失败", e);
throw new RuntimeException("规则测试失败: " + e.getMessage());
}
}
/**
* 校验规则脚本
*/
private void validateRuleScript(String ruleScript) {
if (ruleScript == null || ruleScript.trim().isEmpty()) {
throw new IllegalArgumentException("规则脚本不能为空");
}
try {
GroovyShell shell = new GroovyShell();
shell.parse(ruleScript);
} catch (Exception e) {
throw new IllegalArgumentException("规则脚本语法错误: " + e.getMessage());
}
}
/**
* 导入规则
*/
@Transactional(rollbackFor = Exception.class)
public void importRules(List<Rule> rules) {
for (Rule rule : rules) {
validateRuleScript(rule.getRuleScript());
rule.setCreatedAt(LocalDateTime.now());
rule.setUpdatedAt(LocalDateTime.now());
ruleMapper.insert(rule);
}
log.info("规则导入成功,数量: {}", rules.size());
}
/**
* 导出规则
*/
public List<Rule> exportRules(String serviceType, String ruleType) {
QueryWrapper<Rule> queryWrapper = new QueryWrapper<>();
if (serviceType != null) {
queryWrapper.eq("service_type", serviceType);
}
if (ruleType != null) {
queryWrapper.eq("rule_type", ruleType);
}
return ruleMapper.selectList(queryWrapper);
}
}
六、控制器层
6.1 计费控制器
java
/**
* 计费控制器
*/
@RestController
@RequestMapping("/api/billing")
@Slf4j
public class BillingController {
@Autowired
private BillingService billingService;
/**
* 执行计费
*/
@PostMapping("/calculate")
public Result<BillingResult> calculate(@RequestBody BillingRequest request) {
try {
BillingResult result = billingService.doBilling(request);
return Result.success(result);
} catch (Exception e) {
log.error("计费失败", e);
return Result.error("计费失败: " + e.getMessage());
}
}
/**
* 批量计费
*/
@PostMapping("/batch-calculate")
public Result<List<BillingResult>> batchCalculate(
@RequestBody List<BillingRequest> requests) {
try {
List<BillingResult> results = billingService.batchBilling(requests);
return Result.success(results);
} catch (Exception e) {
log.error("批量计费失败", e);
return Result.error("批量计费失败: " + e.getMessage());
}
}
/**
* 试算
*/
@PostMapping("/trial")
public Result<BillingResult> trial(@RequestBody BillingRequest request) {
try {
BillingResult result = billingService.trialBilling(request);
return Result.success(result);
} catch (Exception e) {
log.error("试算失败", e);
return Result.error("试算失败: " + e.getMessage());
}
}
/**
* 查询计费记录
*/
@GetMapping("/records")
public Result<Page<BillingRecord>> queryRecords(
@RequestParam(required = false) Long userId,
@RequestParam(required = false) String serviceType,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
LocalDateTime startTime,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
LocalDateTime endTime,
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size) {
Page<BillingRecord> records = billingService.queryBillingRecords(
userId, serviceType, startTime, endTime, page, size
);
return Result.success(records);
}
/**
* 统计用户消费
*/
@GetMapping("/statistics/{userId}")
public Result<Map<String, Object>> statistics(
@PathVariable Long userId,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
LocalDateTime startTime,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
LocalDateTime endTime) {
Map<String, Object> stats = billingService.statisticsUserConsumption(
userId, startTime, endTime
);
return Result.success(stats);
}
}
6.2 规则控制器
java
/**
* 规则管理控制器
*/
@RestController
@RequestMapping("/api/rules")
@Slf4j
public class RuleController {
@Autowired
private RuleService ruleService;
/**
* 创建规则
*/
@PostMapping
public Result<Rule> createRule(@RequestBody Rule rule) {
try {
Rule created = ruleService.createRule(rule);
return Result.success(created);
} catch (Exception e) {
log.error("创建规则失败", e);
return Result.error("创建规则失败: " + e.getMessage());
}
}
/**
* 更新规则
*/
@PutMapping("/{id}")
public Result<Rule> updateRule(
@PathVariable Long id,
@RequestBody Rule rule) {
try {
Rule updated = ruleService.updateRule(id, rule);
return Result.success(updated);
} catch (Exception e) {
log.error("更新规则失败", e);
return Result.error("更新规则失败: " + e.getMessage());
}
}
/**
* 删除规则
*/
@DeleteMapping("/{id}")
public Result<Void> deleteRule(@PathVariable Long id) {
try {
ruleService.deleteRule(id);
return Result.success(null);
} catch (Exception e) {
log.error("删除规则失败", e);
return Result.error("删除规则失败: " + e.getMessage());
}
}
/**
* 启用/禁用规则
*/
@PutMapping("/{id}/toggle")
public Result<Void> toggleRule(
@PathVariable Long id,
@RequestParam boolean enabled) {
try {
ruleService.toggleRule(id, enabled);
return Result.success(null);
} catch (Exception e) {
log.error("切换规则状态失败", e);
return Result.error("切换规则状态失败: " + e.getMessage());
}
}
/**
* 查询规则列表
*/
@GetMapping
public Result<Page<Rule>> queryRules(
@RequestParam(required = false) String serviceType,
@RequestParam(required = false) String ruleType,
@RequestParam(required = false) Boolean enabled,
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size) {
Page<Rule> rules = ruleService.queryRules(
serviceType, ruleType, enabled, page, size
);
return Result.success(rules);
}
/**
* 测试规则
*/
@PostMapping("/test")
public Result<Object> testRule(@RequestBody Map<String, Object> request) {
try {
String ruleScript = (String) request.get("ruleScript");
@SuppressWarnings("unchecked")
Map<String, Object> context = (Map<String, Object>) request.get("context");
Object result = ruleService.testRule(ruleScript, context);
return Result.success(result);
} catch (Exception e) {
log.error("测试规则失败", e);
return Result.error("测试规则失败: " + e.getMessage());
}
}
/**
* 导入规则
*/
@PostMapping("/import")
public Result<Void> importRules(@RequestBody List<Rule> rules) {
try {
ruleService.importRules(rules);
return Result.success(null);
} catch (Exception e) {
log.error("导入规则失败", e);
return Result.error("导入规则失败: " + e.getMessage());
}
}
/**
* 导出规则
*/
@GetMapping("/export")
public Result<List<Rule>> exportRules(
@RequestParam(required = false) String serviceType,
@RequestParam(required = false) String ruleType) {
List<Rule> rules = ruleService.exportRules(serviceType, ruleType);
return Result.success(rules);
}
}
七、实战案例
7.1 案例1:电信通话计费
java
/**
* 电信通话计费场景
*/
@Service
@Slf4j
public class TelecomBillingService {
@Autowired
private BillingService billingService;
/**
* 处理通话记录并计费
*/
public BillingResult processCallRecord(CallRecord callRecord) {
// 构建计费请求
BillingRequest request = BillingRequest.builder()
.userId(callRecord.getUserId())
.serviceType("CALL")
.duration(callRecord.getDuration())
.usage(new BigDecimal(callRecord.getDuration() / 60)) // 转换为分钟
.billingTime(callRecord.getEndTime())
.extraParams(buildExtraParams(callRecord))
.build();
// 执行计费
return billingService.doBilling(request);
}
/**
* 构建扩展参数
*/
private Map<String, Object> buildExtraParams(CallRecord callRecord) {
Map<String, Object> params = new HashMap<>();
// 通话类型(本地、长途、国际)
params.put("callType", callRecord.getCallType());
// 被叫号码
params.put("calledNumber", callRecord.getCalledNumber());
// 会员等级
params.put("vipLevel", callRecord.getVipLevel());
return params;
}
}
/**
* 通话记录
*/
@Data
class CallRecord {
private Long userId;
private String callerNumber;
private String calledNumber;
private String callType; // LOCAL/LONG_DISTANCE/INTERNATIONAL
private LocalDateTime startTime;
private LocalDateTime endTime;
private Long duration; // 秒
private String vipLevel;
}
对应的Groovy规则:
groovy
/**
* 长途通话加价规则
* 文件: long_distance_call_rule.groovy
*/
import java.math.BigDecimal
def callType = extraParams?.callType
if (callType == "LONG_DISTANCE") {
// 长途通话加价50%
def surcharge = result.originalFee.multiply(new BigDecimal("0.5"))
result.originalFee = result.originalFee.add(surcharge)
result.appliedRules.add("长途通话加价")
result.details.add(
new com.example.billing.model.BillingDetail(
"长途通话加价",
"长途附加费50%",
surcharge
)
)
log.info("应用长途加价: ${surcharge}元")
} else if (callType == "INTERNATIONAL") {
// 国际通话加价200%
def surcharge = result.originalFee.multiply(new BigDecimal("2.0"))
result.originalFee = result.originalFee.add(surcharge)
result.appliedRules.add("国际通话加价")
result.details.add(
new com.example.billing.model.BillingDetail(
"国际通话加价",
"国际附加费200%",
surcharge
)
)
log.info("应用国际加价: ${surcharge}元")
}
return true
7.2 案例2:云服务按量计费
java
/**
* 云服务计费场景
*/
@Service
@Slf4j
public class CloudBillingService {
@Autowired
private BillingService billingService;
/**
* 计算云资源使用费用
*/
public BillingResult calculateResourceUsage(ResourceUsage usage) {
BillingRequest request = BillingRequest.builder()
.userId(usage.getUserId())
.serviceType(usage.getResourceType()) // CPU/MEMORY/STORAGE/BANDWIDTH
.usage(usage.getUsageAmount())
.billingTime(usage.getUsageTime())
.extraParams(buildCloudParams(usage))
.build();
return billingService.doBilling(request);
}
/**
* 批量计算多种资源
*/
public List<BillingResult> calculateMultiResources(
Long userId,
List<ResourceUsage> usages) {
return usages.stream()
.map(this::calculateResourceUsage)
.collect(Collectors.toList());
}
private Map<String, Object> buildCloudParams(ResourceUsage usage) {
Map<String, Object> params = new HashMap<>();
params.put("region", usage.getRegion());
params.put("instanceType", usage.getInstanceType());
params.put("billingMode", usage.getBillingMode()); // HOURLY/DAILY/MONTHLY
return params;
}
}
/**
* 资源使用记录
*/
@Data
class ResourceUsage {
private Long userId;
private String resourceType; // CPU/MEMORY/STORAGE/BANDWIDTH
private BigDecimal usageAmount; // 使用量
private String region; // 地域
private String instanceType; // 实例类型
private String billingMode; // 计费模式
private LocalDateTime usageTime;
}
对应的Groovy规则:
groovy
/**
* 云资源阶梯计费规则
* 文件: cloud_resource_ladder_rule.groovy
*/
import java.math.BigDecimal
import java.math.RoundingMode
def resourceType = serviceType
def usage = usage as BigDecimal
def region = extraParams?.region ?: "cn-beijing"
// 不同资源类型的阶梯定价
def pricingConfig = [
CPU: [
[limit: 100, price: 0.50], // 0-100核时: 0.50元/核时
[limit: 500, price: 0.45], // 101-500核时: 0.45元/核时
[limit: null, price: 0.40] // 500核时以上: 0.40元/核时
],
MEMORY: [
[limit: 200, price: 0.25], // 0-200GB时: 0.25元/GB时
[limit: 1000, price: 0.22], // 201-1000GB时: 0.22元/GB时
[limit: null, price: 0.20] // 1000GB时以上: 0.20元/GB时
],
STORAGE: [
[limit: 1000, price: 0.10], // 0-1TB: 0.10元/GB月
[limit: 10000, price: 0.08], // 1-10TB: 0.08元/GB月
[limit: null, price: 0.06] // 10TB以上: 0.06元/GB月
]
]
def ladders = pricingConfig[resourceType]
if (!ladders) {
log.warn("未找到资源类型${resourceType}的定价配置")
return true
}
// 地域价格系数
def regionMultiplier = [
"cn-beijing": 1.0,
"cn-shanghai": 1.0,
"cn-guangzhou": 0.95,
"us-west": 1.2
][region] ?: 1.0
def totalFee = BigDecimal.ZERO
def remainingUsage = usage
for (ladder in ladders) {
if (remainingUsage.compareTo(BigDecimal.ZERO) <= 0) {
break
}
def limit = ladder.limit ? new BigDecimal(ladder.limit) : null
def basePrice = new BigDecimal(ladder.price as String)
def price = basePrice.multiply(new BigDecimal(regionMultiplier as String))
def usedInLadder
if (limit == null) {
usedInLadder = remainingUsage
} else {
def consumed = usage.subtract(remainingUsage)
def ladderRemaining = limit.subtract(consumed)
usedInLadder = remainingUsage.min(ladderRemaining)
}
def ladderFee = usedInLadder.multiply(price)
.setScale(2, RoundingMode.HALF_UP)
totalFee = totalFee.add(ladderFee)
remainingUsage = remainingUsage.subtract(usedInLadder)
result.details.add(
new com.example.billing.model.BillingDetail(
"${resourceType}阶梯${ladders.indexOf(ladder) + 1}",
"${usedInLadder} × ${price}元",
ladderFee
)
)
}
result.originalFee = result.originalFee.add(totalFee)
result.appliedRules.add("${resourceType}阶梯计费")
log.info("${resourceType}计费完成,总额: ${totalFee}元")
return true
7.3 案例3:电商优惠券计费
java
/**
* 电商订单计费场景
*/
@Service
@Slf4j
public class OrderBillingService {
@Autowired
private BillingService billingService;
/**
* 计算订单费用
*/
public BillingResult calculateOrderFee(Order order) {
BillingRequest request = BillingRequest.builder()
.userId(order.getUserId())
.serviceType("ORDER")
.usage(order.getTotalAmount())
.billingTime(order.getOrderTime())
.extraParams(buildOrderParams(order))
.build();
return billingService.doBilling(request);
}
private Map<String, Object> buildOrderParams(Order order) {
Map<String, Object> params = new HashMap<>();
params.put("orderItems", order.getItems());
params.put("coupons", order.getCoupons());
params.put("vipLevel", order.getVipLevel());
params.put("shippingAddress", order.getShippingAddress());
return params;
}
}
/**
* 订单
*/
@Data
class Order {
private Long userId;
private BigDecimal totalAmount;
private List<OrderItem> items;
private List<Coupon> coupons;
private String vipLevel;
private String shippingAddress;
private LocalDateTime orderTime;
}
@Data
class OrderItem {
private Long productId;
private String productName;
private BigDecimal price;
private Integer quantity;
}
@Data
class Coupon {
private String couponCode;
private String couponType; // FIXED/PERCENT
private BigDecimal discountValue;
private BigDecimal minAmount;
}
对应的Groovy规则:
groovy
/**
* 优惠券计算规则
* 文件: coupon_discount_rule.groovy
*/
import java.math.BigDecimal
import java.math.RoundingMode
def coupons = extraParams?.coupons ?: []
def totalDiscount = BigDecimal.ZERO
for (coupon in coupons) {
def minAmount = new BigDecimal(coupon.minAmount?.toString() ?: "0")
// 检查是否满足最低消费
if (result.originalFee.compareTo(minAmount) < 0) {
log.info("优惠券${coupon.couponCode}不满足最低消费条件")
continue
}
def discount = BigDecimal.ZERO
if (coupon.couponType == "FIXED") {
// 固定金额优惠券
discount = new BigDecimal(coupon.discountValue.toString())
} else if (coupon.couponType == "PERCENT") {
// 百分比优惠券
def percent = new BigDecimal(coupon.discountValue.toString())
discount = result.originalFee
.multiply(percent)
.divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP)
}
totalDiscount = totalDiscount.add(discount)
result.details.add(
new com.example.billing.model.BillingDetail(
"优惠券",
coupon.couponCode,
discount.negate()
)
)
log.info("应用优惠券${coupon.couponCode},优惠${discount}元")
}
if (totalDiscount.compareTo(BigDecimal.ZERO) > 0) {
result.discountAmount = result.discountAmount.add(totalDiscount)
result.appliedRules.add("优惠券折扣")
}
return true
八、性能优化
8.1 规则缓存优化
java
/**
* 增强的规则加载器(支持多级缓存)
*/
@Component
@Slf4j
public class EnhancedRuleLoader extends RuleLoader {
// 本地缓存(Caffeine)
private final Cache<Long, Script> localCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.recordStats()
.build();
/**
* 多级缓存加载
*/
@Override
public Script loadRule(Long ruleId) {
// L1: 本地缓存
Script cached = localCache.getIfPresent(ruleId);
if (cached != null) {
log.debug("从本地缓存加载规则: {}", ruleId);
return cached;
}
// L2: Redis缓存
cached = super.getCachedRule(ruleId);
if (cached != null) {
localCache.put(ruleId, cached);
log.debug("从Redis缓存加载规则: {}", ruleId);
return cached;
}
// L3: 数据库
Script script = super.loadRule(ruleId);
localCache.put(ruleId, script);
return script;
}
/**
* 清除本地缓存
*/
public void clearLocalCache() {
localCache.invalidateAll();
}
/**
* 获取缓存统计
*/
public CacheStats getCacheStats() {
return localCache.stats();
}
}
8.2 并行计费
java
/**
* 并行计费优化
*/
@Service
@Slf4j
public class ParallelBillingService {
@Autowired
private BillingService billingService;
private final ExecutorService executorService =
Executors.newFixedThreadPool(10);
/**
* 并行批量计费
*/
public List<BillingResult> parallelBatchBilling(List<BillingRequest> requests) {
List<CompletableFuture<BillingResult>> futures = requests.stream()
.map(request -> CompletableFuture.supplyAsync(
() -> billingService.doBilling(request),
executorService
))
.collect(Collectors.toList());
return futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
}
/**
* 分批并行计费
*/
public List<BillingResult> batchParallelBilling(
List<BillingRequest> requests,
int batchSize) {
List<BillingResult> results = new ArrayList<>();
for (int i = 0; i < requests.size(); i += batchSize) {
int end = Math.min(i + batchSize, requests.size());
List<BillingRequest> batch = requests.subList(i, end);
List<BillingResult> batchResults = parallelBatchBilling(batch);
results.addAll(batchResults);
}
return results;
}
}
8.3 异步计费
java
/**
* 异步计费配置
*/
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "billingExecutor")
public Executor billingExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("billing-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
/**
* 异步计费服务
*/
@Service
@Slf4j
public class AsyncBillingService {
@Autowired
private BillingService billingService;
/**
* 异步计费
*/
@Async("billingExecutor")
public CompletableFuture<BillingResult> asyncBilling(BillingRequest request) {
try {
BillingResult result = billingService.doBilling(request);
return CompletableFuture.completedFuture(result);
} catch (Exception e) {
log.error("异步计费失败", e);
return CompletableFuture.failedFuture(e);
}
}
/**
* 批量异步计费
*/
public CompletableFuture<List<BillingResult>> batchAsyncBilling(
List<BillingRequest> requests) {
List<CompletableFuture<BillingResult>> futures = requests.stream()
.map(this::asyncBilling)
.collect(Collectors.toList());
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.thenApply(v -> futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList()));
}
}
九、监控与运维
9.1 计费监控
java
/**
* 计费监控
*/
@Component
@Slf4j
public class BillingMonitor {
private final AtomicLong totalBillingCount = new AtomicLong(0);
private final AtomicLong successCount = new AtomicLong(0);
private final AtomicLong failureCount = new AtomicLong(0);
private final ConcurrentHashMap<String, AtomicLong> billingByType =
new ConcurrentHashMap<>();
/**
* 记录计费成功
*/
public void recordSuccess(String serviceType) {
totalBillingCount.incrementAndGet();
successCount.incrementAndGet();
billingByType.computeIfAbsent(serviceType, k -> new AtomicLong(0))
.incrementAndGet();
}
/**
* 记录计费失败
*/
public void recordFailure(String serviceType, Exception e) {
totalBillingCount.incrementAndGet();
failureCount.incrementAndGet();
log.error("计费失败 - 类型: {}, 错误: {}", serviceType, e.getMessage());
}
/**
* 获取统计信息
*/
public Map<String, Object> getStatistics() {
Map<String, Object> stats = new HashMap<>();
stats.put("totalCount", totalBillingCount.get());
stats.put("successCount", successCount.get());
stats.put("failureCount", failureCount.get());
stats.put("successRate", calculateSuccessRate());
stats.put("billingByType", billingByType);
return stats;
}
private double calculateSuccessRate() {
long total = totalBillingCount.get();
if (total == 0) {
return 0.0;
}
return (double) successCount.get() / total * 100;
}
/**
* 重置统计
*/
public void reset() {
totalBillingCount.set(0);
successCount.set(0);
failureCount.set(0);
billingByType.clear();
}
}
/**
* 监控切面
*/
@Aspect
@Component
@Slf4j
public class BillingMonitorAspect {
@Autowired
private BillingMonitor billingMonitor;
@Around("execution(* com.example.billing.service.BillingService.doBilling(..))")
public Object monitorBilling(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
BillingRequest request = (BillingRequest) joinPoint.getArgs()[0];
String serviceType = request.getServiceType();
try {
Object result = joinPoint.proceed();
long duration = System.currentTimeMillis() - startTime;
billingMonitor.recordSuccess(serviceType);
log.info("计费成功 - 类型: {}, 耗时: {}ms", serviceType, duration);
return result;
} catch (Exception e) {
billingMonitor.recordFailure(serviceType, e);
throw e;
}
}
}
9.2 规则热更新
java
/**
* 规则热更新监听器
*/
@Component
@Slf4j
public class RuleHotReloadListener {
@Autowired
private RuleLoader ruleLoader;
@Autowired
private RedisMessageListenerContainer redisMessageListenerContainer;
@PostConstruct
public void init() {
// 监听规则更新消息
redisMessageListenerContainer.addMessageListener(
(message, pattern) -> {
String ruleId = new String(message.getBody());
log.info("收到规则更新通知: {}", ruleId);
// 清除缓存
ruleLoader.clearRuleCache(Long.parseLong(ruleId));
},
new PatternTopic("billing:rule:update")
);
}
/**
* 发布规则更新消息
*/
public void publishRuleUpdate(Long ruleId) {
redisTemplate.convertAndSend("billing:rule:update", ruleId.toString());
}
}
十、最佳实践
10.1 规则编写规范
Groovy规则编写最佳实践
┌─────────────────────────────────────────┐
│ 1. 规则结构 │
│ ✓ 使用清晰的注释 │
│ ✓ 定义明确的输入输出 │
│ ✓ 合理的错误处理 │
│ │
│ 2. 性能优化 │
│ ✓ 避免不必要的循环 │
│ ✓ 合理使用缓存 │
│ ✓ 减少对象创建 │
│ │
│ 3. 可维护性 │
│ ✓ 规则拆分细粒度 │
│ ✓ 避免硬编码 │
│ ✓ 使用配置化参数 │
│ │
│ 4. 安全性 │
│ ✓ 输入参数校验 │
│ ✓ 防止注入攻击 │
│ ✓ 限制资源访问 │
└─────────────────────────────────────────┘
10.2 性能调优建议
计费引擎性能优化策略
┌─────────────────────────────────────────┐
│ 缓存策略 │
│ - 多级缓存(本地 + Redis) │
│ - 合理的TTL设置 │
│ - 缓存预热 │
├─────────────────────────────────────────┤
│ 并发优化 │
│ - 使用线程池 │
│ - 异步处理 │
│ - 批量计费 │
├─────────────────────────────────────────┤
│ 数据库优化 │
│ - 索引优化 │
│ - 分表分库 │
│ - 读写分离 │
├─────────────────────────────────────────┤
│ 规则优化 │
│ - 规则编译缓存 │
│ - 减少规则复杂度 │
│ - 合理的规则优先级 │
└─────────────────────────────────────────┘
十一、总结
核心知识点回顾
markdown
Java + Groovy计费引擎核心要点
│
├── 架构设计
│ ├── 规则加载器
│ ├── 规则执行器
│ ├── 计费引擎
│ └── 多级缓存
│
├── Groovy集成
│ ├── GroovyShell
│ ├── GroovyClassLoader
│ ├── 动态脚本编译
│ └── 规则热加载
│
├── 计费规则
│ ├── 基础计费
│ ├── 套餐优惠
│ ├── 阶梯计费
│ ├── 时段优惠
│ └── 会员折扣
│
├── 性能优化
│ ├── 规则缓存
│ ├── 并行计费
│ ├── 异步处理
│ └── 批量操作
│
├── 监控运维
│ ├── 计费监控
│ ├── 规则热更新
│ └── 统计分析
│
└── 实战应用
├── 电信计费
├── 云服务计费
└── 电商计费
Java + Groovy计费引擎结合了Java的稳定性和Groovy的灵活性,可以构建一个高性能、易维护的计费系统。通过合理的架构设计、规则管理和性能优化,能够满足复杂多变的业务需求。