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,42 @@
<?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-subscription</artifactId>
<groupId>com.xspaceagi</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>app-platform-subscription-application</artifactId>
<properties>
<maven.deploy.skip>true</maven.deploy.skip>
</properties>
<dependencies>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-subscription-domain</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-subscription-infra</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-subscription-sdk</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-credit-sdk</artifactId>
</dependency>
<dependency>
<groupId>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,26 @@
package com.xspaceagi.subscription.app.service;
import com.xspaceagi.subscription.sdk.dto.PlanDTO;
import com.xspaceagi.subscription.sdk.dto.PlanQueryRequest;
import com.xspaceagi.subscription.spec.enums.BizTypeEnum;
import java.util.List;
public interface SubscriptionPlanAppService {
Long createPlan(PlanDTO dto);
boolean updatePlan(PlanDTO dto);
boolean offlinePlan(Long id);
boolean deletePlan(Long id);
PlanDTO getPlanById(Long id);
PlanDTO getPlanById(Long id, boolean showPlanDescItems);
List<PlanDTO> listPlans(PlanQueryRequest query);
PlanDTO getFreePlan(BizTypeEnum bizType, String bizId, boolean showPlanDescItems);
}

View File

@@ -0,0 +1,33 @@
package com.xspaceagi.subscription.app.service;
import com.xspaceagi.subscription.sdk.dto.*;
import com.xspaceagi.subscription.spec.enums.BizTypeEnum;
import java.util.List;
public interface UserSubscriptionAppService {
List<UserSubscriptionDTO> getUserSubscriptions(SubscriptionQueryRequest query);
/**
* 获取用户当前指定业务(智能体)订阅
*/
UserSubscriptionDTO getUserCurrentSubscription(Long userId, BizTypeEnum bizType, String bizId);
/**
* 获取用户当前系统订阅
*/
UserSubscriptionDTO getUserCurrentSystemSubscription(Long userId, boolean showPlanDescItems);
UserSubscriptionDTO createSubscription(CreateSubscriptionRequest request);
SubscriptionStatsDTO getSubscriptionStats(SubscriptionStatsQueryRequest request);
void reset(UserSubscriptionDTO subscriptionDTO);
int incrementCallCount(Long subscriptionId, Integer count);
List<UserSubscriptionDTO> getNeedResetSubscriptions();
List<UserSubscriptionDTO> getExpiredSubscriptions();
}

View File

