Merge branch 'dev/dev_xp' into dev/3.1_release_merge

# Conflicts:
#	src/main/java/com/ai/da/controller/AccountController.java
This commit is contained in:
2025-12-11 16:12:41 +08:00
20 changed files with 1221 additions and 26 deletions

View File

@@ -88,7 +88,7 @@ public class SendRequestUtil {
}
}
public String sendFluxPost(String url, String requestBodyStr){
/*public String sendFluxPost(String url, String requestBodyStr){
int status;
String body;
try (HttpResponse execute = HttpRequest.post(url)
@@ -103,9 +103,63 @@ public class SendRequestUtil {
if (status == 200) {
return body;
}
if (status == 402 || status == 403) {
SendEmailUtil.commonExceptionReminder("Flux账户积分不足flux生成任务",
new String[]{"xupei3360@163.com, fangjianliao@aidlab.hk, investigation@aidlab.hk"});
}
}
log.warn("请求失败,状态码为 {}", status);
return null;
}*/
public String sendFluxPost(String url, String requestBodyStr) {
// 尝试两个API key
String[] apiKeys = {"d447a0ac-2291-4f1c-9a36-f7614c385989",
"84e8f5d5-b0b3-49aa-b244-ab7ba27e7ae7"};
boolean[] notified = {false, false}; // 记录是否已发送过不足提醒
for (int i = 0; i < apiKeys.length; i++) {
int status;
String body;
try (HttpResponse execute = HttpRequest.post(url)
.header(Header.CONTENT_TYPE, "application/json")
.header("x-key", apiKeys[i])
.body(requestBodyStr)
.timeout(180000)
.execute()) {
status = execute.getStatus();
body = execute.body();
if (status == 200) {
return body;
}
// 余额不足处理
if (status == 402 || status == 403) {
if (!notified[i]) {
SendEmailUtil.commonExceptionReminder(
"Flux账户积分不足flux生成任务失败",
new String[]{"xupei3360@163.com, fangjianliao@aidlab.hk, investigation@aidlab.hk"}
);
notified[i] = true;
}
continue; // 尝试下一个key
}
// 其他错误直接返回null
log.warn("请求失败,状态码为:{}使用key{}", status, apiKeys[i]);
return null;
} catch (Exception e) {
log.error("请求异常使用key{}", apiKeys[i], e);
if (i == apiKeys.length - 1) return null; // 最后一个key也失败则返回null
}
}
log.warn("所有API key均余额不足");
return null;
}
public String sendPost(String url, String requestBodyStr){

View File

@@ -289,7 +289,7 @@ public class AccountController {
@PostMapping("organizationNameSearch")
@Operation(summary = "组织名模糊查询")
public Response<Set<String>> organizationNameSearch(@RequestParam("type") String type, @RequestParam("name") String name) {
public Response<Set<String>> organizationNameSearch(@RequestParam("type") String type, @RequestParam(value = "name", required = false) String name) {
return Response.success(accountService.organizationNameSearch(type, name));
}

View File

@@ -70,7 +70,7 @@ public class StripeController {
Long size = redisUtil.getSize(key_1);
// 给我发送邮件
if (webhookReminderFlag.equals("1") && size == 3){
SendEmailUtil.commonExceptionReminder("Stripe Webhook 回调", new String[]{"xupei3360@163.com"});
SendEmailUtil.commonExceptionReminder("Stripe Webhook 回调处理出现异常", new String[]{"xupei3360@163.com"});
}
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}

View File

@@ -0,0 +1,76 @@
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();
}
@ApiOperation("activeSubscriptionPlan")
@GetMapping("/activeSubscriptionPlan")
public Response<String> activeSubscriptionPlan() {
subscriptionPlanService.activeSubscriptionPlan();
return Response.success();
}
@ApiOperation("expireSubscription")
@GetMapping("/expireSubscription")
public Response<String> expireSubscription() {
subscriptionPlanService.expireSubscription();
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,26 @@
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);
void activeSubscriptionPlan();
void expireSubscription();
}

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());
@@ -3569,7 +3593,7 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
@Override
public Set<String> organizationNameSearch(String type, String name) {
QueryWrapper<Account> qw = new QueryWrapper<>();
/*QueryWrapper<Account> qw = new QueryWrapper<>();
qw.lambda().ne(Account::getOrganizationName, "").isNotNull(Account::getOrganizationName);
if (!StringUtil.isNullOrEmpty(name)) {
qw.lambda().like(Account::getOrganizationName, name);
@@ -3591,6 +3615,20 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
if (CollectionUtil.isNotEmpty(accountList)) {
return accountList.stream().map(Account::getOrganizationName).collect(Collectors.toSet());
}
return new HashSet<>();*/
QueryWrapper<Organization> queryWrapper = new QueryWrapper<>();
if (StringUtils.isNotBlank(type)){
type = type.equals("Enterprise") ? "Enterprise" : "Education";
queryWrapper.lambda().eq(Organization::getType, type);
}
if (StringUtils.isNotBlank(name)) {
queryWrapper.lambda().like(Organization::getName, name);
}
List<Organization> organizations = organizationMapper.selectList(queryWrapper);
if (CollectionUtil.isNotEmpty(organizations)) {
return organizations.stream().map(Organization::getName).collect(Collectors.toSet());
}
return new HashSet<>();
}

View File

@@ -1101,9 +1101,9 @@ public class StripeServiceImpl implements StripeService {
setSubscriptionParams(paymentInfo, subscriptionInfo, orderByOrderNo, emailParamsDTO, language);
log.info("SEND EMAIL: type={}, params={}, language={}, receiver={}", type, JSONObject.toJSON(emailParamsDTO), language, account.getUserEmail());
// boolean b = SendEmailUtil.subscriptionEmailReminder(type, emailParamsDTO, language, account.getUserEmail());
boolean b = SendEmailUtil.subscriptionEmailReminder(type, emailParamsDTO, language, account.getUserEmail());
// boolean b = emailService.subscriptionEmailReminder(type, emailParamsDTO, language, account.getUserEmail());
// if (!b) return false;
if (!b) return false;
// 邮件通知成功后,更新标志
if (!type.startsWith("reminder") && !type.equals("cancel")){

View File

@@ -0,0 +1,742 @@
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.response.ResultEnum;
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("page.num.limit");
}
if (query.getSize() <= 0 || query.getSize() > 100) {
throw new BusinessException("page.size.limit");
}
// 时间格式校验
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("the.start.time.cannot.be.later.than.the.end.time");
}
} catch (DateTimeParseException e) {
throw new BusinessException("invalid.time.format");
}
}
if (!CollectionUtils.isEmpty(query.getStatus())) {
for (String status : query.getStatus()) {
try {
SubscriptionPlan.SubscriptionStatus.valueOf(status.toUpperCase());
} catch (IllegalArgumentException e) {
log.error("未知订阅状态:{}", status);
throw new BusinessException("unknown.subscription.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.cannot.be.empty.and.must.be.greater.than.0");
}
// 2. 检查数据是否存在
SubscriptionPlan plan = baseMapper.selectById(id);
if (plan == null) {
throw new BusinessException("subscription.plan.does.not.exist");
}
// 3. 检查是否已被删除(防止重复删除) 一般情况下走不到逻辑删除的数据通过mybatis-plus查不到
if (plan.getIsDeleted() == 1) {
throw new BusinessException("subscription.plan.has.been.deleted");
}
// 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("deletion.failed.please.try.again.later");
}
// 6. 记录操作日志
log.info("管理员{} 删除订阅计划 {}", adminAccountId, plan);
}
/**
* 检查业务约束条件
*/
private void checkBusinessConstraints(SubscriptionPlan plan) {
// 检查是否有活跃的用户关联
Long activeSubAcc = countActiveSubAccByPlanId(plan.getId());
if (activeSubAcc > 0) {
throw new BusinessException("users.currently.using.this.plan");
}
// 检查是否在有效期内
if (plan.getCurrentPeriodEnd() != null && isExpired(plan.getCurrentPeriodEnd())) {
throw new BusinessException("valid.subscription.period");
}
}
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);
// 3. 修改订阅状态
plan.setStatus(SubscriptionPlan.SubscriptionStatus.ACTIVE.name());
plan.setUpdateTime(LocalDateTime.now());
baseMapper.updateById(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());
return;
}
// 4. 更新管理员账号信息
updateAdminAccount(adminAccount, plan);
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(plan.getCurrentPeriodEnd() * 1000);
// ② 根据积分上限更新管理员的积分credits和身份systemUser
// 暂时保留管理员自己购买的积分
if ((account.getSystemUser() == 5 || account.getSystemUser() == 7)
&& Objects.nonNull(account.getCreditsUsageLimit())) {
BigDecimal leftCredits = account.getCredits().add(account.getCreditsUsage()).subtract(account.getCreditsUsageLimit());
if (leftCredits.compareTo(BigDecimal.ZERO) > 0) {
updateAccount.setCredits(plan.getCreditLimit().add(leftCredits));
} else {
updateAccount.setCredits(plan.getCreditLimit());
}
} 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(plan.getCurrentPeriodEnd()),
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)) {
log.error("未知组织id: {}", orgId);
throw new BusinessException("unknown.organization");
}
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();
if (CollectionUtils.isEmpty(recentlyExpiredPlans)) {
log.info("过去24h没有过期的订阅计划");
return;
}
log.info("发现{}个过去24h过期的订阅计划", recentlyExpiredPlans.size());
// 2. 更新与订阅相关账号的状态
for (SubscriptionPlan subscriptionPlan : recentlyExpiredPlans) {
processExpiringSubscriptionPlan(subscriptionPlan);
// 6. 修改订阅状态
subscriptionPlan.setStatus(SubscriptionPlan.SubscriptionStatus.EXPIRED.name());
subscriptionPlan.setUpdateTime(LocalDateTime.now());
baseMapper.updateById(subscriptionPlan);
}
}
/**
* 查找最近过期的订阅过去24小时内到期的
*/
private List<SubscriptionPlan> findRecentlyExpiredPlans() {
long now = System.currentTimeMillis() / 1000;
long yesterday = now - (24 * 60 * 60); // 24小时前
log.debug("扫描时间范围: {} - {} (过去24h)", formatTimestamp(yesterday), formatTimestamp(now));
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());
}
}
/**
* 获取管理员当前激活的订阅计划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;
}
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);
// 4. 修改订阅状态
earliestActivePlan.setStatus(SubscriptionPlan.SubscriptionStatus.ACTIVE.name());
earliestActivePlan.setUpdateTime(LocalDateTime.now());
baseMapper.updateById(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 (subscriptionPlan.getStatus().equals(SubscriptionPlan.SubscriptionStatus.EXPIRED.name())) {
throw new BusinessException("subscription.has.expired");
}
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("unknown.administrator.user");
}
if (!adminAccId.equals(subscriptionPlan.getAdminAccId())) {
log.info("用户:{} 没有权限管理订阅:{}", adminAccId, subscriptionPlanId);
throw new BusinessException("no.permission.manage.subscription", ResultEnum.PROMPT.getCode());
}
if (account.getSubscriptionPlanId().equals(subscriptionPlanId)) {
log.info("用户:{} 当前正在管理订阅 {}", adminAccId, subscriptionPlanId);
return;
}
updateAdminAccount(account, subscriptionPlan);
}
}

View File

@@ -26,7 +26,8 @@
<update id="toVisitor">
update t_account
set is_trial = 0, credits = 0, system_user = 0,
organization_name = null, credits_usage = null, credits_usage_limit = null, sub_account_num = null
organization_name = null, organization_id = null, credits_usage = null, credits_usage_limit = null, sub_account_num = null,
parent_id = null, subscription_plan_id = null
where id = #{id}
</update>

View File

@@ -189,6 +189,22 @@ 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
unknown.subscription.status=Unknown subscription status.
subscription.has.expired=Switch failed. The subscription has expired.
unknown.administrator.user=Switch failed. Unknown administrator user.
no.permission.manage.subscription=Switch failed. You do not have permission to manage this subscription.
unknown.organization=Unknown organization.
valid.subscription.period=The plan is still within its valid period. Please delete it after it expires.
users.currently.using.this.plan=There are users currently using this plan, so it cannot be deleted.
deletion.failed.please.try.again.later=Deletion failed. Please try again later.
subscription.plan.has.been.deleted=This subscription plan has been deleted.
subscription.plan.does.not.exist=The subscription plan does not exist.
ID.cannot.be.empty.and.must.be.greater.than.0=ID cannot be empty and must be greater than 0.
invalid.time.format=Invalid time format. Please use the yyyy-MM-dd HH:mm:ss format.
the.start.time.cannot.be.later.than.the.end.time=The start time cannot be later than the end time.
page.size.limit=The number of items per page must be between 1 and 100.
page.num.limit=The page number must be greater than 0.
# 可能会报异常
# Informative:

View File

@@ -185,6 +185,22 @@ do.not.have.the.permission.to.delete.this.comment=您没有权限删除此评论
unknow.affiliate=未知推广者id
unknown.operationType=未知操作类型
unknown.mode=未知模式
unknown.subscription.plan=未知订阅计划
unknown.subscription.status=未知订阅状态
subscription.has.expired=切换失败,订阅已过期
unknown.administrator.user=切换失败,未知管理员用户
no.permission.manage.subscription=切换失败,您没有权限管理该订阅
unknown.organization=未知组织
valid.subscription.period=计划仍在有效期内,请到期后再删除
users.currently.using.this.plan=存在用户正在使用此计划,无法删除
deletion.failed.please.try.again.later=删除失败,请稍后重试
subscription.plan.has.been.deleted=该订阅计划已被删除
subscription.plan.does.not.exist=订阅计划不存在
ID.cannot.be.empty.and.must.be.greater.than.0=ID不能为空且必须大于0
invalid.time.format=时间格式错误请使用yyyy-MM-dd HH:mm:ss格式
the.start.time.cannot.be.later.than.the.end.time=开始时间不能晚于结束时间
page.size.limit=每页数量必须在1-100之间
page.num.limit=页码必须大于0
# 可能会报异常
# Informative:

View File

@@ -27,20 +27,20 @@ paypal.webhook_id=1D107312EX592781K
##### Stripe
# developer
stripe.private-key=sk_test_51P4ZZL02n1TEydyN8qQHjOA9imsFU7Oxs2HMHGy2urHnnQgSHnZuu5vVP6pKhEACwUpsKNyrbZpdcg5TJWJLRHcY008dEO1fn2
#stripe.private-key=sk_test_51P4ZZL02n1TEydyN8qQHjOA9imsFU7Oxs2HMHGy2urHnnQgSHnZuu5vVP6pKhEACwUpsKNyrbZpdcg5TJWJLRHcY008dEO1fn2
# dev 端点
stripe.webhook-sign-secret=whsec_e0dBiJngx6qqgJj6yPyJ2A9ouh1Cjv5w
#stripe.webhook-sign-secret=whsec_e0dBiJngx6qqgJj6yPyJ2A9ouh1Cjv5w
# local 端点
#stripe.webhook-sign-secret=whsec_TJcMSnAkh4uktrNY1M6Iy8XaVze4Rzqm
# kim - test
#stripe.private-key=sk_test_51LwPrxH7nPZ8bkrNj67TFD7sxucaTANs1lf0KGSu1QSJfxYXcnigq2wTaZyZzST7y0fMbhhvaJZ4LjjFhr95M83a00eXrmOTL0
stripe.private-key=sk_test_51LwPrxH7nPZ8bkrNj67TFD7sxucaTANs1lf0KGSu1QSJfxYXcnigq2wTaZyZzST7y0fMbhhvaJZ4LjjFhr95M83a00eXrmOTL0
# prod 端点
#stripe.webhook-sign-secret=whsec_GoyVEAaBtuGD5Rt55z83JnPnLDAZTN3u
# local 端点
#stripe.webhook-sign-secret=whsec_NvwM3hDQiN5GXclYOYekE9IKHLjmROF8
# dev 端点
#stripe.webhook-sign-secret=whsec_pX0pPMQm85PaUSWnFMEzoccb3MGNkjoL
stripe.webhook-sign-secret=whsec_pX0pPMQm85PaUSWnFMEzoccb3MGNkjoL
# kim - live
#stripe.private-key=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