From 7f094265da6a54186e8c0e55e2bc40d23009af73 Mon Sep 17 00:00:00 2001 From: xupei Date: Thu, 11 Dec 2025 09:44:25 +0800 Subject: [PATCH] =?UTF-8?q?TASK:=E6=96=B0=E5=A2=9E=E8=AE=A2=E9=98=85?= =?UTF-8?q?=E8=AE=A1=E5=88=92=E6=A6=82=E5=BF=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SubscriptionPlanController.java | 62 ++ .../primary/SubscriptionPlanMapper.java | 7 + .../ai/da/mapper/primary/entity/Account.java | 2 + .../primary/entity/SubscriptionPlan.java | 87 +++ .../ai/da/model/dto/SubscriptionPlanDTO.java | 39 + .../model/dto/SubscriptionPlanPageQuery.java | 21 + .../model/dto/UpdateSubscriptionPlanDTO.java | 33 + .../ai/da/model/vo/SubscriptionPlanVO.java | 37 + .../java/com/ai/da/python/PythonService.java | 2 +- .../da/service/SubscriptionPlanService.java | 22 + .../da/service/impl/AccountServiceImpl.java | 52 +- .../impl/SubscriptionPlanServiceImpl.java | 711 ++++++++++++++++++ src/main/resources/messages_en.properties | 1 + src/main/resources/messages_zh.properties | 1 + 14 files changed, 1062 insertions(+), 15 deletions(-) create mode 100644 src/main/java/com/ai/da/controller/SubscriptionPlanController.java create mode 100644 src/main/java/com/ai/da/mapper/primary/SubscriptionPlanMapper.java create mode 100644 src/main/java/com/ai/da/mapper/primary/entity/SubscriptionPlan.java create mode 100644 src/main/java/com/ai/da/model/dto/SubscriptionPlanDTO.java create mode 100644 src/main/java/com/ai/da/model/dto/SubscriptionPlanPageQuery.java create mode 100644 src/main/java/com/ai/da/model/dto/UpdateSubscriptionPlanDTO.java create mode 100644 src/main/java/com/ai/da/model/vo/SubscriptionPlanVO.java create mode 100644 src/main/java/com/ai/da/service/SubscriptionPlanService.java create mode 100644 src/main/java/com/ai/da/service/impl/SubscriptionPlanServiceImpl.java diff --git a/src/main/java/com/ai/da/controller/SubscriptionPlanController.java b/src/main/java/com/ai/da/controller/SubscriptionPlanController.java new file mode 100644 index 00000000..6619de92 --- /dev/null +++ b/src/main/java/com/ai/da/controller/SubscriptionPlanController.java @@ -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 createPlan(@Valid @RequestBody SubscriptionPlanDTO subscriptionPlanDTO) { + subscriptionPlanService.createPlan(subscriptionPlanDTO); + return Response.success(); + } + + @ApiOperation("更新订阅计划") + @PostMapping("/updatePlan") + public Response updatePlan(@Valid @RequestBody UpdateSubscriptionPlanDTO updateDTO) { + subscriptionPlanService.updatePlan(updateDTO); + return Response.success(); + } + + @ApiOperation("搜索订阅计划") + @PostMapping("/searchByPage") + public Response> searchByPage(@Valid @RequestBody SubscriptionPlanPageQuery subscriptionPlanPageQuery) { + IPage subscriptionPlanVOIPage = subscriptionPlanService.searchByPage(subscriptionPlanPageQuery); + return Response.success(subscriptionPlanVOIPage); + } + + @ApiOperation("删除订阅计划") + @GetMapping("/deletePlan") + public Response deletePlan(@RequestParam Long id) { + subscriptionPlanService.deletePlan(id); + return Response.success(); + } + + @ApiOperation("管理员切换订阅计划") + @GetMapping("/switchSubscriptionPlan") + public Response switchSubscriptionPlan(@RequestParam Long subscriptionPlanId, @RequestParam Long adminAccId) { + subscriptionPlanService.switchSubscriptionPlan(subscriptionPlanId, adminAccId); + return Response.success(); + } + + +} diff --git a/src/main/java/com/ai/da/mapper/primary/SubscriptionPlanMapper.java b/src/main/java/com/ai/da/mapper/primary/SubscriptionPlanMapper.java new file mode 100644 index 00000000..05c490e8 --- /dev/null +++ b/src/main/java/com/ai/da/mapper/primary/SubscriptionPlanMapper.java @@ -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 { +} diff --git a/src/main/java/com/ai/da/mapper/primary/entity/Account.java b/src/main/java/com/ai/da/mapper/primary/entity/Account.java index 7549d175..5a39c197 100644 --- a/src/main/java/com/ai/da/mapper/primary/entity/Account.java +++ b/src/main/java/com/ai/da/mapper/primary/entity/Account.java @@ -140,4 +140,6 @@ public class Account implements Serializable { @ApiModelProperty("givenName") private String givenName; + + private Long subscriptionPlanId; } diff --git a/src/main/java/com/ai/da/mapper/primary/entity/SubscriptionPlan.java b/src/main/java/com/ai/da/mapper/primary/entity/SubscriptionPlan.java new file mode 100644 index 00000000..bb632a98 --- /dev/null +++ b/src/main/java/com/ai/da/mapper/primary/entity/SubscriptionPlan.java @@ -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; + } + } + +} diff --git a/src/main/java/com/ai/da/model/dto/SubscriptionPlanDTO.java b/src/main/java/com/ai/da/model/dto/SubscriptionPlanDTO.java new file mode 100644 index 00000000..ec7e5132 --- /dev/null +++ b/src/main/java/com/ai/da/model/dto/SubscriptionPlanDTO.java @@ -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; + +} diff --git a/src/main/java/com/ai/da/model/dto/SubscriptionPlanPageQuery.java b/src/main/java/com/ai/da/model/dto/SubscriptionPlanPageQuery.java new file mode 100644 index 00000000..60910b27 --- /dev/null +++ b/src/main/java/com/ai/da/model/dto/SubscriptionPlanPageQuery.java @@ -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 status; +} diff --git a/src/main/java/com/ai/da/model/dto/UpdateSubscriptionPlanDTO.java b/src/main/java/com/ai/da/model/dto/UpdateSubscriptionPlanDTO.java new file mode 100644 index 00000000..f42d39c1 --- /dev/null +++ b/src/main/java/com/ai/da/model/dto/UpdateSubscriptionPlanDTO.java @@ -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; + +} diff --git a/src/main/java/com/ai/da/model/vo/SubscriptionPlanVO.java b/src/main/java/com/ai/da/model/vo/SubscriptionPlanVO.java new file mode 100644 index 00000000..9a9897f2 --- /dev/null +++ b/src/main/java/com/ai/da/model/vo/SubscriptionPlanVO.java @@ -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; + + +} diff --git a/src/main/java/com/ai/da/python/PythonService.java b/src/main/java/com/ai/da/python/PythonService.java index a97875f5..04ece223 100644 --- a/src/main/java/com/ai/da/python/PythonService.java +++ b/src/main/java/com/ai/da/python/PythonService.java @@ -2237,7 +2237,7 @@ public class PythonService { private List resolve(List list) { List integerList = Lists.newArrayList(); list.forEach(l -> { - integerList.add(new Integer(l.intValue())); + integerList.add(l.intValue()); }); return integerList; } diff --git a/src/main/java/com/ai/da/service/SubscriptionPlanService.java b/src/main/java/com/ai/da/service/SubscriptionPlanService.java new file mode 100644 index 00000000..e059b6a6 --- /dev/null +++ b/src/main/java/com/ai/da/service/SubscriptionPlanService.java @@ -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 { + + void createPlan(SubscriptionPlanDTO subscriptionPlanDTO); + + void updatePlan(UpdateSubscriptionPlanDTO updateDTO); + + IPage searchByPage(SubscriptionPlanPageQuery subscriptionPlanPageQuery); + + void deletePlan(Long id); + + void switchSubscriptionPlan(Long subscriptionPlanId, Long adminAccId); +} diff --git a/src/main/java/com/ai/da/service/impl/AccountServiceImpl.java b/src/main/java/com/ai/da/service/impl/AccountServiceImpl.java index 7d8335a9..503fbdc7 100644 --- a/src/main/java/com/ai/da/service/impl/AccountServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/AccountServiceImpl.java @@ -135,6 +135,9 @@ public class AccountServiceImpl extends ServiceImpl 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 impl @Transactional(rollbackFor = Exception.class) public Boolean createSubAccount(AddSubAccountDTO addSubAccountDTO, Account adminAcc, int subUserRole) { QueryWrapper qw = new QueryWrapper<>(); - qw.lambda().eq(Account::getOrganizationName, adminAcc.getOrganizationName()); + qw.lambda().eq(Account::getOrganizationId, adminAcc.getOrganizationId()); List accounts = accountMapper.selectList(qw); // 校验子账号总数是否达上限 @@ -2460,12 +2463,12 @@ public class AccountServiceImpl extends ServiceImpl 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 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 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 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 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 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 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 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 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 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 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 subAccountList(SubAccountPageDTO subAccountPageDTO) { AuthPrincipalVo authPrincipalVo = UserContext.getUserHolder(); Account account = accountMapper.selectById(authPrincipalVo); QueryWrapper 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 impl int subUserRole = getSubUserRole(adminAcc.getSystemUser()); List accounts = accountMapper.selectList(new QueryWrapper() - .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 impl // 只有当前子账号数量为0时允许批量上传 QueryWrapper qw = new QueryWrapper<>(); qw.lambda().eq(Account::getSystemUser, 8) - .eq(Account::getOrganizationName, parent.getOrganizationName()); + .eq(Account::getOrganizationId, parent.getOrganizationId()); List accounts = accountMapper.selectList(qw); if (!accounts.isEmpty()) { throw new BusinessException("permit.bulk.creation", ResultEnum.PROMPT.getCode()); diff --git a/src/main/java/com/ai/da/service/impl/SubscriptionPlanServiceImpl.java b/src/main/java/com/ai/da/service/impl/SubscriptionPlanServiceImpl.java new file mode 100644 index 00000000..012151ca --- /dev/null +++ b/src/main/java/com/ai/da/service/impl/SubscriptionPlanServiceImpl.java @@ -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 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 searchByPage(SubscriptionPlanPageQuery subscriptionPlanPageQuery) { + // 1. 参数校验 + validatePageQuery(subscriptionPlanPageQuery); + + // 2. 构建查询条件 + LambdaQueryWrapper queryWrapper = buildQueryWrapper(subscriptionPlanPageQuery); + + // 3. 执行分页查询 + Page page = new Page<>(subscriptionPlanPageQuery.getPage(), subscriptionPlanPageQuery.getSize()); + IPage resultPage = baseMapper.selectPage(page, queryWrapper); + + // 4. 转换为VO并返回 + return resultPage.convert((Function) 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 buildQueryWrapper(SubscriptionPlanPageQuery query) { + LambdaQueryWrapper 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 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 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 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 findTodayActivePlans() { + // 获取今天的开始和结束时间戳(秒级) + long todayStart = getTodayStartTimestamp(); + long todayEnd = getTodayEndTimestamp(); + + log.debug("扫描时间范围: {} - {} (今日)", formatTimestamp(todayStart), formatTimestamp(todayEnd)); + + // 查询今天开始生效的订阅计划 + QueryWrapper 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 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 recentlyExpiredPlans = findRecentlyExpiredPlans(); + // 2. 更新与订阅相关账号的状态 + for (SubscriptionPlan subscriptionPlan : recentlyExpiredPlans) { + processExpiringSubscriptionPlan(subscriptionPlan); + } + } + + /** + * 查找最近过期的订阅(过去24小时内到期的) + */ + private List findRecentlyExpiredPlans() { + long now = System.currentTimeMillis() / 1000; + long yesterday = now - (24 * 60 * 60); // 24小时前 + + QueryWrapper 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 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 getAccountActiveSubscriptions(Account account, Long excludePlanId) { + long now = System.currentTimeMillis() / 1000; + + QueryWrapper 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 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 findChildAccountsBySubscription(SubscriptionPlan expiredPlan) { + QueryWrapper 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); + } + +} diff --git a/src/main/resources/messages_en.properties b/src/main/resources/messages_en.properties index 11bce256..68e47a9c 100644 --- a/src/main/resources/messages_en.properties +++ b/src/main/resources/messages_en.properties @@ -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: diff --git a/src/main/resources/messages_zh.properties b/src/main/resources/messages_zh.properties index 051a4194..32bb3c1e 100644 --- a/src/main/resources/messages_zh.properties +++ b/src/main/resources/messages_zh.properties @@ -185,6 +185,7 @@ do.not.have.the.permission.to.delete.this.comment=您没有权限删除此评论 unknow.affiliate=未知推广者id unknown.operationType=未知操作类型 unknown.mode=未知模式 +unknown.subscription.plan=未知订阅计划 # 可能会报异常 # Informative: