TASK:新增订阅计划概念

This commit is contained in:
2025-12-11 09:44:25 +08:00
parent 22bc8750c8
commit 7f094265da
14 changed files with 1062 additions and 15 deletions

View File

@@ -0,0 +1,62 @@
package com.ai.da.controller;
import com.ai.da.common.response.Response;
import com.ai.da.model.dto.SubscriptionPlanDTO;
import com.ai.da.model.dto.SubscriptionPlanPageQuery;
import com.ai.da.model.dto.UpdateSubscriptionPlanDTO;
import com.ai.da.model.vo.SubscriptionPlanVO;
import com.ai.da.service.SubscriptionPlanService;
import com.baomidou.mybatisplus.core.metadata.IPage;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
@Api(tags = "订阅计划模块")
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/api/subscription_plan")
public class SubscriptionPlanController {
private final SubscriptionPlanService subscriptionPlanService;
@ApiOperation("创建订阅计划")
@PostMapping("/createPlan")
public Response<String> createPlan(@Valid @RequestBody SubscriptionPlanDTO subscriptionPlanDTO) {
subscriptionPlanService.createPlan(subscriptionPlanDTO);
return Response.success();
}
@ApiOperation("更新订阅计划")
@PostMapping("/updatePlan")
public Response<String> updatePlan(@Valid @RequestBody UpdateSubscriptionPlanDTO updateDTO) {
subscriptionPlanService.updatePlan(updateDTO);
return Response.success();
}
@ApiOperation("搜索订阅计划")
@PostMapping("/searchByPage")
public Response<IPage<SubscriptionPlanVO>> searchByPage(@Valid @RequestBody SubscriptionPlanPageQuery subscriptionPlanPageQuery) {
IPage<SubscriptionPlanVO> subscriptionPlanVOIPage = subscriptionPlanService.searchByPage(subscriptionPlanPageQuery);
return Response.success(subscriptionPlanVOIPage);
}
@ApiOperation("删除订阅计划")
@GetMapping("/deletePlan")
public Response<String> deletePlan(@RequestParam Long id) {
subscriptionPlanService.deletePlan(id);
return Response.success();
}
@ApiOperation("管理员切换订阅计划")
@GetMapping("/switchSubscriptionPlan")
public Response<String> switchSubscriptionPlan(@RequestParam Long subscriptionPlanId, @RequestParam Long adminAccId) {
subscriptionPlanService.switchSubscriptionPlan(subscriptionPlanId, adminAccId);
return Response.success();
}
}

View File

@@ -0,0 +1,7 @@
package com.ai.da.mapper.primary;
import com.ai.da.mapper.primary.entity.SubscriptionPlan;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
public interface SubscriptionPlanMapper extends BaseMapper<SubscriptionPlan> {
}

View File

@@ -140,4 +140,6 @@ public class Account implements Serializable {
@ApiModelProperty("givenName")
private String givenName;
private Long subscriptionPlanId;
}

View File

@@ -0,0 +1,87 @@
package com.ai.da.mapper.primary.entity;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
@EqualsAndHashCode(callSuper = true)
@Data
@NoArgsConstructor
@TableName("t_subscription_plan")
public class SubscriptionPlan extends BaseEntity{
/**
* 组织id
*/
private Long organizationId;
/**
* 订阅命名
*/
private String name;
/**
* 当前订阅开始时间
*/
private Long currentPeriodStart;
/**
* 当前订阅结束时间
*/
private Long currentPeriodEnd;
/**
* 当前订阅总的子账号数量
*/
private Integer accountNum;
/**
* 当前订阅可用积分上限
*/
private BigDecimal creditLimit;
/**
* 当前订阅已使用积分
*/
private BigDecimal creditUsage;
/**
* 管理员账户id
*/
private Long adminAccId;
@TableLogic(value = "0", delval = "1")
private Integer isDeleted;
/**
* 删除人的用户id
*/
private Long deleteBy;
/**
* 状态
*/
private String status;
// 在类内部定义的枚举
@Getter
public enum SubscriptionStatus {
PENDING("待激活", 0),
ACTIVE("已激活", 1),
EXPIRED("已过期", 2),
CANCELLED("已取消", 3);
private final String desc;
private final int code;
SubscriptionStatus(String desc, int code) {
this.desc = desc;
this.code = code;
}
}
}

View File

@@ -0,0 +1,39 @@
package com.ai.da.model.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
@Data
@ApiModel( value = "创建订阅计划入参")
public class SubscriptionPlanDTO {
@ApiModelProperty("组织id")
@NotNull(message = "Please select an organizationId.")
private Long organizationId;
@ApiModelProperty("当前订阅开始时间")
@NotNull(message = "Please set a subscription start time.")
private Long currentPeriodStart;
@ApiModelProperty("当前订阅结束时间")
@NotNull(message = "Please set a subscription end time.")
private Long currentPeriodEnd;
@ApiModelProperty("当前订阅总的子账号数量")
@NotNull(message = "Please set the sub-account number.")
private Integer accountNum;
@ApiModelProperty("当前订阅可用积分上限")
@NotNull(message = "Please set the credits limit.")
private BigDecimal creditLimit;
@ApiModelProperty("管理员账户id")
@NotNull(message = "Please assign an administrator account.")
private Long adminAccId;
}

View File

