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,22 @@
<?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-api</artifactId>
<dependencies>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-subscription-application</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,107 @@
package com.xspaceagi.subscription.api.rpc;
import com.xspaceagi.subscription.app.service.SubscriptionPlanAppService;
import com.xspaceagi.subscription.app.service.UserSubscriptionAppService;
import com.xspaceagi.subscription.sdk.dto.*;
import com.xspaceagi.subscription.sdk.rpc.ISubscriptionRpcService;
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.util.List;
@Slf4j
@Service
public class SubscriptionRpcService implements ISubscriptionRpcService {
@Resource
private UserSubscriptionAppService userSubscriptionAppService;
@Resource
private SubscriptionPlanAppService subscriptionPlanAppService;
@Override
public List<UserSubscriptionDTO> getUserSubscriptions(SubscriptionQueryRequest request) {
Assert.notNull(request, "参数不能为空");
Assert.notNull(request.getUserId(), "用户ID不能为空");
Assert.notNull(request.getTenantId(), "租户ID不能为空");
if (RequestContext.get() == null) {
RequestContext.setThreadTenantId(request.getTenantId());
try {
return userSubscriptionAppService.getUserSubscriptions(request);
} finally {
RequestContext.remove();
}
}
return userSubscriptionAppService.getUserSubscriptions(request);
}
@Override
public UserSubscriptionDTO getUserCurrentSystemSubscription(Long userId) {
return userSubscriptionAppService.getUserCurrentSystemSubscription(userId, false);
}
@Override
public UserSubscriptionDTO createSubscription(CreateSubscriptionRequest request) {
Assert.notNull(request, "参数不能为空");
Assert.notNull(request.getUserId(), "用户ID不能为空");
Assert.notNull(request.getTenantId(), "租户ID不能为空");
Assert.notNull(request.getPlanId(), "计划ID不能为空");
if (RequestContext.get() == null) {
RequestContext.setThreadTenantId(request.getTenantId());
try {
return userSubscriptionAppService.createSubscription(request);
} finally {
RequestContext.remove();
}
}
return userSubscriptionAppService.createSubscription(request);
}
@Override
public Long createPlan(PlanDTO dto) {
Assert.notNull(dto, "参数不能为空");
Assert.notNull(dto.getBizType(), "业务类型不能为空");
return subscriptionPlanAppService.createPlan(dto);
}
@Override
public boolean updatePlan(PlanDTO dto) {
Assert.notNull(dto, "参数不能为空");
Assert.notNull(dto.getId(), "计划ID不能为空");
return subscriptionPlanAppService.updatePlan(dto);
}
@Override
public boolean offlinePlan(Long tenantId, Long planId) {
Assert.notNull(planId, "计划ID不能为空");
return subscriptionPlanAppService.offlinePlan(planId);
}
@Override
public PlanDTO getPlan(Long planId) {
return subscriptionPlanAppService.getPlanById(planId);
}
@Override
public List<PlanDTO> listPlans(PlanQueryRequest request) {
Assert.notNull(request, "参数不能为空");
return subscriptionPlanAppService.listPlans(request);
}
@Override
public int incrementCallCount(Long tenantId, Long subscriptionId, Integer count) {
Assert.notNull(subscriptionId, "订阅ID不能为空");
if (RequestContext.get() == null) {
RequestContext.setThreadTenantId(tenantId);
try {
return userSubscriptionAppService.incrementCallCount(subscriptionId, count);
} finally {
RequestContext.remove();
}
}
return userSubscriptionAppService.incrementCallCount(subscriptionId, count);
}
}

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

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-subscription</artifactId>
<groupId>com.xspaceagi</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>app-platform-subscription-domain</artifactId>
<properties>
<maven.deploy.skip>true</maven.deploy.skip>
</properties>
<dependencies>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-subscription-spec</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-subscription-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,43 @@
<?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-infra</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>system-sdk</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>system-application</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-pay-sdk</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-agent-core-sdk</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-knowledge-sdk</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,61 @@
package com.xspaceagi.subscription.infra.dao.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
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("subscription_plan")
public class Plan {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
private String description;
private BigDecimal price;
private BigDecimal firstPrice;
private Integer period;
private BigDecimal creditAmount;
private Integer callLimitCount;
private Boolean functionOnly;
private Boolean isHot;
private Integer status;
private String bizType;
private String bizId;
private String groupIds;
private String extra;
private Integer sort;
@TableField("_tenant_id")
private Long tenantId;
private Date created;
private Date modified;
}

View File

@@ -0,0 +1,53 @@
package com.xspaceagi.subscription.infra.dao.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.xspaceagi.subscription.spec.enums.BizTypeEnum;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("user_subscription")
public class UserSubscription {
@TableId(type = IdType.AUTO)
private Long id;
private Long userId;
private Long planId;
private BizTypeEnum bizType;
private String bizId;
private Integer period;
private Date startTime;
private Date endTime;
private Integer status;
private Integer callUsedCount;
private Date nextResetTime;
@TableField("_tenant_id")
private Long tenantId;
private String extra;
private Date created;
private Date modified;
}

