|
|
@@ -1,19 +1,27 @@
|
|
|
package com.fs.proxy.service.impl;
|
|
|
|
|
|
+import com.fs.common.annotation.DataSource;
|
|
|
+import com.fs.common.enums.DataSourceType;
|
|
|
+import com.fs.common.utils.StringUtils;
|
|
|
+import com.fs.company.domain.CompanyVoiceApiTenant;
|
|
|
+import com.fs.company.mapper.CompanyVoiceApiTenantMapper;
|
|
|
+import com.fs.proxy.domain.CompanySmsApiTenant;
|
|
|
+import com.fs.proxy.domain.ServiceFeeConfig;
|
|
|
import com.fs.proxy.domain.TenantBalance;
|
|
|
import com.fs.proxy.domain.TenantConsumeRecord;
|
|
|
-import com.fs.proxy.domain.ServiceFeeConfig;
|
|
|
-import com.fs.proxy.domain.CompanySmsApiTenant;
|
|
|
+import com.fs.proxy.domain.TenantTrafficPricing;
|
|
|
+import com.fs.proxy.enums.ConsumeServiceResult;
|
|
|
import com.fs.proxy.enums.ConsumeTypeEnum;
|
|
|
+import com.fs.proxy.mapper.CompanySmsApiTenantMapper;
|
|
|
+import com.fs.proxy.mapper.ServiceFeeConfigMapper;
|
|
|
import com.fs.proxy.mapper.TenantBalanceMapper;
|
|
|
import com.fs.proxy.mapper.TenantConsumeRecordMapper;
|
|
|
-import com.fs.proxy.mapper.ServiceFeeConfigMapper;
|
|
|
-import com.fs.proxy.domain.TenantTrafficPricing;
|
|
|
import com.fs.proxy.mapper.TenantTrafficPricingMapper;
|
|
|
-import com.fs.proxy.mapper.CompanySmsApiTenantMapper;
|
|
|
-import com.fs.company.domain.CompanyVoiceApiTenant;
|
|
|
-import com.fs.company.mapper.CompanyVoiceApiTenantMapper;
|
|
|
+import com.fs.proxy.model.ConsumeServiceOutcome;
|
|
|
+import com.fs.proxy.model.UnitPricePair;
|
|
|
import com.fs.proxy.service.BalanceService;
|
|
|
+import org.redisson.api.RLock;
|
|
|
+import org.redisson.api.RedissonClient;
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
@@ -23,6 +31,7 @@ import java.util.Arrays;
|
|
|
import java.util.Date;
|
|
|
import java.util.List;
|
|
|
import java.util.UUID;
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
|
|
|
/**
|
|
|
* 余额消费服务实现类
|
|
|
@@ -51,7 +60,13 @@ public class BalanceServiceImpl implements BalanceService {
|
|
|
@Autowired
|
|
|
private CompanyVoiceApiTenantMapper voiceApiTenantMapper;
|
|
|
|
|
|
+ @Autowired
|
|
|
+ private RedissonClient redissonClient;
|
|
|
+
|
|
|
+ private static final String CONSUME_LOCK_PREFIX = "tenant:balance:consume:";
|
|
|
+
|
|
|
@Override
|
|
|
+ @DataSource(DataSourceType.MASTER)
|
|
|
public TenantBalance getTenantBalance(Long tenantId) {
|
|
|
return balanceMapper.selectBalanceByTenantId(tenantId);
|
|
|
}
|
|
|
@@ -157,26 +172,205 @@ public class BalanceServiceImpl implements BalanceService {
|
|
|
|
|
|
@Override
|
|
|
@Transactional
|
|
|
+ @DataSource(DataSourceType.MASTER)
|
|
|
public boolean consumeService(Long tenantId, ConsumeTypeEnum consumeType, Integer quantity, String remark) {
|
|
|
+ String orderNo = UUID.randomUUID().toString().replace("-", "").substring(0, 20);
|
|
|
+ ConsumeServiceOutcome outcome = doConsumeService(tenantId, consumeType, quantity, remark, orderNo);
|
|
|
+ return outcome.getResult() == ConsumeServiceResult.SUCCESS;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ @DataSource(DataSourceType.MASTER)
|
|
|
+ public ConsumeServiceOutcome consumeServiceIdempotent(Long tenantId, ConsumeTypeEnum consumeType,
|
|
|
+ Integer quantity, String orderNo, String remark) {
|
|
|
+ if (tenantId == null || consumeType == null || quantity == null || quantity <= 0
|
|
|
+ || StringUtils.isBlank(orderNo)) {
|
|
|
+ return ConsumeServiceOutcome.builder().result(ConsumeServiceResult.INVALID_PARAM).build();
|
|
|
+ }
|
|
|
+ String lockKey = CONSUME_LOCK_PREFIX + tenantId + ":" + orderNo;
|
|
|
+ RLock lock = redissonClient.getLock(lockKey);
|
|
|
+ boolean locked = false;
|
|
|
+ try {
|
|
|
+ locked = lock.tryLock(5, 30, TimeUnit.SECONDS);
|
|
|
+ if (!locked) {
|
|
|
+ return ConsumeServiceOutcome.builder().result(ConsumeServiceResult.LOCK_TIMEOUT).orderNo(orderNo).build();
|
|
|
+ }
|
|
|
+ TenantConsumeRecord existing = recordMapper.selectByTenantAndOrderNo(tenantId, orderNo);
|
|
|
+ if (existing != null && Integer.valueOf(1).equals(existing.getStatus())) {
|
|
|
+ return ConsumeServiceOutcome.builder()
|
|
|
+ .result(ConsumeServiceResult.ALREADY_CONSUMED)
|
|
|
+ .amount(existing.getAmount())
|
|
|
+ .quantity(existing.getQuantity())
|
|
|
+ .unitPrice(existing.getUnitPrice())
|
|
|
+ .orderNo(orderNo)
|
|
|
+ .recordId(existing.getRecordId())
|
|
|
+ .build();
|
|
|
+ }
|
|
|
+ return doConsumeService(tenantId, consumeType, quantity, remark, orderNo);
|
|
|
+ } catch (InterruptedException ex) {
|
|
|
+ Thread.currentThread().interrupt();
|
|
|
+ return ConsumeServiceOutcome.builder().result(ConsumeServiceResult.FAILED).orderNo(orderNo).build();
|
|
|
+ } finally {
|
|
|
+ if (locked && lock.isHeldByCurrentThread()) {
|
|
|
+ lock.unlock();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ @DataSource(DataSourceType.MASTER)
|
|
|
+ public boolean checkPayAsYouGoBalance(Long tenantId, ConsumeTypeEnum consumeType, Integer quantity) {
|
|
|
+ if (tenantId == null || consumeType == null || quantity == null || quantity <= 0) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (consumeType == ConsumeTypeEnum.MANUAL_CALL) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (!isPayAsYouGo(consumeType)) {
|
|
|
+ return checkBalance(tenantId, consumeType, quantity);
|
|
|
+ }
|
|
|
+ TenantBalance balance = balanceMapper.selectBalanceByTenantId(tenantId);
|
|
|
+ if (balance == null || balance.getTotalBalance() == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ BigDecimal unitPrice = resolveUnitPrice(tenantId, consumeType);
|
|
|
+ if (unitPrice == null || unitPrice.compareTo(BigDecimal.ZERO) <= 0) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ BigDecimal needed = unitPrice.multiply(BigDecimal.valueOf(quantity));
|
|
|
+ return balance.getTotalBalance().compareTo(needed) >= 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ @DataSource(DataSourceType.MASTER)
|
|
|
+ public BigDecimal resolveUnitPrice(Long tenantId, ConsumeTypeEnum consumeType) {
|
|
|
+ UnitPricePair pair = resolveUnitPricePair(tenantId, consumeType);
|
|
|
+ return pair != null ? pair.getUnitPrice() : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private ConsumeServiceOutcome doConsumeService(Long tenantId, ConsumeTypeEnum consumeType,
|
|
|
+ Integer quantity, String remark, String orderNo) {
|
|
|
TenantBalance balance = balanceMapper.selectBalanceByTenantIdForUpdate(tenantId);
|
|
|
if (balance == null) {
|
|
|
- return false;
|
|
|
+ return ConsumeServiceOutcome.builder().result(ConsumeServiceResult.FAILED).orderNo(orderNo).build();
|
|
|
}
|
|
|
|
|
|
- // 手拨外呼不再单独计费,由通话接口定价体系(company_voice_api_tenant)覆盖
|
|
|
if (consumeType == ConsumeTypeEnum.MANUAL_CALL) {
|
|
|
- return true;
|
|
|
+ return ConsumeServiceOutcome.builder().result(ConsumeServiceResult.SUCCESS).orderNo(orderNo).build();
|
|
|
+ }
|
|
|
+
|
|
|
+ UnitPricePair pricePair = resolveUnitPricePair(tenantId, consumeType);
|
|
|
+ if (pricePair == null) {
|
|
|
+ return ConsumeServiceOutcome.builder().result(ConsumeServiceResult.FAILED).orderNo(orderNo).build();
|
|
|
}
|
|
|
|
|
|
+ BigDecimal unitPrice = pricePair.getUnitPrice();
|
|
|
+ BigDecimal platformCost = pricePair.getPlatformCost();
|
|
|
+ BigDecimal totalCost = unitPrice.multiply(BigDecimal.valueOf(quantity));
|
|
|
+
|
|
|
+ if (isPayAsYouGo(consumeType)) {
|
|
|
+ BigDecimal beforeBalance = balance.getTotalBalance();
|
|
|
+ int rows = balanceMapper.decreaseTotalBalance(tenantId, totalCost);
|
|
|
+ if (rows <= 0) {
|
|
|
+ return ConsumeServiceOutcome.builder()
|
|
|
+ .result(ConsumeServiceResult.INSUFFICIENT_BALANCE)
|
|
|
+ .amount(totalCost)
|
|
|
+ .quantity(quantity)
|
|
|
+ .unitPrice(unitPrice)
|
|
|
+ .orderNo(orderNo)
|
|
|
+ .build();
|
|
|
+ }
|
|
|
+
|
|
|
+ TenantBalance updated = balanceMapper.selectBalanceByTenantId(tenantId);
|
|
|
+ TenantConsumeRecord record = buildConsumeRecord(balance, consumeType, quantity, remark, orderNo,
|
|
|
+ unitPrice, platformCost, totalCost, beforeBalance, updated.getTotalBalance());
|
|
|
+ int inserted = recordMapper.insertTenantConsumeRecord(record);
|
|
|
+ if (inserted <= 0) {
|
|
|
+ return ConsumeServiceOutcome.builder().result(ConsumeServiceResult.FAILED).orderNo(orderNo).build();
|
|
|
+ }
|
|
|
+ return ConsumeServiceOutcome.builder()
|
|
|
+ .result(ConsumeServiceResult.SUCCESS)
|
|
|
+ .amount(totalCost)
|
|
|
+ .quantity(quantity)
|
|
|
+ .unitPrice(unitPrice)
|
|
|
+ .orderNo(orderNo)
|
|
|
+ .recordId(record.getRecordId())
|
|
|
+ .build();
|
|
|
+ }
|
|
|
+
|
|
|
+ BigDecimal currentBalance = getServiceBalance(balance, consumeType);
|
|
|
+ if (currentBalance.compareTo(BigDecimal.valueOf(quantity)) < 0) {
|
|
|
+ return ConsumeServiceOutcome.builder()
|
|
|
+ .result(ConsumeServiceResult.INSUFFICIENT_BALANCE)
|
|
|
+ .amount(totalCost)
|
|
|
+ .quantity(quantity)
|
|
|
+ .unitPrice(unitPrice)
|
|
|
+ .orderNo(orderNo)
|
|
|
+ .build();
|
|
|
+ }
|
|
|
+
|
|
|
+ String balanceType = getBalanceType(consumeType);
|
|
|
+ int rows = balanceMapper.decreaseBalanceByType(tenantId, BigDecimal.valueOf(quantity), balanceType);
|
|
|
+ if (rows <= 0) {
|
|
|
+ return ConsumeServiceOutcome.builder()
|
|
|
+ .result(ConsumeServiceResult.INSUFFICIENT_BALANCE)
|
|
|
+ .amount(totalCost)
|
|
|
+ .quantity(quantity)
|
|
|
+ .unitPrice(unitPrice)
|
|
|
+ .orderNo(orderNo)
|
|
|
+ .build();
|
|
|
+ }
|
|
|
+
|
|
|
+ TenantBalance updated = balanceMapper.selectBalanceByTenantId(tenantId);
|
|
|
+ TenantConsumeRecord record = buildConsumeRecord(balance, consumeType, quantity, remark, orderNo,
|
|
|
+ unitPrice, platformCost, totalCost, currentBalance, getServiceBalance(updated, consumeType));
|
|
|
+ int inserted = recordMapper.insertTenantConsumeRecord(record);
|
|
|
+ if (inserted <= 0) {
|
|
|
+ return ConsumeServiceOutcome.builder().result(ConsumeServiceResult.FAILED).orderNo(orderNo).build();
|
|
|
+ }
|
|
|
+ return ConsumeServiceOutcome.builder()
|
|
|
+ .result(ConsumeServiceResult.SUCCESS)
|
|
|
+ .amount(totalCost)
|
|
|
+ .quantity(quantity)
|
|
|
+ .unitPrice(unitPrice)
|
|
|
+ .orderNo(orderNo)
|
|
|
+ .recordId(record.getRecordId())
|
|
|
+ .build();
|
|
|
+ }
|
|
|
+
|
|
|
+ private TenantConsumeRecord buildConsumeRecord(TenantBalance balance, ConsumeTypeEnum consumeType, Integer quantity,
|
|
|
+ String remark, String orderNo, BigDecimal unitPrice,
|
|
|
+ BigDecimal platformCost, BigDecimal totalCost,
|
|
|
+ BigDecimal beforeBalance, BigDecimal afterBalance) {
|
|
|
+ TenantConsumeRecord record = new TenantConsumeRecord();
|
|
|
+ record.setTenantId(balance.getTenantId());
|
|
|
+ record.setTenantName(balance.getTenantName());
|
|
|
+ record.setConsumeType(consumeType.getCode());
|
|
|
+ record.setConsumeTypeName(consumeType.getName());
|
|
|
+ record.setAmount(totalCost);
|
|
|
+ record.setUnitPrice(unitPrice);
|
|
|
+ record.setPlatformCost(platformCost);
|
|
|
+ record.setTenantPrice(unitPrice);
|
|
|
+ record.setQuantity(quantity);
|
|
|
+ record.setBeforeBalance(beforeBalance);
|
|
|
+ record.setAfterBalance(afterBalance);
|
|
|
+ record.setOrderNo(orderNo);
|
|
|
+ record.setStatus(1);
|
|
|
+ record.setConsumeTime(new Date());
|
|
|
+ record.setRemark(remark);
|
|
|
+ return record;
|
|
|
+ }
|
|
|
+
|
|
|
+ private UnitPricePair resolveUnitPricePair(Long tenantId, ConsumeTypeEnum consumeType) {
|
|
|
ServiceFeeConfig config = getFeeConfig(consumeType.getCode());
|
|
|
if (config == null) {
|
|
|
- return false;
|
|
|
+ return null;
|
|
|
}
|
|
|
|
|
|
BigDecimal unitPrice = config.getFeeStandard();
|
|
|
- BigDecimal platformCost = config.getPlatformCost();
|
|
|
+ BigDecimal platformCost = config.getPlatformCost() != null ? config.getPlatformCost() : BigDecimal.ZERO;
|
|
|
|
|
|
- // 短信发送:优先使用租户绑定的接口售价(按优先级取第一个)
|
|
|
if (consumeType == ConsumeTypeEnum.SMS_SEND) {
|
|
|
List<CompanySmsApiTenant> smsBindings = smsApiTenantMapper.selectActiveByCompanyAndType(tenantId, null);
|
|
|
if (smsBindings != null && !smsBindings.isEmpty()) {
|
|
|
@@ -190,13 +384,11 @@ public class BalanceServiceImpl implements BalanceService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 通用租户定价覆盖:优先查 tenant_traffic_pricing,未配置则回退全局 service_fee_config
|
|
|
- // 注意:SMS_SEND 使用专属 company_sms_api_tenant,AI_CALL 使用复合定价
|
|
|
if (consumeType != ConsumeTypeEnum.SMS_SEND && consumeType != ConsumeTypeEnum.AI_CALL) {
|
|
|
TenantTrafficPricing trafficPricing = trafficPricingMapper
|
|
|
- .selectByTenantAndType(tenantId, consumeType.getCode());
|
|
|
+ .selectByTenantAndType(tenantId, consumeType.getCode());
|
|
|
if (trafficPricing != null && trafficPricing.getPrice() != null
|
|
|
- && trafficPricing.getPrice().compareTo(BigDecimal.ZERO) > 0) {
|
|
|
+ && trafficPricing.getPrice().compareTo(BigDecimal.ZERO) > 0) {
|
|
|
unitPrice = trafficPricing.getPrice();
|
|
|
if (trafficPricing.getCostPrice() != null) {
|
|
|
platformCost = trafficPricing.getCostPrice();
|
|
|
@@ -204,7 +396,6 @@ public class BalanceServiceImpl implements BalanceService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // AI外呼:复合定价 = 语音单价(租户绑定) + AI附加费(租户 > 全局 service_fee_config)
|
|
|
if (consumeType == ConsumeTypeEnum.AI_CALL) {
|
|
|
List<CompanyVoiceApiTenant> voiceBindings = voiceApiTenantMapper.selectEnabledApisByTenantId(tenantId);
|
|
|
if (voiceBindings != null && !voiceBindings.isEmpty()) {
|
|
|
@@ -212,13 +403,12 @@ public class BalanceServiceImpl implements BalanceService {
|
|
|
if (voiceBinding.getSalePrice() != null && voiceBinding.getSalePrice().compareTo(BigDecimal.ZERO) > 0) {
|
|
|
BigDecimal voicePrice = voiceBinding.getSalePrice();
|
|
|
BigDecimal voiceCost = voiceBinding.getCostPrice() != null ? voiceBinding.getCostPrice() : BigDecimal.ZERO;
|
|
|
- // AI附加费:优先查租户定价,未配置则使用全局 service_fee_config
|
|
|
BigDecimal aiSurcharge = config.getFeeStandard();
|
|
|
BigDecimal aiCost = config.getPlatformCost() != null ? config.getPlatformCost() : BigDecimal.ZERO;
|
|
|
TenantTrafficPricing aiTrafficPricing = trafficPricingMapper
|
|
|
- .selectByTenantAndType(tenantId, ConsumeTypeEnum.AI_CALL.getCode());
|
|
|
+ .selectByTenantAndType(tenantId, ConsumeTypeEnum.AI_CALL.getCode());
|
|
|
if (aiTrafficPricing != null && aiTrafficPricing.getPrice() != null
|
|
|
- && aiTrafficPricing.getPrice().compareTo(BigDecimal.ZERO) > 0) {
|
|
|
+ && aiTrafficPricing.getPrice().compareTo(BigDecimal.ZERO) > 0) {
|
|
|
aiSurcharge = aiTrafficPricing.getPrice();
|
|
|
if (aiTrafficPricing.getCostPrice() != null) {
|
|
|
aiCost = aiTrafficPricing.getCostPrice();
|
|
|
@@ -230,68 +420,7 @@ public class BalanceServiceImpl implements BalanceService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- BigDecimal totalCost = unitPrice.multiply(BigDecimal.valueOf(quantity));
|
|
|
-
|
|
|
- if (isPayAsYouGo(consumeType)) {
|
|
|
- BigDecimal beforeBalance = balance.getTotalBalance();
|
|
|
-
|
|
|
- int rows = balanceMapper.decreaseTotalBalance(tenantId, totalCost);
|
|
|
- if (rows <= 0) {
|
|
|
- return false;
|
|
|
- }
|
|
|
-
|
|
|
- TenantBalance updated = balanceMapper.selectBalanceByTenantId(tenantId);
|
|
|
-
|
|
|
- TenantConsumeRecord record = new TenantConsumeRecord();
|
|
|
- record.setTenantId(tenantId);
|
|
|
- record.setTenantName(balance.getTenantName());
|
|
|
- record.setConsumeType(consumeType.getCode());
|
|
|
- record.setConsumeTypeName(consumeType.getName());
|
|
|
- record.setAmount(totalCost);
|
|
|
- record.setUnitPrice(unitPrice);
|
|
|
- record.setPlatformCost(platformCost);
|
|
|
- record.setTenantPrice(unitPrice);
|
|
|
- record.setQuantity(quantity);
|
|
|
- record.setBeforeBalance(beforeBalance);
|
|
|
- record.setAfterBalance(updated.getTotalBalance());
|
|
|
- record.setOrderNo(UUID.randomUUID().toString().replace("-", "").substring(0, 20));
|
|
|
- record.setStatus(1);
|
|
|
- record.setConsumeTime(new Date());
|
|
|
- record.setRemark(remark);
|
|
|
-
|
|
|
- return recordMapper.insertTenantConsumeRecord(record) > 0;
|
|
|
- } else {
|
|
|
- BigDecimal currentBalance = getServiceBalance(balance, consumeType);
|
|
|
- if (currentBalance.compareTo(BigDecimal.valueOf(quantity)) < 0) {
|
|
|
- return false;
|
|
|
- }
|
|
|
-
|
|
|
- String balanceType = getBalanceType(consumeType);
|
|
|
- BigDecimal amount = BigDecimal.valueOf(quantity);
|
|
|
- int rows = balanceMapper.decreaseBalanceByType(tenantId, amount, balanceType);
|
|
|
- if (rows <= 0) {
|
|
|
- return false;
|
|
|
- }
|
|
|
-
|
|
|
- TenantBalance updated = balanceMapper.selectBalanceByTenantId(tenantId);
|
|
|
-
|
|
|
- TenantConsumeRecord record = new TenantConsumeRecord();
|
|
|
- record.setTenantId(tenantId);
|
|
|
- record.setTenantName(balance.getTenantName());
|
|
|
- record.setConsumeType(consumeType.getCode());
|
|
|
- record.setConsumeTypeName(consumeType.getName());
|
|
|
- record.setAmount(totalCost);
|
|
|
- record.setUnitPrice(unitPrice);
|
|
|
- record.setQuantity(quantity);
|
|
|
- record.setBeforeBalance(currentBalance);
|
|
|
- record.setAfterBalance(getServiceBalance(updated, consumeType));
|
|
|
- record.setOrderNo(UUID.randomUUID().toString().replace("-", "").substring(0, 20));
|
|
|
- record.setStatus(1);
|
|
|
- record.setConsumeTime(new Date());
|
|
|
- record.setRemark(remark);
|
|
|
-
|
|
|
- return recordMapper.insertTenantConsumeRecord(record) > 0;
|
|
|
- }
|
|
|
+ return new UnitPricePair(unitPrice, platformCost);
|
|
|
}
|
|
|
|
|
|
/**
|