@@ -0,0 +1,21 @@
package com.ai.da.model.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
@Data
@ApiModel
public class SubscriptionPlanPageQuery extends QueryPageByTimeDTO {
@ApiModelProperty("组织id")
private Long organizationId;
@ApiModelProperty("管理id")
private Long adminAccId;
@ApiModelProperty("状态 PENDING||ACTIVE||EXPIRED")
private List<String> status;
}

View File

@@ -0,0 +1,33 @@
package com.ai.da.model.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
@Data
@ApiModel
public class UpdateSubscriptionPlanDTO {
@ApiModelProperty("id")
@NotNull(message = "subscription plan id cannot be empty")
private Long id;
@ApiModelProperty("当前订阅开始时间")
private Long currentPeriodStart;
@ApiModelProperty("当前订阅结束时间")
private Long currentPeriodEnd;
@ApiModelProperty("当前订阅总的子账号数量")
private Integer accountNum;
@ApiModelProperty("当前订阅可用积分上限")
private BigDecimal creditLimit;
@ApiModelProperty("管理员账户id")
private Long adminAccId;
}

View File

@@ -0,0 +1,37 @@
package com.ai.da.model.vo;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
public class SubscriptionPlanVO {
@ApiModelProperty("id")
private Long id;
@ApiModelProperty("组织id")
private Long organizationId;
@ApiModelProperty("当前订阅开始时间")
private Long currentPeriodStart;
@ApiModelProperty("当前订阅结束时间")
private Long currentPeriodEnd;
@ApiModelProperty("当前订阅总的子账号数量")
private Integer accountNum;
@ApiModelProperty("当前订阅可用积分上限")
private BigDecimal creditLimit;
@ApiModelProperty("管理员账户id")
private Long adminAccId;
@ApiModelProperty("创建时间")
private LocalDateTime createTime;
}

View File

