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,38 @@
<?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>
<artifactId>app-platform-credit</artifactId>
<groupId>com.xspaceagi</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>app-platform-credit-application</artifactId>
<properties>
<maven.deploy.skip>true</maven.deploy.skip>
</properties>
<dependencies>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-credit-domain</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-credit-infra</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-credit-sdk</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,18 @@
package com.xspaceagi.credit.app.service;
import com.xspaceagi.credit.sdk.dto.CreditPackageDTO;
import java.util.List;
public interface CreditPackageService {
Long createPackage(CreditPackageDTO dto);
boolean updatePackage(CreditPackageDTO dto);
boolean deletePackage(Long id);
CreditPackageDTO getPackageById(Long id);
List<CreditPackageDTO> getPackageList(Integer status);
}

View File

@@ -0,0 +1,56 @@
package com.xspaceagi.credit.app.service;
import com.xspaceagi.credit.sdk.dto.*;
import com.xspaceagi.credit.spec.enums.CreditTypeEnum;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
public interface CreditService {
/**
* 添加用户积分
*/
String addCredit(CreditAddRequest request);
/**
* 扣减用户积分
*/
boolean deductCredit(CreditDeductRequest request);
/**
* 获取用户总积分
*/
UserCreditSummary getUserCreditSummary(Long userId);
/**
* 分页查询用户积分汇总列表
*/
UserCreditSummaryPageDTO queryUserCreditSummary(Long userId, Integer pageNum, Integer pageSize);
/**
* 获取用户积分批次列表
*/
List<UserCreditBatchDTO> getUserCreditBatches(Long userId, CreditTypeEnum creditType, Boolean expired);
/**
* 获取用户积分流水明细keyset分页lastId为空查第一页
*/
List<CreditFlowDTO> getCreditFlows(Long userId, CreditTypeEnum creditType, Long lastId, Integer pageSize);
/**
* 获取用户指定类型的积分数量
*/
BigDecimal getCreditsByType(Long userId, CreditTypeEnum creditType);
/**
* 检查用户积分是否足够
*/
boolean checkBalance(Long userId, BigDecimal requiredAmount);
/**
* 获取用户积分统计信息
*/
Map<String, Object> getCreditStatistics(Long userId);
}

View File

@@ -0,0 +1,80 @@
package com.xspaceagi.credit.app.service.impl;
import com.xspaceagi.credit.app.service.CreditPackageService;
import com.xspaceagi.credit.infra.dao.entity.CreditPackage;
import com.xspaceagi.credit.infra.dao.mapper.CreditPackageMapper;
import com.xspaceagi.credit.sdk.dto.CreditPackageDTO;
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
import com.xspaceagi.system.spec.exception.BizException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import jakarta.annotation.Resource;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@Service
public class CreditPackageServiceImpl implements CreditPackageService {
@Resource
private CreditPackageMapper creditPackageMapper;
@Override
public Long createPackage(CreditPackageDTO dto) {
CreditPackage creditPackage = new CreditPackage();
BeanUtils.copyProperties(dto, creditPackage);
if (creditPackage.getStatus() == null) {
creditPackage.setStatus(1);
}
if (creditPackage.getSort() == null) {
creditPackage.setSort(0);
}
creditPackageMapper.insert(creditPackage);
return creditPackage.getId();
}
@Override
public boolean updatePackage(CreditPackageDTO dto) {
if (dto.getId() == null) {
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "plan id cannot be null");
}
CreditPackage creditPackage = new CreditPackage();
BeanUtils.copyProperties(dto, creditPackage);
int updated = creditPackageMapper.updateById(creditPackage);
return updated > 0;
}
@Override
public boolean deletePackage(Long id) {
int deleted = creditPackageMapper.deleteById(id);
return deleted > 0;
}
@Override
public CreditPackageDTO getPackageById(Long id) {
CreditPackage creditPackage = creditPackageMapper.selectById(id);
if (creditPackage == null) {
return null;
}
return convertToDTO(creditPackage);
}
@Override
public List<CreditPackageDTO> getPackageList(Integer status) {
List<CreditPackage> packages = creditPackageMapper.selectList(status);
return packages.stream().map(this::convertToDTO).collect(Collectors.toList());
}
private CreditPackageDTO convertToDTO(CreditPackage creditPackage) {
CreditPackageDTO dto = new CreditPackageDTO();
BeanUtils.copyProperties(creditPackage, dto);
return dto;
}
}