View File

@@ -0,0 +1,15 @@
package com.xspaceagi.subscription.infra.dao.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xspaceagi.subscription.infra.dao.entity.Plan;
import com.xspaceagi.subscription.sdk.dto.PlanQueryRequest;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface PlanMapper extends BaseMapper<Plan> {
List<Plan> selectListWithFilters(@Param("query") PlanQueryRequest query);
}

View File

@@ -0,0 +1,44 @@
package com.xspaceagi.subscription.infra.dao.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xspaceagi.subscription.infra.dao.entity.UserSubscription;
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.util.Date;
import java.util.List;
@Mapper
public interface UserSubscriptionMapper extends BaseMapper<UserSubscription> {
@Select("SELECT * FROM user_subscription WHERE user_id = #{userId} AND plan_id = #{planId}")
UserSubscription selectByUserIdAndPlanId(@Param("userId") Long userId, @Param("planId") Long planId);
@Update("UPDATE user_subscription SET end_time = #{endTime}, modified = NOW() WHERE id = #{id}")
int updateEndTime(@Param("id") Long id, @Param("endTime") Date endTime);
@Update("UPDATE user_subscription SET start_time = #{startTime}, end_time = #{endTime}, status = #{status}, modified = NOW() WHERE id = #{id}")
int updatePeriod(@Param("id") Long id, @Param("startTime") Date startTime,
@Param("endTime") Date endTime, @Param("status") Integer status);
java.util.Map<String, Object> selectStatsByPlanBiz(@Param("bizType") String bizType,
@Param("bizId") String bizId,
@Param("todayStart") java.util.Date todayStart,
@Param("monthStart") java.util.Date monthStart);
List<UserSubscription> selectByPlanBiz(@Param("bizType") String bizType,
@Param("bizId") String bizId,
@Param("offset") int offset,
@Param("limit") int limit);
Long countByPlanBiz(@Param("bizType") String bizType,
@Param("bizId") String bizId);
@Update("UPDATE user_subscription SET call_used_count = 0, next_reset_time = #{nextResetTime}, modified = NOW() WHERE id = #{id}")
int resetCallCount(@Param("id") Long id, @Param("nextResetTime") Date nextResetTime);
@Update("UPDATE user_subscription SET call_used_count = call_used_count + #{count}, modified = NOW() WHERE id = #{id}")
int incrementCallCount(@Param("id") Long id, @Param("count") Integer count);
}

View File

@@ -0,0 +1,7 @@
package com.xspaceagi.subscription.infra.dao.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.xspaceagi.subscription.infra.dao.entity.Plan;
public interface IPlanService extends IService<Plan> {
}

View File

@@ -0,0 +1,7 @@
package com.xspaceagi.subscription.infra.dao.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.xspaceagi.subscription.infra.dao.entity.UserSubscription;
public interface IUserSubscriptionService extends IService<UserSubscription> {
}

View File

@@ -0,0 +1,11 @@
package com.xspaceagi.subscription.infra.dao.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xspaceagi.subscription.infra.dao.entity.Plan;
import com.xspaceagi.subscription.infra.dao.mapper.PlanMapper;
import com.xspaceagi.subscription.infra.dao.service.IPlanService;
import org.springframework.stereotype.Service;
@Service
public class PlanServiceImpl extends ServiceImpl<PlanMapper, Plan> implements IPlanService {
}

View File

@@ -0,0 +1,11 @@
package com.xspaceagi.subscription.infra.dao.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xspaceagi.subscription.infra.dao.entity.UserSubscription;
import com.xspaceagi.subscription.infra.dao.mapper.UserSubscriptionMapper;
import com.xspaceagi.subscription.infra.dao.service.IUserSubscriptionService;
import org.springframework.stereotype.Service;
@Service
public class UserSubscriptionServiceImpl extends ServiceImpl<UserSubscriptionMapper, UserSubscription> implements IUserSubscriptionService {
}

View File