@@ -2237,7 +2237,7 @@ public class PythonService {
private List<Integer> resolve(List<BigDecimal> list) {
List<Integer> integerList = Lists.newArrayList();
list.forEach(l -> {
integerList.add(new Integer(l.intValue()));
integerList.add(l.intValue());
});
return integerList;
}

View File

@@ -0,0 +1,22 @@
package com.ai.da.service;
import com.ai.da.mapper.primary.entity.SubscriptionPlan;
import com.ai.da.model.dto.SubscriptionPlanDTO;
import com.ai.da.model.dto.SubscriptionPlanPageQuery;
import com.ai.da.model.dto.UpdateSubscriptionPlanDTO;
import com.ai.da.model.vo.SubscriptionPlanVO;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
public interface SubscriptionPlanService extends IService<SubscriptionPlan> {
void createPlan(SubscriptionPlanDTO subscriptionPlanDTO);
void updatePlan(UpdateSubscriptionPlanDTO updateDTO);
IPage<SubscriptionPlanVO> searchByPage(SubscriptionPlanPageQuery subscriptionPlanPageQuery);
void deletePlan(Long id);
void switchSubscriptionPlan(Long subscriptionPlanId, Long adminAccId);
}

View File

@@ -135,6 +135,9 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
@Resource
private UserFollowService userFollowService;
@Resource
private SubscriptionPlanMapper subscriptionPlanMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public AccountPreLoginVO preLogin(AccountPreLoginDTO accountDTO) {
@@ -2440,7 +2443,7 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
@Transactional(rollbackFor = Exception.class)
public Boolean createSubAccount(AddSubAccountDTO addSubAccountDTO, Account adminAcc, int subUserRole) {
QueryWrapper<Account> qw = new QueryWrapper<>();
qw.lambda().eq(Account::getOrganizationName, adminAcc.getOrganizationName());
qw.lambda().eq(Account::getOrganizationId, adminAcc.getOrganizationId());
List<Account> accounts = accountMapper.selectList(qw);
// 校验子账号总数是否达上限
@@ -2460,12 +2463,12 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
}
// 校验邮箱是否已加入组织
if (isUserEmailExists(adminAcc.getOrganizationName(), addSubAccountDTO.getUserEmail())) {
if (isUserEmailExists(adminAcc.getOrganizationId(), addSubAccountDTO.getUserEmail())) {
throw new BusinessException("This organization already has an account with the same email.", ResultEnum.PROMPT.getCode());
}
// 校验用户名是否同名
if (isUsernameExists(adminAcc.getOrganizationName(), addSubAccountDTO.getUserName())) {
if (isUsernameExists(adminAcc.getOrganizationId(), addSubAccountDTO.getUserName())) {
throw new BusinessException("This organization already has an account with the same username.");
}
@@ -2477,7 +2480,7 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
BigDecimal remainingCredits = adminRemainingCredits(adminAcc);
// 将个人账号加入组织
if (Objects.nonNull(subAccount) && personAccRole.contains(subAccount.getSystemUser())) {
log.info("将用户{} 加入组织{}", addSubAccountDTO.getUserEmail(), adminAcc.getOrganizationName());
log.info("将用户{} 加入组织{}", addSubAccountDTO.getUserEmail(), adminAcc.getOrganizationId());
subAccount.setUserName(addSubAccountDTO.getUserName());
if (!StringUtil.isNullOrEmpty(addSubAccountDTO.getUserPassword())){
subAccount.setUserPassword(addSubAccountDTO.getUserPassword());
@@ -2489,8 +2492,9 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
}
subAccount.setSystemUser(subUserRole);
subAccount.setOrganizationName(adminAcc.getOrganizationName());
subAccount.setOrganizationId(adminAcc.getOrganizationId());
subAccount.setParentId(adminAcc.getId());
subAccount.setSubscriptionPlanId(adminAcc.getSubscriptionPlanId());
if (Objects.nonNull(addSubAccountDTO.getCreditsUsageLimit())) {
if (remainingCredits.compareTo(addSubAccountDTO.getCreditsUsageLimit()) < 0) {
throw new BusinessException("Insufficient credits (Balance: " + remainingCredits + ").", ResultEnum.PROMPT.getCode());
@@ -2553,10 +2557,13 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
subAccount.setIsTrial(0);
subAccount.setIsBeginner(1);
subAccount.setParentId(adminAcc.getId());
subAccount.setOrganizationName(adminAcc.getOrganizationName());
// subAccount.setOrganizationName(adminAcc.getOrganizationName());
subAccount.setOrganizationId(adminAcc.getOrganizationId());
subAccount.setSubscriptionPlanId(adminAcc.getSubscriptionPlanId());
accountMapper.insert(subAccount);
}
updateById(adminAcc);
syncAdminAccToSubscriptionPlan(adminAcc);
return Boolean.TRUE;
}
@@ -2569,7 +2576,7 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
// 校验用户名是否同名
if (!StringUtil.isNullOrEmpty(addSubAccountDTO.getUserName())
&& !exAccountInfo.getUserName().equals(addSubAccountDTO.getUserName())
&& isUsernameExists(adminAcc.getOrganizationName(), addSubAccountDTO.getUserName())) {
&& isUsernameExists(adminAcc.getOrganizationId(), addSubAccountDTO.getUserName())) {
throw new BusinessException("This organization already has an account with the same username.");
} else if (!StringUtil.isNullOrEmpty(addSubAccountDTO.getUserName())
&& !exAccountInfo.getUserName().equals(addSubAccountDTO.getUserName())) {
@@ -2632,19 +2639,20 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
} else {
baseMapper.updateById(exAccountInfo);
baseMapper.updateById(adminAcc);
syncAdminAccToSubscriptionPlan(adminAcc);
}
return Boolean.TRUE;
}
private boolean isUserEmailExists(String organizationName, String email) {
private boolean isUserEmailExists(Long organizationId, String email) {
QueryWrapper<Account> qw = new QueryWrapper<>();
qw.lambda().eq(Account::getOrganizationName, organizationName).eq(Account::getUserEmail, email);
qw.lambda().eq(Account::getOrganizationId, organizationId).eq(Account::getUserEmail, email);
return accountMapper.selectCount(qw) > 0;
}
private boolean isUsernameExists(String organizationName, String userName) {
private boolean isUsernameExists(Long organizationId, String userName) {
QueryWrapper<Account> qw = new QueryWrapper<>();
qw.lambda().eq(Account::getOrganizationName, organizationName).eq(Account::getUserName, userName);
qw.lambda().eq(Account::getOrganizationId, organizationId).eq(Account::getUserName, userName);
return accountMapper.selectCount(qw) > 0;
}
@@ -2738,6 +2746,7 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
.set(Account::getCredits, finalCredits)
.set(Account::getCreditsUsage, null)
.set(Account::getCreditsUsageLimit, null)
.set(Account::getSubscriptionPlanId, null)
.set(Account::getUpdateDate, new Date());
baseMapper.update(null, updateWrapper);
@@ -2756,6 +2765,7 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
adminAcc.setCredits(adminAcc.getCredits().add(unusedCreditsTotal));
adminAcc.setUpdateDate(new Date());
baseMapper.updateById(adminAcc);
syncAdminAccToSubscriptionPlan(adminAcc);
}
}
@@ -2776,13 +2786,27 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
return 0;
}
// 用于管理员分配积分后,账号信息更新的同时,更新关联订阅计划的信息
private void syncAdminAccToSubscriptionPlan(Account adminAcc) {
if (Objects.isNull(adminAcc.getSubscriptionPlanId())) {
return ;
}
SubscriptionPlan subscriptionPlan = subscriptionPlanMapper.selectById(adminAcc.getSubscriptionPlanId());
if (Objects.nonNull(subscriptionPlan)) {
subscriptionPlan.setCreditUsage(adminAcc.getCreditsUsage());
subscriptionPlan.setUpdateTime(LocalDateTime.now());
subscriptionPlanMapper.updateById(subscriptionPlan);
}
}
@Override
public PageBaseResponse<Account> subAccountList(SubAccountPageDTO subAccountPageDTO) {
AuthPrincipalVo authPrincipalVo = UserContext.getUserHolder();
Account account = accountMapper.selectById(authPrincipalVo);
QueryWrapper<Account> qw = new QueryWrapper<>();
qw.lambda().ne(Account::getId, account.getId());
qw.lambda().eq(Account::getOrganizationName, account.getOrganizationName());
qw.lambda().eq(Account::getOrganizationId, account.getOrganizationId());
if (StringUtils.isNotBlank(subAccountPageDTO.getStartTime())) {
qw.lambda().ge(Account::getCreateDate, subAccountPageDTO.getStartTime());
}
@@ -3443,7 +3467,7 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
int subUserRole = getSubUserRole(adminAcc.getSystemUser());
List<Account> accounts = accountMapper.selectList(new QueryWrapper<Account>()
.eq("organization_name", adminAcc.getOrganizationName())
.eq("organization_id", adminAcc.getOrganizationId())
.eq("system_user", subUserRole)
.select("user_name", "user_email", "user_password", "credits_usage_limit"));
@@ -3516,7 +3540,7 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
// 只有当前子账号数量为0时允许批量上传
QueryWrapper<Account> qw = new QueryWrapper<>();
qw.lambda().eq(Account::getSystemUser, 8)
.eq(Account::getOrganizationName, parent.getOrganizationName());
.eq(Account::getOrganizationId, parent.getOrganizationId());
List<Account> accounts = accountMapper.selectList(qw);
if (!accounts.isEmpty()) {
throw new BusinessException("permit.bulk.creation", ResultEnum.PROMPT.getCode());

View File

@@ -0,0 +1,711 @@
package com.ai.da.service.impl;
import com.ai.da.common.config.exception.BusinessException;
import com.ai.da.common.context.UserContext;
import com.ai.da.common.utils.CopyUtil;
import com.ai.da.common.utils.RedisUtil;
import com.ai.da.mapper.primary.AccountMapper;
import com.ai.da.mapper.primary.OrganizationMapper;
import com.ai.da.mapper.primary.SubscriptionPlanMapper;
import com.ai.da.mapper.primary.entity.Account;
import com.ai.da.mapper.primary.entity.Organization;
import com.ai.da.mapper.primary.entity.SubscriptionPlan;
import com.ai.da.model.dto.SubscriptionPlanDTO;
import com.ai.da.model.dto.SubscriptionPlanPageQuery;
import com.ai.da.model.dto.UpdateSubscriptionPlanDTO;
import com.ai.da.model.vo.SubscriptionPlanVO;
import com.ai.da.service.SubscriptionPlanService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.common.base.Function;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.List;
import java.util.Objects;
@Slf4j
@Service
@RequiredArgsConstructor
public class SubscriptionPlanServiceImpl extends ServiceImpl<SubscriptionPlanMapper, SubscriptionPlan> implements SubscriptionPlanService {
private final AccountMapper accountMapper;
private final OrganizationMapper organizationMapper;
private final RedisUtil redisUtil;
// 创建
@Override
public void createPlan(SubscriptionPlanDTO subscriptionPlanDTO){
SubscriptionPlan subscriptionPlan = CopyUtil.copyObject(subscriptionPlanDTO, SubscriptionPlan.class);
subscriptionPlan.setStatus(SubscriptionPlan.SubscriptionStatus.PENDING.name());
subscriptionPlan.setName("DEFAULT_NAME");
subscriptionPlan.setCreditUsage(BigDecimal.ZERO);
subscriptionPlan.setCreateTime(LocalDateTime.now());
if (Objects.isNull(subscriptionPlanDTO.getCreditLimit())) {
subscriptionPlan.setCreditLimit(BigDecimal.ZERO);
}
baseMapper.insert(subscriptionPlan);
}
// 更新 到期时间、积分总量、已使用积分量
@Override
public void updatePlan(UpdateSubscriptionPlanDTO updateDTO){
if (Objects.isNull(updateDTO.getId())) {
throw new BusinessException("id.cannot.be.empty");
}
SubscriptionPlan subscriptionPlan = baseMapper.selectById(updateDTO.getId());
if (Objects.isNull(subscriptionPlan)) {
throw new BusinessException("unknown.subscription.plan");
}
if (Objects.nonNull(updateDTO.getCurrentPeriodStart()) && !updateDTO.getCurrentPeriodStart().equals(subscriptionPlan.getCurrentPeriodStart())) {
subscriptionPlan.setCurrentPeriodStart(updateDTO.getCurrentPeriodStart());
}
if (Objects.nonNull(updateDTO.getCurrentPeriodEnd()) && !updateDTO.getCurrentPeriodEnd().equals(subscriptionPlan.getCurrentPeriodEnd())) {
subscriptionPlan.setCurrentPeriodEnd(updateDTO.getCurrentPeriodEnd());
}
if (Objects.nonNull(updateDTO.getAccountNum()) && !updateDTO.getAccountNum().equals(subscriptionPlan.getAccountNum())) {
subscriptionPlan.setAccountNum(updateDTO.getAccountNum());
}
if (Objects.nonNull(updateDTO.getCreditLimit()) && !updateDTO.getCreditLimit().equals(subscriptionPlan.getCreditLimit())) {
subscriptionPlan.setCreditLimit(updateDTO.getCreditLimit());
}
if (Objects.nonNull(updateDTO.getAdminAccId()) && !updateDTO.getAdminAccId().equals(subscriptionPlan.getAdminAccId())) {
subscriptionPlan.setAdminAccId(updateDTO.getAdminAccId());
}
subscriptionPlan.setUpdateTime(LocalDateTime.now());
updateById(subscriptionPlan);
}
public void updatePlan(){
}
// 查找 根据入参提供的参数进行分页查询
@Override
public IPage<SubscriptionPlanVO> searchByPage(SubscriptionPlanPageQuery subscriptionPlanPageQuery) {
// 1. 参数校验
validatePageQuery(subscriptionPlanPageQuery);
// 2. 构建查询条件
LambdaQueryWrapper<SubscriptionPlan> queryWrapper = buildQueryWrapper(subscriptionPlanPageQuery);
// 3. 执行分页查询
Page<SubscriptionPlan> page = new Page<>(subscriptionPlanPageQuery.getPage(), subscriptionPlanPageQuery.getSize());
IPage<SubscriptionPlan> resultPage = baseMapper.selectPage(page, queryWrapper);
// 4. 转换为VO并返回
return resultPage.convert((Function<SubscriptionPlan, SubscriptionPlanVO>) plan -> CopyUtil.copyObject(plan, SubscriptionPlanVO.class)) ;
}
/**
* 参数校验
*/
private void validatePageQuery(SubscriptionPlanPageQuery query) {
// 基本分页参数校验JSR-303已校验这里做额外业务校验
if (query.getPage() <= 0) {
throw new BusinessException("页码必须大于0");
}
if (query.getSize() <= 0 || query.getSize() > 100) {
throw new BusinessException("每页数量必须在1-100之间");
}
// 时间格式校验
if (StringUtils.isNotBlank(query.getStartTime()) && StringUtils.isNotBlank(query.getEndTime())) {
try {
LocalDateTime start = parseDateTime(query.getStartTime());
LocalDateTime end = parseDateTime(query.getEndTime());
assert start != null;
if (start.isAfter(end)) {
throw new BusinessException("开始时间不能晚于结束时间");
}
} catch (DateTimeParseException e) {
throw new BusinessException("时间格式错误请使用yyyy-MM-dd HH:mm:ss格式");
}
}
if (!CollectionUtils.isEmpty(query.getStatus())) {
for (String status : query.getStatus()) {
try {
SubscriptionPlan.SubscriptionStatus.valueOf(status.toUpperCase());
} catch (IllegalArgumentException e) {
throw new BusinessException("未知订阅状态:" + status);
}
}
}
}
/**
* 构建查询条件
*/
private LambdaQueryWrapper<SubscriptionPlan> buildQueryWrapper(SubscriptionPlanPageQuery query) {
LambdaQueryWrapper<SubscriptionPlan> wrapper = new LambdaQueryWrapper<>();
// 精确匹配条件
if (query.getId() != null) {
wrapper.eq(SubscriptionPlan::getId, query.getId());
}
if (query.getOrganizationId() != null) {
wrapper.eq(SubscriptionPlan::getOrganizationId, query.getOrganizationId());
}
if (query.getAdminAccId() != null) {
wrapper.eq(SubscriptionPlan::getAdminAccId, query.getAdminAccId());
}
// 时间范围查询
if (StringUtils.isNotBlank(query.getStartTime())) {
LocalDateTime startTime = parseDateTime(query.getStartTime());
wrapper.ge(SubscriptionPlan::getCreateTime, startTime);
}
if (StringUtils.isNotBlank(query.getEndTime())) {
LocalDateTime endTime = parseDateTime(query.getEndTime());
wrapper.le(SubscriptionPlan::getCreateTime, endTime);
}
// 状态匹配
if (!CollectionUtils.isEmpty(query.getStatus())) {
wrapper.in(SubscriptionPlan::getStatus, query.getStatus());
}
// 默认按创建时间倒序排序
wrapper.eq(SubscriptionPlan::getIsDeleted, 0)
.orderByDesc(SubscriptionPlan::getCreateTime);
return wrapper;
}
/**
* 时间字符串解析(支持多种格式)
*/
private LocalDateTime parseDateTime(String timeStr) {
if (StringUtils.isBlank(timeStr)) {
return null;
}
// 尝试解析完整时间格式
try {
return LocalDateTime.parse(timeStr, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
} catch (DateTimeParseException e1) {
// 尝试解析日期格式自动补全时间为00:00:00
try {
LocalDate date = LocalDate.parse(timeStr, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
return date.atStartOfDay();
} catch (DateTimeParseException e2) {
throw new DateTimeParseException("时间格式错误请使用yyyy-MM-dd或yyyy-MM-dd HH:mm:ss格式", timeStr, 0);
}
}
}
// 删除(逻辑删除)
@Override
public void deletePlan(Long id) {
Long adminAccountId = UserContext.getUserHolder().getId();
// 1. 参数基础校验
if (id == null || id <= 0) {
throw new BusinessException("ID不能为空且必须大于0");
}
// 2. 检查数据是否存在
SubscriptionPlan plan = baseMapper.selectById(id);
if (plan == null) {
throw new BusinessException("订阅计划不存在");
}
// 3. 检查是否已被删除(防止重复删除) 一般情况下走不到逻辑删除的数据通过mybatis-plus查不到
if (plan.getIsDeleted() == 1) {
throw new BusinessException("该订阅计划已被删除");
}
// 4. 检查业务约束条件(例如:是否有正在使用的订单等)
checkBusinessConstraints(plan);
// 5. 执行逻辑删除
LambdaUpdateWrapper<SubscriptionPlan> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(SubscriptionPlan::getId, id)
.set(SubscriptionPlan::getIsDeleted, 1)
.set(SubscriptionPlan::getDeleteBy, adminAccountId);
int rows = baseMapper.update(null, wrapper);
if (rows == 0) {
throw new BusinessException("删除失败,请稍后重试");
}
// 6. 记录操作日志
log.info("管理员{} 删除订阅计划 {}", adminAccountId, plan);
}
/**
* 检查业务约束条件
*/
private void checkBusinessConstraints(SubscriptionPlan plan) {
// 检查是否有活跃的用户关联
Long activeSubAcc = countActiveSubAccByPlanId(plan.getId());
if (activeSubAcc > 0) {
throw new BusinessException("存在" + activeSubAcc + "个用户使用此计划,无法删除");
}
// 检查是否在有效期内
if (plan.getCurrentPeriodEnd() != null && isExpired(plan.getCurrentPeriodEnd())) {
throw new BusinessException("计划仍在有效期内,请到期后再删除");
}
}
private Long countActiveSubAccByPlanId(Long planId) {
QueryWrapper<Account> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(Account::getSubscriptionPlanId, planId);
return accountMapper.selectCount(queryWrapper);
}
// 判断是否已过期
private boolean isExpired(Long currentPeriodEnd) {
if (currentPeriodEnd == null) {
return false; // 永久有效
}
long currentTimestamp = System.currentTimeMillis() / 1000;
return currentPeriodEnd < currentTimestamp;
}
// todo 切换管理员的账号
// 定时器更新管理员状态
/* public void updateEduAdminAccount() {
// 1. 扫描所有的订阅计划的开始时间currentPeriodStart
// 2. 当检测到有今天内开始有效的订阅,更新绑定的管理员的账号
// 3. 更新管理的信息包括,①根据订阅结束时间延长管理员账号有效期 ②根据积分上限更新管理员的积分,如果当前管理员正处于一个订阅中,则不做更新,但是允许切换
}*/
public void activeSubscriptionPlan() {
log.info("开始执行订阅计划生效检查...");
// 1. 扫描所有的订阅计划的开始时间currentPeriodStart找出今天开始生效的计划
List<SubscriptionPlan> todayActivePlans = findTodayActivePlans();
if (CollectionUtils.isEmpty(todayActivePlans)) {
log.info("今日没有需要生效的订阅计划");
return;
}
log.info("发现{}个今日生效的订阅计划", todayActivePlans.size());
// 2. 处理每个今天开始生效的订阅计划
for (SubscriptionPlan plan : todayActivePlans) {
try {
processActiveSubscriptionPlan(plan);
} catch (Exception e) {
log.error("处理订阅计划失败ID: {}, 错误: {}", plan.getId(), e.getMessage(), e);
// 继续处理其他计划,不中断整体流程
}
}
log.info("订阅计划生效检查执行完成");
}
/**
* 查找今天开始生效的订阅计划
*/
private List<SubscriptionPlan> findTodayActivePlans() {
// 获取今天的开始和结束时间戳(秒级)
long todayStart = getTodayStartTimestamp();
long todayEnd = getTodayEndTimestamp();
log.debug("扫描时间范围: {} - {} (今日)", formatTimestamp(todayStart), formatTimestamp(todayEnd));
// 查询今天开始生效的订阅计划
QueryWrapper<SubscriptionPlan> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("is_deleted", 0) // 未删除
.between("current_period_start", todayStart, todayEnd) // 今天开始生效
.orderByAsc("current_period_start"); // 按开始时间排序
return baseMapper.selectList(queryWrapper);
}
/**
* 处理单个生效的订阅计划
*/
private void processActiveSubscriptionPlan(SubscriptionPlan plan) {
log.info("处理生效订阅计划ID: {}, 组织ID: {}, 管理员ID: {}",
plan.getId(), plan.getOrganizationId(), plan.getAdminAccId());
// 获取关联的管理员账号
Account adminAccount = accountMapper.selectById(plan.getAdminAccId());
if (adminAccount == null) {
log.error("管理员账号不存在ID: {}", plan.getAdminAccId());
return;
}
// 3. 检查管理员当前是否处于其他有效订阅中
if (isAccountInActiveSubscription(adminAccount, plan.getId())) {
log.info("管理员ID: {} 已处于有效订阅中,本次不修改账户信息", adminAccount.getId());
}
// 4. 更新管理员账号信息
updateAdminAccount(adminAccount, plan);
// 5. 修改订阅状态
plan.setStatus(SubscriptionPlan.SubscriptionStatus.ACTIVE.name());
log.info("订阅计划处理完成ID: {}", plan.getId());
}
/**
* 更新管理员账号信息
*/
private void updateAdminAccount(Account account, SubscriptionPlan plan) {
Account updateAccount = new Account();
updateAccount.setId(account.getId());
// ① 根据订阅结束时间延长管理员账号有效期validEndTime
Long newValidEndTime = calculateNewValidEndTime(account, plan);
updateAccount.setValidEndTime(newValidEndTime);
// ② 根据积分上限更新管理员的积分credits和身份systemUser
// 暂时保留管理员自己购买的积分
if (Objects.nonNull(account.getCreditsUsageLimit())) {
BigDecimal leftCredits = account.getCredits().add(account.getCreditsUsage()).subtract(account.getCreditsUsageLimit());
updateAccount.setCredits(plan.getCreditLimit().add(leftCredits));
} else {
updateAccount.setCredits(plan.getCreditLimit().add(account.getCredits()));
}
// 根据组织ID判断用户类型
Integer newSystemUser = determineSystemUserType(plan);
updateAccount.setSystemUser(newSystemUser);
// 更新子账号数量限制
updateAccount.setCreditsUsage(plan.getCreditUsage());
updateAccount.setCreditsUsageLimit(plan.getCreditLimit());
updateAccount.setSubAccountNum(plan.getAccountNum());
// 关联订阅计划ID
updateAccount.setSubscriptionPlanId(plan.getId());
// 更新组织信息
updateAccount.setOrganizationId(plan.getOrganizationId());
// 如果是组织管理员设置isAdmin标志
/* if (isOrganizationAdmin(newSystemUser)) {
updateAccount.setIsAdmin(1);
}*/
// 执行更新
int rows = accountMapper.updateById(updateAccount);
if (rows > 0) {
log.info("管理员账号更新成功ID: {}, 有效期至: {}, 用户类型: {}, 积分: {}",
account.getId(),
formatTimestamp(newValidEndTime),
newSystemUser,
plan.getCreditLimit());
} else {
log.error("管理员账号更新失败ID: {}", account.getId());
}
}
/**
* 计算新的有效期结束时间
*/
private Long calculateNewValidEndTime(Account account, SubscriptionPlan plan) {
long now = System.currentTimeMillis() / 1000;
Long currentValidEndTime = account.getValidEndTime();
// 如果当前有效期晚于现在,则延长有效期
if (currentValidEndTime != null && currentValidEndTime > now) {
// 计算订阅的时长(秒)
long subscriptionDuration = plan.getCurrentPeriodEnd() - plan.getCurrentPeriodStart();
// 延长现有有效期
return currentValidEndTime + subscriptionDuration * 1000;
} else {
// 否则使用订阅结束时间
return plan.getCurrentPeriodEnd() * 1000;
}
}
/**
* 根据计划确定用户类型
*/
private Integer determineSystemUserType(SubscriptionPlan plan) {
// 根据组织ID, 去organization表中查询判断是学校还是企业
Long orgId = plan.getOrganizationId();
Organization organization = organizationMapper.selectById(orgId);
if (Objects.isNull(organization)) {
throw new BusinessException("未知组织id: " + orgId);
}
if (organization.getType().equals("Enterprise")) {
return 5; // 学校管理员
} else if (organization.getType().equals("Education")) {
return 7; // 企业管理员
} else {
log.error("组织id未知组织类型");
}
// 默认为教育管理员
return 7;
}
/**
* 获取今天开始的秒级时间戳
*/
private long getTodayStartTimestamp() {
LocalDate today = LocalDate.now();
LocalDateTime startOfDay = today.atStartOfDay();
return startOfDay.atZone(ZoneId.systemDefault()).toEpochSecond();
}
/**
* 获取今天结束的秒级时间戳
*/
private long getTodayEndTimestamp() {
LocalDate today = LocalDate.now();
LocalDateTime endOfDay = today.atTime(23, 59, 59);
return endOfDay.atZone(ZoneId.systemDefault()).toEpochSecond();
}
/**
* 格式化时间戳为可读字符串
*/
private String formatTimestamp(Long timestamp) {
if (timestamp == null) return "null";
Instant instant = Instant.ofEpochSecond(timestamp);
LocalDateTime dateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
return dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
/**
* 判断管理员在当前时间是否有活跃订阅(用于新订阅激活判断)
* 规则:只要有一个活跃订阅,就不更新管理员信息
*/
private boolean isAccountInActiveSubscription(Account account, Long excludePlanId) {
long now = System.currentTimeMillis() / 1000;
QueryWrapper<SubscriptionPlan> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("is_deleted", 0)
.eq("admin_acc_id", account.getId())
.ne(excludePlanId != null, "id", excludePlanId)
.le("current_period_start", now) // 已开始
.ge("current_period_end", now); // 未结束
Long count = baseMapper.selectCount(queryWrapper);
return count != null && count > 0;
}
// 定时器清除到期订阅,查看管理员是否还有其他处于订阅中的计划并更新管理员信息
public void expireSubscription() {
// 1. 查询有哪些已过期订阅
List<SubscriptionPlan> recentlyExpiredPlans = findRecentlyExpiredPlans();
// 2. 更新与订阅相关账号的状态
for (SubscriptionPlan subscriptionPlan : recentlyExpiredPlans) {
processExpiringSubscriptionPlan(subscriptionPlan);
}
}
/**
* 查找最近过期的订阅过去24小时内到期的
*/
private List<SubscriptionPlan> findRecentlyExpiredPlans() {
long now = System.currentTimeMillis() / 1000;
long yesterday = now - (24 * 60 * 60); // 24小时前
QueryWrapper<SubscriptionPlan> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("is_deleted", 0)
.between("current_period_end", yesterday, now) // 过去24小时内到期
.orderByAsc("current_period_end");
return baseMapper.selectList(queryWrapper);
}
/**
* 完整的订阅到期处理流程
*/
private void processExpiringSubscriptionPlan(SubscriptionPlan plan) {
long now = System.currentTimeMillis() / 1000;
// 1. 检查订阅是否真的过期
if (now <= plan.getCurrentPeriodEnd()) {
log.info("订阅{}尚未完全过期,跳过处理", plan.getId());
return;
}
// 2. 获取关联的管理员账号
Account adminAccount = accountMapper.selectById(plan.getAdminAccId());
if (adminAccount == null) {
log.error("关联的管理员账号不存在ID: {}", plan.getAdminAccId());
return;
}
// 3. 检查管理员当前激活的订阅是否是这一个
SubscriptionPlan currentActivatedPlan = getCurrentActivatedSubscription(adminAccount);
boolean isCurrentlyActivated = currentActivatedPlan != null &&
currentActivatedPlan.getId().equals(plan.getId());
if (isCurrentlyActivated) {
// 4. 管理员当前正使用这个订阅,需要处理切换或降级
handleSubscriptionExpiration(adminAccount, plan);
} else {
// 5. 管理员当前未使用这个订阅,只处理订阅关系状态
log.info("订阅{}已过期,但管理员{}未激活此订阅", plan.getId(), adminAccount.getId());
}
// 6. 修改订阅状态
plan.setStatus(SubscriptionPlan.SubscriptionStatus.EXPIRED.name());
}
/**
* 获取管理员当前激活的订阅计划account.subscriptionPlanId对应的
*/
private SubscriptionPlan getCurrentActivatedSubscription(Account account) {
if (account.getSubscriptionPlanId() == null) {
return null;
}
SubscriptionPlan plan = baseMapper.selectById(account.getSubscriptionPlanId());
if (plan == null || plan.getIsDeleted() == 1) {
return null;
}
long now = System.currentTimeMillis() / 1000;
if (plan.getCurrentPeriodStart() > now || plan.getCurrentPeriodEnd() < now) {
return null; // 不在有效期内
}
return plan;
}
/**
* 订阅到期处理逻辑(使用 getAccountActiveSubscriptions
*/
private void handleSubscriptionExpiration(Account adminAccount, SubscriptionPlan expiringPlan) {
log.info("开始处理订阅{}到期管理员ID: {}", expiringPlan.getId(), adminAccount.getId());
// 1. 获取管理员的其他活跃订阅(排除当前到期订阅)
List<SubscriptionPlan> otherActiveSubscriptions =
getAccountActiveSubscriptions(adminAccount, expiringPlan.getId());
if (!otherActiveSubscriptions.isEmpty()) {
// 2. 有其他活跃订阅,找到开始最早的进行切换
SubscriptionPlan earliestActivePlan = otherActiveSubscriptions.get(0); // 已按开始时间排序
log.info("管理员{}还有其他{}个活跃订阅,切换到开始最早的订阅{}",
adminAccount.getId(), otherActiveSubscriptions.size(), earliestActivePlan.getId());
// 3. 切换到开始最早的订阅
updateAdminAccount(adminAccount, earliestActivePlan);
} else {
// 5. 没有其他活跃订阅,将管理员降级为游客
log.info("管理员{}没有其他活跃订阅,降级为游客", adminAccount.getId());
// downgradeAccountToVisitor(adminAccount);
// todo toVisitor 需要更新其他字段如subscriptionPlanId, parentId
accountMapper.toVisitor(adminAccount.getId());
log.info("管理员账号{}已降级为游客", adminAccount.getId());
}
// 7. 处理该订阅下的子账号
processChildAccountsForExpiredSubscription(expiringPlan);
}
/**
* 获取管理员当前的活跃订阅(用于订阅到期判断)
* 返回:按开始时间排序的活跃订阅列表
*/
private List<SubscriptionPlan> getAccountActiveSubscriptions(Account account, Long excludePlanId) {
long now = System.currentTimeMillis() / 1000;
QueryWrapper<SubscriptionPlan> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(SubscriptionPlan::getIsDeleted, 0)
.eq(SubscriptionPlan::getAdminAccId, account.getId())
.ne(excludePlanId != null, SubscriptionPlan::getId, excludePlanId)
.le(SubscriptionPlan::getCurrentPeriodStart, now) // 已开始
.ge(SubscriptionPlan::getCurrentPeriodEnd, now) // 未结束
.orderByAsc(SubscriptionPlan::getCurrentPeriodStart); // 按开始时间升序
return baseMapper.selectList(queryWrapper);
}
private void processChildAccountsForExpiredSubscription(SubscriptionPlan expiredPlan) {
log.info("开始处理过期订阅下的子账号订阅ID: {}组织ID: {}",
expiredPlan.getId(), expiredPlan.getOrganizationId());
// 1. 查找该订阅下的所有子账号
List<Account> childAccounts = findChildAccountsBySubscription(expiredPlan);
if (CollectionUtils.isEmpty(childAccounts)) {
log.info("订阅{}下没有需要处理的子账号", expiredPlan.getId());
return;
}
log.info("找到{}个子账号需要处理订阅ID: {}", childAccounts.size(), expiredPlan.getId());
for (Account account : childAccounts) {
accountMapper.toVisitor(account.getId());
log.info("账号{}已降级为游客", account.getId());
}
}
private List<Account> findChildAccountsBySubscription(SubscriptionPlan expiredPlan) {
QueryWrapper<Account> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(Account::getOrganizationId, expiredPlan.getOrganizationId())
.eq(Account::getSubscriptionPlanId, expiredPlan.getId());
return accountMapper.selectList(queryWrapper);
}
/**
* 管理员切换当前管理的订阅
*/
public void switchSubscriptionPlan(Long subscriptionPlanId, Long adminAccId) {
// 1. 权限校验
Long accountId = UserContext.getUserHolder().getId();
SubscriptionPlan subscriptionPlan = baseMapper.selectById(subscriptionPlanId);
if (Objects.isNull(subscriptionPlan)) {
throw new BusinessException("unknown.subscription.plan");
}
if (!accountId.equals(subscriptionPlan.getAdminAccId()) && !accountId.equals(87L)) {
throw new BusinessException("have.no.permission");
}
// 2. 更新管理员积分
if (!accountId.equals(87L)) {
adminAccId = accountId;
}
Account account = accountMapper.selectById(adminAccId);
if (Objects.isNull(account)) {
throw new BusinessException("切换失败,未知管理员用户");
}
updateAdminAccount(account, subscriptionPlan);
}
}

View File

@@ -189,6 +189,7 @@ do.not.have.the.permission.to.delete.this.comment=You do not have the permission
unknow.affiliate=Unknown affiliate id.
unknown.operationType=Unknown operationType.
unknown.mode=unknown mode
unknown.subscription.plan=unknown subscription plan
# 可能会报异常
# Informative:

View File

@@ -185,6 +185,7 @@ do.not.have.the.permission.to.delete.this.comment=您没有权限删除此评论
unknow.affiliate=未知推广者id
unknown.operationType=未知操作类型
unknown.mode=未知模式
unknown.subscription.plan=未知订阅计划
# 可能会报异常
# Informative: