chore: initialize qiming workspace repository
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
<?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-api</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-credit-application</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-context</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.xspaceagi.credit.api.rpc;
|
||||
|
||||
import com.xspaceagi.credit.app.service.CreditService;
|
||||
import com.xspaceagi.credit.sdk.dto.CreditAddRequest;
|
||||
import com.xspaceagi.credit.sdk.dto.CreditDeductRequest;
|
||||
import com.xspaceagi.credit.sdk.dto.CreditRequest;
|
||||
import com.xspaceagi.credit.sdk.dto.UserCreditSummary;
|
||||
import com.xspaceagi.credit.sdk.rpc.ICreditRpcService;
|
||||
import com.xspaceagi.system.spec.common.RequestContext;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class CreditRpcService implements ICreditRpcService {
|
||||
|
||||
@Resource
|
||||
private CreditService creditService;
|
||||
|
||||
@Override
|
||||
public String addCredit(CreditAddRequest request) {
|
||||
assertRequest(request);
|
||||
if (RequestContext.get() == null) {
|
||||
RequestContext.setThreadTenantId(request.getTenantId());
|
||||
try {
|
||||
return creditService.addCredit(request);
|
||||
} finally {
|
||||
RequestContext.remove();
|
||||
}
|
||||
}
|
||||
return creditService.addCredit(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deductCredit(CreditDeductRequest request) {
|
||||
assertRequest(request);
|
||||
if (RequestContext.get() == null) {
|
||||
RequestContext.setThreadTenantId(request.getTenantId());
|
||||
try {
|
||||
return creditService.deductCredit(request);
|
||||
} finally {
|
||||
RequestContext.remove();
|
||||
}
|
||||
}
|
||||
return creditService.deductCredit(request);
|
||||
}
|
||||
|
||||
private void assertRequest(CreditRequest request) {
|
||||
Assert.notNull(request, "Request cannot be null");
|
||||
Assert.hasText(request.getBizNo(), "BizNo cannot be empty");
|
||||
Assert.notNull(request.getUserId(), "User ID cannot be empty");
|
||||
Assert.notNull(request.getTenantId(), "Tenant ID cannot be empty");
|
||||
Assert.notNull(request.getCreditType(), "Credit type cannot be empty");
|
||||
Assert.notNull(request.getAmount(), "Credit amount cannot be empty");
|
||||
Assert.isTrue(request.getAmount().compareTo(BigDecimal.ZERO) > 0, "Credit amount must be greater than 0");
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserCreditSummary getUserCreditSummary(Long userId) {
|
||||
return creditService.getUserCreditSummary(userId);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?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-domain</artifactId>
|
||||
|
||||
<properties>
|
||||
<maven.deploy.skip>true</maven.deploy.skip>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-credit-spec</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-credit-sdk</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>${lombok.version}</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,28 @@
|
||||
<?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>
|
||||
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-credit-infra</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>system-sdk</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.xspaceagi.credit.infra.dao.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@TableName("credit_flow")
|
||||
public class CreditFlow {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
private Long userId;
|
||||
|
||||
private String batchNo;
|
||||
|
||||
private Integer creditType;
|
||||
|
||||
private Integer operationType;
|
||||
|
||||
private BigDecimal amount;
|
||||
|
||||
private BigDecimal beforeAmount;
|
||||
|
||||
private BigDecimal afterAmount;
|
||||
|
||||
private String bizNo;
|
||||
|
||||
private Date created;
|
||||
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.xspaceagi.credit.infra.dao.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@TableName("credit_package")
|
||||
public class CreditPackage {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
private String packageName;
|
||||
|
||||
private BigDecimal creditAmount;
|
||||
|
||||
private BigDecimal price;
|
||||
|
||||
private Integer sort;
|
||||
|
||||
private Integer status;
|
||||
|
||||
private Date created;
|
||||
|
||||
private Integer period;
|
||||
|
||||
private Date modified;
|
||||
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.xspaceagi.credit.infra.dao.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@TableName("user_credit")
|
||||
public class UserCredit {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
private Long userId;
|
||||
|
||||
private String batchNo;
|
||||
|
||||
private Integer creditType;
|
||||
|
||||
private BigDecimal totalAmount;
|
||||
|
||||
private BigDecimal usedAmount;
|
||||
|
||||
private BigDecimal remainAmount;
|
||||
|
||||
private Date expireTime;
|
||||
|
||||
private Date created;
|
||||
|
||||
private Date modified;
|
||||
|
||||
private BigDecimal repaidAmount;
|
||||
|
||||
private Integer repayStatus;
|
||||
|
||||
private String remark;
|
||||
|
||||
private Integer version;
|
||||
|
||||
private String extra;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.xspaceagi.credit.infra.dao.mapper;
|
||||
|
||||
import com.xspaceagi.credit.infra.dao.entity.CreditFlow;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface CreditFlowMapper {
|
||||
|
||||
int insert(CreditFlow creditFlow);
|
||||
|
||||
int batchInsert(@Param("list") List<CreditFlow> list);
|
||||
|
||||
/**
|
||||
* 获取用户指定类型的前N条流水记录(按ID降序)
|
||||
*/
|
||||
List<CreditFlow> selectByUserIdAndType(@Param("userId") Long userId,
|
||||
@Param("creditType") Integer creditType,
|
||||
@Param("limit") Integer limit);
|
||||
|
||||
/**
|
||||
* 获取用户指定类型且ID小于指定值的流水记录(用于分页)
|
||||
*/
|
||||
List<CreditFlow> selectByUserIdAndTypeWithOffset(@Param("userId") Long userId,
|
||||
@Param("creditType") Integer creditType,
|
||||
@Param("lastId") Long lastId,
|
||||
@Param("limit") Integer limit);
|
||||
|
||||
CreditFlow selectByBizNo(@Param("bizNo") String bizNo, @Param("operationType") Integer operationType);
|
||||
|
||||
int countByUserId(@Param("userId") Long userId);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.xspaceagi.credit.infra.dao.mapper;
|
||||
|
||||
import com.xspaceagi.credit.infra.dao.entity.CreditPackage;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface CreditPackageMapper {
|
||||
|
||||
int insert(CreditPackage creditPackage);
|
||||
|
||||
int updateById(CreditPackage creditPackage);
|
||||
|
||||
int deleteById(@Param("id") Long id);
|
||||
|
||||
CreditPackage selectById(@Param("id") Long id);
|
||||
|
||||
List<CreditPackage> selectList(@Param("status") Integer status);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.xspaceagi.credit.infra.dao.mapper;
|
||||
|
||||
import com.xspaceagi.credit.infra.dao.entity.UserCredit;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface UserCreditMapper {
|
||||
|
||||
int insert(UserCredit userCredit);
|
||||
|
||||
int updateById(UserCredit userCredit);
|
||||
|
||||
UserCredit selectById(@Param("id") Long id);
|
||||
|
||||
UserCredit selectByBatchNo(@Param("batchNo") String batchNo);
|
||||
|
||||
List<UserCredit> selectByUserId(@Param("userId") Long userId);
|
||||
|
||||
List<UserCredit> selectByUserIdAndType(@Param("userId") Long userId, @Param("creditType") Integer creditType);
|
||||
|
||||
List<UserCredit> selectExpiredCredits(@Param("userId") Long userId, @Param("creditType") Integer creditType);
|
||||
|
||||
List<UserCredit> selectValidCredits(@Param("userId") Long userId, @Param("creditType") Integer creditType);
|
||||
|
||||
List<UserCredit> selectValidCreditsOrderByExpireTime(@Param("userId") Long userId);
|
||||
|
||||
BigDecimal sumRemainAmountByUserId(@Param("userId") Long userId);
|
||||
|
||||
BigDecimal sumRemainAmountByUserIdAndType(@Param("userId") Long userId, @Param("creditType") Integer creditType);
|
||||
|
||||
/**
|
||||
* 带乐观锁的余额更新,使用version字段保证原子性
|
||||
*/
|
||||
@Update("UPDATE user_credit " +
|
||||
"SET remain_amount = remain_amount - #{deductAmount}, " +
|
||||
" used_amount = used_amount + #{deductAmount}, " +
|
||||
" version = version + 1, " +
|
||||
" modified = NOW() " +
|
||||
"WHERE id = #{id} AND remain_amount >= #{deductAmount} AND version = #{version}")
|
||||
int updateRemainAmountWithVersion(@Param("id") Long id,
|
||||
@Param("deductAmount") BigDecimal deductAmount,
|
||||
@Param("version") Integer version);
|
||||
|
||||
|
||||
List<java.util.Map<String, Object>> selectSummaryList(@Param("userId") Long userId, @Param("offset") Integer offset, @Param("limit") Integer limit);
|
||||
|
||||
Long countSummaryList(@Param("userId") Long userId);
|
||||
|
||||
List<UserCredit> selectUnpaidLoans(@Param("userId") Long userId);
|
||||
|
||||
int updateRepayAmount(@Param("id") Long id,
|
||||
@Param("repayAmount") BigDecimal repayAmount,
|
||||
@Param("repayStatus") Integer repayStatus,
|
||||
@Param("version") Integer version);
|
||||
|
||||
/**
|
||||
* 获取指定时间内即将过期的积分数量
|
||||
*/
|
||||
@Select("SELECT COALESCE(SUM(remain_amount), 0) FROM user_credit " +
|
||||
"WHERE user_id = #{userId} " +
|
||||
"AND expire_time IS NOT NULL " +
|
||||
"AND expire_time <= #{expireDate} " +
|
||||
"AND expire_time > NOW()")
|
||||
BigDecimal sumExpiringCredits(@Param("userId") Long userId, @Param("expireDate") Date expireDate);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.xspaceagi.credit.infra.dao.mapper.CreditFlowMapper">
|
||||
|
||||
<resultMap id="BaseResultMap" type="com.xspaceagi.credit.infra.dao.entity.CreditFlow">
|
||||
<id column="id" property="id"/>
|
||||
<result column="user_id" property="userId"/>
|
||||
<result column="batch_no" property="batchNo"/>
|
||||
<result column="credit_type" property="creditType"/>
|
||||
<result column="operation_type" property="operationType"/>
|
||||
<result column="amount" property="amount"/>
|
||||
<result column="before_amount" property="beforeAmount"/>
|
||||
<result column="after_amount" property="afterAmount"/>
|
||||
<result column="biz_no" property="bizNo"/>
|
||||
<result column="created" property="created"/>
|
||||
<result column="remark" property="remark"/>
|
||||
</resultMap>
|
||||
|
||||
<insert id="insert" parameterType="com.xspaceagi.credit.infra.dao.entity.CreditFlow" useGeneratedKeys="true" keyProperty="id">
|
||||
INSERT INTO credit_flow (user_id, batch_no, credit_type, operation_type, amount, before_amount, after_amount, biz_no, remark)
|
||||
VALUES (#{userId}, #{batchNo}, #{creditType}, #{operationType}, #{amount}, #{beforeAmount}, #{afterAmount}, #{bizNo}, #{remark})
|
||||
</insert>
|
||||
|
||||
<insert id="batchInsert" parameterType="java.util.List">
|
||||
INSERT INTO credit_flow (user_id, batch_no, credit_type, operation_type, amount, before_amount, after_amount, biz_no, remark)
|
||||
VALUES
|
||||
<foreach collection="list" item="item" separator=",">
|
||||
(#{item.userId}, #{item.batchNo}, #{item.creditType}, #{item.operationType}, #{item.amount}, #{item.beforeAmount}, #{item.afterAmount}, #{item.bizNo}, #{item.remark})
|
||||
</foreach>
|
||||
</insert>
|
||||
|
||||
<select id="selectById" resultMap="BaseResultMap">
|
||||
SELECT * FROM credit_flow WHERE id = #{id}
|
||||
</select>
|
||||
|
||||
<select id="selectByUserIdAndType" resultMap="BaseResultMap">
|
||||
SELECT * FROM credit_flow
|
||||
WHERE user_id = #{userId}
|
||||
<if test="creditType != null">AND credit_type = #{creditType}</if>
|
||||
ORDER BY id DESC
|
||||
LIMIT #{limit}
|
||||
</select>
|
||||
|
||||
<select id="selectByUserIdAndTypeWithOffset" resultMap="BaseResultMap">
|
||||
SELECT * FROM credit_flow
|
||||
WHERE user_id = #{userId}
|
||||
<if test="creditType != null">AND credit_type = #{creditType}</if>
|
||||
AND id < #{lastId}
|
||||
ORDER BY id DESC
|
||||
LIMIT #{limit}
|
||||
</select>
|
||||
|
||||
<select id="selectByBizNo" resultMap="BaseResultMap">
|
||||
SELECT * FROM credit_flow WHERE biz_no = #{bizNo} AND operation_type = #{operationType} LIMIT 1
|
||||
</select>
|
||||
|
||||
<select id="countByUserId" resultType="int">
|
||||
SELECT COUNT(*) FROM credit_flow WHERE user_id = #{userId}
|
||||
</select>
|
||||
|
||||
<select id="countByUserIdAndType" resultType="int">
|
||||
SELECT COUNT(*) FROM credit_flow WHERE user_id = #{userId} AND credit_type = #{creditType}
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.xspaceagi.credit.infra.dao.mapper.CreditPackageMapper">
|
||||
|
||||
<resultMap id="BaseResultMap" type="com.xspaceagi.credit.infra.dao.entity.CreditPackage">
|
||||
<id column="id" property="id"/>
|
||||
<result column="package_name" property="packageName"/>
|
||||
<result column="credit_amount" property="creditAmount"/>
|
||||
<result column="price" property="price"/>
|
||||
<result column="sort" property="sort"/>
|
||||
<result column="status" property="status"/>
|
||||
<result column="period" property="period"/>
|
||||
<result column="created" property="created"/>
|
||||
<result column="modified" property="modified"/>
|
||||
<result column="remark" property="remark"/>
|
||||
</resultMap>
|
||||
|
||||
<insert id="insert" parameterType="com.xspaceagi.credit.infra.dao.entity.CreditPackage" useGeneratedKeys="true" keyProperty="id">
|
||||
INSERT INTO credit_package (package_name, credit_amount, price, sort, status, period, remark)
|
||||
VALUES (#{packageName}, #{creditAmount}, #{price}, #{sort}, #{status}, #{period}, #{remark})
|
||||
</insert>
|
||||
|
||||
<update id="updateById" parameterType="com.xspaceagi.credit.infra.dao.entity.CreditPackage">
|
||||
UPDATE credit_package
|
||||
<set>
|
||||
<if test="packageName != null">package_name = #{packageName},</if>
|
||||
<if test="creditAmount != null">credit_amount = #{creditAmount},</if>
|
||||
<if test="price != null">price = #{price},</if>
|
||||
<if test="sort != null">sort = #{sort},</if>
|
||||
<if test="status != null">status = #{status},</if>
|
||||
<if test="period != null">period = #{period},</if>
|
||||
<if test="remark != null">remark = #{remark},</if>
|
||||
</set>
|
||||
WHERE id = #{id}
|
||||
</update>
|
||||
|
||||
<delete id="deleteById">
|
||||
DELETE FROM credit_package WHERE id = #{id}
|
||||
</delete>
|
||||
|
||||
<select id="selectById" resultMap="BaseResultMap">
|
||||
SELECT * FROM credit_package WHERE id = #{id}
|
||||
</select>
|
||||
|
||||
<select id="selectList" resultMap="BaseResultMap">
|
||||
SELECT * FROM credit_package
|
||||
<where>
|
||||
<if test="status != null">AND status = #{status}</if>
|
||||
</where>
|
||||
ORDER BY sort ASC, id DESC
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,153 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.xspaceagi.credit.infra.dao.mapper.UserCreditMapper">
|
||||
|
||||
<resultMap id="BaseResultMap" type="com.xspaceagi.credit.infra.dao.entity.UserCredit">
|
||||
<id column="id" property="id"/>
|
||||
<result column="user_id" property="userId"/>
|
||||
<result column="batch_no" property="batchNo"/>
|
||||
<result column="credit_type" property="creditType"/>
|
||||
<result column="total_amount" property="totalAmount"/>
|
||||
<result column="used_amount" property="usedAmount"/>
|
||||
<result column="remain_amount" property="remainAmount"/>
|
||||
<result column="expire_time" property="expireTime"/>
|
||||
<result column="created" property="created"/>
|
||||
<result column="modified" property="modified"/>
|
||||
<result column="version" property="version"/>
|
||||
<result column="repaid_amount" property="repaidAmount"/>
|
||||
<result column="repay_status" property="repayStatus"/>
|
||||
<result column="remark" property="remark"/>
|
||||
<result column="extra" property="extra"/>
|
||||
</resultMap>
|
||||
|
||||
<insert id="insert" parameterType="com.xspaceagi.credit.infra.dao.entity.UserCredit" useGeneratedKeys="true" keyProperty="id">
|
||||
INSERT INTO user_credit (user_id, batch_no, credit_type, total_amount, used_amount, remain_amount, expire_time, repaid_amount, repay_status, version, remark, extra)
|
||||
VALUES (#{userId}, #{batchNo}, #{creditType}, #{totalAmount}, #{usedAmount}, #{remainAmount}, #{expireTime}, #{repaidAmount}, #{repayStatus}, 1, #{remark}, #{extra})
|
||||
</insert>
|
||||
|
||||
<update id="updateById" parameterType="com.xspaceagi.credit.infra.dao.entity.UserCredit">
|
||||
UPDATE user_credit
|
||||
<set>
|
||||
<if test="usedAmount != null">used_amount = #{usedAmount},</if>
|
||||
<if test="remainAmount != null">remain_amount = #{remainAmount},</if>
|
||||
<if test="remark != null">remark = #{remark},</if>
|
||||
</set>
|
||||
WHERE id = #{id}
|
||||
</update>
|
||||
|
||||
<update id="updateRemainAmount">
|
||||
UPDATE user_credit
|
||||
SET used_amount = used_amount + #{deductAmount},
|
||||
remain_amount = remain_amount - #{deductAmount}
|
||||
WHERE id = #{id} AND remain_amount >= #{deductAmount}
|
||||
</update>
|
||||
|
||||
<select id="selectById" resultMap="BaseResultMap">
|
||||
SELECT * FROM user_credit WHERE id = #{id}
|
||||
</select>
|
||||
|
||||
<select id="selectByBatchNo" resultMap="BaseResultMap">
|
||||
SELECT * FROM user_credit WHERE batch_no = #{batchNo} LIMIT 1
|
||||
</select>
|
||||
|
||||
<select id="selectByUserId" resultMap="BaseResultMap">
|
||||
SELECT * FROM user_credit WHERE user_id = #{userId} ORDER BY created DESC
|
||||
</select>
|
||||
|
||||
<select id="selectByUserIdAndType" resultMap="BaseResultMap">
|
||||
SELECT * FROM user_credit WHERE user_id = #{userId} AND credit_type = #{creditType} ORDER BY created DESC
|
||||
</select>
|
||||
|
||||
<select id="selectExpiredCredits" resultMap="BaseResultMap">
|
||||
SELECT * FROM user_credit
|
||||
WHERE user_id = #{userId}
|
||||
<if test="creditType != null">AND credit_type = #{creditType}</if>
|
||||
AND expire_time IS NOT NULL
|
||||
AND expire_time < NOW()
|
||||
AND remain_amount > 0
|
||||
ORDER BY expire_time ASC
|
||||
</select>
|
||||
|
||||
<select id="selectValidCredits" resultMap="BaseResultMap">
|
||||
SELECT * FROM user_credit
|
||||
WHERE user_id = #{userId}
|
||||
<if test="creditType != null">AND credit_type = #{creditType}</if>
|
||||
AND remain_amount > 0
|
||||
AND (expire_time IS NULL OR expire_time > NOW())
|
||||
ORDER BY
|
||||
CASE WHEN expire_time IS NULL THEN 1 ELSE 0 END,
|
||||
expire_time ASC
|
||||
</select>
|
||||
|
||||
<select id="selectValidCreditsOrderByExpireTime" resultMap="BaseResultMap">
|
||||
SELECT * FROM user_credit
|
||||
WHERE user_id = #{userId}
|
||||
AND (expire_time IS NULL OR expire_time > NOW())
|
||||
AND remain_amount > 0
|
||||
ORDER BY
|
||||
CASE WHEN expire_time IS NULL THEN 0 ELSE 1 END,
|
||||
expire_time ASC,
|
||||
created ASC
|
||||
</select>
|
||||
|
||||
<select id="sumRemainAmountByUserId" resultType="java.math.BigDecimal">
|
||||
SELECT COALESCE(SUM(remain_amount), 0) + COALESCE(SUM(CASE WHEN credit_type = 5 THEN repaid_amount - total_amount ELSE 0 END), 0)
|
||||
FROM user_credit
|
||||
WHERE user_id = #{userId}
|
||||
AND (remain_amount > 0 OR (credit_type = 5 AND repaid_amount < total_amount))
|
||||
AND (expire_time IS NULL OR expire_time > NOW())
|
||||
</select>
|
||||
|
||||
<select id="sumRemainAmountByUserIdAndType" resultType="java.math.BigDecimal">
|
||||
SELECT COALESCE(SUM(remain_amount), 0)
|
||||
FROM user_credit
|
||||
WHERE user_id = #{userId}
|
||||
AND credit_type = #{creditType}
|
||||
AND remain_amount > 0
|
||||
AND (expire_time IS NULL OR expire_time > NOW())
|
||||
</select>
|
||||
|
||||
<select id="selectSummaryList" resultType="java.util.HashMap">
|
||||
SELECT
|
||||
user_id,
|
||||
COALESCE(SUM(remain_amount), 0) + COALESCE(SUM(CASE WHEN credit_type = 5 THEN repaid_amount - total_amount ELSE 0 END), 0) AS totalCredit,
|
||||
COALESCE(SUM(CASE WHEN credit_type = 1 THEN remain_amount ELSE 0 END), 0) AS subscriptionCredit,
|
||||
COALESCE(SUM(CASE WHEN credit_type = 2 THEN remain_amount ELSE 0 END), 0) AS purchaseCredit,
|
||||
COALESCE(SUM(CASE WHEN credit_type = 3 THEN remain_amount ELSE 0 END), 0) AS activityCredit,
|
||||
COALESCE(SUM(CASE WHEN credit_type = 4 THEN remain_amount ELSE 0 END), 0) AS manualCredit
|
||||
FROM user_credit
|
||||
WHERE (remain_amount > 0 OR (credit_type = 5 AND repaid_amount < total_amount))
|
||||
AND (expire_time IS NULL OR expire_time > NOW())
|
||||
<if test="userId != null">AND user_id = #{userId}</if>
|
||||
GROUP BY user_id
|
||||
ORDER BY user_id ASC
|
||||
LIMIT #{offset}, #{limit}
|
||||
</select>
|
||||
|
||||
<select id="countSummaryList" resultType="java.lang.Long">
|
||||
SELECT COUNT(DISTINCT user_id)
|
||||
FROM user_credit
|
||||
WHERE (remain_amount > 0 OR (credit_type = 5 AND repaid_amount < total_amount))
|
||||
AND (expire_time IS NULL OR expire_time > NOW())
|
||||
<if test="userId != null">AND user_id = #{userId}</if>
|
||||
</select>
|
||||
|
||||
<select id="selectUnpaidLoans" resultMap="BaseResultMap">
|
||||
SELECT * FROM user_credit
|
||||
WHERE user_id = #{userId}
|
||||
AND credit_type = 5
|
||||
AND total_amount > repaid_amount
|
||||
AND repay_status = 0
|
||||
ORDER BY created ASC
|
||||
</select>
|
||||
|
||||
<update id="updateRepayAmount">
|
||||
UPDATE user_credit
|
||||
SET repaid_amount = repaid_amount + #{repayAmount},
|
||||
repay_status = #{repayStatus},
|
||||
version = version + 1,
|
||||
modified = NOW()
|
||||
WHERE id = #{id} AND version = #{version}
|
||||
</update>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,36 @@
|
||||
<?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">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-credit</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>app-platform-credit-sdk</artifactId>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-credit-spec</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.swagger.core.v3</groupId>
|
||||
<artifactId>swagger-annotations</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.xspaceagi.credit.sdk.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@Schema(description = "用户积分批次添加请求")
|
||||
public class CreditAddRequest extends CreditRequest {
|
||||
|
||||
@Schema(description = "过期时间,为空表示永不过期")
|
||||
private Date expireTime;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "扩展字段", hidden = true)
|
||||
private Map<String, Object> extra;
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,15 @@
|
||||
package com.xspaceagi.credit.sdk.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "用户积分扣减请求")
|
||||
public class CreditDeductRequest extends CreditRequest {
|
||||
|
||||
@Schema(description = "是否允许透支,默认false")
|
||||
private Boolean allowNegative = false;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.xspaceagi.credit.sdk.dto;
|
||||
|
||||
import com.xspaceagi.credit.spec.enums.CreditTypeEnum;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@Schema(description = "积分流水信息")
|
||||
public class CreditFlowDTO {
|
||||
|
||||
@Schema(description = "流水ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "用户ID")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "批次号")
|
||||
private String batchNo;
|
||||
|
||||
@Schema(description = "积分类型")
|
||||
private CreditTypeEnum creditType;
|
||||
|
||||
@Schema(description = "积分类型名称")
|
||||
private String creditTypeName;
|
||||
|
||||
@Schema(description = "操作类型:1-增加,2-扣减")
|
||||
private Integer operationType;
|
||||
|
||||
@Schema(description = "操作类型名称")
|
||||
private String operationTypeName;
|
||||
|
||||
@Schema(description = "积分数量")
|
||||
private BigDecimal amount;
|
||||
|
||||
@Schema(description = "操作前积分")
|
||||
private BigDecimal beforeAmount;
|
||||
|
||||
@Schema(description = "操作后积分")
|
||||
private BigDecimal afterAmount;
|
||||
|
||||
@Schema(description = "业务单号")
|
||||
private String bizNo;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private Date created;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "用户信息")
|
||||
private CreditUser user;
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,42 @@
|
||||
package com.xspaceagi.credit.sdk.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@Schema(description = "积分套餐信息")
|
||||
public class CreditPackageDTO {
|
||||
|
||||
@Schema(description = "套餐ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "套餐名称")
|
||||
private String packageName;
|
||||
|
||||
@Schema(description = "积分数量")
|
||||
private BigDecimal creditAmount;
|
||||
|
||||
@Schema(description = "价格")
|
||||
private BigDecimal price;
|
||||
|
||||
@Schema(description = "排序")
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "状态:0-禁用,1-启用")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private Date created;
|
||||
|
||||
@Schema(description = "有效期(月)")
|
||||
private Integer period;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
private Date modified;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.xspaceagi.credit.sdk.dto;
|
||||
|
||||
import com.xspaceagi.credit.spec.enums.CreditTypeEnum;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Data
|
||||
@Schema(description = "用户积分请求")
|
||||
public class CreditRequest implements Serializable {
|
||||
|
||||
@Schema(description = "租户ID", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Long tenantId;
|
||||
|
||||
@Schema(description = "用户ID", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "积分类型:SUBSCRIPTION-订阅积分,PURCHASE-增购积分,ACTIVITY-活动积分,MANUAL-手动发放", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private CreditTypeEnum creditType;
|
||||
|
||||
@Schema(description = "积分数量", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private BigDecimal amount;
|
||||
|
||||
@Schema(description = "业务单号,用于幂等")
|
||||
private String bizNo;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.xspaceagi.credit.sdk.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Data
|
||||
public class CreditUser {
|
||||
|
||||
private String username;
|
||||
private String phone;
|
||||
private String email;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.xspaceagi.credit.sdk.dto;
|
||||
|
||||
import com.xspaceagi.credit.spec.enums.CreditTypeEnum;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@Schema(description = "用户积分批次信息")
|
||||
public class UserCreditBatchDTO {
|
||||
|
||||
@Schema(description = "批次ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "用户ID")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "批次号")
|
||||
private String batchNo;
|
||||
|
||||
@Schema(description = "积分类型")
|
||||
private CreditTypeEnum creditType;
|
||||
|
||||
@Schema(description = "积分类型名称")
|
||||
private String creditTypeName;
|
||||
|
||||
@Schema(description = "总积分")
|
||||
private BigDecimal totalAmount;
|
||||
|
||||
@Schema(description = "已使用积分")
|
||||
private BigDecimal usedAmount;
|
||||
|
||||
@Schema(description = "剩余积分")
|
||||
private BigDecimal remainAmount;
|
||||
|
||||
@Schema(description = "过期时间")
|
||||
private Date expireTime;
|
||||
|
||||
@Schema(description = "是否已过期")
|
||||
private Boolean expired;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private Date created;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "扩展信息")
|
||||
private Map<String, Object> extra;
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,35 @@
|
||||
package com.xspaceagi.credit.sdk.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Data
|
||||
@Schema(description = "用户积分汇总信息")
|
||||
public class UserCreditSummary {
|
||||
|
||||
@Schema(description = "用户ID")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "总积分")
|
||||
private BigDecimal totalCredit;
|
||||
|
||||
@Schema(description = "订阅积分")
|
||||
private BigDecimal subscriptionCredit;
|
||||
|
||||
@Schema(description = "增购积分")
|
||||
private BigDecimal purchaseCredit;
|
||||
|
||||
@Schema(description = "活动积分")
|
||||
private BigDecimal activityCredit;
|
||||
|
||||
@Schema(description = "手动发放积分")
|
||||
private BigDecimal manualCredit;
|
||||
|
||||
@Schema(description = "每日赠积分")
|
||||
private BigDecimal dailyGiftCredit;
|
||||
|
||||
@Schema(description = "用户信息")
|
||||
private CreditUser user;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.xspaceagi.credit.sdk.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Schema(description = "用户积分汇总分页结果")
|
||||
public class UserCreditSummaryPageDTO implements Serializable {
|
||||
|
||||
@Schema(description = "记录列表")
|
||||
private List<UserCreditSummary> records;
|
||||
|
||||
@Schema(description = "总记录数")
|
||||
private Long total;
|
||||
|
||||
@Schema(description = "页码")
|
||||
private Integer pageNum;
|
||||
|
||||
@Schema(description = "每页大小")
|
||||
private Integer pageSize;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.xspaceagi.credit.sdk.rpc;
|
||||
|
||||
import com.xspaceagi.credit.sdk.dto.CreditAddRequest;
|
||||
import com.xspaceagi.credit.sdk.dto.CreditDeductRequest;
|
||||
import com.xspaceagi.credit.sdk.dto.UserCreditSummary;
|
||||
|
||||
public interface ICreditRpcService {
|
||||
|
||||
String addCredit(CreditAddRequest request);
|
||||
|
||||
boolean deductCredit(CreditDeductRequest request);
|
||||
|
||||
UserCreditSummary getUserCreditSummary(Long userId);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?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-spec</artifactId>
|
||||
|
||||
<properties>
|
||||
<maven.deploy.skip>true</maven.deploy.skip>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>system-sdk</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
Binary file not shown.
@@ -0,0 +1,15 @@
|
||||
package com.xspaceagi.credit.spec.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum CreditOperationTypeEnum {
|
||||
|
||||
ADD(1, "增加"),
|
||||
DEDUCT(2, "扣减");
|
||||
|
||||
private final Integer code;
|
||||
private final String desc;
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,34 @@
|
||||
package com.xspaceagi.credit.spec.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum CreditTypeEnum {
|
||||
|
||||
SUBSCRIPTION(1, "订阅积分"),
|
||||
PURCHASE(2, "增购积分"),
|
||||
ACTIVITY(3, "活动积分"),
|
||||
MANUAL(4, "系统发放"),
|
||||
LOAN(5, "借贷"),
|
||||
MODEL_CALL(10, "模型调用"),
|
||||
AGENT_CALL(11, "智能体调用"),
|
||||
TOOL_CALL(12, "工具调用"),
|
||||
MANUAL_DEDUCT(13, "系统扣减");
|
||||
|
||||
private final Integer code;
|
||||
private final String desc;
|
||||
|
||||
public static CreditTypeEnum getByCode(Integer code) {
|
||||
if (code == null) {
|
||||
return null;
|
||||
}
|
||||
for (CreditTypeEnum typeEnum : values()) {
|
||||
if (typeEnum.getCode().equals(code)) {
|
||||
return typeEnum;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?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-web</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-credit-application</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-credit-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-bill-sdk</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>system-application</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,115 @@
|
||||
package com.xspaceagi.credit.web.controller;
|
||||
|
||||
import com.xspaceagi.credit.app.service.CreditService;
|
||||
import com.xspaceagi.credit.sdk.dto.*;
|
||||
import com.xspaceagi.credit.spec.enums.CreditTypeEnum;
|
||||
import com.xspaceagi.system.sdk.server.IUserRpcService;
|
||||
import com.xspaceagi.system.sdk.service.dto.UserDetailDto;
|
||||
import com.xspaceagi.system.spec.annotation.RequireResource;
|
||||
import com.xspaceagi.system.spec.common.UserContext;
|
||||
import com.xspaceagi.system.spec.dto.ReqResult;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.xspaceagi.system.spec.enums.ResourceEnum.SUBSCRIPTION_POINTS_MODIFY;
|
||||
import static com.xspaceagi.system.spec.enums.ResourceEnum.SUBSCRIPTION_POINTS_QUERY;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/system/credit")
|
||||
@Tag(name = "用户积分管理(管理端)")
|
||||
public class CreditManController {
|
||||
|
||||
@Resource
|
||||
private CreditService creditService;
|
||||
|
||||
@Resource
|
||||
private IUserRpcService userRpcService;
|
||||
|
||||
@RequireResource(SUBSCRIPTION_POINTS_QUERY)
|
||||
@GetMapping("/summary/list")
|
||||
@Operation(summary = "用户积分查询")
|
||||
public ReqResult<UserCreditSummaryPageDTO> getUserCreditSummary(@RequestParam(required = false) String usernamePhoneOrEmail,
|
||||
@RequestParam(required = false) Long userId,
|
||||
@RequestParam(defaultValue = "1") Integer pageNum,
|
||||
@RequestParam(defaultValue = "20") Integer pageSize) {
|
||||
Long queryUserId = userId;
|
||||
if (StringUtils.isNotBlank(usernamePhoneOrEmail)) {
|
||||
UserDetailDto userDetailDto = userRpcService.queryUserDetailByName(usernamePhoneOrEmail);
|
||||
if (userDetailDto != null) {
|
||||
queryUserId = userDetailDto.getId();
|
||||
} else {
|
||||
UserCreditSummaryPageDTO emptyPage = new UserCreditSummaryPageDTO();
|
||||
emptyPage.setRecords(List.of());
|
||||
emptyPage.setTotal(0L);
|
||||
emptyPage.setPageNum(pageNum);
|
||||
emptyPage.setPageSize(pageSize);
|
||||
return ReqResult.success(emptyPage);
|
||||
}
|
||||
}
|
||||
UserCreditSummaryPageDTO page = creditService.queryUserCreditSummary(queryUserId, pageNum, pageSize);
|
||||
if (!page.getRecords().isEmpty()) {
|
||||
List<Long> userIds = page.getRecords().stream().map(UserCreditSummary::getUserId).distinct().collect(Collectors.toList());
|
||||
List<UserContext> userContexts = userRpcService.queryUserListByIds(userIds);
|
||||
Map<Long, UserContext> userContextMap = userContexts.stream().collect(Collectors.toMap(UserContext::getUserId, u -> u, (a, b) -> a));
|
||||
page.getRecords().forEach(summary -> {
|
||||
UserContext userContext = userContextMap.get(summary.getUserId());
|
||||
if (userContext != null) {
|
||||
summary.setUser(new CreditUser(userContext.getUserName(), userContext.getPhone(), userContext.getEmail()));
|
||||
}
|
||||
});
|
||||
}
|
||||
return ReqResult.success(page);
|
||||
}
|
||||
|
||||
@RequireResource(SUBSCRIPTION_POINTS_QUERY)
|
||||
@GetMapping("/flow/list")
|
||||
@Operation(summary = "查询用户积分流水明细,传入lastId翻页")
|
||||
public ReqResult<List<CreditFlowDTO>> getCreditFlows(
|
||||
@RequestParam(required = false) String usernamePhoneOrEmail,
|
||||
@RequestParam(required = false) Long userId,
|
||||
@RequestParam(required = false) CreditTypeEnum creditType,
|
||||
@RequestParam(required = false) Long lastId,
|
||||
@RequestParam(defaultValue = "20") Integer pageSize) {
|
||||
UserDetailDto userDetailDto = null;
|
||||
if (StringUtils.isNotBlank(usernamePhoneOrEmail)) {
|
||||
userDetailDto = userRpcService.queryUserDetailByName(usernamePhoneOrEmail);
|
||||
}
|
||||
if (userId != null && userDetailDto == null) {
|
||||
userDetailDto = userRpcService.queryUserDetailById(userId);
|
||||
}
|
||||
if (userDetailDto == null) {
|
||||
return ReqResult.success(List.of());
|
||||
}
|
||||
List<CreditFlowDTO> creditFlows = creditService.getCreditFlows(userDetailDto.getId(), creditType, lastId == null ? Long.MAX_VALUE : lastId, pageSize);
|
||||
for (CreditFlowDTO creditFlowDTO : creditFlows) {
|
||||
creditFlowDTO.setUser(new CreditUser(userDetailDto.getUserName(), userDetailDto.getPhone(), userDetailDto.getEmail()));
|
||||
}
|
||||
return ReqResult.success(creditFlows);
|
||||
}
|
||||
|
||||
@RequireResource(SUBSCRIPTION_POINTS_MODIFY)
|
||||
@PostMapping("/add")
|
||||
@Operation(summary = "积分发放")
|
||||
public ReqResult<String> addCredit(@RequestBody CreditAddRequest request) {
|
||||
request.setCreditType(request.getCreditType() == null || (request.getCreditType() != CreditTypeEnum.MANUAL && request.getCreditType() != CreditTypeEnum.ACTIVITY)
|
||||
? CreditTypeEnum.MANUAL : request.getCreditType());//手动积分发放
|
||||
return ReqResult.success(creditService.addCredit(request));
|
||||
}
|
||||
|
||||
@RequireResource(SUBSCRIPTION_POINTS_MODIFY)
|
||||
@PostMapping("/deduct")
|
||||
@Operation(summary = "积分扣减")
|
||||
public ReqResult<Boolean> deductCredit(@RequestBody CreditDeductRequest request) {
|
||||
request.setCreditType(CreditTypeEnum.MANUAL_DEDUCT);
|
||||
return ReqResult.success(creditService.deductCredit(request));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.xspaceagi.credit.web.controller;
|
||||
|
||||
import com.xspaceagi.credit.app.service.CreditPackageService;
|
||||
import com.xspaceagi.credit.sdk.dto.CreditPackageDTO;
|
||||
import com.xspaceagi.credit.web.controller.dto.SortUpdateDTO;
|
||||
import com.xspaceagi.system.spec.annotation.RequireResource;
|
||||
import com.xspaceagi.system.spec.dto.ReqResult;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.xspaceagi.system.spec.enums.ResourceEnum.*;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/system/credit/package")
|
||||
@Tag(name = "积分套餐管理(管理端)")
|
||||
public class CreditPackageController {
|
||||
|
||||
@Resource
|
||||
private CreditPackageService creditPackageService;
|
||||
|
||||
@RequireResource(SUBSCRIPTION_POINTS_MODIFY)
|
||||
@PostMapping("/create")
|
||||
@Operation(summary = "创建积分套餐")
|
||||
public ReqResult<Long> createPackage(@RequestBody CreditPackageDTO dto) {
|
||||
return ReqResult.success(creditPackageService.createPackage(dto));
|
||||
}
|
||||
|
||||
@RequireResource(SUBSCRIPTION_POINTS_MODIFY)
|
||||
@PostMapping("/update")
|
||||
@Operation(summary = "更新积分套餐")
|
||||
public ReqResult<Boolean> updatePackage(@RequestBody CreditPackageDTO dto) {
|
||||
return ReqResult.success(creditPackageService.updatePackage(dto));
|
||||
}
|
||||
|
||||
@RequireResource(SUBSCRIPTION_POINTS_MODIFY)
|
||||
@PostMapping("/sort/update")
|
||||
@Operation(summary = "更新积分套餐排序")
|
||||
public ReqResult<Void> updatePackageSort(@RequestBody List<SortUpdateDTO> updateDTOS) {
|
||||
updateDTOS.forEach(updateDTO -> {
|
||||
CreditPackageDTO dto = new CreditPackageDTO();
|
||||
dto.setId(updateDTO.getId());
|
||||
dto.setSort(updateDTO.getSort());
|
||||
creditPackageService.updatePackage(dto);
|
||||
});
|
||||
return ReqResult.success();
|
||||
}
|
||||
|
||||
@RequireResource(SUBSCRIPTION_POINTS_DELETE)
|
||||
@PostMapping("/{id}/delete")
|
||||
@Operation(summary = "删除积分套餐")
|
||||
public ReqResult<Boolean> deletePackage(@PathVariable Long id) {
|
||||
return ReqResult.success(creditPackageService.deletePackage(id));
|
||||
}
|
||||
|
||||
@RequireResource(SUBSCRIPTION_POINTS_QUERY)
|
||||
@GetMapping("/list")
|
||||
@Operation(summary = "查询积分套餐列表")
|
||||
public ReqResult<List<CreditPackageDTO>> getPackageList(
|
||||
@RequestParam(required = false) Integer status) {
|
||||
return ReqResult.success(creditPackageService.getPackageList(status));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package com.xspaceagi.credit.web.controller;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.xspaceagi.bill.sdk.dto.CreateOrderRequest;
|
||||
import com.xspaceagi.bill.sdk.dto.OrderDTO;
|
||||
import com.xspaceagi.bill.sdk.rpc.IBillRpcService;
|
||||
import com.xspaceagi.bill.spec.enums.BizTypeEnum;
|
||||
import com.xspaceagi.bill.spec.enums.TargetTypeEnum;
|
||||
import com.xspaceagi.credit.app.service.CreditPackageService;
|
||||
import com.xspaceagi.credit.app.service.CreditService;
|
||||
import com.xspaceagi.credit.sdk.dto.*;
|
||||
import com.xspaceagi.credit.spec.enums.CreditTypeEnum;
|
||||
import com.xspaceagi.system.application.dto.TenantConfigDto;
|
||||
import com.xspaceagi.system.spec.common.RequestContext;
|
||||
import com.xspaceagi.system.spec.dto.ReqResult;
|
||||
import com.xspaceagi.system.spec.utils.I18nUtil;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/credit")
|
||||
@Tag(name = "用户积分相关接口")
|
||||
public class UserCreditController {
|
||||
|
||||
@Resource
|
||||
private CreditService creditService;
|
||||
|
||||
@Resource
|
||||
private CreditPackageService creditPackageService;
|
||||
|
||||
@Resource
|
||||
private IBillRpcService iBillRpcService;
|
||||
|
||||
@GetMapping("/summary")
|
||||
@Operation(summary = "查询当前登录用户总积分")
|
||||
public ReqResult<UserCreditSummary> getUserCreditSummary() {
|
||||
TenantConfigDto tenantConfigDto = (TenantConfigDto) RequestContext.get().getTenantConfig();
|
||||
int dailyGiftCredit = 0;
|
||||
if (tenantConfigDto.getEnableDailyGiftCredit() != null && tenantConfigDto.getEnableDailyGiftCredit() == 1) {
|
||||
dailyGiftCredit = tenantConfigDto.getDailyGiftCreditAmount() == null ? 0 : tenantConfigDto.getDailyGiftCreditAmount();
|
||||
}
|
||||
//每日积分发放,bizNo一样,只会发放一次
|
||||
if (dailyGiftCredit > 0) {
|
||||
String bizNo = "DGC" + RequestContext.get().getUserId() + "-" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
|
||||
CreditAddRequest request = new CreditAddRequest();
|
||||
request.setCreditType(CreditTypeEnum.ACTIVITY);
|
||||
request.setUserId(RequestContext.get().getUserId());
|
||||
request.setAmount(BigDecimal.valueOf(dailyGiftCredit));
|
||||
request.setTenantId(RequestContext.get().getTenantId());
|
||||
request.setBizNo(bizNo);
|
||||
request.setExpireTime(Date.from(LocalDate.now().atTime(23, 59, 59).atZone(ZoneId.systemDefault()).toInstant()));
|
||||
request.setRemark(I18nUtil.systemMessage("Backend.Credit.DailyGift.Remark"));
|
||||
creditService.addCredit(request);
|
||||
}
|
||||
UserCreditSummary userCreditSummary = creditService.getUserCreditSummary(RequestContext.get().getUserId());
|
||||
userCreditSummary.setDailyGiftCredit(BigDecimal.valueOf(dailyGiftCredit));
|
||||
return ReqResult.success(userCreditSummary);
|
||||
}
|
||||
|
||||
@GetMapping("/batches")
|
||||
@Operation(summary = "查询用户积分批次列表")
|
||||
public ReqResult<List<UserCreditBatchDTO>> getUserCreditBatches(
|
||||
@RequestParam(required = false) CreditTypeEnum creditType,
|
||||
@RequestParam(required = false) Boolean expired) {
|
||||
return ReqResult.success(creditService.getUserCreditBatches(RequestContext.get().getUserId(), creditType, expired));
|
||||
}
|
||||
|
||||
@GetMapping("/flows")
|
||||
@Operation(summary = "查询用户积分流水明细")
|
||||
public ReqResult<List<CreditFlowDTO>> getCreditFlows(
|
||||
@RequestParam(required = false) CreditTypeEnum creditType,
|
||||
@RequestParam(required = false) Long lastId,
|
||||
@RequestParam(defaultValue = "20") Integer pageSize) {
|
||||
return ReqResult.success(creditService.getCreditFlows(RequestContext.get().getUserId(), creditType, lastId == null ? Long.MAX_VALUE : lastId, pageSize));
|
||||
}
|
||||
|
||||
@GetMapping("/package/list")
|
||||
@Operation(summary = "查询可增购的积分套餐列表")
|
||||
public ReqResult<List<CreditPackageDTO>> getPackageList() {
|
||||
return ReqResult.success(creditPackageService.getPackageList(1));
|
||||
}
|
||||
|
||||
@PostMapping("/order/create")
|
||||
@Operation(summary = "创建积分增购订单")
|
||||
public ReqResult<OrderDTO> createOrder(@RequestParam Long packageId, HttpServletRequest httpServletRequest) {
|
||||
CreditPackageDTO packageById = creditPackageService.getPackageById(packageId);
|
||||
if (packageById == null) {
|
||||
return ReqResult.error(I18nUtil.systemMessage("Backend.Credit.Package.Error.NotFound"));
|
||||
}
|
||||
CreateOrderRequest request = new CreateOrderRequest();
|
||||
request.setTenantId(RequestContext.get().getTenantId());
|
||||
request.setUserId(RequestContext.get().getUserId());
|
||||
request.setDescription(I18nUtil.systemMessage("Backend.Credit.Order.Description", packageById.getPackageName()));
|
||||
request.setBizType(BizTypeEnum.CREDIT_PURCHASE);
|
||||
CreateOrderRequest.CreateOrderItem item = new CreateOrderRequest.CreateOrderItem();
|
||||
item.setTargetName(packageById.getPackageName());
|
||||
item.setTargetType(TargetTypeEnum.CREDIT_PACKAGE.getCode());
|
||||
item.setTargetId(packageById.getId());
|
||||
item.setPrice(packageById.getPrice());
|
||||
item.setCount(1);
|
||||
item.setSnapshot(JSON.parseObject(JSON.toJSONString(packageById)));
|
||||
request.setItems(List.of(item));
|
||||
OrderDTO order = iBillRpcService.createOrder(request);
|
||||
return ReqResult.success(order);
|
||||
}
|
||||
|
||||
|
||||
private Date addMonths(Date from, int months) {
|
||||
Calendar cal = Calendar.getInstance();
|
||||
cal.setTime(from);
|
||||
cal.add(Calendar.MONTH, months);
|
||||
return cal.getTime();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.xspaceagi.credit.web.controller.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class PayInfoDTO {
|
||||
|
||||
private Long orderId;
|
||||
private String cashierUrl;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.xspaceagi.credit.web.controller.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class SortUpdateDTO {
|
||||
|
||||
private Long id;
|
||||
private Integer sort;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?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-modules</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>app-platform-credit</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<packaging>pom</packaging>
|
||||
<properties/>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>system-sdk</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-credit-application</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-credit-infra</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-credit-domain</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-credit-spec</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-credit-api</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-credit-sdk</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-credit-web</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<modules>
|
||||
<module>app-platform-credit-application</module>
|
||||
<module>app-platform-credit-domain</module>
|
||||
<module>app-platform-credit-infra</module>
|
||||
<module>app-platform-credit-spec</module>
|
||||
<module>app-platform-credit-api</module>
|
||||
<module>app-platform-credit-web</module>
|
||||
<module>app-platform-credit-sdk</module>
|
||||
</modules>
|
||||
</project>
|
||||
Reference in New Issue
Block a user