@@ -0,0 +1,282 @@
package com.xspaceagi.subscription.app.service.impl;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xspaceagi.agent.core.sdk.IAgentRpcService;
import com.xspaceagi.agent.core.sdk.IModelRpcService;
import com.xspaceagi.agent.core.sdk.dto.AgentInfoDto;
import com.xspaceagi.agent.core.sdk.dto.ModelInfoDto;
import com.xspaceagi.knowledge.sdk.response.KnowledgeConfigVo;
import com.xspaceagi.knowledge.sdk.sevice.IKnowledgeConfigRpcService;
import com.xspaceagi.subscription.app.service.SubscriptionPlanAppService;
import com.xspaceagi.subscription.infra.dao.entity.Plan;
import com.xspaceagi.subscription.infra.dao.entity.UserSubscription;
import com.xspaceagi.subscription.infra.dao.mapper.PlanMapper;
import com.xspaceagi.subscription.infra.dao.mapper.UserSubscriptionMapper;
import com.xspaceagi.subscription.infra.dao.service.IPlanService;
import com.xspaceagi.subscription.sdk.dto.PlanDTO;
import com.xspaceagi.subscription.sdk.dto.PlanItemGroupDTO;
import com.xspaceagi.subscription.sdk.dto.PlanQueryRequest;
import com.xspaceagi.subscription.spec.enums.BizTypeEnum;
import com.xspaceagi.subscription.spec.enums.PlanPeriodEnum;
import com.xspaceagi.subscription.spec.enums.PlanStatusEnum;
import com.xspaceagi.system.sdk.permission.IPermissionCacheRpcSerivce;
import com.xspaceagi.system.sdk.permission.IUserDataPermissionRpcService;
import com.xspaceagi.system.sdk.service.dto.MergedGroupDataPermissionDto;
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.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@Service
public class SubscriptionPlanAppServiceImpl implements SubscriptionPlanAppService {
@Resource
private IPlanService planService;
@Resource
private PlanMapper planMapper;
@Resource
private UserSubscriptionMapper userSubscriptionMapper;
@Resource
private IUserDataPermissionRpcService iUserDataPermissionRpcService;
@Resource
private IModelRpcService iModelRpcService;
@Resource
private IAgentRpcService iAgentRpcService;
@Resource
private IKnowledgeConfigRpcService iKnowledgeConfigRpcService;
@Resource
private IPermissionCacheRpcSerivce iPermissionCacheRpcSerivce;
@Override
public Long createPlan(PlanDTO dto) {
if (dto.getName() == null || dto.getName().isBlank()) {
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "计划名称不能为空");
}
if (dto.getBizType() == null) {
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "业务类型不能为空");
}
if (!BizTypeEnum.SYSTEM.equals(dto.getBizType()) && (dto.getBizId() == null || dto.getBizId().isBlank())) {
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "非系统业务类型必须指定业务对象ID");
}
Plan entity = convertToEntity(dto);
if (entity.getStatus() == null) {
entity.setStatus(PlanStatusEnum.ONLINE.getCode());
}
if (entity.getIsHot() == null) {
entity.setIsHot(false);
}
if (entity.getCreditAmount() == null) {
entity.setCreditAmount(java.math.BigDecimal.ZERO);
}
if (entity.getCallLimitCount() == null) {
entity.setCallLimitCount(-1);
}
if (entity.getFunctionOnly() == null) {
entity.setFunctionOnly(false);
}
planService.save(entity);
return entity.getId();
}
@Override
public boolean updatePlan(PlanDTO dto) {
if (dto.getId() == null) {
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "计划ID不能为空");
}
PlanDTO oldDto = getPlanById(dto.getId());
Plan entity = convertToEntity(dto);
try {
return planService.updateById(entity);
} finally {
if (oldDto != null && dto.getGroupIds() != null) {
iPermissionCacheRpcSerivce.clearCacheAllByTenant(oldDto.getTenantId());
}
}
}
@Override
public boolean offlinePlan(Long id) {
Plan entity = new Plan();
entity.setId(id);
entity.setStatus(PlanStatusEnum.OFFLINE.getCode());
return planService.updateById(entity);
}
@Override
public boolean deletePlan(Long id) {
Long count = userSubscriptionMapper.selectCount(
new LambdaQueryWrapper<UserSubscription>()
.eq(UserSubscription::getPlanId, id));
if (count != null && count > 0) {
throw new BizException("PLAN_HAS_SUBSCRIPTION", "该计划已有用户订阅,无法删除");
}
return planService.removeById(id);
}
@Override
public PlanDTO getPlanById(Long id) {
Plan entity = planService.getById(id);
return entity != null ? convertToDTO(entity, true) : null;
}
@Override
public PlanDTO getPlanById(Long id, boolean showPlanDescItems) {
Plan entity = planService.getById(id);
return entity != null ? convertToDTO(entity, showPlanDescItems) : null;
}
@Override
public List<PlanDTO> listPlans(PlanQueryRequest query) {
List<Plan> plans = planMapper.selectListWithFilters(query);
return plans.stream().map((Plan entity) -> convertToDTO(entity, true)).collect(Collectors.toList());
}
@Override
public PlanDTO getFreePlan(BizTypeEnum bizType, String bizId, boolean showPlanDescItems) {
Plan freePlan = planService.lambdaQuery()
.eq(Plan::getBizType, bizType.getCode())
.eq(bizType != BizTypeEnum.SYSTEM, Plan::getBizId, bizId)
.eq(Plan::getStatus, PlanStatusEnum.ONLINE.getCode())
.eq(Plan::getPrice, BigDecimal.ZERO)
.orderByAsc(Plan::getSort)
.orderByAsc(Plan::getId)
.last("LIMIT 1")
.one();
return convertToDTO(freePlan, showPlanDescItems);
}
private Plan convertToEntity(PlanDTO dto) {
Plan entity = new Plan();
BeanUtils.copyProperties(dto, entity);
if (dto.getBizType() != null) {
entity.setBizType(dto.getBizType().getCode());
}
entity.setPeriod(dto.getPeriod() != null ? dto.getPeriod().getCode() : PlanPeriodEnum.FOREVER.getCode());
if (dto.getExtra() != null) {
entity.setExtra(JSON.toJSONString(dto.getExtra()));
}
if (dto.getGroupIds() != null) {
entity.setGroupIds(JSON.toJSONString(dto.getGroupIds()));
}
return entity;
}
private PlanDTO convertToDTO(Plan entity, boolean showPlanDescItems) {
if (entity == null) {
return null;
}
PlanDTO dto = new PlanDTO();
BeanUtils.copyProperties(entity, dto);
dto.setBizType(BizTypeEnum.fromCode(entity.getBizType()));
dto.setPeriod(PlanPeriodEnum.getByCode(entity.getPeriod()));
List<PlanItemGroupDTO> itemGroups = new ArrayList<>();
PlanItemGroupDTO itemGroup = new PlanItemGroupDTO();
itemGroups.add(itemGroup);
itemGroup.setName("基础权限");
itemGroup.setDescription("基础权限");
itemGroup.setGroupType(PlanItemGroupDTO.GroupType.BASE);
itemGroup.setItems(new ArrayList<>());
if (dto.getBizType() == BizTypeEnum.SYSTEM) {
dto.setItemGroups(itemGroups);
}
try {
if (entity.getExtra() != null) {
dto.setExtra(JSON.parseObject(entity.getExtra()));
}
if (entity.getGroupIds() != null) {
dto.setGroupIds(JSON.parseArray(entity.getGroupIds(), Long.class));
if (CollectionUtils.isNotEmpty(dto.getGroupIds()) && showPlanDescItems) {
MergedGroupDataPermissionDto mergedGroupDataPermission = iUserDataPermissionRpcService.getMergedGroupDataPermission(dto.getGroupIds());
if (mergedGroupDataPermission != null) {
itemGroup.getItems().add(new PlanItemGroupDTO.ItemDTO("工作空间", "可创建工作空间数量 " + (mergedGroupDataPermission.getMaxSpaceCount() == null || mergedGroupDataPermission.getMaxSpaceCount() < 0 ? "无限制" : mergedGroupDataPermission.getMaxSpaceCount()), "workspace", mergedGroupDataPermission.getMaxSpaceCount() != null && mergedGroupDataPermission.getMaxSpaceCount() != 0));
itemGroup.getItems().add(new PlanItemGroupDTO.ItemDTO("智能体", "可创建智能体数量 " + (mergedGroupDataPermission.getMaxAgentCount() == null || mergedGroupDataPermission.getMaxAgentCount() < 0 ? "无限制" : mergedGroupDataPermission.getMaxAgentCount()), "agent", mergedGroupDataPermission.getMaxAgentCount() != null && mergedGroupDataPermission.getMaxAgentCount() != 0));
itemGroup.getItems().add(new PlanItemGroupDTO.ItemDTO("网页应用", "可创建网页应用数量 " + (mergedGroupDataPermission.getMaxPageAppCount() == null || mergedGroupDataPermission.getMaxPageAppCount() < 0 ? "无限制" : mergedGroupDataPermission.getMaxPageAppCount()), "pageApp", mergedGroupDataPermission.getMaxPageAppCount() != null && mergedGroupDataPermission.getMaxPageAppCount() != 0));
itemGroup.getItems().add(new PlanItemGroupDTO.ItemDTO("知识库", "可创建知识库数量 " + (mergedGroupDataPermission.getMaxKnowledgeCount() == null || mergedGroupDataPermission.getMaxKnowledgeCount() < 0 ? "无限制" : mergedGroupDataPermission.getMaxKnowledgeCount()), "kb", mergedGroupDataPermission.getMaxKnowledgeCount() != null && mergedGroupDataPermission.getMaxKnowledgeCount() != 0));
itemGroup.getItems().add(new PlanItemGroupDTO.ItemDTO("知识库", "知识库存储空间上限 " + (mergedGroupDataPermission.getKnowledgeStorageLimitGb() == null || mergedGroupDataPermission.getKnowledgeStorageLimitGb().doubleValue() < 0 ? "无限制" : mergedGroupDataPermission.getKnowledgeStorageLimitGb() + " GB"), "kb", mergedGroupDataPermission.getKnowledgeStorageLimitGb() != null && mergedGroupDataPermission.getKnowledgeStorageLimitGb().doubleValue() != 0));
itemGroup.getItems().add(new PlanItemGroupDTO.ItemDTO("数据表", "可创建数据表数量 " + (mergedGroupDataPermission.getMaxDataTableCount() == null || mergedGroupDataPermission.getMaxDataTableCount() < 0 ? "无限制" : mergedGroupDataPermission.getMaxDataTableCount()), "table", mergedGroupDataPermission.getKnowledgeStorageLimitGb() != null && mergedGroupDataPermission.getKnowledgeStorageLimitGb().doubleValue() != 0));
itemGroup.getItems().add(new PlanItemGroupDTO.ItemDTO("定时任务", "可创建定时任务数量 " + (mergedGroupDataPermission.getMaxScheduledTaskCount() == null || mergedGroupDataPermission.getMaxScheduledTaskCount() < 0 ? "无限制" : mergedGroupDataPermission.getMaxScheduledTaskCount()), "task", mergedGroupDataPermission.getMaxScheduledTaskCount() != null && mergedGroupDataPermission.getMaxScheduledTaskCount() != 0));
itemGroup.getItems().add(new PlanItemGroupDTO.ItemDTO("智能体电脑", "个人智能体电脑内存 " + (mergedGroupDataPermission.getAgentComputerMemoryGb() == null || mergedGroupDataPermission.getAgentComputerMemoryGb().doubleValue() < 0 ? "无限制" : mergedGroupDataPermission.getAgentComputerMemoryGb() + " GB"), "agentComputer", mergedGroupDataPermission.getAgentComputerMemoryGb() != null && mergedGroupDataPermission.getAgentComputerMemoryGb() != 0));
//模型权限
List<ModelInfoDto> modelInfoList = iModelRpcService.getModelInfoList(mergedGroupDataPermission.getModelIds());
if (CollectionUtils.isNotEmpty(modelInfoList)) {
itemGroup = new PlanItemGroupDTO();
itemGroups.add(itemGroup);
itemGroup.setName("模型权限");
itemGroup.setDescription("模型权限");
itemGroup.setGroupType(PlanItemGroupDTO.GroupType.MODEL);
itemGroup.setItems(modelInfoList.stream().map(modelInfo -> new PlanItemGroupDTO.ItemDTO(modelInfo.getName(), modelInfo.getDescription(), modelInfo.getIcon(), true)).collect(Collectors.toList()));
}
//智能体权限
List<AgentInfoDto> agentInfoList = iAgentRpcService.queryAgentInfoList(mergedGroupDataPermission.getAgentIds()).getData();
if (CollectionUtils.isNotEmpty(agentInfoList)) {
itemGroup = new PlanItemGroupDTO();
itemGroups.add(itemGroup);
itemGroup.setName("智能体权限");
itemGroup.setDescription("智能体权限");
itemGroup.setGroupType(PlanItemGroupDTO.GroupType.AGENT);
itemGroup.setItems(agentInfoList.stream().map(agentInfo -> new PlanItemGroupDTO.ItemDTO(agentInfo.getName(), agentInfo.getDescription(), agentInfo.getIcon(), true)).collect(Collectors.toList()));
}
//网页应用权限
List<AgentInfoDto> pageAgentInfoList = iAgentRpcService.queryAgentInfoList(mergedGroupDataPermission.getPageAgentIds()).getData();
if (CollectionUtils.isNotEmpty(pageAgentInfoList)) {
itemGroup = new PlanItemGroupDTO();
itemGroups.add(itemGroup);
itemGroup.setName("网页应用权限");
itemGroup.setDescription("网页应用权限");
itemGroup.setGroupType(PlanItemGroupDTO.GroupType.APP);
itemGroup.setItems(pageAgentInfoList.stream().map(agentInfo -> new PlanItemGroupDTO.ItemDTO(agentInfo.getName(), agentInfo.getDescription(), agentInfo.getIcon(), true)).collect(Collectors.toList()));
}
//知识库权限
List<KnowledgeConfigVo> knowledgeInfoList = iKnowledgeConfigRpcService.listByIds(mergedGroupDataPermission.getKnowledgeIds());
if (CollectionUtils.isNotEmpty(knowledgeInfoList)) {
itemGroup = new PlanItemGroupDTO();
itemGroups.add(itemGroup);
itemGroup.setName("知识库权限");
itemGroup.setDescription("知识库权限");
itemGroup.setGroupType(PlanItemGroupDTO.GroupType.KB);
itemGroup.setItems(knowledgeInfoList.stream().map(knowledgeInfo -> new PlanItemGroupDTO.ItemDTO(knowledgeInfo.getName(), knowledgeInfo.getDescription(), knowledgeInfo.getIcon(), true)).collect(Collectors.toList()));
}
//API权限
List<MergedGroupDataPermissionDto.OpenApiConfig> openApiConfigs = mergedGroupDataPermission.getOpenApiConfigs();
if (CollectionUtils.isNotEmpty(openApiConfigs)) {
itemGroup = new PlanItemGroupDTO();
itemGroups.add(itemGroup);
itemGroup.setName("API权限");
itemGroup.setDescription("API权限");
itemGroup.setGroupType(PlanItemGroupDTO.GroupType.API);
itemGroup.setOpenApiConfigs(openApiConfigs);
}
}
}
}
} catch (Exception e) {
// ignore
}
return dto;
}
}

