BUGFIX:更新订阅计划时根据业务需要对参数进行判断并在需要时更新管理员信息

This commit is contained in:
2026-01-06 17:29:33 +08:00
parent 3beb27e491
commit e64add14af
7 changed files with 255 additions and 59 deletions

View File

@@ -15,16 +15,16 @@ import com.ai.da.model.vo.PersonalHomepageVO;
import com.ai.da.service.AccountService; import com.ai.da.service.AccountService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -139,21 +139,21 @@ public class AccountController {
@Operation(summary = "aws状态检测") @Operation(summary = "aws状态检测")
@GetMapping("/healthy") @GetMapping("/healthy")
@ResponseStatus(HttpStatus.OK) @ResponseStatus(HttpStatus.OK)
public Response<Map<String,Integer>> checkStatus(){ public Response<Map<String, Integer>> checkStatus() {
Map<String,Integer> returnMap = new HashMap<>(); Map<String, Integer> returnMap = new HashMap<>();
returnMap.put("code",200); returnMap.put("code", 200);
return Response.success(returnMap); return Response.success(returnMap);
} }
@Operation(summary = "查询账号到期时间") @Operation(summary = "查询账号到期时间")
@PostMapping("/getExpiredTime") @PostMapping("/getExpiredTime")
public Response<Long> getExpiredTime(){ public Response<Long> getExpiredTime() {
return Response.success(accountService.getExpiredTime()); return Response.success(accountService.getExpiredTime());
} }
@Operation(summary = "免密登录") @Operation(summary = "免密登录")
@PostMapping("/noLoginRequired") @PostMapping("/noLoginRequired")
public Response<AccountLoginVO> noLoginRequired(@RequestBody NoLoginRequiredDTO noLoginRequiredDTO, HttpServletRequest request){ public Response<AccountLoginVO> noLoginRequired(@RequestBody NoLoginRequiredDTO noLoginRequiredDTO, HttpServletRequest request) {
return Response.success(accountService.noLoginRequired(noLoginRequiredDTO, request)); return Response.success(accountService.noLoginRequired(noLoginRequiredDTO, request));
} }
@@ -191,6 +191,7 @@ public class AccountController {
/** /**
* 参与活动 获取福利 * 参与活动 获取福利
*
* @return * @return
*/ */
/* @Operation(summary = "参与活动 获取福利") /* @Operation(summary = "参与活动 获取福利")
@@ -201,7 +202,7 @@ public class AccountController {
@Operation(summary = "将用户账号过期时间设置为过期当天的235959") @Operation(summary = "将用户账号过期时间设置为过期当天的235959")
@GetMapping("/setUserValidToDayEnd") @GetMapping("/setUserValidToDayEnd")
public Response<List<Long>> setUserValidToDayEnd(){ public Response<List<Long>> setUserValidToDayEnd() {
return Response.success(accountService.setUserValidToDayEnd()); return Response.success(accountService.setUserValidToDayEnd());
} }
@@ -223,19 +224,19 @@ public class AccountController {
@Operation(summary = "获取个人主页信息") @Operation(summary = "获取个人主页信息")
@GetMapping("/personalHomepage") @GetMapping("/personalHomepage")
public Response<PersonalHomepageVO> getPersonalHomepage(@RequestParam("id") Long id){ public Response<PersonalHomepageVO> getPersonalHomepage(@RequestParam("id") Long id) {
return Response.success(accountService.getPersonalHomepage(id)); return Response.success(accountService.getPersonalHomepage(id));
} }
@Operation(summary = "getUsernameModifyTimes") @Operation(summary = "getUsernameModifyTimes")
@GetMapping("/getNicknameModifyTimes") @GetMapping("/getNicknameModifyTimes")
public Response<Long> getNicknameModifyTimes(){ public Response<Long> getNicknameModifyTimes() {
return Response.success(accountService.getNicknameModifyTimes()); return Response.success(accountService.getNicknameModifyTimes());
} }
@Operation(summary = "editUserName") @Operation(summary = "editUserName")
@GetMapping("/editUserName") @GetMapping("/editUserName")
public Response<String> editUserName(@RequestParam("newUserName") String newUserName){ public Response<String> editUserName(@RequestParam("newUserName") String newUserName) {
accountService.editUserName(newUserName); accountService.editUserName(newUserName);
return Response.success("success"); return Response.success("success");
} }

View File

@@ -82,7 +82,7 @@ public class SubscriptionPlanController {
@Operation(summary = "activeSubscriptionPlan") @Operation(summary = "activeSubscriptionPlan")
@GetMapping("/activeSubscriptionPlan") @GetMapping("/activeSubscriptionPlan")
public Response<String> activeSubscriptionPlan() { public Response<String> activeSubscriptionPlan() {
subscriptionPlanService.activeSubscriptionPlan(); subscriptionPlanService.activeSubscriptionPlan(null);
return Response.success(); return Response.success();
} }

View File

@@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
import java.io.Serializable; import java.io.Serializable;
@@ -76,13 +77,13 @@ public class Account implements Serializable {
/** /**
* 创建时间 * 创建时间
*/ */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createDate; private Date createDate;
/** /**
* 更新时间 * 更新时间
*/ */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date updateDate; private Date updateDate;
private Integer isTrial; private Integer isTrial;
@@ -142,4 +143,26 @@ public class Account implements Serializable {
private String givenName; private String givenName;
private Long subscriptionPlanId; private Long subscriptionPlanId;
// 在类内部定义的枚举
@Getter
public enum SystemRole {
VISITOR("游客", 0),
YEARLY("年付用户", 1),
MONTHLY("月付用户", 2),
TRIAL("试用用户", 3),
EVENT_USER("参加活动获取30天有效期和6000个积分的用户", 4),
ENTERPRISE_ADMIN("企业管理员账号", 5),
ENTERPRISE_SUB("企业子账号", 6),
EDUCATION_ADMIN("学校管理员", 7),
EDUCATION_SUB("学校子账号", 8);
private final String desc;
private final int code;
SystemRole(String desc, int code) {
this.desc = desc;
this.code = code;
}
}
} }

View File

@@ -26,7 +26,7 @@ public interface SubscriptionPlanService extends IService<SubscriptionPlan> {
void switchSubAccSubscriptionPlan(Long subscriptionPlanId, Long subAccId); void switchSubAccSubscriptionPlan(Long subscriptionPlanId, Long subAccId);
void activeSubscriptionPlan(); void activeSubscriptionPlan(Long planId);
void expireSubscription(); void expireSubscription();
} }

