chore: initialize qiming workspace repository

This commit is contained in:
Codex
2026-05-29 14:22:48 +08:00
commit bfd67a0f2c
10750 changed files with 1885711 additions and 0 deletions

View File

@@ -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>

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}