View File

@@ -0,0 +1,75 @@
package com.xspaceagi.subscription.app.service.impl;
import com.xspaceagi.subscription.app.service.UserSubscriptionAppService;
import com.xspaceagi.subscription.infra.dao.entity.UserSubscription;
import com.xspaceagi.subscription.infra.dao.service.IUserSubscriptionService;
import com.xspaceagi.subscription.spec.enums.BizTypeEnum;
import com.xspaceagi.subscription.spec.enums.SubscriptionStatusEnum;
import com.xspaceagi.system.sdk.permission.IPermissionCacheRpcSerivce;
import com.xspaceagi.system.sdk.service.AbstractTaskExecuteService;
import com.xspaceagi.system.sdk.service.ScheduleTaskApiService;
import com.xspaceagi.system.sdk.service.dto.ScheduleTaskDto;
import com.xspaceagi.system.spec.common.RequestContext;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.Map;
@Slf4j
@Service("subscriptionResetTaskService")
public class SubscriptionResetTaskServiceImpl extends AbstractTaskExecuteService {
@Resource
private ScheduleTaskApiService scheduleTaskApiService;
@Resource
private UserSubscriptionAppService userSubscriptionAppService;
@Resource
private IUserSubscriptionService userSubscriptionService;
@Resource
private IPermissionCacheRpcSerivce iPermissionCacheRpcSerivce;
@PostConstruct
public void init() {
scheduleTaskApiService.start(ScheduleTaskDto.builder()
.taskId("subscriptionResetTaskService")
.beanId("subscriptionResetTaskService")
.maxExecTimes(Long.MAX_VALUE)
.cron(ScheduleTaskDto.Cron.EVERY_10_SECOND.getCron())
.params(Map.of())
.build());
}
@Override
protected boolean execute(ScheduleTaskDto scheduleTaskDto) {
userSubscriptionAppService.getNeedResetSubscriptions().forEach(subscription -> {
try {
RequestContext.setThreadTenantId(subscription.getTenantId());
userSubscriptionAppService.reset(subscription);
} finally {
RequestContext.remove();
}
});
userSubscriptionAppService.getExpiredSubscriptions().forEach(subscription -> {
try {
RequestContext.setThreadTenantId(subscription.getTenantId());
UserSubscription update = new UserSubscription();
update.setId(subscription.getId());
update.setStatus(SubscriptionStatusEnum.EXPIRED.getCode());
userSubscriptionService.updateById(update);
if (subscription.getBizType() == BizTypeEnum.SYSTEM) {
iPermissionCacheRpcSerivce.clearCacheByTenantAndUserIds(subscription.getTenantId(), Collections.singletonList(subscription.getUserId()));
log.info("用户订阅已过期清除用户权限缓存用户ID{}", subscription.getUserId());
}
} finally {
RequestContext.remove();
}
});
return false;
}
}

