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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 &lt; #{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>

View File

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

View File

@@ -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 &lt; NOW()
AND remain_amount &gt; 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 &gt; 0
AND (expire_time IS NULL OR expire_time &gt; 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 &gt; NOW())
AND remain_amount &gt; 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 &gt; 0 OR (credit_type = 5 AND repaid_amount &lt; total_amount))
AND (expire_time IS NULL OR expire_time &gt; 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 &gt; 0
AND (expire_time IS NULL OR expire_time &gt; 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 &gt; 0 OR (credit_type = 5 AND repaid_amount &lt; total_amount))
AND (expire_time IS NULL OR expire_time &gt; 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 &gt; 0 OR (credit_type = 5 AND repaid_amount &lt; total_amount))
AND (expire_time IS NULL OR expire_time &gt; 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 &gt; 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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,10 @@
package com.xspaceagi.credit.web.controller.dto;
import lombok.Data;
@Data
public class PayInfoDTO {
private Long orderId;
private String cashierUrl;
}

View File

@@ -0,0 +1,10 @@
package com.xspaceagi.credit.web.controller.dto;
import lombok.Data;
@Data
public class SortUpdateDTO {
private Long id;
private Integer sort;
}

View File

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