@@ -0,0 +1,18 @@
<?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.subscription.infra.dao.mapper.PlanMapper">
<select id="selectListWithFilters" resultType="com.xspaceagi.subscription.infra.dao.entity.Plan">
SELECT * FROM subscription_plan
<where>
<if test="query.bizType != null">AND biz_type = #{query.bizType.code}</if>
<if test="query.bizId != null">AND biz_id = #{query.bizId}</if>
<if test="query.status != null">AND status = #{query.status}</if>
<if test="query.keyword != null and query.keyword != ''">
AND name LIKE CONCAT('%', #{query.keyword}, '%')
</if>
</where>
ORDER BY sort ASC, id ASC
</select>
</mapper>

View File

@@ -0,0 +1,30 @@
<?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.subscription.infra.dao.mapper.UserSubscriptionMapper">
<select id="selectStatsByPlanBiz" resultType="java.util.HashMap">
SELECT
COUNT(*) AS totalCount,
SUM(CASE WHEN us.created >= #{todayStart} THEN 1 ELSE 0 END) AS todayCount,
SUM(CASE WHEN us.created >= #{monthStart} THEN 1 ELSE 0 END) AS monthCount
FROM user_subscription us
INNER JOIN subscription_plan sp ON us.plan_id = sp.id
WHERE sp.biz_type = #{bizType} AND sp.biz_id = #{bizId}
</select>
<select id="selectByPlanBiz" resultType="com.xspaceagi.subscription.infra.dao.entity.UserSubscription">
SELECT us.*
FROM user_subscription us
WHERE us.biz_type = #{bizType} AND us.biz_id = #{bizId}
ORDER BY us.created DESC
LIMIT #{offset}, #{limit}
</select>
<select id="countByPlanBiz" resultType="java.lang.Long">
SELECT COUNT(*)
FROM user_subscription us
INNER JOIN subscription_plan sp ON us.plan_id = sp.id
WHERE sp.biz_type = #{bizType} AND sp.biz_id = #{bizId}
</select>
</mapper>

View File

@@ -0,0 +1,35 @@
<?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-subscription</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>app-platform-subscription-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-subscription-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,39 @@
package com.xspaceagi.subscription.sdk.dto;
import com.alibaba.fastjson2.JSON;
import com.xspaceagi.subscription.spec.enums.BizTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Map;
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Data
@Schema(description = "创建订阅请求")
public class CreateSubscriptionRequest implements Serializable {
@Schema(description = "租户ID", requiredMode = Schema.RequiredMode.REQUIRED)
private Long tenantId;
@Schema(description = "用户ID", requiredMode = Schema.RequiredMode.REQUIRED)
private Long userId;
@Schema(description = "计划ID", requiredMode = Schema.RequiredMode.REQUIRED)
private Long planId;
@Schema(description = "业务类型")
private BizTypeEnum bizType;
@Schema(description = "扩展信息,记录订阅快照等")
private Map<String, Object> extra;
public String getExtraJsonString() {
return extra == null ? null : JSON.toJSONString(extra);
}
}

View File

@@ -0,0 +1,87 @@
package com.xspaceagi.subscription.sdk.dto;
import com.xspaceagi.subscription.spec.enums.BizTypeEnum;
import com.xspaceagi.subscription.spec.enums.PlanPeriodEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import java.util.Map;
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Data
@Schema(description = "订阅计划信息")
public class PlanDTO implements Serializable {
@Schema(description = "计划ID")
private Long id;
@Schema(description = "租户ID")
private Long tenantId;
@Schema(description = "计划名称")
private String name;
@Schema(description = "计划描述")
private String description;
@Schema(description = "价格")
private BigDecimal price;
@Schema(description = "首次订阅价格")
private BigDecimal firstPrice;
@Schema(description = "周期1-月3-季度12-年")
private PlanPeriodEnum period;
@Schema(description = "每月赠送积分")
private BigDecimal creditAmount;
@Schema(description = "可调用次数,-1表示不限制")
private Integer callLimitCount;
@Schema(description = "是否仅为功能订阅true时资源消耗费用另计")
private Boolean functionOnly;
//从全局配置读取,无需字段存储
@Schema(description = "每日登录赠送积分")
private BigDecimal dailyGiftCreditAmount;
@Schema(description = "是否热门")
private Boolean isHot;
@Schema(description = "状态0-下线1-上线")
private Integer status;
@Schema(description = "业务类型SYSTEM-系统AGENT-智能体SKILL-技能")
private BizTypeEnum bizType;
@Schema(description = "业务对象ID非SYSTEM时必填")
private String bizId;
@Schema(description = "关联用户组IDJSON数组")
private List<Long> groupIds;
@Schema(description = "扩展字段JSON")
private Map<String, Object> extra;
@Schema(description = "排序,越小越靠前,前端支持拖拽")
private Integer sort;
@Schema(description = "创建时间")
private Date created;
@Schema(description = "修改时间")
private Date modified;
@Schema(description = "订阅计划的权限分组项")
private List<PlanItemGroupDTO> itemGroups;
}

View File

@@ -0,0 +1,48 @@
package com.xspaceagi.subscription.sdk.dto;
import com.xspaceagi.system.sdk.service.dto.MergedGroupDataPermissionDto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
public class PlanItemGroupDTO {
@Schema(description = "分组描述")
private String name;
@Schema(description = "分组描述")
private String description;
@Schema(description = "分组类型")
private GroupType groupType;
@Schema(description = "分组项")
private List<ItemDTO> items;
@Schema(description = "有权限访问的开放 API, groupType为API时有效")
private List<MergedGroupDataPermissionDto.OpenApiConfig> openApiConfigs;
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Data
public static class ItemDTO {
@Schema(description = "名称")
private String name;
@Schema(description = "描述")
private String description;
@Schema(description = "图标")
private String icon;
@Schema(description = "是否选中")
private boolean selected;
}
public enum GroupType {
BASE,
MODEL,
AGENT,
APP,
KB,
API,
}
}

View File

@@ -0,0 +1,33 @@
package com.xspaceagi.subscription.sdk.dto;
import com.xspaceagi.subscription.spec.enums.BizTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Data
@Schema(description = "订阅计划查询请求")
public class PlanQueryRequest implements Serializable {
@Schema(description = "租户ID", requiredMode = Schema.RequiredMode.REQUIRED)
private Long tenantId;
@Schema(description = "业务类型过滤")
private BizTypeEnum bizType;
@Schema(description = "业务对象ID过滤")
private String bizId;
@Schema(description = "状态过滤0-下线1-上线")
private Integer status;
@Schema(description = "关键词搜索(名称)")
private String keyword;
}

View File

@@ -0,0 +1,30 @@
package com.xspaceagi.subscription.sdk.dto;
import com.xspaceagi.subscription.spec.enums.BizTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
@Data
@Schema(description = "用户订阅查询请求")
public class SubscriptionQueryRequest implements Serializable {
@Schema(description = "租户ID", requiredMode = Schema.RequiredMode.REQUIRED)
private Long tenantId;
@Schema(description = "用户ID", requiredMode = Schema.RequiredMode.REQUIRED)
private Long userId;
@Schema(description = "业务类型过滤")
private BizTypeEnum bizType;
@Schema(description = "业务ID过滤")
private String bizId;
@Schema(description = "状态过滤0-生效中1-已过期2-已取消")
private Integer status;
@Schema(description = "是否展示计划详细描述列表")
private boolean showPlanDescItems;
}

View File

@@ -0,0 +1,33 @@
package com.xspaceagi.subscription.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 SubscriptionStatsDTO implements Serializable {
@Schema(description = "总订阅数")
private Long totalCount;
@Schema(description = "今日新订阅数")
private Long todayCount;
@Schema(description = "本月新订阅数")
private Long monthCount;
@Schema(description = "订阅用户列表")
private List<UserSubscriptionDTO> subscribers;
@Schema(description = "总记录数(分页)")
private Long total;
@Schema(description = "当前页码")
private Integer pageNum;
@Schema(description = "每页数量")
private Integer pageSize;
}

View File

@@ -0,0 +1,24 @@
package com.xspaceagi.subscription.sdk.dto;
import com.xspaceagi.subscription.spec.enums.BizTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
@Data
@Schema(description = "订阅统计查询请求")
public class SubscriptionStatsQueryRequest implements Serializable {
@Schema(description = "业务类型", requiredMode = Schema.RequiredMode.REQUIRED)
private BizTypeEnum bizType;
@Schema(description = "业务对象ID", requiredMode = Schema.RequiredMode.REQUIRED)
private String bizId;
@Schema(description = "页码从1开始")
private Integer pageNum = 1;
@Schema(description = "每页数量")
private Integer pageSize = 20;
}

View File

@@ -0,0 +1,90 @@
package 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 io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Date;
import java.util.Map;
@Data
@Schema(description = "用户订阅信息")
public class UserSubscriptionDTO implements Serializable {
@Schema(description = "订阅记录ID")
private Long id;
@Schema(description = "租户ID")
private Long tenantId;
@Schema(description = "用户ID")
private Long userId;
@Schema(description = "计划ID")
private Long planId;
@Schema(description = "计划名称")
private String planName;
private BizTypeEnum bizType;
private String bizId;
@Schema(description = "业务对象名称")
private String bizName;
@Schema(description = "图标")
private String icon;
@Schema(description = "周期1-月3-季度12-年")
private PlanPeriodEnum period;
@Schema(description = "开始时间")
private Date startTime;
@Schema(description = "结束时间,为空时为买断,永不过期")
private Date endTime;
@Schema(description = "状态0-生效中1-已过期2-已取消")
private SubscriptionStatusEnum status;
@Schema(description = "计划订阅时的快照")
private PlanDTO plan;
@Schema(description = "已使用调用次数")
private Integer callUsedCount;
@Schema(description = "下次重置时间(每月重置)")
private Date nextResetTime;
@Schema(description = "扩展字段,记录订阅快照等")
private Map<String, Object> extra;
@Schema(description = "创建时间")
private Date created;
@Schema(description = "修改时间")
private Date modified;
@Schema(description = "订阅者信息")
private Subscriber subscriber;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class Subscriber {
@Schema(description = "订阅者ID")
private Long id;
@Schema(description = "订阅者名称")
private String name;
@Schema(description = "订阅者头像")
private String avatar;
}
}

View File

@@ -0,0 +1,50 @@
package com.xspaceagi.subscription.sdk.rpc;
import com.xspaceagi.subscription.sdk.dto.*;
import java.util.List;
public interface ISubscriptionRpcService {
/**
* 查询用户当前订阅信息(可指定业务类型)
*/
List<UserSubscriptionDTO> getUserSubscriptions(SubscriptionQueryRequest request);
/**
* 获取用户当前系统订阅
*/
UserSubscriptionDTO getUserCurrentSystemSubscription(Long userId);
/**
* 创建用户订阅(内部判断新建、续期、升级、变更计划)
*/
UserSubscriptionDTO createSubscription(CreateSubscriptionRequest request);
/**
* 创建订阅计划
*/
Long createPlan(PlanDTO dto);
/**
* 修改订阅计划
*/
boolean updatePlan(PlanDTO dto);
/**
* 下架订阅计划
*/
boolean offlinePlan(Long tenantId, Long planId);
PlanDTO getPlan(Long planId);
/**
* 查询订阅计划列表
*/
List<PlanDTO> listPlans(PlanQueryRequest request);
/**
* 增加订阅调用次数(自动判断是否需要月度重置)
*/
int incrementCallCount(Long tenantId, Long subscriptionId, Integer count);
}

View File

@@ -0,0 +1,22 @@
<?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-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,28 @@
package com.xspaceagi.subscription.spec.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum BizTypeEnum {
SYSTEM("SYSTEM", "系统全局"),
AGENT("AGENT", "智能体"),
SKILL("SKILL", "技能");
private final String code;
private final String desc;
public static BizTypeEnum fromCode(String code) {
if (code == null) {
return null;
}
for (BizTypeEnum e : values()) {
if (e.getCode().equals(code)) {
return e;
}
}
return null;
}
}

View File

@@ -0,0 +1,29 @@
package com.xspaceagi.subscription.spec.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum PlanPeriodEnum {
MONTH(1, ""),
QUARTER(3, "季度"),
YEAR(12, ""),
FOREVER(0, "永久");
private final Integer code;
private final String desc;
public static PlanPeriodEnum getByCode(Integer code) {
if (code == null) {
return null;
}
for (PlanPeriodEnum e : values()) {
if (e.getCode().equals(code)) {
return e;
}
}
return null;
}
}

View File

@@ -0,0 +1,31 @@
package com.xspaceagi.subscription.spec.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum PlanStatusEnum {
OFFLINE(0, "下线"),
ONLINE(1, "上线");
private final Integer code;
private final String desc;
public boolean isOnline() {
return ONLINE.equals(this);
}
public static PlanStatusEnum getByCode(Integer code) {
if (code == null) {
return null;
}
for (PlanStatusEnum e : values()) {
if (e.getCode().equals(code)) {
return e;
}
}
return null;
}
}

View File

@@ -0,0 +1,28 @@
package com.xspaceagi.subscription.spec.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum SubscriptionStatusEnum {
ACTIVE(0, "生效中"),
EXPIRED(1, "已过期"),
CANCELLED(2, "已取消");
private final Integer code;
private final String desc;
public static SubscriptionStatusEnum getByCode(Integer code) {
if (code == null) {
return null;
}
for (SubscriptionStatusEnum e : values()) {
if (e.getCode().equals(code)) {
return e;
}
}
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-subscription</artifactId>
<groupId>com.xspaceagi</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>app-platform-subscription-web</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-subscription-application</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-subscription-api</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-agent-core-sdk</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-bill-sdk</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,132 @@
package com.xspaceagi.subscription.web.controller;
import com.xspaceagi.agent.core.sdk.IAgentRpcService;
import com.xspaceagi.agent.core.sdk.dto.AgentInfoDto;
import com.xspaceagi.subscription.app.service.SubscriptionPlanAppService;
import com.xspaceagi.subscription.app.service.UserSubscriptionAppService;
import com.xspaceagi.subscription.sdk.dto.PlanDTO;
import com.xspaceagi.subscription.sdk.dto.PlanQueryRequest;
import com.xspaceagi.subscription.sdk.dto.SubscriptionStatsDTO;
import com.xspaceagi.subscription.sdk.dto.SubscriptionStatsQueryRequest;
import com.xspaceagi.subscription.spec.enums.BizTypeEnum;
import com.xspaceagi.subscription.web.controller.dto.SortUpdateDTO;
import com.xspaceagi.system.sdk.permission.SpacePermissionService;
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.util.Assert;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Slf4j
@RestController
@RequestMapping("/api/agent/plan")
@Tag(name = "智能体订阅计划管理")
public class AgentPlanManController {
@Resource
private SubscriptionPlanAppService subscriptionPlanAppService;
@Resource
private UserSubscriptionAppService userSubscriptionAppService;
@Resource
private IAgentRpcService iAgentRpcService;
@Resource
private SpacePermissionService spacePermissionService;
@GetMapping("/subscription/stats")
@Operation(summary = "查询智能体订阅统计")
public ReqResult<SubscriptionStatsDTO> getSubscriptionStats(
@RequestParam Long agentId,
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "20") Integer pageSize) {
checkAgentPermission(agentId);
SubscriptionStatsQueryRequest request = new SubscriptionStatsQueryRequest();
request.setBizType(BizTypeEnum.AGENT);
request.setBizId(agentId.toString());
request.setPageNum(pageNum);
request.setPageSize(pageSize);
return ReqResult.success(userSubscriptionAppService.getSubscriptionStats(request));
}
@PostMapping(value = "/create")
@Operation(summary = "添加订阅计划")
public ReqResult<Long> createPlan(@RequestBody PlanDTO dto) {
Assert.notNull(dto.getBizId(), "智能体ID不能为空");
dto.setBizType(BizTypeEnum.AGENT);
checkAgentPermission(Long.parseLong(dto.getBizId()));
return ReqResult.success(subscriptionPlanAppService.createPlan(dto));
}
@PostMapping(value = "/update")
@Operation(summary = "修改订阅计划")
public ReqResult<Boolean> updatePlan(@RequestBody PlanDTO dto) {
Assert.notNull(dto.getId(), "计划ID不能为空");
dto.setBizId(null);
dto.setBizType(BizTypeEnum.AGENT);
PlanDTO planById = subscriptionPlanAppService.getPlanById(dto.getId());
Assert.isTrue(planById != null && planById.getBizType() == BizTypeEnum.AGENT, "计划不存在");
checkAgentPermission(Long.parseLong(planById.getBizId()));
return ReqResult.success(subscriptionPlanAppService.updatePlan(dto));
}
@PostMapping(value = "/sort/update")
@Operation(summary = "修改订阅计划排序")
public ReqResult<Void> updatePlanSort(@RequestBody List<SortUpdateDTO> updateDTOS) {
updateDTOS.forEach(updateDTO -> {
PlanDTO planById = subscriptionPlanAppService.getPlanById(updateDTO.getId());
Assert.isTrue(planById != null && planById.getBizType() == BizTypeEnum.AGENT, "计划不存在");
checkAgentPermission(Long.parseLong(planById.getBizId()));
PlanDTO dto = new PlanDTO();
dto.setId(updateDTO.getId());
dto.setSort(updateDTO.getSort());
subscriptionPlanAppService.updatePlan(dto);
});
return ReqResult.success();
}
@GetMapping("/list")
@Operation(summary = "查询订阅计划列表")
public ReqResult<List<PlanDTO>> listPlans(
@RequestParam Long agentId,
@RequestParam(required = false) Integer status,
@RequestParam(required = false) String keyword) {
PlanQueryRequest query = new PlanQueryRequest();
query.setBizType(BizTypeEnum.AGENT);
query.setBizId(agentId.toString());
query.setStatus(status);
query.setKeyword(keyword);
return ReqResult.success(subscriptionPlanAppService.listPlans(query));
}
@PostMapping("/{id}/offline")
@Operation(summary = "下架订阅计划")
public ReqResult<Boolean> offlinePlan(@PathVariable Long id) {
PlanDTO planById = subscriptionPlanAppService.getPlanById(id);
Assert.isTrue(planById != null && planById.getBizType() == BizTypeEnum.AGENT, "计划不存在");
checkAgentPermission(Long.parseLong(planById.getBizId()));
return ReqResult.success(subscriptionPlanAppService.offlinePlan(id));
}
@PostMapping("/{id}/delete")
@Operation(summary = "删除订阅计划")
public ReqResult<Boolean> deletePlan(@PathVariable Long id) {
PlanDTO planById = subscriptionPlanAppService.getPlanById(id);
Assert.isTrue(planById != null && planById.getBizType() == BizTypeEnum.AGENT, "计划不存在");
checkAgentPermission(Long.parseLong(planById.getBizId()));
return ReqResult.success(subscriptionPlanAppService.deletePlan(id));
}
private void checkAgentPermission(Long agentId) {
com.xspaceagi.agent.core.sdk.dto.ReqResult<List<AgentInfoDto>> listReqResult = iAgentRpcService.queryAgentInfoList(List.of(agentId));
if (listReqResult.getData() == null || listReqResult.getData().isEmpty()) {
throw new IllegalArgumentException("错误的智能体ID");
}
spacePermissionService.checkSpaceUserPermission(listReqResult.getData().get(0).getSpaceId());
}
}

View File

@@ -0,0 +1,137 @@
package com.xspaceagi.subscription.web.controller;
import com.xspaceagi.agent.core.sdk.IAgentRpcService;
import com.xspaceagi.agent.core.sdk.dto.AgentInfoDto;
import com.xspaceagi.agent.core.sdk.dto.SkillInfoDto;
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.TargetTypeEnum;
import com.xspaceagi.subscription.api.rpc.SubscriptionRpcService;
import com.xspaceagi.subscription.app.service.SubscriptionPlanAppService;
import com.xspaceagi.subscription.app.service.UserSubscriptionAppService;
import com.xspaceagi.subscription.sdk.dto.*;
import com.xspaceagi.subscription.spec.enums.BizTypeEnum;
import com.xspaceagi.subscription.web.controller.dto.MySubscriptionDTO;
import com.xspaceagi.system.application.util.DefaultIconUrlUtil;
import com.xspaceagi.system.spec.common.RequestContext;
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.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
@RestController
@RequestMapping("/api/subscription")
@Tag(name = "用户订阅相关接口")
public class SubscriptionController {
@Resource
private UserSubscriptionAppService userSubscriptionAppService;
@Resource
private SubscriptionPlanAppService subscriptionPlanAppService;
@Resource
private SubscriptionRpcService subscriptionRpcService;
@Resource
private IBillRpcService iBillRpcService;
@Resource
private IAgentRpcService iAgentRpcService;
@PostMapping("/order/create")
@Operation(summary = "创建订阅订单")
public ReqResult<OrderDTO> createOrder(@RequestParam Long planId) {
PlanDTO planDTO = subscriptionPlanAppService.getPlanById(planId);
if (planDTO == null) {
return ReqResult.error("Invalid planId");
}
if (planDTO.getPrice().compareTo(BigDecimal.ZERO) <= 0) {
RequestContext<Object> ctx = RequestContext.get();
CreateSubscriptionRequest request = new CreateSubscriptionRequest();
request.setTenantId(ctx.getTenantId());
request.setUserId(ctx.getUserId());
request.setPlanId(planId);
request.setBizType(planDTO.getBizType());
request.setExtra(Map.of("plan", planDTO));
subscriptionRpcService.createSubscription(request);
return ReqResult.success();
}
CreateOrderRequest request = new CreateOrderRequest();
request.setTenantId(RequestContext.get().getTenantId());
request.setUserId(RequestContext.get().getUserId());
request.setDescription("订阅[" + planDTO.getName() + "]");
request.setBizType(com.xspaceagi.bill.spec.enums.BizTypeEnum.SUBSCRIPTION);
CreateOrderRequest.CreateOrderItem item = new CreateOrderRequest.CreateOrderItem();
item.setTargetName(planDTO.getName());
item.setTargetType(TargetTypeEnum.PLAN.getCode());
item.setTargetId(planDTO.getId());
item.setPrice(planDTO.getPrice());
item.setCount(1);
item.setSnapshot(Map.of("plan", planDTO));
request.setItems(List.of(item));
OrderDTO order = iBillRpcService.createOrder(request);
return ReqResult.success(order);
}
@GetMapping("/system/plans")
@Operation(summary = "查询可订阅的系统计划列表")
public ReqResult<List<PlanDTO>> listPlans() {
PlanQueryRequest query = new PlanQueryRequest();
query.setBizType(BizTypeEnum.SYSTEM);
query.setBizId(null);
query.setStatus(1);
return ReqResult.success(subscriptionPlanAppService.listPlans(query));
}
@GetMapping("/my")
@Operation(summary = "查询我的订阅")
public ReqResult<MySubscriptionDTO> getMySubscriptions(@RequestParam(required = false) BizTypeEnum bizType, @RequestParam(required = false) String bizId) {
SubscriptionQueryRequest query = new SubscriptionQueryRequest();
query.setUserId(RequestContext.get().getUserId());
query.setBizType(bizType);
query.setBizId(bizId);
if (bizType == null) {
query.setBizType(BizTypeEnum.SYSTEM);
query.setBizId(null);
}
List<UserSubscriptionDTO> subscriptions = userSubscriptionAppService.getUserSubscriptions(query);
MySubscriptionDTO dto = new MySubscriptionDTO();
dto.setSubscriptions(subscriptions);
if (bizType == BizTypeEnum.SYSTEM || StringUtils.isNotBlank(bizId)) {
dto.setCurrentSubscription(subscriptions.stream().findFirst().orElse(null));
}
if (bizType == BizTypeEnum.AGENT && CollectionUtils.isNotEmpty(subscriptions)) {
List<Long> agentIds = subscriptions.stream().map(subscriptionDTO -> Long.parseLong(subscriptionDTO.getBizId())).collect(Collectors.toList());
Map<Long, AgentInfoDto> agentInfoDtoMap = iAgentRpcService.queryAgentInfoList(agentIds).getData().stream().collect(Collectors.toMap(AgentInfoDto::getId, agentInfoDto -> agentInfoDto, (a, b) -> a));
subscriptions.forEach(subscriptionDTO -> {
AgentInfoDto agentInfoDto = agentInfoDtoMap.get(Long.parseLong(subscriptionDTO.getBizId()));
if (agentInfoDto != null) {
subscriptionDTO.setBizName(agentInfoDto.getName());
subscriptionDTO.setIcon(DefaultIconUrlUtil.setDefaultIconUrl(agentInfoDto.getIcon(), agentInfoDto.getName(), "agent"));
}
});
}
if (bizType == BizTypeEnum.SKILL && CollectionUtils.isNotEmpty(subscriptions)) {
subscriptions.forEach(subscriptionDTO -> {
SkillInfoDto publishedSkillInfo = iAgentRpcService.getPublishedSkillInfo(Long.parseLong(subscriptionDTO.getBizId()), null).getData();
if (publishedSkillInfo != null) {
subscriptionDTO.setBizName(publishedSkillInfo.getName());
subscriptionDTO.setIcon(DefaultIconUrlUtil.setDefaultIconUrl(publishedSkillInfo.getIcon(), publishedSkillInfo.getName(), "skill"));
}
});
}
return ReqResult.success(dto);
}
}

View File

@@ -0,0 +1,106 @@
package com.xspaceagi.subscription.web.controller;
import com.xspaceagi.subscription.app.service.SubscriptionPlanAppService;
import com.xspaceagi.subscription.app.service.UserSubscriptionAppService;
import com.xspaceagi.subscription.sdk.dto.PlanDTO;
import com.xspaceagi.subscription.sdk.dto.PlanQueryRequest;
import com.xspaceagi.subscription.sdk.dto.SubscriptionStatsDTO;
import com.xspaceagi.subscription.sdk.dto.SubscriptionStatsQueryRequest;
import com.xspaceagi.subscription.spec.enums.BizTypeEnum;
import com.xspaceagi.subscription.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/plan")
@Tag(name = "系统订阅计划管理")
public class SystemPlanManController {
@Resource
private SubscriptionPlanAppService subscriptionPlanAppService;
@Resource
private UserSubscriptionAppService userSubscriptionAppService;
@RequireResource(SUBSCRIPTION_POINTS_QUERY)
@GetMapping("/subscription/stats")
@Operation(summary = "查询指定对象的订阅统计")
public ReqResult<SubscriptionStatsDTO> getSubscriptionStats(
@RequestParam BizTypeEnum bizType,
@RequestParam String bizId,
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "20") Integer pageSize) {
SubscriptionStatsQueryRequest request = new SubscriptionStatsQueryRequest();
request.setBizType(bizType);
request.setBizId(bizId);
request.setPageNum(pageNum);
request.setPageSize(pageSize);
return ReqResult.success(userSubscriptionAppService.getSubscriptionStats(request));
}
@RequireResource(SUBSCRIPTION_POINTS_MODIFY)
@PostMapping(value = "/create")
@Operation(summary = "添加订阅计划")
public ReqResult<Long> createPlan(@RequestBody PlanDTO dto) {
dto.setBizType(BizTypeEnum.SYSTEM);
return ReqResult.success(subscriptionPlanAppService.createPlan(dto));
}
@RequireResource(SUBSCRIPTION_POINTS_MODIFY)
@PostMapping(value = "/update")
@Operation(summary = "修改订阅计划")
public ReqResult<Boolean> updatePlan(@RequestBody PlanDTO dto) {
return ReqResult.success(subscriptionPlanAppService.updatePlan(dto));
}
@RequireResource(SUBSCRIPTION_POINTS_MODIFY)
@PostMapping(value = "/sort/update")
@Operation(summary = "修改订阅计划排序")
public ReqResult<Void> updatePlanSort(@RequestBody List<SortUpdateDTO> updateDTOS) {
updateDTOS.forEach(updateDTO -> {
PlanDTO dto = new PlanDTO();
dto.setId(updateDTO.getId());
dto.setSort(updateDTO.getSort());
subscriptionPlanAppService.updatePlan(dto);
});
return ReqResult.success();
}
@RequireResource(SUBSCRIPTION_POINTS_QUERY)
@GetMapping("/list")
@Operation(summary = "查询订阅计划列表")
public ReqResult<List<PlanDTO>> listPlans(
@RequestParam(required = false) Integer status,
@RequestParam(required = false) String keyword) {
PlanQueryRequest query = new PlanQueryRequest();
query.setBizType(BizTypeEnum.SYSTEM);
query.setStatus(status);
query.setKeyword(keyword);
return ReqResult.success(subscriptionPlanAppService.listPlans(query));
}
@RequireResource(SUBSCRIPTION_POINTS_MODIFY)
@PostMapping("/{id}/offline")
@Operation(summary = "下架订阅计划")
public ReqResult<Boolean> offlinePlan(@PathVariable Long id) {
return ReqResult.success(subscriptionPlanAppService.offlinePlan(id));
}
@RequireResource(SUBSCRIPTION_POINTS_DELETE)
@PostMapping("/{id}/delete")
@Operation(summary = "删除订阅计划")
public ReqResult<Boolean> deletePlan(@PathVariable Long id) {
return ReqResult.success(subscriptionPlanAppService.deletePlan(id));
}
}

View File

@@ -0,0 +1,17 @@
package com.xspaceagi.subscription.web.controller.dto;
import com.xspaceagi.subscription.sdk.dto.UserSubscriptionDTO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Data
public class MySubscriptionDTO {
@Schema(description = "当前订阅")
private UserSubscriptionDTO currentSubscription;
@Schema(description = "所有订阅")
private List<UserSubscriptionDTO> subscriptions;
}

View File

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

View File

@@ -0,0 +1,67 @@
<?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-subscription</artifactId>
<packaging>pom</packaging>
<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-subscription-application</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-subscription-infra</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-subscription-domain</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-subscription-spec</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-subscription-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-subscription-sdk</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-subscription-web</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<modules>
<module>app-platform-subscription-spec</module>
<module>app-platform-subscription-sdk</module>
<module>app-platform-subscription-domain</module>
<module>app-platform-subscription-infra</module>
<module>app-platform-subscription-application</module>
<module>app-platform-subscription-api</module>
<module>app-platform-subscription-web</module>
</modules>
</project>