View File

@@ -0,0 +1,527 @@
package com.xspaceagi.credit.app.service.impl;
import com.alibaba.fastjson2.JSON;
import com.xspaceagi.credit.app.service.CreditService;
import com.xspaceagi.credit.infra.dao.entity.CreditFlow;
import com.xspaceagi.credit.infra.dao.entity.UserCredit;
import com.xspaceagi.credit.infra.dao.mapper.CreditFlowMapper;
import com.xspaceagi.credit.infra.dao.mapper.UserCreditMapper;
import com.xspaceagi.credit.sdk.dto.*;
import com.xspaceagi.credit.spec.enums.CreditOperationTypeEnum;
import com.xspaceagi.credit.spec.enums.CreditTypeEnum;
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
import com.xspaceagi.system.spec.exception.BizException;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;
@Slf4j
@Service
public class CreditServiceImpl implements CreditService {
@Resource
private UserCreditMapper userCreditMapper;
@Resource
private CreditFlowMapper creditFlowMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public String addCredit(CreditAddRequest request) {
// Idempotency check
if (request.getBizNo() != null && !request.getBizNo().isEmpty()) {
CreditFlow existing = creditFlowMapper.selectByBizNo(request.getBizNo(), CreditOperationTypeEnum.ADD.getCode());
if (existing != null) {
log.info("Credit grant idempotent return, bizNo={}, batchNo={}", request.getBizNo(), existing.getBatchNo());
return existing.getBatchNo();
}
}
String batchNo = generateBatchNo();
List<CreditFlow> flowList = new ArrayList<>();
BigDecimal beforeAdd = userCreditMapper.sumRemainAmountByUserId(request.getUserId());
// 1. Create full credit batch
UserCredit batch = createUserCredit(request, batchNo);
userCreditMapper.insert(batch);
// 2. Add credit flow
CreditFlow addFlow = createCreditFlow(request, batchNo, beforeAdd);
flowList.add(addFlow);
// 3. Auto-repay loans: deduct from new batch
if (request.getCreditType().getCode() <= CreditTypeEnum.MANUAL.getCode()) {
repayLoansFromBatch(batch.getId());
}
// 4. Batch insert flows
creditFlowMapper.batchInsert(flowList);
return batchNo;
}
private static @NotNull CreditFlow createCreditFlow(CreditAddRequest request, String batchNo, BigDecimal beforeAdd) {
CreditFlow addFlow = new CreditFlow();
addFlow.setUserId(request.getUserId());
addFlow.setBatchNo(batchNo);
addFlow.setCreditType(request.getCreditType().getCode());
addFlow.setOperationType(CreditOperationTypeEnum.ADD.getCode());
addFlow.setAmount(request.getAmount());
addFlow.setBeforeAmount(beforeAdd);
addFlow.setAfterAmount(beforeAdd.add(request.getAmount()));
addFlow.setBizNo(request.getBizNo());
addFlow.setRemark(request.getRemark());
return addFlow;
}
private static @NotNull UserCredit createUserCredit(CreditAddRequest request, String batchNo) {
UserCredit batch = new UserCredit();
batch.setUserId(request.getUserId());
batch.setBatchNo(batchNo);
batch.setCreditType(request.getCreditType().getCode());
batch.setTotalAmount(request.getAmount());
batch.setUsedAmount(BigDecimal.ZERO);
batch.setRemainAmount(request.getAmount());
batch.setExpireTime(request.getExpireTime());
batch.setRepaidAmount(BigDecimal.ZERO);
batch.setRepayStatus(0);
batch.setRemark(request.getRemark());
if (request.getExtra() != null) {
batch.setExtra(JSON.toJSONString(request.getExtra()));
}
return batch;
}
/**
* Deduct credits from specified batch for auto loan repayment
*/
private void repayLoansFromBatch(Long batchId) {
UserCredit batch = userCreditMapper.selectById(batchId);
List<UserCredit> unpaidLoans = userCreditMapper.selectUnpaidLoans(batch.getUserId());
BigDecimal available = batch.getRemainAmount();
int batchVersion = batch.getVersion();
for (UserCredit loan : unpaidLoans) {
if (available.compareTo(BigDecimal.ZERO) <= 0) {
break;
}
BigDecimal loanDebt = loan.getTotalAmount().subtract(
loan.getRepaidAmount() != null ? loan.getRepaidAmount() : BigDecimal.ZERO);
BigDecimal repayAmount = loanDebt.min(available);
// Update loan batch
int newStatus = loanDebt.compareTo(repayAmount) <= 0 ? 1 : 0;
int loanUpdated = userCreditMapper.updateRepayAmount(loan.getId(), repayAmount, newStatus, loan.getVersion());
if (loanUpdated == 0) {
log.warn("Loan repayment optimistic lock conflict, loanId={}", loan.getId());
throw new BizException("OPTIMISTIC_LOCK_CONFLICT", "Loan repayment failed, please retry");
}
// Deduct from new batch
int batchUpdated = userCreditMapper.updateRemainAmountWithVersion(batchId, repayAmount, batchVersion);
if (batchUpdated == 0) {
log.warn("Loan repayment deduction optimistic lock conflict, batchId={}", batchId);
throw new BizException("OPTIMISTIC_LOCK_CONFLICT", "Loan repayment deduction failed, please retry");
}
batchVersion++;
available = available.subtract(repayAmount);
log.info("Loan repayment success, userId={}, loanBatchNo={}, repayAmount={}, remainDebt={}",
batch.getUserId(), loan.getBatchNo(), repayAmount, loanDebt.subtract(repayAmount));
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deductCredit(CreditDeductRequest request) {
if (request.getAmount() == null || request.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "Deduction amount must be greater than 0");
}
if (request.getCreditType() == null) {
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "Deduction type cannot be empty");
}
// Idempotency check
if (request.getBizNo() != null && !request.getBizNo().isEmpty()) {
CreditFlow existing = creditFlowMapper.selectByBizNo(request.getBizNo(), CreditOperationTypeEnum.DEDUCT.getCode());
if (existing != null) {
log.info("Credit deduction idempotent return, bizNo={}", request.getBizNo());
return true;
}
}
boolean allowNegative = Boolean.TRUE.equals(request.getAllowNegative());
BigDecimal totalAmount = userCreditMapper.sumRemainAmountByUserId(request.getUserId());
if (!allowNegative && (totalAmount == null || totalAmount.compareTo(request.getAmount()) < 0)) {
throw new BizException("INSUFFICIENT_CREDIT", "Insufficient credit balance, current balance: " + totalAmount);
}
// 3. Get credit batches by priority: subscription > purchase > activity
List<UserCredit> allValidCredits = userCreditMapper.selectValidCreditsOrderByExpireTime(request.getUserId());
// 4. Group and sort by type
List<UserCredit> activityCredits = filterCreditsByType(allValidCredits, CreditTypeEnum.ACTIVITY.getCode());
List<UserCredit> subscriptionCredits = filterCreditsByType(allValidCredits, CreditTypeEnum.SUBSCRIPTION.getCode());
List<UserCredit> purchaseCredits = filterCreditsByType(allValidCredits, CreditTypeEnum.PURCHASE.getCode());
List<UserCredit> manualCredits = filterCreditsByType(allValidCredits, CreditTypeEnum.MANUAL.getCode());
// 5. Deduct credits
List<CreditFlow> flowList = new ArrayList<>();
BigDecimal remainAmount = request.getAmount();
remainAmount = deductCreditsByType(activityCredits, remainAmount, request, flowList);
remainAmount = deductCreditsByType(manualCredits, remainAmount, request, flowList);
remainAmount = deductCreditsByType(subscriptionCredits, remainAmount, request, flowList);
remainAmount = deductCreditsByType(purchaseCredits, remainAmount, request, flowList);
if (!allowNegative && remainAmount.compareTo(BigDecimal.ZERO) > 0) {
throw new BizException("INSUFFICIENT_CREDIT", "Insufficient credit balance, remaining undeducted: " + remainAmount);
}
if (allowNegative && remainAmount.compareTo(BigDecimal.ZERO) > 0) {
log.info("Create loan batch, userId={}, amount={}, loan={}", request.getUserId(), request.getAmount(), remainAmount);
String loanBatchNo = generateBatchNo();
UserCredit loanBatch = createLoanBatch(request, loanBatchNo, remainAmount);
userCreditMapper.insert(loanBatch);
deductCreditsByType(List.of(loanBatch), remainAmount, request, flowList);
}
// 6. Batch insert flows
if (!flowList.isEmpty()) {
creditFlowMapper.batchInsert(flowList);
}
return true;
}
private static @NotNull UserCredit createLoanBatch(CreditDeductRequest request, String loanBatchNo, BigDecimal remainAmount) {
UserCredit loanBatch = new UserCredit();
loanBatch.setUserId(request.getUserId());
loanBatch.setBatchNo(loanBatchNo);
loanBatch.setCreditType(CreditTypeEnum.LOAN.getCode());
loanBatch.setTotalAmount(remainAmount);
loanBatch.setUsedAmount(BigDecimal.ZERO);
loanBatch.setRemainAmount(remainAmount);
loanBatch.setRepaidAmount(BigDecimal.ZERO);
loanBatch.setRepayStatus(0);
loanBatch.setVersion(1);
return loanBatch;
}
/**
* Filter by credit type
*/
private List<UserCredit> filterCreditsByType(List<UserCredit> credits, Integer creditType) {
return credits.stream()
.filter(credit -> credit.getCreditType().equals(creditType))
.sorted(Comparator.comparing(UserCredit::getExpireTime, Comparator.nullsFirst(Comparator.naturalOrder())))
.collect(Collectors.toList());
}
/**
* Deduct from specified credit type batches
*/
private BigDecimal deductCreditsByType(List<UserCredit> credits, BigDecimal needAmount,
CreditDeductRequest request, List<CreditFlow> flowList) {
BigDecimal remainNeed = needAmount;
Long userId = request.getUserId();
// Sort by expiry time, deduct soon-to-expire first
for (UserCredit credit : credits) {
if (remainNeed.compareTo(BigDecimal.ZERO) <= 0) {
break;
}
BigDecimal availableAmount = credit.getRemainAmount();
if (availableAmount.compareTo(BigDecimal.ZERO) <= 0) {
continue;
}
BigDecimal deductAmount = availableAmount.min(remainNeed);
BigDecimal beforeAmount = userCreditMapper.sumRemainAmountByUserId(userId);
// Use optimistic lock to update balance
int updated = userCreditMapper.updateRemainAmountWithVersion(
credit.getId(),
deductAmount,
credit.getVersion()
);
if (updated > 0) {
BigDecimal afterAmount = beforeAmount.subtract(deductAmount);
// Create flow record, creditType uses deduction type (e.g. model call/agent call/tool call)
CreditFlow flow = new CreditFlow();
flow.setUserId(userId);
flow.setBatchNo(credit.getBatchNo());
flow.setCreditType(request.getCreditType().getCode());
flow.setOperationType(CreditOperationTypeEnum.DEDUCT.getCode());
flow.setAmount(deductAmount);
flow.setBeforeAmount(beforeAmount);
flow.setAfterAmount(afterAmount);
flow.setBizNo(request.getBizNo());
flow.setRemark(request.getRemark());
flowList.add(flow);
remainNeed = remainNeed.subtract(deductAmount);
} else {
// Optimistic lock conflict, retry or throw
log.warn("Optimistic lock conflict, userId={} batchNo={} deduction failed", userId, credit.getBatchNo());
throw new BizException("OPTIMISTIC_LOCK_CONFLICT", "Credit deduction failed, please retry");
}
}
return remainNeed;
}
@Override
public UserCreditSummary getUserCreditSummary(Long userId) {
UserCreditSummary summary = new UserCreditSummary();
summary.setUserId(userId);
// Use cache to optimize query
BigDecimal totalCredit = userCreditMapper.sumRemainAmountByUserId(userId);
summary.setTotalCredit(totalCredit != null ? totalCredit : BigDecimal.ZERO);
BigDecimal subscriptionCredit = userCreditMapper.sumRemainAmountByUserIdAndType(userId, CreditTypeEnum.SUBSCRIPTION.getCode());
summary.setSubscriptionCredit(subscriptionCredit != null ? subscriptionCredit : BigDecimal.ZERO);
BigDecimal purchaseCredit = userCreditMapper.sumRemainAmountByUserIdAndType(userId, CreditTypeEnum.PURCHASE.getCode());
summary.setPurchaseCredit(purchaseCredit != null ? purchaseCredit : BigDecimal.ZERO);
BigDecimal activityCredit = userCreditMapper.sumRemainAmountByUserIdAndType(userId, CreditTypeEnum.ACTIVITY.getCode());
summary.setActivityCredit(activityCredit != null ? activityCredit : BigDecimal.ZERO);
BigDecimal manualCredit = userCreditMapper.sumRemainAmountByUserIdAndType(userId, CreditTypeEnum.MANUAL.getCode());
summary.setManualCredit(manualCredit != null ? manualCredit : BigDecimal.ZERO);
return summary;
}
@Override
public UserCreditSummaryPageDTO queryUserCreditSummary(Long userId, Integer pageNum, Integer pageSize) {
int page = pageNum != null && pageNum > 0 ? pageNum : 1;
int size = pageSize != null && pageSize > 0 ? pageSize : 20;
int offset = (page - 1) * size;
List<Map<String, Object>> rows = userCreditMapper.selectSummaryList(userId, offset, size);
Long total = userCreditMapper.countSummaryList(userId);
List<UserCreditSummary> records = rows.stream().map(row -> {
UserCreditSummary summary = new UserCreditSummary();
summary.setUserId(toLong(row.get("user_id")));
summary.setTotalCredit(toBigDecimal(row.get("totalCredit")));
summary.setSubscriptionCredit(toBigDecimal(row.get("subscriptionCredit")));
summary.setPurchaseCredit(toBigDecimal(row.get("purchaseCredit")));
summary.setActivityCredit(toBigDecimal(row.get("activityCredit")));
summary.setManualCredit(toBigDecimal(row.get("manualCredit")));
return summary;
}).collect(Collectors.toList());
UserCreditSummaryPageDTO pageDTO = new UserCreditSummaryPageDTO();
pageDTO.setRecords(records);
pageDTO.setTotal(total);
pageDTO.setPageNum(page);
pageDTO.setPageSize(size);
return pageDTO;
}
@Override
public List<UserCreditBatchDTO> getUserCreditBatches(Long userId, CreditTypeEnum creditType, Boolean expired) {
if (userId == null) {
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "User ID cannot be empty");
}
List<UserCredit> credits;
Integer typeCode = creditType != null ? creditType.getCode() : null;
if (expired != null && expired) {
credits = userCreditMapper.selectExpiredCredits(userId, typeCode);
} else if (expired != null) {
credits = userCreditMapper.selectValidCredits(userId, typeCode);
} else {
if (typeCode != null) {
credits = userCreditMapper.selectByUserIdAndType(userId, typeCode);
} else {
credits = userCreditMapper.selectByUserId(userId);
}
}
// Sort: created time descending
credits.sort(Comparator.comparing(UserCredit::getCreated, Comparator.nullsLast(Comparator.reverseOrder())));
return credits.stream().map(this::convertToBatchDTO).collect(Collectors.toList());
}
@Override
public List<CreditFlowDTO> getCreditFlows(Long userId, CreditTypeEnum creditType, Long lastId, Integer pageSize) {
if (userId == null) {
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "User ID cannot be empty");
}
if (pageSize == null || pageSize < 1 || pageSize > 100) {
pageSize = 20;
}
Integer typeCode = creditType != null ? creditType.getCode() : null;
List<CreditFlow> flows;
if (lastId != null) {
flows = creditFlowMapper.selectByUserIdAndTypeWithOffset(userId, typeCode, lastId, pageSize);
} else {
flows = creditFlowMapper.selectByUserIdAndType(userId, typeCode, pageSize);
}
List<CreditFlowDTO> creditFlowDTOS = flows.stream().map(this::convertToFlowDTO).collect(Collectors.toList());
Map<String, List<CreditFlowDTO>> grouped = creditFlowDTOS.stream()
.filter(dto -> dto.getBizNo() != null)
.collect(Collectors.groupingBy(CreditFlowDTO::getBizNo));
Set<Long> toRemove = new HashSet<>();
for (List<CreditFlowDTO> group : grouped.values()) {
if (group.size() > 1) {
group.sort(Comparator.comparing(CreditFlowDTO::getId));
CreditFlowDTO keeper = group.get(0);
BigDecimal totalAmount = group.stream().map(CreditFlowDTO::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
keeper.setAmount(totalAmount);
group.subList(1, group.size()).forEach(dto -> toRemove.add(dto.getId()));
}
}
creditFlowDTOS.removeIf(dto -> toRemove.contains(dto.getId()));
return creditFlowDTOS;
}
@Override
public BigDecimal getCreditsByType(Long userId, CreditTypeEnum creditType) {
if (userId == null) {
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "User ID cannot be empty");
}
if (creditType == null) {
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "Credit type cannot be empty");
}
return userCreditMapper.sumRemainAmountByUserIdAndType(userId, creditType.getCode());
}
@Override
public boolean checkBalance(Long userId, BigDecimal requiredAmount) {
if (userId == null) {
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "User ID cannot be empty");
}
if (requiredAmount == null || requiredAmount.compareTo(BigDecimal.ZERO) <= 0) {
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "Credit amount must be greater than 0");
}
BigDecimal totalCredit = userCreditMapper.sumRemainAmountByUserId(userId);
return totalCredit != null && totalCredit.compareTo(requiredAmount) >= 0;
}
@Override
public Map<String, Object> getCreditStatistics(Long userId) {
if (userId == null) {
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "User ID cannot be empty");
}
Map<String, Object> statistics = new HashMap<>();
// Get total credits
BigDecimal totalCredit = userCreditMapper.sumRemainAmountByUserId(userId);
statistics.put("totalCredit", totalCredit != null ? totalCredit : BigDecimal.ZERO);
// Get credits by type
Map<String, BigDecimal> typeCredits = new HashMap<>();
typeCredits.put("SUBSCRIPTION", getCreditsByType(userId, CreditTypeEnum.SUBSCRIPTION));
typeCredits.put("PURCHASE", getCreditsByType(userId, CreditTypeEnum.PURCHASE));
typeCredits.put("ACTIVITY", getCreditsByType(userId, CreditTypeEnum.ACTIVITY));
typeCredits.put("MANUAL", getCreditsByType(userId, CreditTypeEnum.MANUAL));
statistics.put("typeCredits", typeCredits);
// Get credits expiring soon (within 30 days)
Date thirtyDaysLater = new Date(System.currentTimeMillis() + 30L * 24 * 60 * 60 * 1000);
BigDecimal expiringSoon = userCreditMapper.sumExpiringCredits(userId, thirtyDaysLater);
statistics.put("expiringSoon", expiringSoon != null ? expiringSoon : BigDecimal.ZERO);
// Get total flow count
int flowCount = creditFlowMapper.countByUserId(userId);
statistics.put("totalFlows", flowCount);
return statistics;
}
private UserCreditBatchDTO convertToBatchDTO(UserCredit credit) {
UserCreditBatchDTO dto = new UserCreditBatchDTO();
dto.setId(credit.getId());
dto.setUserId(credit.getUserId());
dto.setBatchNo(credit.getBatchNo());
CreditTypeEnum typeEnum = CreditTypeEnum.getByCode(credit.getCreditType());
dto.setCreditType(typeEnum);
dto.setCreditTypeName(typeEnum != null ? typeEnum.getDesc() : "");
dto.setTotalAmount(credit.getTotalAmount());
dto.setUsedAmount(credit.getUsedAmount());
dto.setRemainAmount(credit.getRemainAmount());
dto.setExpireTime(credit.getExpireTime());
boolean isExpired = credit.getExpireTime() != null && credit.getExpireTime().before(new Date());
dto.setExpired(isExpired);
dto.setCreated(credit.getCreated());
dto.setRemark(credit.getRemark());
if (JSON.isValidObject(credit.getExtra())){
dto.setExtra(JSON.parseObject(credit.getExtra()));
}
return dto;
}
private CreditFlowDTO convertToFlowDTO(CreditFlow flow) {
CreditFlowDTO dto = new CreditFlowDTO();
dto.setId(flow.getId());
dto.setUserId(flow.getUserId());
dto.setBatchNo(flow.getBatchNo());
CreditTypeEnum typeEnum = CreditTypeEnum.getByCode(flow.getCreditType());
dto.setCreditType(typeEnum);
dto.setCreditTypeName(typeEnum != null ? typeEnum.getDesc() : "");
dto.setOperationType(flow.getOperationType());
dto.setOperationTypeName(flow.getOperationType().equals(CreditOperationTypeEnum.ADD.getCode()) ?
CreditOperationTypeEnum.ADD.getDesc() : CreditOperationTypeEnum.DEDUCT.getDesc());
dto.setAmount(flow.getAmount());
dto.setBeforeAmount(flow.getBeforeAmount());
dto.setAfterAmount(flow.getAfterAmount());
dto.setBizNo(flow.getBizNo());
dto.setCreated(flow.getCreated());
dto.setRemark(flow.getRemark());
return dto;
}
private String generateBatchNo() {
return "CREDIT_" + System.currentTimeMillis() + "_" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
}
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);
}
}