View File

@@ -0,0 +1,400 @@
package com.xspaceagi.subscription.app.service.impl;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.xspaceagi.credit.sdk.dto.CreditAddRequest;
import com.xspaceagi.credit.sdk.rpc.ICreditRpcService;
import com.xspaceagi.credit.spec.enums.CreditTypeEnum;
import com.xspaceagi.subscription.app.service.SubscriptionPlanAppService;
import com.xspaceagi.subscription.app.service.UserSubscriptionAppService;
import com.xspaceagi.subscription.infra.dao.entity.Plan;
import com.xspaceagi.subscription.infra.dao.entity.UserSubscription;
import com.xspaceagi.subscription.infra.dao.mapper.UserSubscriptionMapper;
import com.xspaceagi.subscription.infra.dao.service.IPlanService;
import com.xspaceagi.subscription.infra.dao.service.IUserSubscriptionService;
import com.xspaceagi.subscription.sdk.dto.*;
import com.xspaceagi.subscription.spec.enums.BizTypeEnum;
import com.xspaceagi.subscription.spec.enums.PlanPeriodEnum;
import com.xspaceagi.subscription.spec.enums.SubscriptionStatusEnum;
import com.xspaceagi.system.sdk.permission.IPermissionCacheRpcSerivce;
import com.xspaceagi.system.sdk.server.IUserRpcService;
import com.xspaceagi.system.spec.common.UserContext;
import com.xspaceagi.system.spec.exception.BizException;
import com.xspaceagi.system.spec.tenant.thread.TenantFunctions;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
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 UserSubscriptionAppServiceImpl implements UserSubscriptionAppService {
@Resource
private IUserSubscriptionService userSubscriptionService;
@Resource
private UserSubscriptionMapper userSubscriptionMapper;
@Resource
private SubscriptionPlanAppService subscriptionPlanAppService;
@Resource
private IPlanService planService;
@Resource
private ICreditRpcService iCreditRpcService;
@Resource
private IUserRpcService iUserRpcService;
@Resource
private IPermissionCacheRpcSerivce iPermissionCacheRpcSerivce;
@Override
public List<UserSubscriptionDTO> getUserSubscriptions(SubscriptionQueryRequest query) {
if (query.getBizType() == null) {
query.setBizType(BizTypeEnum.SYSTEM);
}
List<UserSubscription> subscriptions = userSubscriptionService.lambdaQuery()
.eq(UserSubscription::getUserId, query.getUserId())
.eq(query.getBizType() != null, UserSubscription::getBizType, query.getBizType())
.eq(query.getStatus() != null, UserSubscription::getStatus, SubscriptionStatusEnum.getByCode(query.getStatus()))
.eq(query.getBizId() != null, UserSubscription::getBizId, query.getBizId())
.list();
List<UserSubscriptionDTO> result = subscriptions.stream().map((UserSubscription entity) -> convertToDTO(entity, query.isShowPlanDescItems())).collect(Collectors.toCollection(ArrayList::new));
//是否包含可用订阅
boolean hasActiveSubscription = result.stream().anyMatch(sub -> sub.getPlan() != null && sub.getStatus() == SubscriptionStatusEnum.ACTIVE);
boolean shouldAddFreePlan = (BizTypeEnum.SYSTEM.equals(query.getBizType()) && !hasActiveSubscription)
|| (!BizTypeEnum.SYSTEM.equals(query.getBizType()) && query.getBizId() != null && !hasActiveSubscription);
if (shouldAddFreePlan) {
PlanDTO freePlan = subscriptionPlanAppService.getFreePlan(query.getBizType(), query.getBizId(), query.isShowPlanDescItems());
if (freePlan != null) {
result.add(buildFreePlanDTO(freePlan));
}
}
return result;
}
@Override
public UserSubscriptionDTO getUserCurrentSubscription(Long userId, BizTypeEnum bizType, String bizId) {
SubscriptionQueryRequest request = new SubscriptionQueryRequest();
request.setUserId(userId);
request.setBizType(bizType);
request.setBizId(bizId);
request.setStatus(SubscriptionStatusEnum.ACTIVE.getCode());
return getUserSubscriptions(request).stream().findFirst().orElse(null);
}
@Override
public UserSubscriptionDTO getUserCurrentSystemSubscription(Long userId, boolean showPlanDescItems) {
SubscriptionQueryRequest request = new SubscriptionQueryRequest();
request.setUserId(userId);
request.setBizType(BizTypeEnum.SYSTEM);
request.setStatus(SubscriptionStatusEnum.ACTIVE.getCode());
return getUserSubscriptions(request).stream().findFirst().orElse(null);
}
@Override
public SubscriptionStatsDTO getSubscriptionStats(SubscriptionStatsQueryRequest request) {
Calendar cal = Calendar.getInstance();
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
Date todayStart = cal.getTime();
cal.set(Calendar.DAY_OF_MONTH, 1);
Date monthStart = cal.getTime();
Map<String, Object> stats = userSubscriptionMapper.selectStatsByPlanBiz(
request.getBizType().getCode(), request.getBizId(), todayStart, monthStart);
SubscriptionStatsDTO dto = new SubscriptionStatsDTO();
dto.setTotalCount(toLong(stats.get("totalCount")));
dto.setTodayCount(toLong(stats.get("todayCount")));
dto.setMonthCount(toLong(stats.get("monthCount")));
Long total = userSubscriptionMapper.countByPlanBiz(
request.getBizType().getCode(), request.getBizId());
int offset = (request.getPageNum() - 1) * request.getPageSize();
List<UserSubscription> subscribers = userSubscriptionMapper.selectByPlanBiz(
request.getBizType().getCode(), request.getBizId(), offset, request.getPageSize());
dto.setSubscribers(subscribers.stream().map((UserSubscription entity) -> convertToDTO(entity, true)).collect(Collectors.toList()));
dto.setTotal(total);
dto.setPageNum(request.getPageNum());
dto.setPageSize(request.getPageSize());
//补充用户信息
List<Long> userIds = dto.getSubscribers().stream().map(UserSubscriptionDTO::getUserId).toList();
List<UserContext> userContexts = iUserRpcService.queryUserListByIds(userIds);
Map<Long, UserContext> userContextMap = userContexts.stream().collect(Collectors.toMap(UserContext::getUserId, userContext -> userContext, (a, b) -> a));
dto.getSubscribers().forEach(subscriber -> {
UserContext userContext = userContextMap.get(subscriber.getUserId());
if (userContext != null) {
String userName = StringUtils.isNotBlank(userContext.getNickName()) ? userContext.getNickName() : userContext.getUserName();
subscriber.setSubscriber(new UserSubscriptionDTO.Subscriber(userContext.getUserId(), userName, userContext.getAvatar()));
}
});
return dto;
}
@Override
@Transactional(rollbackFor = Exception.class)
public UserSubscriptionDTO createSubscription(CreateSubscriptionRequest request) {
Plan plan = planService.getById(request.getPlanId());
if (plan == null) {
throw new BizException("PLAN_NOT_FOUND", "订阅计划不存在");
}
if (plan.getStatus() == null || plan.getStatus() == 0) {
throw new BizException("PLAN_NOT_AVAILABLE", "订阅计划已下线");
}
UserSubscription existingSub = userSubscriptionMapper.selectByUserIdAndPlanId(
request.getUserId(), request.getPlanId());
if (existingSub == null) {
// Check if user has a subscription to a different plan (switch plan)
UserSubscription otherSub = userSubscriptionService.lambdaQuery()
.eq(UserSubscription::getBizType, plan.getBizType())
.eq(!plan.getBizType().equals(BizTypeEnum.SYSTEM.getCode()), UserSubscription::getBizId, plan.getBizId())
.eq(UserSubscription::getUserId, request.getUserId())
.one();
if (otherSub == null) {
return handleNewSubscription(request, plan);
}
existingSub = otherSub;
}
boolean isSamePlan = existingSub.getPlanId().equals(request.getPlanId());
existingSub.setExtra(request.getExtraJsonString());
if (isSamePlan) {
if (request.getExtraJsonString() != null) {
existingSub.setPeriod(plan.getPeriod());
userSubscriptionService.updateById(existingSub);
}
if (SubscriptionStatusEnum.ACTIVE.getCode().equals(existingSub.getStatus()) && existingSub.getEndTime() != null
&& existingSub.getEndTime().getTime() > System.currentTimeMillis()) {
return handleRenewActive(existingSub, plan);
} else {
return handleRenewExpired(existingSub, plan);
}
} else {
try {
return handleChangePlan(existingSub, plan);
} finally {
//清除用户权限缓存
if (plan.getBizType().equals(BizTypeEnum.SYSTEM.getCode())) {
iPermissionCacheRpcSerivce.clearCacheByTenantAndUserIds(plan.getTenantId(), List.of(request.getUserId()));
}
}
}
}
private UserSubscriptionDTO handleNewSubscription(CreateSubscriptionRequest request, Plan plan) {
Date startTime = new Date();
Date endTime = PlanPeriodEnum.FOREVER.getCode().equals(plan.getPeriod()) ? addMonths(startTime, 1200) : addMonths(startTime, plan.getPeriod());
UserSubscription sub = UserSubscription.builder()
.userId(request.getUserId())
.tenantId(plan.getTenantId())
.planId(plan.getId())
.bizType(BizTypeEnum.valueOf(plan.getBizType()))
.bizId(plan.getBizId())
.period(plan.getPeriod())
.startTime(startTime)
.endTime(endTime)
.status(SubscriptionStatusEnum.ACTIVE.getCode())
.callUsedCount(0)
.nextResetTime(startTime)
.extra(request.getExtraJsonString())
.build();
userSubscriptionService.save(sub);
UserSubscriptionDTO userSubscriptionDTO = convertToDTO(sub, false);
reset(userSubscriptionDTO);//积分发放
log.info("新建订阅, userId={}, planId={}, endTime={}", request.getUserId(), plan.getId(), endTime);
return convertToDTO(sub, false);
}
private UserSubscriptionDTO handleRenewActive(UserSubscription sub, Plan plan) {
Date newEndTime = PlanPeriodEnum.FOREVER.getCode().equals(plan.getPeriod()) ? addMonths(sub.getEndTime(), 1200) : addMonths(sub.getEndTime(), plan.getPeriod());
userSubscriptionMapper.updateEndTime(sub.getId(), newEndTime);
sub.setEndTime(newEndTime);
log.info("续费(生效中), userId={}, planId={}, newEndTime={}", sub.getUserId(), plan.getId(), newEndTime);
return convertToDTO(sub, false);
}
private UserSubscriptionDTO handleRenewExpired(UserSubscription sub, Plan plan) {
Date startTime = new Date();
Date endTime = PlanPeriodEnum.FOREVER.getCode().equals(plan.getPeriod()) ? addMonths(startTime, 1200) : addMonths(startTime, plan.getPeriod());
userSubscriptionMapper.updatePeriod(sub.getId(), startTime, endTime,
SubscriptionStatusEnum.ACTIVE.getCode());
sub.setStartTime(startTime);
sub.setEndTime(endTime);
sub.setStatus(SubscriptionStatusEnum.ACTIVE.getCode());
log.info("续费(已过期/已取消), userId={}, planId={}, endTime={}", sub.getUserId(), plan.getId(), endTime);
UserSubscriptionDTO userSubscriptionDTO = convertToDTO(sub, false);
reset(userSubscriptionDTO);//积分发放
return userSubscriptionDTO;
}
//更新计划,降级不退,升级补差价
private UserSubscriptionDTO handleChangePlan(UserSubscription oldSub, Plan newPlan) {
userSubscriptionMapper.deleteById(oldSub.getId());
Date startTime = new Date();
Date endTime = PlanPeriodEnum.FOREVER.getCode().equals(newPlan.getPeriod()) ? addMonths(startTime, 1200) : addMonths(startTime, newPlan.getPeriod());
UserSubscription sub = UserSubscription.builder()
.userId(oldSub.getUserId())
.tenantId(newPlan.getTenantId())
.planId(newPlan.getId())
.bizType(BizTypeEnum.valueOf(newPlan.getBizType()))
.bizId(newPlan.getBizId())
.period(newPlan.getPeriod())
.startTime(startTime)
.endTime(endTime)
.status(SubscriptionStatusEnum.ACTIVE.getCode())
.callUsedCount(0)
.nextResetTime(startTime) // 后台任务会执行初始化积分发放以及更新下次重置时间
.extra(oldSub.getExtra())
.build();
userSubscriptionService.save(sub);
UserSubscriptionDTO userSubscriptionDTO = convertToDTO(sub, false);
reset(userSubscriptionDTO);// 积分发放
return userSubscriptionDTO;
}
private UserSubscriptionDTO buildFreePlanDTO(PlanDTO plan) {
UserSubscriptionDTO dto = new UserSubscriptionDTO();
dto.setId(0L);
dto.setTenantId(plan.getTenantId());
dto.setPlanId(plan.getId());
dto.setPlanName(plan.getName());
dto.setBizType(plan.getBizType());
dto.setBizId(plan.getBizId());
dto.setPeriod(plan.getPeriod());
dto.setStartTime(new Date());
dto.setEndTime(addMonths(dto.getStartTime(), plan.getPeriod().getCode() == 0 ? 1200 : plan.getPeriod().getCode()));
dto.setStatus(SubscriptionStatusEnum.ACTIVE);
dto.setPlan(plan);
return dto;
}
private Date addMonths(Date from, int months) {
Calendar cal = Calendar.getInstance();
cal.setTime(from);
cal.add(Calendar.MONTH, months);
return cal.getTime();
}
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 UserSubscriptionDTO convertToDTO(UserSubscription entity, boolean showPlanDescItems) {
UserSubscriptionDTO dto = new UserSubscriptionDTO();
dto.setId(entity.getId());
dto.setUserId(entity.getUserId());
dto.setPlanId(entity.getPlanId());
dto.setStartTime(entity.getStartTime());
dto.setEndTime(entity.getEndTime());
dto.setStatus(SubscriptionStatusEnum.getByCode(entity.getStatus()));
dto.setCreated(entity.getCreated());
dto.setModified(entity.getModified());
if (dto.getStatus() == SubscriptionStatusEnum.ACTIVE && entity.getEndTime().getTime() < System.currentTimeMillis()) {
dto.setStatus(SubscriptionStatusEnum.EXPIRED);
if (entity.getId() != null) {
UserSubscription update = new UserSubscription();
update.setId(entity.getId());
update.setStatus(SubscriptionStatusEnum.EXPIRED.getCode());
userSubscriptionService.updateById(update);
iPermissionCacheRpcSerivce.clearCacheByTenantAndUserIds(entity.getTenantId(), Collections.singletonList(entity.getUserId()));
}
}
dto.setCallUsedCount(entity.getCallUsedCount());
dto.setNextResetTime(entity.getNextResetTime());
PlanDTO plan = subscriptionPlanAppService.getPlanById(entity.getPlanId(), showPlanDescItems);
dto.setPlan(plan);
if (dto.getPlan() == null && JSON.isValidObject(entity.getExtra())) {
JSONObject jsonObject = JSON.parseObject(entity.getExtra());
plan = jsonObject.getObject("plan", PlanDTO.class);
dto.setPlan(plan);
}
dto.setPlanName(dto.getPlan().getName());
dto.setPeriod(PlanPeriodEnum.getByCode(entity.getPeriod()));
dto.setBizType(entity.getBizType());
dto.setBizId(entity.getBizId());
dto.setTenantId(entity.getTenantId());
return dto;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void reset(UserSubscriptionDTO subscriptionDTO) {
if (subscriptionDTO.getEndTime() != null && subscriptionDTO.getEndTime().getTime() < System.currentTimeMillis()) {
log.info("用户订阅已过期, userId={}, planId={}", subscriptionDTO.getUserId(), subscriptionDTO.getPlanId());
return;
}
if (subscriptionDTO.getNextResetTime() != null && subscriptionDTO.getNextResetTime().getTime() > System.currentTimeMillis()) {
log.info("用户订阅未到重置时间, userId={}, planId={}", subscriptionDTO.getUserId(), subscriptionDTO.getPlanId());
return;
}
log.info("用户订阅重置, userId={}, planId={}", subscriptionDTO.getUserId(), subscriptionDTO.getPlanId());
Date nextResetTime = addMonths(subscriptionDTO.getStartTime(), 1);
while (nextResetTime.getTime() < System.currentTimeMillis()) {
nextResetTime = addMonths(nextResetTime, 1);
}
if (subscriptionDTO.getBizType() == BizTypeEnum.SYSTEM && subscriptionDTO.getPlan() != null && subscriptionDTO.getPlan().getCreditAmount().compareTo(BigDecimal.ZERO) > 0) {
CreditAddRequest creditAddRequest = new CreditAddRequest();
creditAddRequest.setUserId(subscriptionDTO.getUserId());
creditAddRequest.setAmount(subscriptionDTO.getPlan().getCreditAmount());
creditAddRequest.setCreditType(CreditTypeEnum.SUBSCRIPTION);
creditAddRequest.setRemark("订阅积分发放");
creditAddRequest.setBizNo("sub" + UUID.randomUUID().toString().replace("-", ""));
creditAddRequest.setTenantId(subscriptionDTO.getTenantId());
iCreditRpcService.addCredit(creditAddRequest);
}
userSubscriptionMapper.resetCallCount(subscriptionDTO.getId(), nextResetTime);
log.info("用户订阅重置成功, userId={}, planId={}", subscriptionDTO.getUserId(), subscriptionDTO.getPlanId());
}
@Override
public int incrementCallCount(Long subscriptionId, Integer count) {
int actualCount = count != null ? count : 1;
userSubscriptionMapper.incrementCallCount(subscriptionId, actualCount);
return actualCount;
}
@Override
public List<UserSubscriptionDTO> getNeedResetSubscriptions() {
List<UserSubscription> subscriptions = TenantFunctions.callWithIgnoreCheck(() -> userSubscriptionService.lambdaQuery()
.in(UserSubscription::getBizType, BizTypeEnum.SYSTEM.getCode(), BizTypeEnum.AGENT.getCode())
.le(UserSubscription::getNextResetTime, new Date())
.last("limit 1000")
.list());
return TenantFunctions.callWithIgnoreCheck(() -> subscriptions.stream().map((UserSubscription entity) -> convertToDTO(entity, false)).collect(Collectors.toCollection(ArrayList::new)));
}
@Override
public List<UserSubscriptionDTO> getExpiredSubscriptions() {
List<UserSubscription> subscriptions = TenantFunctions.callWithIgnoreCheck(() -> userSubscriptionService.lambdaQuery()
.ne(UserSubscription::getStatus, SubscriptionStatusEnum.EXPIRED.getCode())
.le(UserSubscription::getEndTime, new Date())
.last("limit 1000")
.list());
return TenantFunctions.callWithIgnoreCheck(() -> subscriptions.stream().map((UserSubscription entity) -> convertToDTO(entity, false)).collect(Collectors.toCollection(ArrayList::new)));
}
}