View File

@@ -21,6 +21,7 @@ import com.ai.da.service.SubscriptionPlanService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -28,6 +29,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import java.math.BigDecimal; import java.math.BigDecimal;
@@ -37,11 +39,9 @@ import java.time.LocalDateTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException; import java.time.format.DateTimeParseException;
import java.util.Arrays; import java.util.*;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import static com.ai.da.mapper.primary.entity.Account.SystemRole.EDUCATION_SUB;
import static com.ai.da.mapper.primary.entity.SubscriptionPlan.SubscriptionStatus.ACTIVE; import static com.ai.da.mapper.primary.entity.SubscriptionPlan.SubscriptionStatus.ACTIVE;
import static com.ai.da.mapper.primary.entity.SubscriptionPlan.SubscriptionStatus.PENDING; import static com.ai.da.mapper.primary.entity.SubscriptionPlan.SubscriptionStatus.PENDING;
@@ -80,7 +80,7 @@ public class SubscriptionPlanServiceImpl extends ServiceImpl<SubscriptionPlanMap
baseMapper.insert(subscriptionPlan); baseMapper.insert(subscriptionPlan);
if (subscriptionPlan.getStatus().equals(SubscriptionPlan.SubscriptionStatus.ACTIVE.name())) { if (subscriptionPlan.getStatus().equals(SubscriptionPlan.SubscriptionStatus.ACTIVE.name())) {
// 执行一次激活扫描器 // 执行一次激活扫描器
activeSubscriptionPlan(); activeSubscriptionPlan(subscriptionPlan.getId());
} }
} }
@@ -107,49 +107,203 @@ public class SubscriptionPlanServiceImpl extends ServiceImpl<SubscriptionPlanMap
} }
// 更新 到期时间、积分总量、已使用积分量 // 更新 到期时间、积分总量、已使用积分量
@Transactional(rollbackFor = Exception.class)
@Override @Override
public void updatePlan(UpdateSubscriptionPlanDTO updateDTO) { public void updatePlan(UpdateSubscriptionPlanDTO dto) {
if (Objects.isNull(updateDTO.getId())) { if (dto.getId() == null) {
throw new BusinessException("id.cannot.be.empty"); throw new BusinessException("id.cannot.be.empty");
} }
SubscriptionPlan subscriptionPlan = baseMapper.selectById(updateDTO.getId()); SubscriptionPlan plan = baseMapper.selectById(dto.getId());
if (Objects.isNull(subscriptionPlan)) { if (plan == null) {
throw new BusinessException("unknown.subscription.plan"); throw new BusinessException("unknown.subscription.plan");
} }
if (Objects.nonNull(updateDTO.getCurrentPeriodStart()) && !updateDTO.getCurrentPeriodStart().equals(subscriptionPlan.getCurrentPeriodStart())) { boolean activateToday = false;
subscriptionPlan.setCurrentPeriodStart(updateDTO.getCurrentPeriodStart());
activateToday = handlePeriodStart(dto, plan);
handlePeriodEnd(dto, plan);
handleAccountNum(dto, plan);
handleCreditLimit(dto, plan);
handleBasicInfo(dto, plan);
plan.setUpdateTime(LocalDateTime.now());
updateById(plan);
postUpdateProcess(plan, activateToday);
} }
if (Objects.nonNull(updateDTO.getCurrentPeriodEnd()) && !updateDTO.getCurrentPeriodEnd().equals(subscriptionPlan.getCurrentPeriodEnd())) { // ===================== 字段处理 =====================
subscriptionPlan.setCurrentPeriodEnd(updateDTO.getCurrentPeriodEnd());
/**
* 处理开始时间,返回是否需要当天激活
*/
private boolean handlePeriodStart(UpdateSubscriptionPlanDTO dto, SubscriptionPlan plan) {
Long newStart = dto.getCurrentPeriodStart();
if (newStart == null || newStart.equals(plan.getCurrentPeriodStart())) {
return false;
} }
if (Objects.nonNull(updateDTO.getAccountNum()) && !updateDTO.getAccountNum().equals(subscriptionPlan.getAccountNum())) { if (ACTIVE.name().equals(plan.getStatus())) {
subscriptionPlan.setAccountNum(updateDTO.getAccountNum()); throw new BusinessException(
"only.subscription.plans.with.a.PENDING.status.can.have.their.start.time.modified"
);
} }
if (Objects.nonNull(updateDTO.getCreditLimit()) && !updateDTO.getCreditLimit().equals(subscriptionPlan.getCreditLimit())) { plan.setCurrentPeriodStart(newStart);
subscriptionPlan.setCreditLimit(updateDTO.getCreditLimit()); return isToday(newStart);
} }
if (Objects.nonNull(updateDTO.getAdminAccId()) && !updateDTO.getAdminAccId().equals(subscriptionPlan.getAdminAccId())) { /**
subscriptionPlan.setAdminAccId(updateDTO.getAdminAccId()); * 处理结束时间(只能延长)
*/
private void handlePeriodEnd(UpdateSubscriptionPlanDTO dto, SubscriptionPlan plan) {
Long newEnd = dto.getCurrentPeriodEnd();
if (newEnd == null || newEnd.equals(plan.getCurrentPeriodEnd())) {
return;
} }
if (StringUtils.isNotBlank(updateDTO.getName()) && !updateDTO.getName().equals(subscriptionPlan.getName())) { if (newEnd < plan.getCurrentPeriodEnd()) {
subscriptionPlan.setName(updateDTO.getName()); throw new BusinessException(
"the.subscription.end.date.can.be.extended.only.not.reduced"
);
} }
subscriptionPlan.setUpdateTime(LocalDateTime.now()); plan.setCurrentPeriodEnd(newEnd);
updateById(subscriptionPlan);
} }
public void updatePlan() { /**
* 处理账号数量
*/
private void handleAccountNum(UpdateSubscriptionPlanDTO dto, SubscriptionPlan plan) {
Integer newAccountNum = dto.getAccountNum();
if (newAccountNum == null || newAccountNum.equals(plan.getAccountNum())) {
return;
}
if (newAccountNum < plan.getAccountNum()) {
long usedSubAccounts = countExistingSubAccounts(plan.getId());
if (newAccountNum < usedSubAccounts + 1) {
throw new BusinessException(
"total.sub-account.quota.cannot.be.lower.than.existing.sub-accounts"
);
}
}
plan.setAccountNum(newAccountNum);
}
/**
* 处理积分上限
*/
private void handleCreditLimit(UpdateSubscriptionPlanDTO dto, SubscriptionPlan plan) {
BigDecimal newLimit = dto.getCreditLimit();
if (newLimit == null || newLimit.equals(plan.getCreditLimit())) {
return;
}
if (newLimit.compareTo(plan.getCreditUsage()) < 0) {
throw new BusinessException(
"the.credit.limit.set.cannot.be.lower.than.the.amount.of.credits.already.used"
);
}
plan.setCreditLimit(newLimit);
}
/**
* 基础字段
*/
private void handleBasicInfo(UpdateSubscriptionPlanDTO dto, SubscriptionPlan plan) {
if (dto.getAdminAccId() != null
&& !dto.getAdminAccId().equals(plan.getAdminAccId())) {
plan.setAdminAccId(dto.getAdminAccId());
}
if (StringUtils.isNotBlank(dto.getName())
&& !dto.getName().equals(plan.getName())) {
plan.setName(dto.getName());
}
}
// ===================== 更新后处理 =====================
private void postUpdateProcess(SubscriptionPlan plan, boolean activateToday) {
if (ACTIVE.name().equals(plan.getStatus())) {
syncAdminAndSubAccounts(plan);
return;
}
if (activateToday) {
activeSubscriptionPlan(plan.getId());
}
}
// ===================== 账号同步 =====================
private void syncAdminAndSubAccounts(SubscriptionPlan plan) {
Account admin = findActiveAdmin(plan);
if (admin != null) {
syncAdminAccount(admin, plan);
}
syncSubAccounts(plan);
}
private Account findActiveAdmin(SubscriptionPlan plan) {
return accountMapper.selectOne(
new QueryWrapper<Account>().lambda()
.eq(Account::getId, plan.getAdminAccId())
.eq(Account::getSubscriptionPlanId, plan.getId())
);
}
private void syncAdminAccount(Account admin, SubscriptionPlan plan) {
long planEndMillis = toMillis(plan.getCurrentPeriodEnd());
if (!Objects.equals(admin.getValidEndTime(), planEndMillis)) {
admin.setValidEndTime(planEndMillis);
}
if (admin.getCreditsUsageLimit().compareTo(plan.getCreditLimit()) != 0) {
// 这里计算修改前后的差值,上限增长,则差为正,上限下降,则差为负;
BigDecimal delta = plan.getCreditLimit()
.subtract(admin.getCreditsUsageLimit());
// 因为管理员的积分中可能包含自己购买的积分所以这里直接将差值添加到管理员的credit中
admin.setCredits(admin.getCredits().add(delta));
admin.setCreditsUsageLimit(plan.getCreditLimit());
}
accountMapper.updateById(admin);
}
private void syncSubAccounts(SubscriptionPlan plan) {
accountMapper.update(
null,
new UpdateWrapper<Account>().lambda()
.set(Account::getValidEndTime, toMillis(plan.getCurrentPeriodEnd()))
.eq(Account::getSubscriptionPlanId, plan.getId())
.eq(Account::getSystemUser, EDUCATION_SUB.getCode())
);
}
// ===================== 辅助方法 =====================
private long countExistingSubAccounts(Long planId) {
return accountMapper.selectCount(
new QueryWrapper<Account>().lambda()
.eq(Account::getSubscriptionPlanId, planId)
.eq(Account::getSystemUser, EDUCATION_SUB.getCode())
);
}
private boolean isToday(Long timestampSeconds) {
return timestampSeconds >= getTodayStartTimestamp()
&& timestampSeconds < getTodayEndTimestamp();
}
private long toMillis(Long seconds) {
return seconds * 1000;
} }
@Override @Override
@@ -407,7 +561,7 @@ public class SubscriptionPlanServiceImpl extends ServiceImpl<SubscriptionPlanMap
} }
// 检查是否在有效期内 // 检查是否在有效期内
if (plan.getCurrentPeriodEnd() != null && isExpired(plan.getCurrentPeriodEnd())) { if (plan.getCurrentPeriodEnd() != null && !isExpired(plan.getCurrentPeriodEnd())) {
throw new BusinessException("valid.subscription.period"); throw new BusinessException("valid.subscription.period");
} }
} }
@@ -427,11 +581,19 @@ public class SubscriptionPlanServiceImpl extends ServiceImpl<SubscriptionPlanMap
return currentPeriodEnd < currentTimestamp; return currentPeriodEnd < currentTimestamp;
} }
public void activeSubscriptionPlan() { public void activeSubscriptionPlan(Long planId) {
log.info("开始执行订阅计划生效检查..."); log.info("开始执行订阅计划生效检查...");
// 支持按id激活
List<SubscriptionPlan> todayActivePlans = new ArrayList<>();
if (Objects.nonNull(planId)) {
SubscriptionPlan subscriptionPlan = baseMapper.selectById(planId);
if (Objects.nonNull(subscriptionPlan)){
todayActivePlans.add(subscriptionPlan);
}
} else {
// 1. 扫描所有的订阅计划的开始时间currentPeriodStart找出今天开始生效的计划 // 1. 扫描所有的订阅计划的开始时间currentPeriodStart找出今天开始生效的计划
List<SubscriptionPlan> todayActivePlans = findTodayActivePlans(); todayActivePlans = findTodayActivePlans();
if (CollectionUtils.isEmpty(todayActivePlans)) { if (CollectionUtils.isEmpty(todayActivePlans)) {
log.info("今日没有需要生效的订阅计划"); log.info("今日没有需要生效的订阅计划");
@@ -439,6 +601,8 @@ public class SubscriptionPlanServiceImpl extends ServiceImpl<SubscriptionPlanMap
} }
log.info("发现{}个今日生效的订阅计划", todayActivePlans.size()); log.info("发现{}个今日生效的订阅计划", todayActivePlans.size());
}
// 2. 处理每个今天开始生效的订阅计划 // 2. 处理每个今天开始生效的订阅计划
for (SubscriptionPlan plan : todayActivePlans) { for (SubscriptionPlan plan : todayActivePlans) {

View File

@@ -209,6 +209,10 @@ end.time.must.be.later.than.the.start.time=The subscription end time must be lat
please.specify.the.organizationId=Please specify the organizationId. please.specify.the.organizationId=Please specify the organizationId.
switch.failed.sub-account.not.under.your.active.subscription=Switch failed. Sub-account not under your active subscription. switch.failed.sub-account.not.under.your.active.subscription=Switch failed. Sub-account not under your active subscription.
Sub-accounts.cannot.be.admins=Sub-accounts in a subscription cannot be designated as admins. Sub-accounts.cannot.be.admins=Sub-accounts in a subscription cannot be designated as admins.
only.subscription.plans.with.a.PENDING.status.can.have.their.start.time.modified=Only subscription plans with a PENDING status can have their start time modified.
the.subscription.end.date.can.be.extended.only.not.reduced=The subscription end date can be extended only, not reduced.
total.sub-account.quota.cannot.be.lower.than.existing.sub-accounts=Total sub-account quota cannot be lower than existing sub-accounts.
the.credit.limit.set.cannot.be.lower.than.the.amount.of.credits.already.used=The credit limit set cannot be lower than the amount of credits already used.
# 可能会报异常 # 可能会报异常
# Informative: # Informative:

View File

@@ -205,6 +205,10 @@ end.time.must.be.later.than.the.start.time=订阅结束时间必须晚于开始
please.specify.the.organizationId=请指定organizationId please.specify.the.organizationId=请指定organizationId
switch.failed.sub-account.not.under.your.active.subscription=切换失败,该子账号不属于您当前管理的订阅计划 switch.failed.sub-account.not.under.your.active.subscription=切换失败,该子账号不属于您当前管理的订阅计划
Sub-accounts.cannot.be.admins=在订阅中的子账号不能被指定为管理员 Sub-accounts.cannot.be.admins=在订阅中的子账号不能被指定为管理员
only.subscription.plans.with.a.PENDING.status.can.have.their.start.time.modified=只有PENDING状态的订阅计划可以修改订阅开始时间
the.subscription.end.date.can.be.extended.only.not.reduced=订阅的到期时间不能缩短,只能延长
total.sub-account.quota.cannot.be.lower.than.existing.sub-accounts=设置的子账号总数量不能低于现存已添加的子账号数量
the.credit.limit.set.cannot.be.lower.than.the.amount.of.credits.already.used=设置的积分上限不能低于已使用的积分量
# 可能会报异常 # 可能会报异常
# Informative: # Informative: