chore: initialize qiming workspace repository
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-bill</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>app-platform-bill-application</artifactId>
|
||||
|
||||
<properties>
|
||||
<maven.deploy.skip>true</maven.deploy.skip>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-bill-domain</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-bill-infra</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-bill-sdk</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>system-sdk</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-agent-core-sdk</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-subscription-sdk</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-credit-sdk</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>system-application</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-context</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-tx</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.xspaceagi.bill.app.service;
|
||||
|
||||
import com.xspaceagi.bill.sdk.dto.CreateOrderRequest;
|
||||
import com.xspaceagi.bill.sdk.dto.OrderDTO;
|
||||
import com.xspaceagi.bill.sdk.dto.OrderPageDTO;
|
||||
import com.xspaceagi.bill.sdk.dto.OrderQueryRequest;
|
||||
import com.xspaceagi.bill.sdk.dto.OrderSettlementStatusResponse;
|
||||
|
||||
public interface BillOrderAppService {
|
||||
|
||||
OrderDTO createOrder(CreateOrderRequest request);
|
||||
|
||||
boolean paymentCallback(Long tenantId, Long orderId, String payStatus);
|
||||
|
||||
OrderPageDTO queryOrders(OrderQueryRequest query);
|
||||
|
||||
OrderDTO queryOrder(Long orderId);
|
||||
|
||||
/** 支付中间结算页轮询:当前用户须为订单所属用户。 */
|
||||
OrderSettlementStatusResponse getOrderSettlementStatus(long userId, Long orderId);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.xspaceagi.bill.app.service;
|
||||
|
||||
import com.xspaceagi.bill.sdk.dto.*;
|
||||
import com.xspaceagi.bill.spec.enums.RevenueStatusEnum;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface BillRevenueAppService {
|
||||
|
||||
boolean addRevenue(AddRevenueRequest request);
|
||||
|
||||
List<DailyRevenueDTO> queryDailyRevenue(RevenueQueryRequest query);
|
||||
|
||||
RevenueDetailPageDTO queryRevenueDetail(RevenueQueryRequest query);
|
||||
|
||||
RevenueStatsDTO getRevenueStats(Long userId);
|
||||
|
||||
RevenueStatsDTO getAdminRevenueStats(String monthStart, String monthEnd, Long userId, RevenueStatusEnum status, Integer pageNum, Integer pageSize);
|
||||
|
||||
RevenueDetailPageDTO getUserRevenueDetails(Long userId, String dt, Integer pageNum, Integer pageSize);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.xspaceagi.bill.app.service;
|
||||
|
||||
import com.xspaceagi.bill.sdk.dto.*;
|
||||
|
||||
public interface BillWithdrawAppService {
|
||||
|
||||
WithdrawConfigDTO getWithdrawConfig(Long tenantId);
|
||||
|
||||
boolean saveWithdrawConfig(SaveWithdrawConfigRequest request);
|
||||
|
||||
WithdrawApplicationDTO createWithdrawApplication(Long tenantId, Long userId);
|
||||
|
||||
WithdrawApplicationPageDTO queryWithdrawApplications(WithdrawQueryRequest query);
|
||||
|
||||
boolean processWithdraw(WithdrawProcessRequest request);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.xspaceagi.bill.app.service;
|
||||
|
||||
import com.xspaceagi.bill.sdk.dto.ResourceStatPageDTO;
|
||||
import com.xspaceagi.bill.sdk.dto.ResourceStatSummaryDTO;
|
||||
|
||||
public interface ResourceStatAppService {
|
||||
|
||||
ResourceStatPageDTO queryResourceStats(Long tenantId, Long userId, String type,
|
||||
String targetType, Long targetId,
|
||||
String dtStart, String dtEnd,
|
||||
Integer pageNum, Integer pageSize);
|
||||
|
||||
ResourceStatSummaryDTO getResourceStatSummary(Long tenantId, Long userId,
|
||||
String dtStart, String dtEnd);
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
package com.xspaceagi.bill.app.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.xspaceagi.agent.core.sdk.IAgentRpcService;
|
||||
import com.xspaceagi.agent.core.sdk.IModelRpcService;
|
||||
import com.xspaceagi.agent.core.sdk.dto.*;
|
||||
import com.xspaceagi.bill.app.service.BillRevenueAppService;
|
||||
import com.xspaceagi.bill.infra.dao.entity.BillResourceStat;
|
||||
import com.xspaceagi.bill.infra.dao.service.IBillResourceStatService;
|
||||
import com.xspaceagi.bill.sdk.dto.AddRevenueRequest;
|
||||
import com.xspaceagi.bill.spec.enums.ResourceStatTypeEnum;
|
||||
import com.xspaceagi.bill.spec.enums.RevenueTargetTypeEnum;
|
||||
import com.xspaceagi.bill.spec.enums.RevenueTypeEnum;
|
||||
import com.xspaceagi.credit.sdk.dto.CreditDeductRequest;
|
||||
import com.xspaceagi.credit.sdk.rpc.ICreditRpcService;
|
||||
import com.xspaceagi.credit.spec.enums.CreditTypeEnum;
|
||||
import com.xspaceagi.pricing.sdk.dto.ModelPriceTierDTO;
|
||||
import com.xspaceagi.pricing.sdk.dto.PricingConfigDTO;
|
||||
import com.xspaceagi.pricing.sdk.dto.QueryPricingInfoRequest;
|
||||
import com.xspaceagi.pricing.sdk.dto.UpdateTrialCountRequest;
|
||||
import com.xspaceagi.pricing.sdk.rpc.IPricingRpcService;
|
||||
import com.xspaceagi.pricing.spec.enums.PricingTypeEnum;
|
||||
import com.xspaceagi.pricing.spec.enums.TargetTypeEnum;
|
||||
import com.xspaceagi.subscription.sdk.rpc.ISubscriptionRpcService;
|
||||
import com.xspaceagi.system.application.dto.TenantConfigDto;
|
||||
import com.xspaceagi.system.application.service.TenantConfigApplicationService;
|
||||
import com.xspaceagi.system.sdk.common.TraceContext;
|
||||
import com.xspaceagi.system.sdk.service.AbstractTaskExecuteService;
|
||||
import com.xspaceagi.system.sdk.service.ScheduleTaskApiService;
|
||||
import com.xspaceagi.system.sdk.service.dto.ScheduleTaskDto;
|
||||
import com.xspaceagi.system.spec.common.RequestContext;
|
||||
import com.xspaceagi.system.spec.utils.RedisUtil;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@Service("billCalculateTaskService")
|
||||
public class BillCalculateTaskServiceImpl extends AbstractTaskExecuteService {
|
||||
|
||||
@Resource
|
||||
private ScheduleTaskApiService scheduleTaskApiService;
|
||||
|
||||
@Resource
|
||||
private ICreditRpcService iCreditRpcService;
|
||||
|
||||
@Resource
|
||||
private IPricingRpcService iPricingRpcService;
|
||||
|
||||
@Resource
|
||||
private BillRevenueAppService billRevenueAppService;
|
||||
|
||||
@Resource
|
||||
private IBillResourceStatService iBillResourceStatService;
|
||||
|
||||
@Resource
|
||||
private TenantConfigApplicationService tenantConfigApplicationService;
|
||||
|
||||
@Resource
|
||||
private ISubscriptionRpcService iSubscriptionRpcService;
|
||||
|
||||
@Resource
|
||||
private IAgentRpcService iAgentRpcService;
|
||||
|
||||
@Resource
|
||||
private IModelRpcService iModelRpcService;
|
||||
|
||||
@Resource
|
||||
private RedisUtil redisUtil;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
scheduleTaskApiService.start(ScheduleTaskDto.builder()
|
||||
.taskId("billCalculateTaskService")
|
||||
.beanId("billCalculateTaskService")
|
||||
.maxExecTimes(Long.MAX_VALUE)
|
||||
.cron(ScheduleTaskDto.Cron.EVERY_10_SECOND.getCron())
|
||||
.params(Map.of())
|
||||
.build());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected boolean execute(ScheduleTaskDto scheduleTaskDto) {
|
||||
try {
|
||||
execute0();
|
||||
} catch (Exception e) {
|
||||
log.error("execute error", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void execute0() {
|
||||
Map<Long, TenantConfigDto> tenantConfigMap = new HashMap<>();
|
||||
Object val = redisUtil.rightPop("bill:queue");
|
||||
while (val != null) {
|
||||
TraceContext traceContext = JSONObject.parseObject(val.toString(), TraceContext.class);
|
||||
if (traceContext != null && CollectionUtils.isNotEmpty(traceContext.getTraceTargets())) {
|
||||
try {
|
||||
TenantConfigDto tenantConfig = tenantConfigMap.get(traceContext.getTenantId());
|
||||
if (tenantConfig == null) {
|
||||
tenantConfig = tenantConfigApplicationService.getTenantConfig(traceContext.getTenantId());
|
||||
tenantConfigMap.put(traceContext.getTenantId(), tenantConfig);
|
||||
}
|
||||
RequestContext<Object> requestContext = new RequestContext<>();
|
||||
requestContext.setTenantId(traceContext.getTenantId());
|
||||
requestContext.setTenantConfig(tenantConfig);
|
||||
RequestContext.set(requestContext);
|
||||
handleCalculateBill(traceContext);
|
||||
} catch (Exception e) {
|
||||
log.error("Calculate bill error {}", val, e);
|
||||
} finally {
|
||||
RequestContext.remove();
|
||||
}
|
||||
}
|
||||
log.info("Calculate bill: {}", val);
|
||||
val = redisUtil.rightPop("bill:queue");
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCalculateBill(TraceContext traceContext) {
|
||||
TenantConfigDto tenantConfig = (TenantConfigDto) RequestContext.get().getTenantConfig();
|
||||
TraceContext.TraceTarget traceTarget = traceContext.getTraceTargets().get(traceContext.getTraceTargets().size() - 1);
|
||||
QueryPricingInfoRequest request = new QueryPricingInfoRequest();
|
||||
request.setTargetId(traceTarget.getTargetId());
|
||||
if (traceTarget.getTargetType() == TraceContext.TraceTargetType.Agent) {
|
||||
request.setTargetType(TargetTypeEnum.AGENT);
|
||||
} else if (traceTarget.getTargetType() == TraceContext.TraceTargetType.Model) {
|
||||
request.setTargetType(TargetTypeEnum.MODEL);
|
||||
} else if (traceTarget.getTargetType() == TraceContext.TraceTargetType.Plugin) {
|
||||
request.setTargetType(TargetTypeEnum.PLUGIN);
|
||||
} else if (traceTarget.getTargetType() == TraceContext.TraceTargetType.Workflow) {
|
||||
request.setTargetType(TargetTypeEnum.WORKFLOW);
|
||||
} else if (traceTarget.getTargetType() == TraceContext.TraceTargetType.Mcp) {
|
||||
request.setTargetType(TargetTypeEnum.MCP);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
request.setTenantId(traceContext.getTenantId());
|
||||
BigDecimal creditAmount = null;
|
||||
BigDecimal feeAmount = null;
|
||||
TraceContext.TokenUsage tokenUsage = traceContext.getTokenUsage();
|
||||
Long salerId = -1L;
|
||||
if (traceTarget.getTargetType() == TraceContext.TraceTargetType.Workflow) {
|
||||
ReqResult<WorkflowInfoDto> publishedWorkflowInfo = iAgentRpcService.getPublishedWorkflowInfo(Long.parseLong(traceTarget.getTargetId()), null);
|
||||
if (publishedWorkflowInfo == null) {
|
||||
log.warn("getPublishedWorkflowInfo error {}", traceTarget.getTargetId());
|
||||
return;
|
||||
}
|
||||
salerId = publishedWorkflowInfo.getData().getCreatorId();
|
||||
}
|
||||
if (traceTarget.getTargetType() == TraceContext.TraceTargetType.Plugin) {
|
||||
ReqResult<PluginInfoDto> publishedPluginInfo = iAgentRpcService.getPublishedPluginInfo(Long.parseLong(traceTarget.getTargetId()), null);
|
||||
if (publishedPluginInfo == null) {
|
||||
log.warn("getPublishedPluginInfo error {}", traceTarget.getTargetId());
|
||||
return;
|
||||
}
|
||||
salerId = publishedPluginInfo.getData().getCreatorId();
|
||||
}
|
||||
|
||||
if (traceTarget.getTargetType() == TraceContext.TraceTargetType.Agent) {
|
||||
ReqResult<AgentInfoDto> publishedAgentInfo = iAgentRpcService.queryPublishedAgentInfo(Long.parseLong(traceTarget.getTargetId()));
|
||||
if (publishedAgentInfo == null || publishedAgentInfo.getData() == null) {
|
||||
log.warn("queryPublishedAgentInfo error {}", traceTarget.getTargetId());
|
||||
return;
|
||||
}
|
||||
salerId = publishedAgentInfo.getData().getCreatorId();
|
||||
}
|
||||
|
||||
if (traceTarget.getTargetType() == TraceContext.TraceTargetType.Model) {
|
||||
ModelInfoDto modelInfo = iModelRpcService.getModelInfo(Long.parseLong(traceTarget.getTargetId()));
|
||||
if (modelInfo == null) {
|
||||
log.warn("getModelInfo error {}", traceTarget.getTargetId());
|
||||
return;
|
||||
}
|
||||
salerId = modelInfo.isTenantModel() ? -1L : modelInfo.getCreatorId();
|
||||
}
|
||||
|
||||
log.debug("开始计费 tenantId={} userId={} targetType={} targetId={} traceId={}", traceContext.getTenantId(), traceContext.getBillUserId(),
|
||||
traceTarget.getTargetType(), traceTarget.getTargetId(), traceContext.getTraceId());
|
||||
if (traceContext.isError()) {
|
||||
log.warn("error call, traceContext {}", traceContext);
|
||||
}
|
||||
|
||||
if (tenantConfig.getEnableSubscription() != null && tenantConfig.getEnableSubscription() == 1 && !traceContext.isError() && !traceContext.isDevTest()) {
|
||||
PricingConfigDTO pricingConfigDTO = iPricingRpcService.queryPricingInfo(request);
|
||||
if (pricingConfigDTO != null && pricingConfigDTO.getStatus() == 1 && (pricingConfigDTO.getPricingType() == PricingTypeEnum.ONE_TIME || pricingConfigDTO.getPricingType() == PricingTypeEnum.TIERED)) {
|
||||
log.info("定价配置匹配成功 pricingType={} price={} trialCount={}",
|
||||
pricingConfigDTO.getPricingType(), pricingConfigDTO.getPrice(), pricingConfigDTO.getTrialCount());
|
||||
boolean isToolCall = false;
|
||||
if (pricingConfigDTO.getPricingType() == PricingTypeEnum.ONE_TIME && (pricingConfigDTO.getTargetType() == TargetTypeEnum.WORKFLOW || pricingConfigDTO.getTargetType() == TargetTypeEnum.PLUGIN)) {
|
||||
//积分扣减计算
|
||||
creditAmount = pricingConfigDTO.getPrice().multiply(BigDecimal.valueOf(tenantConfig.getCreditExchangeRate()));
|
||||
feeAmount = pricingConfigDTO.getPrice();
|
||||
isToolCall = true;
|
||||
}
|
||||
if (pricingConfigDTO.getPricingType() == PricingTypeEnum.TIERED && pricingConfigDTO.getTargetType() == TargetTypeEnum.MODEL) {
|
||||
//阶梯计费
|
||||
if (tokenUsage != null && CollectionUtils.isNotEmpty(pricingConfigDTO.getModelPriceTiers())) {
|
||||
long contextLength = tokenUsage.cacheInputTokens + tokenUsage.inputTokens + tokenUsage.outputTokens;
|
||||
List<ModelPriceTierDTO> sortedTiers = pricingConfigDTO.getModelPriceTiers().stream()
|
||||
.sorted(Comparator.comparingInt(ModelPriceTierDTO::getContextLength))
|
||||
.toList();
|
||||
ModelPriceTierDTO matchedTier = null;
|
||||
for (ModelPriceTierDTO tier : sortedTiers) {
|
||||
matchedTier = tier;
|
||||
if (contextLength <= tier.getContextLength() * 1000L) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (matchedTier != null) {
|
||||
BigDecimal inputPrice = matchedTier.getInputPrice() != null ? matchedTier.getInputPrice() : BigDecimal.ZERO;
|
||||
BigDecimal outputPrice = matchedTier.getOutputPrice() != null ? matchedTier.getOutputPrice() : BigDecimal.ZERO;
|
||||
BigDecimal cachePrice = matchedTier.getCachePrice() != null ? matchedTier.getCachePrice() : BigDecimal.ZERO;
|
||||
feeAmount = BigDecimal.valueOf(tokenUsage.inputTokens).multiply(inputPrice)
|
||||
.add(BigDecimal.valueOf(tokenUsage.outputTokens).multiply(outputPrice))
|
||||
.add(BigDecimal.valueOf(tokenUsage.cacheInputTokens).multiply(cachePrice));
|
||||
feeAmount = feeAmount.divide(BigDecimal.valueOf(1000000), RoundingMode.HALF_UP);
|
||||
creditAmount = feeAmount.multiply(BigDecimal.valueOf(tenantConfig.getCreditExchangeRate()));
|
||||
log.info("阶梯计费匹配档位 contextLength={}k matchedTier={} inputTokens={} outputTokens={} cacheTokens={} feeAmount={} creditAmount={}",
|
||||
matchedTier.getContextLength(), matchedTier.getId(),
|
||||
tokenUsage.inputTokens, tokenUsage.outputTokens, tokenUsage.cacheInputTokens,
|
||||
feeAmount, creditAmount);
|
||||
} else {
|
||||
log.warn("阶梯计费未匹配到合适档位 modelPriceTiers={} contextLength={}",
|
||||
pricingConfigDTO.getModelPriceTiers().size(), contextLength);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Long billUserId = traceContext.getBillUserId();
|
||||
if (traceTarget.getBillUserId() != null) {
|
||||
billUserId = traceTarget.getBillUserId();
|
||||
}
|
||||
//积分扣减
|
||||
CreditDeductRequest creditDeductRequest = new CreditDeductRequest();
|
||||
creditDeductRequest.setTenantId(traceContext.getTenantId());
|
||||
creditDeductRequest.setUserId(billUserId);
|
||||
creditDeductRequest.setCreditType(isToolCall ? CreditTypeEnum.TOOL_CALL : CreditTypeEnum.MODEL_CALL);
|
||||
creditDeductRequest.setAmount(creditAmount);
|
||||
creditDeductRequest.setAllowNegative(true);
|
||||
creditDeductRequest.setBizNo(UUID.randomUUID().toString().replace("-", ""));
|
||||
creditDeductRequest.setRemark(targetsToSting(traceContext.getTraceTargets()));
|
||||
log.info("积分扣减 userId={} creditType={} amount={}", billUserId, isToolCall ? "TOOL_CALL" : "MODEL_CALL", creditAmount);
|
||||
iCreditRpcService.deductCredit(creditDeductRequest);
|
||||
|
||||
//收益发放
|
||||
AddRevenueRequest addRevenueRequest = new AddRevenueRequest();
|
||||
addRevenueRequest.setTenantId(traceContext.getTenantId());
|
||||
addRevenueRequest.setUserId(salerId);
|
||||
addRevenueRequest.setType(isToolCall ? RevenueTypeEnum.TOOL_CALL : RevenueTypeEnum.MODEL_CALL);
|
||||
addRevenueRequest.setAmount(feeAmount);
|
||||
addRevenueRequest.setTargetType(RevenueTargetTypeEnum.fromCode(traceTarget.getTargetType().name()));
|
||||
addRevenueRequest.setTargetId(Long.parseLong(traceTarget.getTargetId()));
|
||||
addRevenueRequest.setBizNo(UUID.randomUUID().toString().replace("-", ""));
|
||||
addRevenueRequest.setRemark(traceTarget.getName() + "[" + traceTarget.getTargetId() + "]");
|
||||
addRevenueRequest.setExtra(Map.of("traceId", traceContext.getTraceId() == null ? "" : traceContext.getTraceId()));
|
||||
billRevenueAppService.addRevenue(addRevenueRequest);
|
||||
log.info("收益发放 salerId={} type={} feeAmount={} bizNo={}",
|
||||
salerId, isToolCall ? "TOOL_CALL" : "MODEL_CALL", feeAmount, traceContext.getTraceId());
|
||||
} else if (pricingConfigDTO != null && pricingConfigDTO.getStatus() == 1 && (pricingConfigDTO.getPricingType() == PricingTypeEnum.MONTHLY || pricingConfigDTO.getPricingType() == PricingTypeEnum.BUYOUT)) {
|
||||
// 在订阅中记录调用次数
|
||||
if (traceContext.getSubscriptionId() != null) {
|
||||
iSubscriptionRpcService.incrementCallCount(traceContext.getTenantId(), traceContext.getSubscriptionId(), 1);
|
||||
}
|
||||
//没有任何订阅的时候更新试用次数
|
||||
if (pricingConfigDTO.getTrialCount() != null && pricingConfigDTO.getTrialCount() > 0 && traceContext.getSubscriptionId() == null) {
|
||||
UpdateTrialCountRequest trialReq = new UpdateTrialCountRequest();
|
||||
trialReq.setTenantId(traceContext.getTenantId());
|
||||
trialReq.setUserId(traceContext.getUserId());
|
||||
trialReq.setTargetType(TargetTypeEnum.AGENT);
|
||||
trialReq.setTargetId(traceTarget.getTargetId());
|
||||
iPricingRpcService.updateTrialCount(trialReq);
|
||||
}
|
||||
} else {
|
||||
log.warn("定价配置未命中或已禁用 tenantId={} targetType={} targetId={} pricingStatus={}",
|
||||
traceContext.getTenantId(), traceTarget.getTargetType(), traceTarget.getTargetId(),
|
||||
pricingConfigDTO != null ? pricingConfigDTO.getStatus() : null);
|
||||
}
|
||||
}
|
||||
String dt = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
|
||||
BillResourceStat userStat = BillResourceStat.builder()
|
||||
.tenantId(tenantConfig.getTenantId())
|
||||
.userId(traceContext.getBillUserId())
|
||||
.type(ResourceStatTypeEnum.CONSUMPTION.getCode())
|
||||
.targetType(traceTarget.getTargetType().name())
|
||||
.targetId(Long.parseLong(traceTarget.getTargetId()))
|
||||
.dt(dt)
|
||||
.callCount(1L)
|
||||
.callFailedCount(traceContext.isError() ? 1L : 0L)
|
||||
.creditAmount(creditAmount)
|
||||
.feeAmount(feeAmount)
|
||||
.cacheInputTokens(tokenUsage != null && !traceContext.isError() ? tokenUsage.cacheInputTokens : 0L)
|
||||
.inputTokens(tokenUsage != null && !traceContext.isError() ? tokenUsage.inputTokens : 0L)
|
||||
.outputTokens(tokenUsage != null && !traceContext.isError() ? tokenUsage.outputTokens : 0L)
|
||||
.build();
|
||||
iBillResourceStatService.appendStat(userStat);
|
||||
|
||||
BillResourceStat salerStat = BillResourceStat.builder()
|
||||
.tenantId(tenantConfig.getTenantId())
|
||||
.userId(salerId)
|
||||
.type(ResourceStatTypeEnum.SALES.getCode())
|
||||
.targetType(traceTarget.getTargetType().name())
|
||||
.targetId(Long.parseLong(traceTarget.getTargetId()))
|
||||
.dt(dt)
|
||||
.callCount(1L)
|
||||
.callFailedCount(traceContext.isError() ? 1L : 0L)
|
||||
.creditAmount(creditAmount)
|
||||
.feeAmount(feeAmount)
|
||||
.cacheInputTokens(tokenUsage != null && !traceContext.isError() ? tokenUsage.cacheInputTokens : 0L)
|
||||
.inputTokens(tokenUsage != null && !traceContext.isError() ? tokenUsage.inputTokens : 0L)
|
||||
.outputTokens(tokenUsage != null && !traceContext.isError() ? tokenUsage.outputTokens : 0L)
|
||||
.build();
|
||||
log.debug("消费统计已记录 salerId={} type=SALES targetType={} targetId={} dt={}",
|
||||
salerId, traceTarget.getTargetType(), traceTarget.getTargetId(), dt);
|
||||
iBillResourceStatService.appendStat(salerStat);
|
||||
}
|
||||
|
||||
private String targetsToSting(List<TraceContext.TraceTarget> traceTargets) {
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
for (TraceContext.TraceTarget traceTarget : traceTargets) {
|
||||
if (traceTarget.getTargetType() == TraceContext.TraceTargetType.Model) {
|
||||
stringBuilder.append("[").append(traceTarget.getDescription()).append("(").append(traceTarget.getName()).append(")").append("]");
|
||||
} else {
|
||||
stringBuilder.append("[").append(traceTarget.getName()).append("]");
|
||||
}
|
||||
}
|
||||
return stringBuilder.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
package com.xspaceagi.bill.app.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.xspaceagi.agent.core.sdk.IAgentRpcService;
|
||||
import com.xspaceagi.agent.core.sdk.dto.AgentInfoDto;
|
||||
import com.xspaceagi.agent.core.sdk.dto.ReqResult;
|
||||
import com.xspaceagi.agent.core.sdk.dto.SkillInfoDto;
|
||||
import com.xspaceagi.bill.app.service.BillOrderAppService;
|
||||
import com.xspaceagi.bill.app.service.BillRevenueAppService;
|
||||
import com.xspaceagi.bill.infra.dao.entity.BillOrder;
|
||||
import com.xspaceagi.bill.infra.dao.entity.BillOrderItem;
|
||||
import com.xspaceagi.bill.infra.dao.mapper.BillOrderMapper;
|
||||
import com.xspaceagi.bill.infra.dao.service.IBillOrderItemService;
|
||||
import com.xspaceagi.bill.infra.dao.service.IBillOrderService;
|
||||
import com.xspaceagi.bill.sdk.dto.*;
|
||||
import com.xspaceagi.bill.spec.enums.*;
|
||||
import com.xspaceagi.credit.sdk.dto.CreditAddRequest;
|
||||
import com.xspaceagi.credit.sdk.rpc.ICreditRpcService;
|
||||
import com.xspaceagi.credit.spec.enums.CreditTypeEnum;
|
||||
import com.xspaceagi.pay.sdk.dto.PaymentOrderCreateResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.ScanOrderCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.enums.PayMode;
|
||||
import com.xspaceagi.pay.sdk.service.IPaymentRpcService;
|
||||
import com.xspaceagi.subscription.sdk.dto.CreateSubscriptionRequest;
|
||||
import com.xspaceagi.subscription.sdk.dto.PlanDTO;
|
||||
import com.xspaceagi.subscription.sdk.rpc.ISubscriptionRpcService;
|
||||
import com.xspaceagi.system.application.dto.TenantConfigDto;
|
||||
import com.xspaceagi.system.application.dto.UserDto;
|
||||
import com.xspaceagi.system.application.service.TenantConfigApplicationService;
|
||||
import com.xspaceagi.system.application.service.UserApplicationService;
|
||||
import com.xspaceagi.system.spec.common.RequestContext;
|
||||
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
|
||||
import com.xspaceagi.system.spec.exception.BizException;
|
||||
import com.xspaceagi.system.spec.utils.I18nUtil;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.apache.commons.lang3.time.DateUtils.addMonths;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class BillOrderAppServiceImpl implements BillOrderAppService {
|
||||
|
||||
@Resource
|
||||
private IBillOrderService billOrderService;
|
||||
|
||||
@Resource
|
||||
private IBillOrderItemService billOrderItemService;
|
||||
|
||||
@Resource
|
||||
private BillOrderMapper billOrderMapper;
|
||||
|
||||
@Resource
|
||||
private ISubscriptionRpcService iSubscriptionRpcService;
|
||||
|
||||
@Resource
|
||||
private BillRevenueAppService billRevenueAppService;
|
||||
|
||||
@Resource
|
||||
private IAgentRpcService iAgentRpcService;
|
||||
|
||||
@Resource
|
||||
private ICreditRpcService iCreditRpcService;
|
||||
|
||||
@Resource
|
||||
private TenantConfigApplicationService tenantConfigApplicationService;
|
||||
|
||||
@Resource
|
||||
private IPaymentRpcService iPaymentRpcService;
|
||||
|
||||
@Resource
|
||||
private UserApplicationService userApplicationService;
|
||||
|
||||
@Override
|
||||
public OrderDTO createOrder(CreateOrderRequest request) {
|
||||
if (request.getItems() == null || request.getItems().isEmpty()) {
|
||||
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), I18nUtil.systemMessage("Backend.Bill.Order.Validate.ItemsEmpty"));
|
||||
}
|
||||
|
||||
BillOrder order = BillOrder.builder()
|
||||
.tenantId(request.getTenantId())
|
||||
.userId(request.getUserId())
|
||||
.description(request.getDescription())
|
||||
.bizType(request.getBizType() != null ? request.getBizType().getCode() : null)
|
||||
.orderStatus(OrderStatusEnum.PENDING.getCode())
|
||||
.payStatus(PayStatusEnum.PENDING.getCode())
|
||||
.amount(BigDecimal.ZERO)
|
||||
.extra(request.getExtra() != null ? JSON.toJSONString(request.getExtra()) : null)
|
||||
.created(new Date())
|
||||
.build();
|
||||
billOrderService.save(order);
|
||||
|
||||
List<BillOrderItem> items = new ArrayList<>();
|
||||
BigDecimal totalAmount = BigDecimal.ZERO;
|
||||
for (CreateOrderRequest.CreateOrderItem itemReq : request.getItems()) {
|
||||
BigDecimal itemPrice = itemReq.getPrice() != null ? itemReq.getPrice() : BigDecimal.ZERO;
|
||||
int count = itemReq.getCount() != null ? itemReq.getCount() : 1;
|
||||
BigDecimal lineTotal = itemPrice.multiply(BigDecimal.valueOf(count));
|
||||
|
||||
BillOrderItem item = BillOrderItem.builder()
|
||||
.orderId(order.getId())
|
||||
.targetType(itemReq.getTargetType())
|
||||
.targetName(itemReq.getTargetName())
|
||||
.targetId(itemReq.getTargetId())
|
||||
.price(itemPrice)
|
||||
.count(count)
|
||||
.snapshot(itemReq.getSnapshot() != null ? JSON.toJSONString(itemReq.getSnapshot()) : null)
|
||||
.build();
|
||||
items.add(item);
|
||||
totalAmount = totalAmount.add(lineTotal);
|
||||
}
|
||||
try {
|
||||
billOrderItemService.saveBatch(items);
|
||||
order.setAmount(totalAmount);
|
||||
|
||||
//创建支付订单
|
||||
ScanOrderCreateRequest paymentScanCreateOrderRequest = new ScanOrderCreateRequest();
|
||||
paymentScanCreateOrderRequest.setBizOrderNo(order.getId().toString());
|
||||
paymentScanCreateOrderRequest.setOrderAmount(totalAmount.multiply(new BigDecimal(100)).longValue());
|
||||
paymentScanCreateOrderRequest.setSubject(request.getDescription());
|
||||
PaymentOrderCreateResponse orderForScan = iPaymentRpcService.createOrderForScan(paymentScanCreateOrderRequest);
|
||||
Map<String, Object> extra = request.getExtra();
|
||||
if (extra == null) {
|
||||
extra = new HashMap<>();
|
||||
}
|
||||
extra.put("gatewayPaymentOrderNo", orderForScan.getGatewayPaymentOrderNo());
|
||||
extra.put("payMode", PayMode.scan.name());
|
||||
order.setExtra(JSON.toJSONString(extra));
|
||||
billOrderService.updateById(order);
|
||||
} catch (Exception e) {
|
||||
//有远程网络调用,不使用事务
|
||||
billOrderService.removeById(order.getId());
|
||||
billOrderItemService.remove(new QueryWrapper<BillOrderItem>().eq("order_id", order.getId()));
|
||||
throw new BizException(e.getMessage());
|
||||
}
|
||||
|
||||
log.info("创建订单, orderId={}, userId={}, amount={}", order.getId(), request.getUserId(), totalAmount);
|
||||
return convertToDTO(order);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean paymentCallback(Long tenantId, Long orderId, String payStatus) {
|
||||
log.info("[payment-callback] tenantId={} orderId={}, payStatus={}", tenantId, orderId, payStatus);
|
||||
BillOrder order = billOrderService.getById(orderId);
|
||||
if (order == null) {
|
||||
throw new BizException("ORDER_NOT_FOUND", "Order does not exist");
|
||||
}
|
||||
|
||||
PayStatusEnum payStatusEnum = PayStatusEnum.fromCode(payStatus);
|
||||
if (payStatusEnum == null) {
|
||||
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "Invalid payment status");
|
||||
}
|
||||
|
||||
if (order.getPayStatus().equals(PayStatusEnum.SUCCESS.getCode())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
UserDto userDto = userApplicationService.queryById(order.getUserId());
|
||||
if (userDto == null) {
|
||||
throw new BizException("USER_NOT_FOUND", "User does not exist");
|
||||
}
|
||||
RequestContext.get().setLangMap(userDto.getLangMap());
|
||||
if (PayStatusEnum.SUCCESS.equals(payStatusEnum)) {
|
||||
order.setOrderStatus(OrderStatusEnum.PAID.getCode());
|
||||
order.setPayStatus(PayStatusEnum.SUCCESS.getCode());
|
||||
//支付成功,处理后续业务
|
||||
OrderDTO orderDTO = convertToDTO(order);
|
||||
if (order.getBizType().equals(BizTypeEnum.SUBSCRIPTION.getCode())) {
|
||||
PlanDTO plan = iSubscriptionRpcService.getPlan(orderDTO.getItems().get(0).getTargetId());
|
||||
if (plan.getBizType() == com.xspaceagi.subscription.spec.enums.BizTypeEnum.AGENT && plan.getPrice().compareTo(BigDecimal.ZERO) > 0) {
|
||||
ReqResult<AgentInfoDto> agentInfoDtoReqResult = iAgentRpcService.queryPublishedAgentInfo(Long.parseLong(plan.getBizId()));
|
||||
if (agentInfoDtoReqResult.getData() != null) {
|
||||
AddRevenueRequest addRevenueRequest = new AddRevenueRequest();
|
||||
addRevenueRequest.setTenantId(tenantId);
|
||||
addRevenueRequest.setUserId(agentInfoDtoReqResult.getData().getCreatorId());
|
||||
addRevenueRequest.setBizNo("ORDER" + orderId);
|
||||
addRevenueRequest.setAmount(orderDTO.getAmount());
|
||||
addRevenueRequest.setType(RevenueTypeEnum.PLAN);
|
||||
addRevenueRequest.setTypeId(plan.getId());
|
||||
addRevenueRequest.setOrderId(orderId);
|
||||
addRevenueRequest.setTargetType(RevenueTargetTypeEnum.AGENT);
|
||||
addRevenueRequest.setTargetId(agentInfoDtoReqResult.getData().getId());
|
||||
addRevenueRequest.setRemark(agentInfoDtoReqResult.getData().getName() + "-" + plan.getName());
|
||||
billRevenueAppService.addRevenue(addRevenueRequest);
|
||||
log.info("订单支付成功,添加收益, orderId={}, agentId={}, amount={}", orderId, agentInfoDtoReqResult.getData().getId(), orderDTO.getAmount());
|
||||
}
|
||||
} else if (plan.getBizType() == com.xspaceagi.subscription.spec.enums.BizTypeEnum.SKILL && plan.getPrice().compareTo(BigDecimal.ZERO) > 0) {
|
||||
ReqResult<SkillInfoDto> publishedSkillInfo = iAgentRpcService.getPublishedSkillInfo(Long.parseLong(plan.getBizId()), null);
|
||||
if (publishedSkillInfo.getData() != null) {
|
||||
AddRevenueRequest addRevenueRequest = new AddRevenueRequest();
|
||||
addRevenueRequest.setTenantId(tenantId);
|
||||
addRevenueRequest.setUserId(publishedSkillInfo.getData().getCreatorId());
|
||||
addRevenueRequest.setBizNo("ORDER" + orderId);
|
||||
addRevenueRequest.setAmount(orderDTO.getAmount());
|
||||
addRevenueRequest.setType(RevenueTypeEnum.PLAN);
|
||||
addRevenueRequest.setTypeId(plan.getId());
|
||||
addRevenueRequest.setOrderId(orderId);
|
||||
addRevenueRequest.setTargetType(RevenueTargetTypeEnum.SKILL);
|
||||
addRevenueRequest.setTargetId(publishedSkillInfo.getData().getId());
|
||||
addRevenueRequest.setRemark(publishedSkillInfo.getData().getName());
|
||||
billRevenueAppService.addRevenue(addRevenueRequest);
|
||||
log.info("订单支付成功,添加收益, orderId={}, skillId={}, amount={}", orderId, publishedSkillInfo.getData().getId(), orderDTO.getAmount());
|
||||
}
|
||||
}
|
||||
CreateSubscriptionRequest createSubscriptionRequest = new CreateSubscriptionRequest();
|
||||
createSubscriptionRequest.setTenantId(tenantId);
|
||||
createSubscriptionRequest.setUserId(orderDTO.getUserId());
|
||||
createSubscriptionRequest.setPlanId(plan.getId());
|
||||
createSubscriptionRequest.setBizType(plan.getBizType());
|
||||
createSubscriptionRequest.setExtra(Map.of("plan", plan));
|
||||
iSubscriptionRpcService.createSubscription(createSubscriptionRequest);
|
||||
log.info("订单支付成功,创建订阅, orderId={}, planId={}, userId={}", orderId, plan.getId(), orderDTO.getUserId());
|
||||
}
|
||||
if (order.getBizType().equals(BizTypeEnum.CREDIT_PURCHASE.getCode())) {
|
||||
Map<String, Object> snapshot = orderDTO.getItems().get(0).getSnapshot();
|
||||
if (snapshot != null && snapshot.containsKey("creditAmount")) {
|
||||
TenantConfigDto tenantConfig = tenantConfigApplicationService.getTenantConfig(tenantId);
|
||||
CreditAddRequest creditAddRequest = new CreditAddRequest();
|
||||
creditAddRequest.setTenantId(tenantId);
|
||||
creditAddRequest.setUserId(orderDTO.getUserId());
|
||||
if (snapshot.get("period") != null) {
|
||||
creditAddRequest.setExpireTime(addMonths(new Date(), Integer.parseInt(snapshot.get("period").toString())));
|
||||
}
|
||||
creditAddRequest.setAmount(new BigDecimal(snapshot.get("creditAmount").toString()));
|
||||
creditAddRequest.setCreditType(CreditTypeEnum.PURCHASE);
|
||||
creditAddRequest.setRemark(I18nUtil.systemMessage("Backend.Bill.Order.CreditPurchaseRemark", String.valueOf(snapshot.get("packageName"))));
|
||||
creditAddRequest.setBizNo("ORDER" + orderId);
|
||||
creditAddRequest.setExtra(Map.of("orderId", orderId, "price", orderDTO.getAmount(), "creditExchangeRate", tenantConfig.getCreditExchangeRate()));
|
||||
iCreditRpcService.addCredit(creditAddRequest);
|
||||
log.info("订单支付成功,添加积分, orderId={}, amount={}", orderId, snapshot.get("creditAmount"));
|
||||
}
|
||||
}
|
||||
|
||||
} else if (PayStatusEnum.FAILED.equals(payStatusEnum) || PayStatusEnum.CLOSED.equals(payStatusEnum)) {
|
||||
order.setOrderStatus(OrderStatusEnum.CANCELLED.getCode());
|
||||
order.setPayStatus(payStatusEnum.getCode());
|
||||
} else {
|
||||
order.setPayStatus(payStatusEnum.getCode());
|
||||
}
|
||||
|
||||
billOrderService.updateById(order);
|
||||
log.info("支付回调, orderId={}, payStatus={}", orderId, payStatus);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OrderPageDTO queryOrders(OrderQueryRequest query) {
|
||||
int pageNum = query.getPageNum() != null ? query.getPageNum() : 1;
|
||||
int pageSize = query.getPageSize() != null ? query.getPageSize() : 20;
|
||||
int offset = (pageNum - 1) * pageSize;
|
||||
List<BillOrder> orders = billOrderMapper.selectListWithFilters(query, offset, pageSize);
|
||||
Long total = billOrderMapper.countWithFilters(query);
|
||||
|
||||
OrderPageDTO page = new OrderPageDTO();
|
||||
page.setRecords(orders.stream().map(this::convertToDTO).collect(Collectors.toList()));
|
||||
page.setTotal(total);
|
||||
page.setPageNum(pageNum);
|
||||
page.setPageSize(pageSize);
|
||||
return page;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OrderDTO queryOrder(Long orderId) {
|
||||
return convertToDTO(billOrderService.getById(orderId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public OrderSettlementStatusResponse getOrderSettlementStatus(long userId, Long orderId) {
|
||||
if (orderId == null) {
|
||||
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), I18nUtil.systemMessage("Backend.Bill.Order.Validate.OrderIdRequired"));
|
||||
}
|
||||
BillOrder order = billOrderService.getById(orderId);
|
||||
if (order == null) {
|
||||
throw new BizException("ORDER_NOT_FOUND", I18nUtil.systemMessage("Backend.Bill.Order.Error.OrderNotFound"));
|
||||
}
|
||||
if (!Long.valueOf(userId).equals(order.getUserId())) {
|
||||
throw new BizException("USER_NOT_MATCH", I18nUtil.systemMessage("Backend.Bill.Order.Error.UserNotMatch"));
|
||||
}
|
||||
PayStatusEnum currentPayStatus = PayStatusEnum.fromCode(order.getPayStatus());
|
||||
if (!isTerminalPayStatus(currentPayStatus)) {
|
||||
try {
|
||||
iPaymentRpcService.syncSettlementForBizOrderNo(String.valueOf(orderId));
|
||||
} catch (Exception e) {
|
||||
log.warn("[settlement-status] sync pay status failed orderId={} msg={}", orderId, e.getMessage());
|
||||
}
|
||||
order = billOrderService.getById(orderId);
|
||||
}
|
||||
OrderDTO dto = convertToDTO(order);
|
||||
PayStatusEnum payStatus = dto.getPayStatus();
|
||||
boolean settled = PayStatusEnum.SUCCESS == payStatus;
|
||||
boolean terminalFailed = PayStatusEnum.FAILED == payStatus || PayStatusEnum.CLOSED == payStatus;
|
||||
String message;
|
||||
if (settled) {
|
||||
message = I18nUtil.systemMessage("Backend.Bill.Order.Status.PaySuccess");
|
||||
} else if (terminalFailed) {
|
||||
message = payStatus != null ? payStatus.getDesc() : I18nUtil.systemMessage("Backend.Bill.Order.Status.PayIncomplete");
|
||||
} else {
|
||||
message = I18nUtil.systemMessage("Backend.Bill.Order.Status.SyncingPayment");
|
||||
}
|
||||
return OrderSettlementStatusResponse.builder()
|
||||
.orderId(orderId)
|
||||
.payStatus(payStatus)
|
||||
.orderStatus(dto.getOrderStatus())
|
||||
.settled(settled)
|
||||
.terminalFailed(terminalFailed)
|
||||
.message(message)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static boolean isTerminalPayStatus(PayStatusEnum payStatus) {
|
||||
return payStatus == PayStatusEnum.SUCCESS
|
||||
|| payStatus == PayStatusEnum.FAILED
|
||||
|| payStatus == PayStatusEnum.CLOSED;
|
||||
}
|
||||
|
||||
private OrderDTO convertToDTO(BillOrder entity) {
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
OrderDTO dto = new OrderDTO();
|
||||
BeanUtils.copyProperties(entity, dto);
|
||||
dto.setBizType(BizTypeEnum.fromCode(entity.getBizType()));
|
||||
dto.setOrderStatus(OrderStatusEnum.fromCode(entity.getOrderStatus()));
|
||||
dto.setPayStatus(PayStatusEnum.fromCode(entity.getPayStatus()));
|
||||
if (entity.getExtra() != null) {
|
||||
try {
|
||||
dto.setExtra(JSON.parseObject(entity.getExtra()));
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
List<BillOrderItem> items = billOrderItemService.lambdaQuery()
|
||||
.eq(BillOrderItem::getOrderId, entity.getId())
|
||||
.list();
|
||||
dto.setItems(items.stream().map(item -> {
|
||||
OrderItemDTO itemDTO = new OrderItemDTO();
|
||||
BeanUtils.copyProperties(item, itemDTO);
|
||||
itemDTO.setTargetType(TargetTypeEnum.fromCode(item.getTargetType()));
|
||||
if (item.getSnapshot() != null) {
|
||||
try {
|
||||
itemDTO.setSnapshot(JSON.parseObject(item.getSnapshot()));
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
return itemDTO;
|
||||
}).collect(Collectors.toList()));
|
||||
//超过24小时则改变状态为关闭
|
||||
if (entity.getCreated().getTime() + 24 * 60 * 60 * 1000 < System.currentTimeMillis() && entity.getOrderStatus().equals(OrderStatusEnum.PENDING.getCode())) {
|
||||
dto.setOrderStatus(OrderStatusEnum.CANCELLED);
|
||||
dto.setPayStatus(PayStatusEnum.CLOSED);
|
||||
BillOrder billOrder = new BillOrder();
|
||||
billOrder.setId(entity.getId());
|
||||
billOrder.setOrderStatus(OrderStatusEnum.CANCELLED.getCode());
|
||||
billOrder.setPayStatus(PayStatusEnum.CLOSED.getCode());
|
||||
billOrderService.updateById(billOrder);
|
||||
}
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
package com.xspaceagi.bill.app.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.xspaceagi.bill.app.service.BillRevenueAppService;
|
||||
import com.xspaceagi.bill.infra.dao.entity.BillDailyRevenue;
|
||||
import com.xspaceagi.bill.infra.dao.entity.BillRevenueDetail;
|
||||
import com.xspaceagi.bill.infra.dao.mapper.BillDailyRevenueMapper;
|
||||
import com.xspaceagi.bill.infra.dao.service.IBillDailyRevenueService;
|
||||
import com.xspaceagi.bill.infra.dao.service.IBillRevenueDetailService;
|
||||
import com.xspaceagi.bill.sdk.dto.*;
|
||||
import com.xspaceagi.bill.spec.enums.RevenueStatusEnum;
|
||||
import com.xspaceagi.bill.spec.enums.RevenueTargetTypeEnum;
|
||||
import com.xspaceagi.bill.spec.enums.RevenueTypeEnum;
|
||||
import com.xspaceagi.system.sdk.server.IUserRpcService;
|
||||
import com.xspaceagi.system.spec.common.UserContext;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class BillRevenueAppServiceImpl implements BillRevenueAppService {
|
||||
|
||||
@Resource
|
||||
private IBillDailyRevenueService billDailyRevenueService;
|
||||
|
||||
@Resource
|
||||
private IBillRevenueDetailService billRevenueDetailService;
|
||||
|
||||
@Resource
|
||||
private BillDailyRevenueMapper billDailyRevenueMapper;
|
||||
|
||||
@Resource
|
||||
private IUserRpcService iUserRpcService;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean addRevenue(AddRevenueRequest request) {
|
||||
log.info("addRevenue: {}", request);
|
||||
BillRevenueDetail existing = billRevenueDetailService.lambdaQuery()
|
||||
.eq(BillRevenueDetail::getBizNo, request.getBizNo())
|
||||
.one();
|
||||
if (existing != null) {
|
||||
return false;
|
||||
}
|
||||
return addRevenue0(request);
|
||||
}
|
||||
|
||||
private boolean addRevenue0(AddRevenueRequest request) {
|
||||
String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
|
||||
BillRevenueDetail detail = new BillRevenueDetail();
|
||||
detail.setTenantId(request.getTenantId());
|
||||
detail.setUserId(request.getUserId());
|
||||
detail.setDt(today);
|
||||
detail.setAmount(request.getAmount());
|
||||
detail.setType(request.getType() != null ? request.getType().getCode() : null);
|
||||
detail.setTypeId(request.getTypeId());
|
||||
detail.setOrderId(request.getOrderId());
|
||||
detail.setTargetType(request.getTargetType() != null ? request.getTargetType().getCode() : null);
|
||||
detail.setTargetId(request.getTargetId());
|
||||
detail.setBizNo(request.getBizNo());
|
||||
detail.setRemark(request.getRemark());
|
||||
detail.setExtra(request.getExtra() != null ? JSON.toJSONString(request.getExtra()) : null);
|
||||
detail.setCreated(new Date());
|
||||
billRevenueDetailService.save(detail);
|
||||
|
||||
BillDailyRevenue dailyRevenue = billDailyRevenueService.lambdaQuery()
|
||||
.eq(BillDailyRevenue::getUserId, request.getUserId())
|
||||
.eq(BillDailyRevenue::getDt, today)
|
||||
.one();
|
||||
if (dailyRevenue == null) {
|
||||
dailyRevenue = BillDailyRevenue.builder()
|
||||
.tenantId(request.getTenantId())
|
||||
.userId(request.getUserId())
|
||||
.dt(today)
|
||||
.amount(request.getAmount())
|
||||
.status(RevenueStatusEnum.PENDING.getCode())
|
||||
.created(new Date())
|
||||
.build();
|
||||
billDailyRevenueService.save(dailyRevenue);
|
||||
} else {
|
||||
dailyRevenue.setAmount(dailyRevenue.getAmount().add(request.getAmount()));
|
||||
dailyRevenue.setModified(new Date());
|
||||
billDailyRevenueService.updateById(dailyRevenue);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DailyRevenueDTO> queryDailyRevenue(RevenueQueryRequest query) {
|
||||
List<BillDailyRevenue> list = billDailyRevenueService.lambdaQuery()
|
||||
.eq(query.getUserId() != null, BillDailyRevenue::getUserId, query.getUserId())
|
||||
.eq(query.getDt() != null, BillDailyRevenue::getDt, query.getDt())
|
||||
.orderByDesc(BillDailyRevenue::getDt)
|
||||
.list();
|
||||
return list.stream().map(this::convertDailyToDTO).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public RevenueDetailPageDTO queryRevenueDetail(RevenueQueryRequest query) {
|
||||
int pageNum = query.getPageNum() != null ? query.getPageNum() : 1;
|
||||
int pageSize = query.getPageSize() != null ? query.getPageSize() : 20;
|
||||
List<BillRevenueDetail> list = billRevenueDetailService.lambdaQuery()
|
||||
.eq(query.getUserId() != null, BillRevenueDetail::getUserId, query.getUserId())
|
||||
.eq(query.getDt() != null, BillRevenueDetail::getDt, query.getDt())
|
||||
.eq(query.getType() != null, BillRevenueDetail::getType, query.getType() != null ? query.getType().getCode() : null)
|
||||
.eq(query.getTargetType() != null, BillRevenueDetail::getTargetType, query.getTargetType() != null ? query.getTargetType().getCode() : null)
|
||||
.eq(query.getTargetId() != null, BillRevenueDetail::getTargetId, query.getTargetId())
|
||||
.orderByDesc(BillRevenueDetail::getCreated)
|
||||
.last("LIMIT " + ((pageNum - 1) * pageSize) + ", " + pageSize)
|
||||
.list();
|
||||
Long total = billRevenueDetailService.lambdaQuery()
|
||||
.eq(query.getUserId() != null, BillRevenueDetail::getUserId, query.getUserId())
|
||||
.eq(query.getDt() != null, BillRevenueDetail::getDt, query.getDt())
|
||||
.eq(query.getType() != null, BillRevenueDetail::getType, query.getType() != null ? query.getType().getCode() : null)
|
||||
.eq(query.getTargetType() != null, BillRevenueDetail::getTargetType, query.getTargetType() != null ? query.getTargetType().getCode() : null)
|
||||
.eq(query.getTargetId() != null, BillRevenueDetail::getTargetId, query.getTargetId())
|
||||
.count();
|
||||
|
||||
RevenueDetailPageDTO page = new RevenueDetailPageDTO();
|
||||
page.setRecords(list.stream().map(this::convertDetailToDTO).collect(Collectors.toList()));
|
||||
page.setTotal(total);
|
||||
page.setPageNum(pageNum);
|
||||
page.setPageSize(pageSize);
|
||||
return page;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RevenueStatsDTO getRevenueStats(Long userId) {
|
||||
String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
|
||||
String monthStart = LocalDate.now().withDayOfMonth(1).format(DateTimeFormatter.ofPattern("yyyyMMdd"));
|
||||
|
||||
Map<String, Object> stats = billDailyRevenueMapper.selectStatsByUserId(userId, today, monthStart);
|
||||
|
||||
RevenueStatsDTO dto = new RevenueStatsDTO();
|
||||
dto.setTotalRevenue(toBigDecimal(stats.get("totalRevenue")));
|
||||
dto.setTodayRevenue(toBigDecimal(stats.get("todayRevenue")));
|
||||
dto.setMonthRevenue(toBigDecimal(stats.get("monthRevenue")));
|
||||
dto.setPendingAmount(toBigDecimal(stats.get("pendingAmount")));
|
||||
dto.setSettledAmount(toBigDecimal(stats.get("settledAmount")));
|
||||
dto.setUnsettledAmount(toBigDecimal(stats.get("unsettledAmount")));
|
||||
|
||||
List<BillDailyRevenue> dailyList = billDailyRevenueService.lambdaQuery()
|
||||
.eq(BillDailyRevenue::getUserId, userId)
|
||||
.orderByDesc(BillDailyRevenue::getDt)
|
||||
.list();
|
||||
dto.setDailyRevenues(dailyList.stream().map(this::convertDailyToDTO).collect(Collectors.toList()));
|
||||
return dto;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RevenueStatsDTO getAdminRevenueStats(String monthStart, String monthEnd, Long userId, RevenueStatusEnum status, Integer pageNum, Integer pageSize) {
|
||||
String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
|
||||
|
||||
Map<String, Object> stats = billDailyRevenueMapper.selectAdminStats(monthStart, monthEnd, today, userId);
|
||||
|
||||
RevenueStatsDTO dto = new RevenueStatsDTO();
|
||||
dto.setTotalRevenue(toBigDecimal(stats.get("totalRevenue")));
|
||||
dto.setTodayRevenue(toBigDecimal(stats.get("todayRevenue")));
|
||||
dto.setMonthRevenue(toBigDecimal(stats.get("monthRevenue")));
|
||||
dto.setPendingAmount(toBigDecimal(stats.get("pendingAmount")));
|
||||
dto.setSettledAmount(toBigDecimal(stats.get("settledAmount")));
|
||||
|
||||
int offset = (pageNum - 1) * pageSize;
|
||||
List<Map<String, Object>> dailyList = billDailyRevenueMapper.selectAdminDailyRevenues(monthStart, monthEnd, userId, status != null ? status.getCode() : null, offset, pageSize);
|
||||
List<DailyRevenueDTO> dailyDTOs = new ArrayList<>();
|
||||
List<Long> userIds = new ArrayList<>();
|
||||
for (Map<String, Object> row : dailyList) {
|
||||
DailyRevenueDTO daily = new DailyRevenueDTO();
|
||||
daily.setDt((String) row.get("dt"));
|
||||
daily.setAmount(toBigDecimal(row.get("amount")));
|
||||
daily.setStatus(RevenueStatusEnum.fromCode((String) row.get("status")));
|
||||
daily.setUserId(toLong(row.get("user_id")));
|
||||
dailyDTOs.add(daily);
|
||||
userIds.add(daily.getUserId());
|
||||
}
|
||||
dto.setDailyRevenues(dailyDTOs);
|
||||
|
||||
List<Map<String, Object>> rankings = billDailyRevenueMapper.selectUserRankings(monthStart, monthEnd, userId);
|
||||
Long total = billDailyRevenueMapper.countAdminDailyRevenues(monthStart, monthEnd, userId, status != null ? status.getCode() : null);
|
||||
|
||||
List<RevenueStatsDTO.UserRevenueRank> userRanks = new ArrayList<>();
|
||||
for (Map<String, Object> row : rankings) {
|
||||
RevenueStatsDTO.UserRevenueRank rank = new RevenueStatsDTO.UserRevenueRank();
|
||||
rank.setUserId(toLong(row.get("userId")));
|
||||
rank.setAmount(toBigDecimal(row.get("amount")));
|
||||
userRanks.add(rank);
|
||||
userIds.add(rank.getUserId());
|
||||
}
|
||||
dto.setUserRankings(userRanks);
|
||||
dto.setTotal(total);
|
||||
dto.setPageNum(pageNum);
|
||||
dto.setPageSize(pageSize);
|
||||
|
||||
//补充用户信息
|
||||
List<UserContext> userContexts = iUserRpcService.queryUserListByIds(userIds);
|
||||
Map<Long, UserContext> userContextMap = userContexts.stream().collect(Collectors.toMap(UserContext::getUserId, u -> u, (a, b) -> a));
|
||||
dailyDTOs.forEach(daily -> {
|
||||
UserContext userContext = userContextMap.get(daily.getUserId());
|
||||
if (userContext != null) {
|
||||
daily.setUserName(userContext.getUserName());
|
||||
daily.setNickName(userContext.getNickName());
|
||||
daily.setPhone(userContext.getPhone());
|
||||
daily.setEmail(userContext.getEmail());
|
||||
}
|
||||
});
|
||||
userRanks.forEach(rank -> {
|
||||
UserContext userContext = userContextMap.get(rank.getUserId());
|
||||
if (userContext != null) {
|
||||
rank.setUserName(StringUtils.isNotBlank(userContext.getNickName()) ? userContext.getNickName() : userContext.getUserName());
|
||||
}
|
||||
});
|
||||
return dto;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RevenueDetailPageDTO getUserRevenueDetails(Long userId, String dt, Integer pageNum, Integer pageSize) {
|
||||
List<BillRevenueDetail> list = billRevenueDetailService.lambdaQuery()
|
||||
.eq(BillRevenueDetail::getUserId, userId)
|
||||
.eq(dt != null, BillRevenueDetail::getDt, dt)
|
||||
.orderByDesc(BillRevenueDetail::getCreated)
|
||||
.last("LIMIT " + ((pageNum - 1) * pageSize) + ", " + pageSize)
|
||||
.list();
|
||||
Long total = billRevenueDetailService.lambdaQuery()
|
||||
.eq(BillRevenueDetail::getUserId, userId)
|
||||
.eq(dt != null, BillRevenueDetail::getDt, dt)
|
||||
.count();
|
||||
|
||||
RevenueDetailPageDTO page = new RevenueDetailPageDTO();
|
||||
page.setRecords(list.stream().map(this::convertDetailToDTO).collect(Collectors.toList()));
|
||||
page.setTotal(total);
|
||||
page.setPageNum(pageNum);
|
||||
page.setPageSize(pageSize);
|
||||
return page;
|
||||
}
|
||||
|
||||
private DailyRevenueDTO convertDailyToDTO(BillDailyRevenue entity) {
|
||||
DailyRevenueDTO dto = new DailyRevenueDTO();
|
||||
BeanUtils.copyProperties(entity, dto);
|
||||
dto.setStatus(RevenueStatusEnum.fromCode(entity.getStatus()));
|
||||
return dto;
|
||||
}
|
||||
|
||||
private RevenueDetailDTO convertDetailToDTO(BillRevenueDetail entity) {
|
||||
RevenueDetailDTO dto = new RevenueDetailDTO();
|
||||
BeanUtils.copyProperties(entity, dto);
|
||||
dto.setType(RevenueTypeEnum.fromCode(entity.getType()));
|
||||
dto.setTargetType(RevenueTargetTypeEnum.fromCode(entity.getTargetType()));
|
||||
if (entity.getExtra() != null) {
|
||||
try {
|
||||
dto.setExtra(JSON.parseObject(entity.getExtra()));
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
return dto;
|
||||
}
|
||||
|
||||
private Long toLong(Object val) {
|
||||
if (val == null) return 0L;
|
||||
if (val instanceof Long) return (Long) val;
|
||||
if (val instanceof Number) return ((Number) val).longValue();
|
||||
return Long.parseLong(val.toString());
|
||||
}
|
||||
|
||||
private BigDecimal toBigDecimal(Object val) {
|
||||
if (val == null) return BigDecimal.ZERO;
|
||||
BigDecimal result;
|
||||
if (val instanceof BigDecimal) result = (BigDecimal) val;
|
||||
else if (val instanceof Number) result = BigDecimal.valueOf(((Number) val).doubleValue());
|
||||
else result = new BigDecimal(val.toString());
|
||||
return result.setScale(2, java.math.RoundingMode.HALF_UP);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
package com.xspaceagi.bill.app.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.xspaceagi.bill.app.service.BillWithdrawAppService;
|
||||
import com.xspaceagi.bill.infra.dao.entity.BillDailyRevenue;
|
||||
import com.xspaceagi.bill.infra.dao.entity.BillWithdrawApplication;
|
||||
import com.xspaceagi.bill.infra.dao.entity.BillWithdrawConfig;
|
||||
import com.xspaceagi.bill.infra.dao.entity.BillWithdrawRevenueRef;
|
||||
import com.xspaceagi.bill.infra.dao.mapper.BillWithdrawApplicationMapper;
|
||||
import com.xspaceagi.bill.infra.dao.mapper.BillWithdrawRevenueRefMapper;
|
||||
import com.xspaceagi.bill.infra.dao.service.IBillDailyRevenueService;
|
||||
import com.xspaceagi.bill.infra.dao.service.IBillWithdrawApplicationService;
|
||||
import com.xspaceagi.bill.infra.dao.service.IBillWithdrawConfigService;
|
||||
import com.xspaceagi.bill.infra.dao.service.IBillWithdrawRevenueRefService;
|
||||
import com.xspaceagi.bill.sdk.dto.*;
|
||||
import com.xspaceagi.bill.spec.enums.LimitModeEnum;
|
||||
import com.xspaceagi.bill.spec.enums.RevenueStatusEnum;
|
||||
import com.xspaceagi.bill.spec.enums.WithdrawStatusEnum;
|
||||
import com.xspaceagi.pay.sdk.dto.DeveloperAccountInfoResponse;
|
||||
import com.xspaceagi.pay.sdk.service.IDeveloperAccountInfoRpcService;
|
||||
import com.xspaceagi.system.application.dto.TenantConfigDto;
|
||||
import com.xspaceagi.system.sdk.server.IUserRpcService;
|
||||
import com.xspaceagi.system.sdk.service.INotificationRpcService;
|
||||
import com.xspaceagi.system.sdk.service.dto.UserDetailDto;
|
||||
import com.xspaceagi.system.spec.common.RequestContext;
|
||||
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
|
||||
import com.xspaceagi.system.spec.exception.BizException;
|
||||
import com.xspaceagi.system.spec.utils.I18nUtil;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class BillWithdrawAppServiceImpl implements BillWithdrawAppService {
|
||||
|
||||
@Resource
|
||||
private IBillWithdrawConfigService billWithdrawConfigService;
|
||||
|
||||
@Resource
|
||||
private IBillWithdrawApplicationService billWithdrawApplicationService;
|
||||
|
||||
@Resource
|
||||
private IBillDailyRevenueService billDailyRevenueService;
|
||||
|
||||
@Resource
|
||||
private IBillWithdrawRevenueRefService billWithdrawRevenueRefService;
|
||||
|
||||
@Resource
|
||||
private BillWithdrawRevenueRefMapper billWithdrawRevenueRefMapper;
|
||||
|
||||
@Resource
|
||||
private BillWithdrawApplicationMapper billWithdrawApplicationMapper;
|
||||
|
||||
@Resource
|
||||
private IUserRpcService iUserRpcService;
|
||||
|
||||
@Resource
|
||||
private INotificationRpcService iNotificationRpcService;
|
||||
|
||||
@Resource
|
||||
private IDeveloperAccountInfoRpcService developerAccountInfoRpcService;
|
||||
|
||||
@Override
|
||||
public WithdrawConfigDTO getWithdrawConfig(Long tenantId) {
|
||||
BillWithdrawConfig config = billWithdrawConfigService.lambdaQuery()
|
||||
.eq(BillWithdrawConfig::getTenantId, tenantId)
|
||||
.one();
|
||||
return config != null ? convertConfigToDTO(config) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean saveWithdrawConfig(SaveWithdrawConfigRequest request) {
|
||||
BillWithdrawConfig existing = billWithdrawConfigService.lambdaQuery()
|
||||
.eq(BillWithdrawConfig::getTenantId, request.getTenantId())
|
||||
.one();
|
||||
if (existing != null) {
|
||||
existing.setMinAmount(request.getMinAmount());
|
||||
existing.setMonthlyLimit(request.getMonthlyLimit());
|
||||
existing.setDailyLimit(request.getDailyLimit());
|
||||
existing.setLimitMode(request.getLimitMode() != null ? request.getLimitMode().getCode() : null);
|
||||
return billWithdrawConfigService.updateById(existing);
|
||||
} else {
|
||||
BillWithdrawConfig config = BillWithdrawConfig.builder()
|
||||
.tenantId(request.getTenantId())
|
||||
.minAmount(request.getMinAmount())
|
||||
.monthlyLimit(request.getMonthlyLimit())
|
||||
.dailyLimit(request.getDailyLimit())
|
||||
.limitMode(request.getLimitMode() != null ? request.getLimitMode().getCode() : null)
|
||||
.build();
|
||||
return billWithdrawConfigService.save(config);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public WithdrawApplicationDTO createWithdrawApplication(Long tenantId, Long userId) {
|
||||
BillWithdrawConfig config = billWithdrawConfigService.lambdaQuery()
|
||||
.eq(BillWithdrawConfig::getTenantId, tenantId)
|
||||
.one();
|
||||
if (config == null) {
|
||||
throw new BizException("CONFIG_NOT_FOUND", I18nUtil.systemMessage("Backend.Bill.Withdraw.Error.ConfigNotFound"));
|
||||
}
|
||||
String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
|
||||
List<BillDailyRevenue> pendingRevenues = billDailyRevenueService.lambdaQuery()
|
||||
.eq(BillDailyRevenue::getUserId, userId)
|
||||
.eq(BillDailyRevenue::getStatus, RevenueStatusEnum.PENDING.getCode())
|
||||
.lt(BillDailyRevenue::getDt, today)
|
||||
.list();
|
||||
if (pendingRevenues.isEmpty()) {
|
||||
throw new BizException("NO_PENDING_REVENUE", I18nUtil.systemMessage("Backend.Bill.Withdraw.Error.NoPendingRevenue"));
|
||||
}
|
||||
|
||||
BigDecimal totalAmount = pendingRevenues.stream()
|
||||
.map(BillDailyRevenue::getAmount)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
if (config.getMinAmount() != null && totalAmount.compareTo(config.getMinAmount()) < 0) {
|
||||
throw new BizException("BELOW_MIN_AMOUNT", I18nUtil.systemMessage("Backend.Bill.Withdraw.Error.BelowMinAmount"));
|
||||
}
|
||||
|
||||
DeveloperAccountInfoResponse developerAccountInfo = developerAccountInfoRpcService.getDeveloperAccountInfo(tenantId, userId);
|
||||
if (developerAccountInfo == null || StringUtils.isBlank(developerAccountInfo.getBankCardNo()) || StringUtils.isBlank(developerAccountInfo.getBankName()) || StringUtils.isBlank(developerAccountInfo.getRealName())) {
|
||||
throw new BizException("DEVELOPER_ACCOUNT_NOT_FOUND", I18nUtil.systemMessage("Backend.Bill.Withdraw.Error.DeveloperAccountIncomplete"));
|
||||
}
|
||||
|
||||
// Check daily limit
|
||||
if (config.getDailyLimit() != null) {
|
||||
long todayCount = billWithdrawApplicationService.lambdaQuery()
|
||||
.eq(BillWithdrawApplication::getUserId, userId)
|
||||
.ge(BillWithdrawApplication::getCreated, today)
|
||||
.count();
|
||||
if (todayCount >= config.getDailyLimit()) {
|
||||
if (LimitModeEnum.ALL.equals(LimitModeEnum.fromCode(config.getLimitMode()))) {
|
||||
throw new BizException("DAILY_LIMIT_EXCEEDED", I18nUtil.systemMessage("Backend.Bill.Withdraw.Error.DailyLimitExceeded"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check monthly limit
|
||||
if (config.getMonthlyLimit() != null) {
|
||||
String monthStart = LocalDate.now().withDayOfMonth(1).format(DateTimeFormatter.ofPattern("yyyyMMdd"));
|
||||
long monthCount = billWithdrawApplicationService.lambdaQuery()
|
||||
.eq(BillWithdrawApplication::getUserId, userId)
|
||||
.ge(BillWithdrawApplication::getCreated, monthStart)
|
||||
.count();
|
||||
if (monthCount >= config.getMonthlyLimit()) {
|
||||
LimitModeEnum limitMode = LimitModeEnum.fromCode(config.getLimitMode());
|
||||
if (limitMode == null || LimitModeEnum.ALL.equals(limitMode) || LimitModeEnum.ANY.equals(limitMode)) {
|
||||
throw new BizException("MONTHLY_LIMIT_EXCEEDED", I18nUtil.systemMessage("Backend.Bill.Withdraw.Error.MonthlyLimitExceeded"));
|
||||
}
|
||||
}
|
||||
}
|
||||
TenantConfigDto tenantConfig = (TenantConfigDto) RequestContext.get().getTenantConfig();
|
||||
double revenueRatio = tenantConfig.getRevenueRatio() == null || tenantConfig.getRevenueRatio() <= 0 || tenantConfig.getRevenueRatio() > 1 ? 0 : tenantConfig.getRevenueRatio();
|
||||
BigDecimal fee = totalAmount.multiply(BigDecimal.valueOf(revenueRatio));
|
||||
BillWithdrawApplication application = BillWithdrawApplication.builder()
|
||||
.userId(userId)
|
||||
.amount(totalAmount)
|
||||
.fee(fee)
|
||||
.actualAmount(totalAmount.subtract(fee))
|
||||
.status(WithdrawStatusEnum.PENDING_REVIEW.getCode())
|
||||
.build();
|
||||
billWithdrawApplicationService.save(application);
|
||||
|
||||
List<BillWithdrawRevenueRef> refs = new ArrayList<>();
|
||||
for (BillDailyRevenue revenue : pendingRevenues) {
|
||||
refs.add(BillWithdrawRevenueRef.builder()
|
||||
.applicationId(application.getId())
|
||||
.revenueId(revenue.getId())
|
||||
.build());
|
||||
}
|
||||
billWithdrawRevenueRefService.saveBatch(refs);
|
||||
|
||||
// Update revenue status to WITHDRAW_APPLYING
|
||||
for (BillDailyRevenue revenue : pendingRevenues) {
|
||||
revenue.setStatus(RevenueStatusEnum.WITHDRAW_APPLYING.getCode());
|
||||
billDailyRevenueService.updateById(revenue);
|
||||
}
|
||||
|
||||
log.info("创建提现申请, applicationId={}, userId={}, amount={}", application.getId(), userId, totalAmount);
|
||||
return convertAppToDTO(application);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WithdrawApplicationPageDTO queryWithdrawApplications(WithdrawQueryRequest query) {
|
||||
int pageNum = query.getPageNum() != null ? query.getPageNum() : 1;
|
||||
int pageSize = query.getPageSize() != null ? query.getPageSize() : 20;
|
||||
int offset = (pageNum - 1) * pageSize;
|
||||
List<BillWithdrawApplication> list = billWithdrawApplicationMapper.selectListWithFilters(query, offset, pageSize);
|
||||
Long total = billWithdrawApplicationMapper.countWithFilters(query);
|
||||
|
||||
WithdrawApplicationPageDTO page = new WithdrawApplicationPageDTO();
|
||||
page.setRecords(list.stream().map(this::convertAppToDTO).collect(Collectors.toList()));
|
||||
page.setTotal(total);
|
||||
page.setPageNum(pageNum);
|
||||
page.setPageSize(pageSize);
|
||||
return page;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean processWithdraw(WithdrawProcessRequest request) {
|
||||
BillWithdrawApplication application = billWithdrawApplicationService.getById(request.getApplicationId());
|
||||
if (application == null) {
|
||||
throw new BizException("APPLICATION_NOT_FOUND", I18nUtil.systemMessage("Backend.Bill.Withdraw.Error.ApplicationNotFound"));
|
||||
}
|
||||
|
||||
List<BillWithdrawRevenueRef> refs = billWithdrawRevenueRefMapper
|
||||
.selectByApplicationId(request.getApplicationId());
|
||||
List<Long> revenueIds = refs.stream()
|
||||
.map(BillWithdrawRevenueRef::getRevenueId)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
switch (request.getAction()) {
|
||||
case "REJECT":
|
||||
if (request.getRejectReason() == null || request.getRejectReason().isBlank()) {
|
||||
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), I18nUtil.systemMessage("Backend.Bill.Withdraw.Validate.RejectReasonRequired"));
|
||||
}
|
||||
application.setStatus(WithdrawStatusEnum.REJECTED.getCode());
|
||||
application.setRejectReason(request.getRejectReason());
|
||||
billWithdrawApplicationService.updateById(application);
|
||||
|
||||
billDailyRevenueService.lambdaUpdate()
|
||||
.set(BillDailyRevenue::getStatus, RevenueStatusEnum.PENDING.getCode())
|
||||
.in(BillDailyRevenue::getId, revenueIds)
|
||||
.update();
|
||||
iNotificationRpcService.sendNotifyMessage(application.getUserId(), I18nUtil.systemMessage("Backend.Bill.Withdraw.Notify.Rejected", request.getRejectReason()));
|
||||
log.info("驳回提现申请, applicationId={}, reason={}", request.getApplicationId(), request.getRejectReason());
|
||||
break;
|
||||
|
||||
case "APPROVE":
|
||||
application.setStatus(WithdrawStatusEnum.APPROVED.getCode());
|
||||
billWithdrawApplicationService.updateById(application);
|
||||
|
||||
billDailyRevenueService.lambdaUpdate()
|
||||
.set(BillDailyRevenue::getStatus, RevenueStatusEnum.PAYING.getCode())
|
||||
.in(BillDailyRevenue::getId, revenueIds)
|
||||
.update();
|
||||
log.info("通过提现申请, applicationId={}", request.getApplicationId());
|
||||
iNotificationRpcService.sendNotifyMessage(application.getUserId(), I18nUtil.systemMessage("Backend.Bill.Withdraw.Notify.Approved"));
|
||||
break;
|
||||
|
||||
case "COMPLETE_PAYMENT":
|
||||
application.setStatus(WithdrawStatusEnum.PAID.getCode());
|
||||
if (request.getPaymentExtra() != null) {
|
||||
application.setPaymentExtra(JSON.toJSONString(request.getPaymentExtra()));
|
||||
}
|
||||
billWithdrawApplicationService.updateById(application);
|
||||
|
||||
billDailyRevenueService.lambdaUpdate()
|
||||
.set(BillDailyRevenue::getStatus, RevenueStatusEnum.SETTLED.getCode())
|
||||
.in(BillDailyRevenue::getId, revenueIds)
|
||||
.update();
|
||||
log.info("打款完成, applicationId={}", request.getApplicationId());
|
||||
iNotificationRpcService.sendNotifyMessage(application.getUserId(), I18nUtil.systemMessage("Backend.Bill.Withdraw.Notify.Paid"));
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), I18nUtil.systemMessage("Backend.Bill.Withdraw.Validate.InvalidAction"));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private WithdrawApplicationDTO convertAppToDTO(BillWithdrawApplication entity) {
|
||||
WithdrawApplicationDTO dto = new WithdrawApplicationDTO();
|
||||
BeanUtils.copyProperties(entity, dto);
|
||||
dto.setStatus(WithdrawStatusEnum.fromCode(entity.getStatus()));
|
||||
if (entity.getPaymentExtra() != null) {
|
||||
try {
|
||||
dto.setPaymentExtra(JSON.parseObject(entity.getPaymentExtra()));
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
UserDetailDto userDetailDto = iUserRpcService.queryUserDetailById(entity.getUserId());
|
||||
if (userDetailDto != null) {
|
||||
dto.setUserName(userDetailDto.getNickName() != null ? userDetailDto.getNickName() : userDetailDto.getUserName());
|
||||
dto.setPhone(userDetailDto.getPhone());
|
||||
dto.setEmail(userDetailDto.getEmail());
|
||||
}
|
||||
|
||||
List<BillWithdrawRevenueRef> refs = billWithdrawRevenueRefMapper
|
||||
.selectByApplicationId(entity.getId());
|
||||
if (!refs.isEmpty()) {
|
||||
List<Long> revenueIds = refs.stream()
|
||||
.map(BillWithdrawRevenueRef::getRevenueId)
|
||||
.collect(Collectors.toList());
|
||||
List<BillDailyRevenue> revenues = billDailyRevenueService.listByIds(revenueIds);
|
||||
dto.setRevenues(revenues.stream().map(r -> {
|
||||
DailyRevenueDTO revDTO = new DailyRevenueDTO();
|
||||
BeanUtils.copyProperties(r, revDTO);
|
||||
revDTO.setStatus(RevenueStatusEnum.fromCode(r.getStatus()));
|
||||
revDTO.setUserName(dto.getUserName());
|
||||
revDTO.setPhone(dto.getPhone());
|
||||
revDTO.setEmail(dto.getEmail());
|
||||
return revDTO;
|
||||
}).collect(Collectors.toList()));
|
||||
}
|
||||
return dto;
|
||||
}
|
||||
|
||||
private WithdrawConfigDTO convertConfigToDTO(BillWithdrawConfig entity) {
|
||||
WithdrawConfigDTO dto = new WithdrawConfigDTO();
|
||||
BeanUtils.copyProperties(entity, dto);
|
||||
dto.setLimitMode(LimitModeEnum.fromCode(entity.getLimitMode()));
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package com.xspaceagi.bill.app.service.impl;
|
||||
|
||||
import com.xspaceagi.agent.core.sdk.IAgentRpcService;
|
||||
import com.xspaceagi.agent.core.sdk.IModelRpcService;
|
||||
import com.xspaceagi.agent.core.sdk.dto.AgentInfoDto;
|
||||
import com.xspaceagi.agent.core.sdk.dto.ModelInfoDto;
|
||||
import com.xspaceagi.agent.core.sdk.dto.PluginInfoDto;
|
||||
import com.xspaceagi.agent.core.sdk.dto.WorkflowInfoDto;
|
||||
import com.xspaceagi.bill.app.service.ResourceStatAppService;
|
||||
import com.xspaceagi.bill.infra.dao.entity.BillResourceStat;
|
||||
import com.xspaceagi.bill.infra.dao.mapper.BillResourceStatMapper;
|
||||
import com.xspaceagi.bill.infra.dao.service.IBillResourceStatService;
|
||||
import com.xspaceagi.bill.sdk.dto.ResourceStatDTO;
|
||||
import com.xspaceagi.bill.sdk.dto.ResourceStatPageDTO;
|
||||
import com.xspaceagi.bill.sdk.dto.ResourceStatSummaryDTO;
|
||||
import com.xspaceagi.bill.spec.enums.ResourceStatTypeEnum;
|
||||
import com.xspaceagi.system.sdk.server.IUserRpcService;
|
||||
import com.xspaceagi.system.spec.common.UserContext;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class ResourceStatAppServiceImpl implements ResourceStatAppService {
|
||||
|
||||
@Resource
|
||||
private IBillResourceStatService billResourceStatService;
|
||||
|
||||
@Resource
|
||||
private BillResourceStatMapper billResourceStatMapper;
|
||||
|
||||
@Resource
|
||||
private IUserRpcService iUserRpcService;
|
||||
|
||||
@Resource
|
||||
private IModelRpcService iModelRpcService;
|
||||
|
||||
@Resource
|
||||
private IAgentRpcService iAgentRpcService;
|
||||
|
||||
@Override
|
||||
public ResourceStatPageDTO queryResourceStats(Long tenantId, Long userId, String type,
|
||||
String targetType, Long targetId,
|
||||
String dtStart, String dtEnd,
|
||||
Integer pageNum, Integer pageSize) {
|
||||
int offset = (pageNum - 1) * pageSize;
|
||||
List<ResourceStatDTO> resourceStatDTOS = billResourceStatService.queryStats(tenantId, userId, type, targetType, targetId, dtStart, dtEnd, offset, pageSize)
|
||||
.stream().map(this::convertToDTO).collect(Collectors.toList());
|
||||
if (!resourceStatDTOS.isEmpty()) {
|
||||
List<Long> userIds = resourceStatDTOS.stream().map(ResourceStatDTO::getUserId).distinct().toList();
|
||||
Map<Long, UserContext> userMap = iUserRpcService.queryUserListByIds(userIds)
|
||||
.stream().collect(Collectors.toMap(UserContext::getUserId, user -> user, (a, b) -> a));
|
||||
List<Long> modelIds = resourceStatDTOS.stream().filter(resourceStatDTO -> resourceStatDTO.getTargetType() != null && resourceStatDTO.getTargetType().equals("Model")).map(ResourceStatDTO::getTargetId).distinct().toList();
|
||||
Map<Long, ModelInfoDto> modelInfoDtoMap = iModelRpcService.getModelInfoList(modelIds).stream().collect(Collectors.toMap(ModelInfoDto::getId, modelInfoDto -> modelInfoDto, (a, b) -> a));
|
||||
|
||||
List<Long> agentIds = resourceStatDTOS.stream().filter(resourceStatDTO -> resourceStatDTO.getTargetType() != null && resourceStatDTO.getTargetType().equals("Agent")).map(ResourceStatDTO::getTargetId).distinct().toList();
|
||||
Map<Long, AgentInfoDto> agentInfoDtoMap = iAgentRpcService.queryAgentInfoList(agentIds).getData().stream().collect(Collectors.toMap(AgentInfoDto::getId, agentInfoDto -> agentInfoDto, (a, b) -> a));
|
||||
|
||||
resourceStatDTOS.forEach(resourceStatDTO -> {
|
||||
UserContext userContext = userMap.get(resourceStatDTO.getUserId());
|
||||
if (userContext != null) {
|
||||
resourceStatDTO.setPhone(userContext.getPhone());
|
||||
resourceStatDTO.setEmail(userContext.getEmail());
|
||||
resourceStatDTO.setUserName(userContext.getUserName());
|
||||
resourceStatDTO.setNickName(userContext.getNickName());
|
||||
}
|
||||
if (resourceStatDTO.getTargetType() != null && resourceStatDTO.getTargetType().equals("Model")) {
|
||||
ModelInfoDto modelInfoDto = modelInfoDtoMap.get(resourceStatDTO.getTargetId());
|
||||
if (modelInfoDto != null) {
|
||||
resourceStatDTO.setTargetName(modelInfoDto.getName());
|
||||
}
|
||||
}
|
||||
|
||||
if (resourceStatDTO.getTargetType() != null && resourceStatDTO.getTargetType().equals("Agent")) {
|
||||
AgentInfoDto agentInfoDto = agentInfoDtoMap.get(resourceStatDTO.getTargetId());
|
||||
if (agentInfoDto != null) {
|
||||
resourceStatDTO.setTargetName(agentInfoDto.getName());
|
||||
}
|
||||
}
|
||||
|
||||
if (resourceStatDTO.getTargetType() != null && resourceStatDTO.getTargetType().equals("Plugin")) {
|
||||
PluginInfoDto pluginInfoDto = iAgentRpcService.getPublishedPluginInfo(resourceStatDTO.getTargetId(), null).getData();
|
||||
if (pluginInfoDto != null) {
|
||||
resourceStatDTO.setTargetName(pluginInfoDto.getName());
|
||||
}
|
||||
}
|
||||
if (resourceStatDTO.getTargetType() != null && resourceStatDTO.getTargetType().equals("Workflow")) {
|
||||
WorkflowInfoDto workflowInfoDto = iAgentRpcService.getPublishedWorkflowInfo(resourceStatDTO.getTargetId(), null).getData();
|
||||
if (workflowInfoDto != null) {
|
||||
resourceStatDTO.setTargetName(workflowInfoDto.getName());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (userId == null) {
|
||||
resourceStatDTOS.removeIf(resourceStatDTO -> resourceStatDTO.getUserId() == -1);
|
||||
}
|
||||
|
||||
Long total = billResourceStatService.countStats(tenantId, userId, type, targetType, targetId, dtStart, dtEnd);
|
||||
|
||||
ResourceStatPageDTO page = new ResourceStatPageDTO();
|
||||
page.setRecords(resourceStatDTOS);
|
||||
page.setTotal(total);
|
||||
page.setPageNum(pageNum);
|
||||
page.setPageSize(pageSize);
|
||||
return page;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceStatSummaryDTO getResourceStatSummary(Long tenantId, Long userId,
|
||||
String dtStart, String dtEnd) {
|
||||
List<Map<String, Object>> rows = billResourceStatMapper.selectSummary(tenantId, userId, dtStart, dtEnd);
|
||||
|
||||
ResourceStatSummaryDTO dto = new ResourceStatSummaryDTO();
|
||||
for (Map<String, Object> row : rows) {
|
||||
ResourceStatSummaryDTO.StatGroup group = new ResourceStatSummaryDTO.StatGroup();
|
||||
group.setTotalInputTokens(toLong(row.get("totalInputTokens")));
|
||||
group.setTotalOutputTokens(toLong(row.get("totalOutputTokens")));
|
||||
group.setTotalCacheInputTokens(toLong(row.get("totalCacheInputTokens")));
|
||||
group.setToolCount(toLong(row.get("toolCount")));
|
||||
group.setToolCallCount(toLong(row.get("toolCallCount")));
|
||||
group.setAgentCount(toLong(row.get("agentCount")));
|
||||
group.setAgentCallCount(toLong(row.get("agentCallCount")));
|
||||
group.setModelCallCount(toLong(row.get("modelCallCount")));
|
||||
group.setFailedModelCallCount(toLong(row.get("failedModelCallCount")));
|
||||
group.setFailedToolCallCount(toLong(row.get("failedToolCallCount")));
|
||||
group.setFailedAgentCallCount(toLong(row.get("failedAgentCallCount")));
|
||||
group.setTotalCreditAmount(toBigDecimal(row.get("totalCreditAmount")));
|
||||
group.setTotalAmount(toBigDecimal(row.get("totalAmount")));
|
||||
|
||||
String type = (String) row.get("type");
|
||||
if ("CONSUMPTION".equals(type)) {
|
||||
dto.setConsumption(group);
|
||||
} else if ("SALES".equals(type)) {
|
||||
dto.setSales(group);
|
||||
}
|
||||
}
|
||||
return dto;
|
||||
}
|
||||
|
||||
private ResourceStatDTO convertToDTO(BillResourceStat entity) {
|
||||
ResourceStatDTO dto = new ResourceStatDTO();
|
||||
BeanUtils.copyProperties(entity, dto);
|
||||
dto.setType(ResourceStatTypeEnum.fromCode(entity.getType()));
|
||||
return dto;
|
||||
}
|
||||
|
||||
private Long toLong(Object val) {
|
||||
if (val == null) return 0L;
|
||||
if (val instanceof Long) return (Long) val;
|
||||
if (val instanceof Number) return ((Number) val).longValue();
|
||||
return Long.parseLong(val.toString());
|
||||
}
|
||||
|
||||
private BigDecimal toBigDecimal(Object val) {
|
||||
if (val == null) return BigDecimal.ZERO;
|
||||
if (val instanceof BigDecimal) return (BigDecimal) val;
|
||||
if (val instanceof Number) return BigDecimal.valueOf(((Number) val).doubleValue());
|
||||
return new BigDecimal(val.toString());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user