feat: 配额优化
This commit is contained in:
@@ -4,9 +4,7 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import cn.iocoder.yudao.module.tik.file.service.TikOssInitService;
|
||||
import cn.iocoder.yudao.module.tik.file.vo.app.AppTikUserOssInitRespVO;
|
||||
import cn.iocoder.yudao.module.tik.quota.service.TikUserQuotaService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -17,7 +15,7 @@ import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* 用户 App - 测试 Controller
|
||||
* 用于测试升级会员和创建OSS目录
|
||||
* 用于测试创建OSS目录
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@@ -31,34 +29,6 @@ public class AppTikTestController {
|
||||
@Resource
|
||||
private TikOssInitService ossInitService;
|
||||
|
||||
@Resource
|
||||
private TikUserQuotaService quotaService;
|
||||
|
||||
@PostMapping("/upgrade-vip")
|
||||
@Operation(summary = "测试:升级会员", description = "升级当前用户的VIP等级和配额")
|
||||
@Parameter(name = "vipLevel", description = "VIP等级", example = "1")
|
||||
@Parameter(name = "totalStorage", description = "总存储空间(字节)", example = "10737418240")
|
||||
@Parameter(name = "totalQuota", description = "总配额(积分/额度)", example = "10000")
|
||||
public CommonResult<Boolean> upgradeVip(
|
||||
@RequestParam(value = "vipLevel", defaultValue = "1") Integer vipLevel,
|
||||
@RequestParam(value = "totalStorage", required = false) Long totalStorage,
|
||||
@RequestParam(value = "totalQuota", required = false) Long totalQuota) {
|
||||
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
||||
|
||||
// 默认值:10GB存储空间,10000配额
|
||||
if (totalStorage == null) {
|
||||
totalStorage = 10L * 1024 * 1024 * 1024; // 10GB
|
||||
}
|
||||
if (totalQuota == null) {
|
||||
totalQuota = 10000L;
|
||||
}
|
||||
|
||||
quotaService.updateVipLevel(userId, vipLevel, totalStorage, totalQuota);
|
||||
log.info("[upgradeVip][用户({})升级VIP成功,等级({}),存储({}),配额({})]",
|
||||
userId, vipLevel, totalStorage, totalQuota);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/init-oss")
|
||||
@Operation(summary = "测试:初始化OSS目录", description = "为当前用户创建OSS目录结构")
|
||||
public CommonResult<AppTikUserOssInitRespVO> initOss() {
|
||||
@@ -68,34 +38,5 @@ public class AppTikTestController {
|
||||
return success(result);
|
||||
}
|
||||
|
||||
@PostMapping("/test-all")
|
||||
@Operation(summary = "测试:一键执行", description = "同时执行升级会员和创建OSS目录")
|
||||
@Parameter(name = "vipLevel", description = "VIP等级", example = "1")
|
||||
@Parameter(name = "totalStorage", description = "总存储空间(字节)", example = "10737418240")
|
||||
@Parameter(name = "totalQuota", description = "总配额(积分/额度)", example = "10000")
|
||||
public CommonResult<AppTikUserOssInitRespVO> testAll(
|
||||
@RequestParam(value = "vipLevel", defaultValue = "1") Integer vipLevel,
|
||||
@RequestParam(value = "totalStorage", required = false) Long totalStorage,
|
||||
@RequestParam(value = "totalQuota", required = false) Long totalQuota) {
|
||||
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
||||
|
||||
// 1. 升级会员
|
||||
if (totalStorage == null) {
|
||||
totalStorage = 10L * 1024 * 1024 * 1024; // 10GB
|
||||
}
|
||||
if (totalQuota == null) {
|
||||
totalQuota = 10000L;
|
||||
}
|
||||
quotaService.updateVipLevel(userId, vipLevel, totalStorage, totalQuota);
|
||||
log.info("[testAll][用户({})升级VIP成功,等级({}),存储({}),配额({})]",
|
||||
userId, vipLevel, totalStorage, totalQuota);
|
||||
|
||||
// 2. 初始化OSS目录
|
||||
AppTikUserOssInitRespVO ossResult = ossInitService.initOssDirectory(userId);
|
||||
log.info("[testAll][用户({})OSS目录初始化成功]", userId);
|
||||
|
||||
return success(ossResult);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,10 @@ import cn.iocoder.yudao.module.tik.file.dal.dataobject.TikUserFileDO;
|
||||
import cn.iocoder.yudao.module.tik.file.vo.app.AppTikUserFilePageReqVO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 用户文件 Mapper
|
||||
@@ -32,4 +34,12 @@ public interface TikUserFileMapper extends BaseMapperX<TikUserFileDO> {
|
||||
.orderByDesc(TikUserFileDO::getId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计每个用户的实际存储使用量(字节)
|
||||
* @return Map<userId, totalSize>
|
||||
*/
|
||||
@Select("SELECT user_id, SUM(file_size) as total_size FROM tik_user_file " +
|
||||
"WHERE deleted = 0 GROUP BY user_id")
|
||||
List<Map<String, Object>> selectStorageByUser();
|
||||
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import cn.iocoder.yudao.module.tik.file.dal.mysql.TikUserFileMapper;
|
||||
import cn.iocoder.yudao.module.tik.file.enums.TikFileCategoryEnum;
|
||||
import cn.iocoder.yudao.module.tik.file.vo.app.AppTikUserFilePageReqVO;
|
||||
import cn.iocoder.yudao.module.tik.file.vo.app.AppTikUserFileRespVO;
|
||||
import cn.iocoder.yudao.module.tik.quota.service.TikUserQuotaService;
|
||||
import cn.iocoder.yudao.module.tik.muye.memberuserprofile.service.MemberUserProfileService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -77,7 +77,7 @@ public class TikUserFileServiceImpl implements TikUserFileService {
|
||||
private TikOssInitService ossInitService;
|
||||
|
||||
@Resource
|
||||
private TikUserQuotaService quotaService;
|
||||
private MemberUserProfileService memberUserProfileService;
|
||||
|
||||
@Resource
|
||||
private FileApi fileApi;
|
||||
@@ -137,7 +137,7 @@ public class TikUserFileServiceImpl implements TikUserFileService {
|
||||
// 2. 校验配额
|
||||
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
||||
if (fileSize != null && fileSize > 0) {
|
||||
quotaService.validateStorage(userId, fileSize);
|
||||
memberUserProfileService.validateStorage(String.valueOf(userId), fileSize);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,7 +270,7 @@ public class TikUserFileServiceImpl implements TikUserFileService {
|
||||
userFileMapper.insert(userFile);
|
||||
|
||||
// 10. 更新配额
|
||||
quotaService.increaseUsedStorage(userId, file.getSize());
|
||||
memberUserProfileService.increaseUsedStorage(String.valueOf(userId), file.getSize());
|
||||
|
||||
log.info("[saveFileRecord][用户({})保存文件记录成功,文件编号({}),infra文件编号({})]",
|
||||
userId, userFile.getId(), infraFileId);
|
||||
@@ -439,7 +439,7 @@ public class TikUserFileServiceImpl implements TikUserFileService {
|
||||
|
||||
// 释放配额
|
||||
long totalSize = files.stream().mapToLong(TikUserFileDO::getFileSize).sum();
|
||||
quotaService.decreaseUsedStorage(userId, totalSize);
|
||||
memberUserProfileService.decreaseUsedStorage(String.valueOf(userId), totalSize);
|
||||
|
||||
log.info("[deleteFiles][用户({})删除文件成功,文件数量({})]", userId, fileIds.size());
|
||||
}
|
||||
@@ -692,7 +692,7 @@ public class TikUserFileServiceImpl implements TikUserFileService {
|
||||
userFileMapper.insert(userFile);
|
||||
|
||||
// 6. 更新配额
|
||||
quotaService.increaseUsedStorage(userId, fileSize);
|
||||
memberUserProfileService.increaseUsedStorage(String.valueOf(userId), fileSize);
|
||||
|
||||
log.info("[completeUpload][用户({})直传上传完成,infraFileId({}),userFileId({})]",
|
||||
userId, infraFileId, userFile.getId());
|
||||
|
||||
@@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.tik.member.mq.consumer;
|
||||
|
||||
import cn.iocoder.yudao.module.member.api.message.user.MemberUserCreateMessage;
|
||||
import cn.iocoder.yudao.module.tik.muye.memberuserprofile.service.MemberUserProfileService;
|
||||
import cn.iocoder.yudao.module.tik.quota.service.TikUserQuotaService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -12,7 +11,7 @@ import jakarta.annotation.Resource;
|
||||
/**
|
||||
* 会员用户创建事件监听器
|
||||
*
|
||||
* 功能:用户注册后自动初始化配额和档案
|
||||
* 功能:用户注册后自动初始化档案和配额
|
||||
* 触发时机:用户注册时(MemberUserServiceImpl.createUser())
|
||||
*
|
||||
* @author 芋道源码
|
||||
@@ -21,8 +20,6 @@ import jakarta.annotation.Resource;
|
||||
@Slf4j
|
||||
public class MemberUserCreateConsumer {
|
||||
|
||||
@Resource
|
||||
private TikUserQuotaService quotaService;
|
||||
@Resource
|
||||
private MemberUserProfileService memberUserProfileService;
|
||||
|
||||
@@ -38,11 +35,7 @@ public class MemberUserCreateConsumer {
|
||||
|
||||
Long userId = message.getUserId();
|
||||
|
||||
// 初始化用户配额(默认VIP等级0)
|
||||
quotaService.initQuota(userId, 0);
|
||||
log.info("[onMessage][用户({})配额初始化成功]", userId);
|
||||
|
||||
// 初始化会员档案
|
||||
// 初始化会员档案(包含存储配额)
|
||||
memberUserProfileService.createIfAbsent(userId);
|
||||
log.info("[onMessage][用户({})档案初始化成功]", userId);
|
||||
}
|
||||
|
||||
@@ -101,4 +101,11 @@ public class AiModelConfigController {
|
||||
BeanUtils.toBean(list, AiModelConfigRespVO.class));
|
||||
}
|
||||
|
||||
@GetMapping("/list-enabled")
|
||||
@Operation(summary = "获取所有启用的模型配置列表(前端积分显示用)")
|
||||
@PreAuthorize("@ss.hasPermission('muye:ai-model-config:query')")
|
||||
public CommonResult<Map<String, List<AiModelConfigService.ModelConfigSimpleVO>>> getEnabledModelConfigList() {
|
||||
return success(aiModelConfigService.getEnabledModelConfigList());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package cn.iocoder.yudao.module.tik.muye.aimodelconfig;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.module.tik.muye.aimodelconfig.service.AiModelConfigService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* 用户 App - AI模型配置
|
||||
*/
|
||||
@Tag(name = "用户 App - AI模型配置")
|
||||
@RestController
|
||||
@RequestMapping("/api/tik/ai-model-config")
|
||||
@Validated
|
||||
public class AppAiModelConfigController {
|
||||
|
||||
@Resource
|
||||
private AiModelConfigService aiModelConfigService;
|
||||
|
||||
@GetMapping("/list-enabled")
|
||||
@Operation(summary = "获取所有启用的模型配置列表(前端积分显示用)")
|
||||
public CommonResult<Map<String, List<AiModelConfigService.ModelConfigSimpleVO>>> getEnabledModelConfigList() {
|
||||
return success(aiModelConfigService.getEnabledModelConfigList());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -49,4 +49,13 @@ public interface AiModelConfigMapper extends BaseMapperX<AiModelConfigDO> {
|
||||
.eq(AiModelConfigDO::getStatus, 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询所有启用的模型配置
|
||||
*/
|
||||
default List<AiModelConfigDO> selectEnabledList() {
|
||||
return selectList(new LambdaQueryWrapperX<AiModelConfigDO>()
|
||||
.eq(AiModelConfigDO::getStatus, 1)
|
||||
.orderByAsc(AiModelConfigDO::getPlatform, AiModelConfigDO::getModelCode));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package cn.iocoder.yudao.module.tik.muye.aimodelconfig.service;
|
||||
|
||||
import java.util.*;
|
||||
import lombok.Data;
|
||||
|
||||
import cn.iocoder.yudao.module.tik.muye.aimodelconfig.dal.AiModelConfigDO;
|
||||
import cn.iocoder.yudao.module.tik.muye.aimodelconfig.vo.AiModelConfigPageReqVO;
|
||||
@@ -60,4 +61,21 @@ public interface AiModelConfigService {
|
||||
*/
|
||||
PageResult<AiModelConfigDO> getAiModelConfigPage(AiModelConfigPageReqVO pageReqVO);
|
||||
|
||||
/**
|
||||
* 获取所有启用的模型配置列表(前端积分显示用)
|
||||
*
|
||||
* @return 模型配置列表(按平台分组)
|
||||
*/
|
||||
Map<String, List<ModelConfigSimpleVO>> getEnabledModelConfigList();
|
||||
|
||||
/**
|
||||
* 启用的模型配置简单VO(用于前端显示)
|
||||
*/
|
||||
@Data
|
||||
class ModelConfigSimpleVO {
|
||||
private String modelCode;
|
||||
private String modelName;
|
||||
private Integer consumePoints;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -77,4 +77,18 @@ public class AiModelConfigServiceImpl implements AiModelConfigService {
|
||||
return aiModelConfigMapper.selectPage(pageReqVO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<ModelConfigSimpleVO>> getEnabledModelConfigList() {
|
||||
List<AiModelConfigDO> configList = aiModelConfigMapper.selectEnabledList();
|
||||
Map<String, List<ModelConfigSimpleVO>> result = new HashMap<>();
|
||||
for (AiModelConfigDO config : configList) {
|
||||
ModelConfigSimpleVO simpleVO = new ModelConfigSimpleVO();
|
||||
simpleVO.setModelCode(config.getModelCode());
|
||||
simpleVO.setModelName(config.getModelName());
|
||||
simpleVO.setConsumePoints(config.getConsumePoints());
|
||||
result.computeIfAbsent(config.getPlatform(), k -> new ArrayList<>()).add(simpleVO);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package cn.iocoder.yudao.module.tik.muye.memberuserprofile.job;
|
||||
|
||||
import cn.iocoder.yudao.module.tik.file.dal.mysql.TikUserFileMapper;
|
||||
import cn.iocoder.yudao.module.tik.muye.memberuserprofile.dal.MemberUserProfileDO;
|
||||
import cn.iocoder.yudao.module.tik.muye.memberuserprofile.mapper.MemberUserProfileMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* OSS配额每日对账定时任务
|
||||
*
|
||||
* 功能:对比 tik_user_file 实际存储与 muye_member_user_profile 记录的存储
|
||||
* 如果存在差异,则更新 member_user_profile 表
|
||||
* 执行频率:每天凌晨3点执行
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class OssQuotaReconcileJob {
|
||||
|
||||
/**
|
||||
* GB转字节的常量
|
||||
*/
|
||||
private static final BigDecimal GB_TO_BYTES = new BigDecimal("1073741824");
|
||||
|
||||
@Resource
|
||||
private TikUserFileMapper userFileMapper;
|
||||
|
||||
@Resource
|
||||
private MemberUserProfileMapper profileMapper;
|
||||
|
||||
/**
|
||||
* 每天凌晨3点执行对账
|
||||
* cron: 秒 分 时 日 月 周
|
||||
*/
|
||||
@Scheduled(cron = "0 0 3 * * ?")
|
||||
public void reconcileStorage() {
|
||||
log.info("[reconcileStorage][开始OSS配额对账]");
|
||||
int updatedCount = 0;
|
||||
int errorCount = 0;
|
||||
|
||||
try {
|
||||
// 1. 获取每个用户的实际存储使用量(字节)
|
||||
List<Map<String, Object>> storageList = userFileMapper.selectStorageByUser();
|
||||
|
||||
for (Map<String, Object> record : storageList) {
|
||||
try {
|
||||
String userId = String.valueOf(record.get("user_id"));
|
||||
Long actualSizeBytes = ((Number) record.get("total_size")).longValue();
|
||||
|
||||
// 2. 获取用户档案中记录的存储
|
||||
MemberUserProfileDO profile = profileMapper.selectByUserId(userId);
|
||||
if (profile == null) {
|
||||
log.warn("[reconcileStorage][用户({})档案不存在,跳过]", userId);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. 将实际存储(字节)转换为GB
|
||||
BigDecimal actualSizeGb = new BigDecimal(actualSizeBytes)
|
||||
.divide(GB_TO_BYTES, 6, RoundingMode.HALF_UP);
|
||||
|
||||
// 4. 对比记录的存储(GB)
|
||||
BigDecimal recordedSizeGb = profile.getUsedStorage();
|
||||
if (recordedSizeGb == null) {
|
||||
recordedSizeGb = BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
// 5. 检查差异(允许0.01GB的误差)
|
||||
BigDecimal diff = actualSizeGb.subtract(recordedSizeGb).abs();
|
||||
if (diff.compareTo(new BigDecimal("0.01")) > 0) {
|
||||
// 存在差异,更新记录
|
||||
BigDecimal totalStorage = profile.getTotalStorage();
|
||||
if (totalStorage == null) {
|
||||
totalStorage = BigDecimal.ZERO;
|
||||
}
|
||||
BigDecimal remainingStorage = totalStorage.subtract(actualSizeGb);
|
||||
if (remainingStorage.compareTo(BigDecimal.ZERO) < 0) {
|
||||
remainingStorage = BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
// 更新档案
|
||||
MemberUserProfileDO updateProfile = new MemberUserProfileDO();
|
||||
updateProfile.setId(profile.getId());
|
||||
updateProfile.setUsedStorage(actualSizeGb);
|
||||
updateProfile.setRemainingStorage(remainingStorage);
|
||||
profileMapper.updateById(updateProfile);
|
||||
|
||||
log.info("[reconcileStorage][用户({})存储已修正:{}GB -> {}GB]",
|
||||
userId, recordedSizeGb, actualSizeGb);
|
||||
updatedCount++;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[reconcileStorage][处理用户存储记录失败,record({})]", record, e);
|
||||
errorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
log.info("[reconcileStorage][对账完成,共检查{}个用户,修正{}个,失败{}个]",
|
||||
storageList.size(), updatedCount, errorCount);
|
||||
} catch (Exception e) {
|
||||
log.error("[reconcileStorage][对账任务执行失败]", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -60,4 +60,30 @@ public interface MemberUserProfileMapper extends BaseMapperX<MemberUserProfileDO
|
||||
"WHERE user_id = #{userId} AND remaining_points >= #{points}")
|
||||
int updatePointsDeduct(@Param("userId") String userId, @Param("points") Integer points);
|
||||
|
||||
/**
|
||||
* 原子增加已用存储(乐观锁)
|
||||
* @param userId 用户ID
|
||||
* @param storageGb 增加的存储量(GB)
|
||||
* @return 影响行数,0表示余额不足
|
||||
*/
|
||||
@Update("UPDATE muye_member_user_profile " +
|
||||
"SET used_storage = used_storage + #{storageGb}, " +
|
||||
" remaining_storage = remaining_storage - #{storageGb}, " +
|
||||
" update_time = NOW() " +
|
||||
"WHERE user_id = #{userId} AND remaining_storage >= #{storageGb}")
|
||||
int updateStorageIncrease(@Param("userId") String userId, @Param("storageGb") String storageGb);
|
||||
|
||||
/**
|
||||
* 原子减少已用存储
|
||||
* @param userId 用户ID
|
||||
* @param storageGb 减少的存储量(GB)
|
||||
* @return 影响行数,0表示已用存储不足
|
||||
*/
|
||||
@Update("UPDATE muye_member_user_profile " +
|
||||
"SET used_storage = used_storage - #{storageGb}, " +
|
||||
" remaining_storage = remaining_storage + #{storageGb}, " +
|
||||
" update_time = NOW() " +
|
||||
"WHERE user_id = #{userId} AND used_storage >= #{storageGb}")
|
||||
int updateStorageDecrease(@Param("userId") String userId, @Param("storageGb") String storageGb);
|
||||
|
||||
}
|
||||
@@ -69,4 +69,33 @@ public interface MemberUserProfileService {
|
||||
*/
|
||||
MemberUserProfileDO createIfAbsent(Long userId);
|
||||
|
||||
// ========== 存储空间管理 ==========
|
||||
|
||||
/**
|
||||
* 校验存储空间是否足够
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param fileSizeBytes 文件大小(字节)
|
||||
* @throws cn.iocoder.yudao.framework.common.exception.ServiceException 存储空间不足时抛出异常
|
||||
*/
|
||||
void validateStorage(String userId, long fileSizeBytes);
|
||||
|
||||
/**
|
||||
* 增加已使用存储空间
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param fileSizeBytes 文件大小(字节)
|
||||
* @return 是否成功(余额不足返回false)
|
||||
*/
|
||||
boolean increaseUsedStorage(String userId, long fileSizeBytes);
|
||||
|
||||
/**
|
||||
* 减少已使用存储空间
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param fileSizeBytes 文件大小(字节)
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean decreaseUsedStorage(String userId, long fileSizeBytes);
|
||||
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.*;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
@@ -114,4 +115,45 @@ public class MemberUserProfileServiceImpl implements MemberUserProfileService {
|
||||
return profile;
|
||||
}
|
||||
|
||||
// ========== 存储空间管理 ==========
|
||||
|
||||
/**
|
||||
* GB转字节的常量
|
||||
*/
|
||||
private static final BigDecimal GB_TO_BYTES = new BigDecimal("1073741824");
|
||||
|
||||
@Override
|
||||
public void validateStorage(String userId, long fileSizeBytes) {
|
||||
MemberUserProfileDO profile = memberUserProfileMapper.selectByUserId(userId);
|
||||
if (profile == null) {
|
||||
throw exception(new ErrorCode(1004, "会员用户档案不存在"));
|
||||
}
|
||||
|
||||
// 将剩余存储空间(GB)转换为字节进行比较
|
||||
BigDecimal remainingBytes = profile.getRemainingStorage().multiply(GB_TO_BYTES);
|
||||
if (remainingBytes.compareTo(new BigDecimal(fileSizeBytes)) < 0) {
|
||||
throw exception(new ErrorCode(1005, "存储空间不足,剩余 " + profile.getRemainingStorage() + " GB"));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean increaseUsedStorage(String userId, long fileSizeBytes) {
|
||||
// 将字节转换为GB(保留6位小数)
|
||||
BigDecimal storageGb = new BigDecimal(fileSizeBytes).divide(GB_TO_BYTES, 6, RoundingMode.HALF_UP);
|
||||
String storageGbStr = storageGb.toPlainString();
|
||||
|
||||
int affectedRows = memberUserProfileMapper.updateStorageIncrease(userId, storageGbStr);
|
||||
return affectedRows > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean decreaseUsedStorage(String userId, long fileSizeBytes) {
|
||||
// 将字节转换为GB(保留6位小数)
|
||||
BigDecimal storageGb = new BigDecimal(fileSizeBytes).divide(GB_TO_BYTES, 6, RoundingMode.HALF_UP);
|
||||
String storageGbStr = storageGb.toPlainString();
|
||||
|
||||
int affectedRows = memberUserProfileMapper.updateStorageDecrease(userId, storageGbStr);
|
||||
return affectedRows > 0;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package cn.iocoder.yudao.module.tik.quota.dal.dataobject;
|
||||
|
||||
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.*;
|
||||
|
||||
/**
|
||||
* 用户配额 DO
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@TableName("tik_user_quota")
|
||||
@KeySequence("tik_user_quota_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class TikUserQuotaDO extends TenantBaseDO {
|
||||
|
||||
/**
|
||||
* 配额编号
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
/**
|
||||
* 用户编号
|
||||
*/
|
||||
private Long userId;
|
||||
/**
|
||||
* 总存储空间(字节)
|
||||
*/
|
||||
private Long totalStorage;
|
||||
/**
|
||||
* 已使用存储空间(字节)
|
||||
*/
|
||||
private Long usedStorage;
|
||||
/**
|
||||
* 总配额(积分/额度)
|
||||
*/
|
||||
private Long totalQuota;
|
||||
/**
|
||||
* 已使用配额(积分/额度)
|
||||
*/
|
||||
private Long usedQuota;
|
||||
/**
|
||||
* VIP等级
|
||||
*/
|
||||
private Integer vipLevel;
|
||||
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package cn.iocoder.yudao.module.tik.quota.dal.mysql;
|
||||
|
||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import cn.iocoder.yudao.module.tik.quota.dal.dataobject.TikUserQuotaDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 用户配额 Mapper
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Mapper
|
||||
public interface TikUserQuotaMapper extends BaseMapperX<TikUserQuotaDO> {
|
||||
|
||||
default TikUserQuotaDO selectByUserId(Long userId) {
|
||||
return selectOne(TikUserQuotaDO::getUserId, userId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
package cn.iocoder.yudao.module.tik.quota.service;
|
||||
|
||||
import cn.iocoder.yudao.module.tik.quota.dal.dataobject.TikUserQuotaDO;
|
||||
|
||||
/**
|
||||
* 用户配额 Service 接口
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface TikUserQuotaService {
|
||||
|
||||
/**
|
||||
* 初始化用户配额
|
||||
*
|
||||
* @param userId 用户编号
|
||||
* @param vipLevel VIP等级
|
||||
* @return 配额编号
|
||||
*/
|
||||
Long initQuota(Long userId, Integer vipLevel);
|
||||
|
||||
/**
|
||||
* 获取用户配额
|
||||
*
|
||||
* @param userId 用户编号
|
||||
* @return 配额信息
|
||||
*/
|
||||
TikUserQuotaDO getQuota(Long userId);
|
||||
|
||||
/**
|
||||
* 获取或创建用户配额(如果不存在则创建)
|
||||
*
|
||||
* @param userId 用户编号
|
||||
* @return 配额信息
|
||||
*/
|
||||
TikUserQuotaDO getOrCreateQuota(Long userId);
|
||||
|
||||
/**
|
||||
* 校验存储空间是否足够
|
||||
*
|
||||
* @param userId 用户编号
|
||||
* @param fileSize 文件大小(字节)
|
||||
*/
|
||||
void validateStorage(Long userId, Long fileSize);
|
||||
|
||||
/**
|
||||
* 增加已使用存储空间
|
||||
*
|
||||
* @param userId 用户编号
|
||||
* @param fileSize 文件大小(字节)
|
||||
*/
|
||||
void increaseUsedStorage(Long userId, Long fileSize);
|
||||
|
||||
/**
|
||||
* 减少已使用存储空间
|
||||
*
|
||||
* @param userId 用户编号
|
||||
* @param fileSize 文件大小(字节)
|
||||
*/
|
||||
void decreaseUsedStorage(Long userId, Long fileSize);
|
||||
|
||||
/**
|
||||
* 更新VIP等级和配额
|
||||
*
|
||||
* @param userId 用户编号
|
||||
* @param vipLevel VIP等级
|
||||
* @param totalStorage 总存储空间(字节)
|
||||
* @param totalQuota 总配额(积分/额度)
|
||||
*/
|
||||
void updateVipLevel(Long userId, Integer vipLevel, Long totalStorage, Long totalQuota);
|
||||
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
package cn.iocoder.yudao.module.tik.quota.service;
|
||||
|
||||
import cn.iocoder.yudao.module.tik.quota.dal.dataobject.TikUserQuotaDO;
|
||||
import cn.iocoder.yudao.module.tik.quota.dal.mysql.TikUserQuotaMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.module.tik.enums.ErrorCodeConstants.QUOTA_NOT_ENOUGH;
|
||||
import static cn.iocoder.yudao.module.tik.enums.ErrorCodeConstants.QUOTA_NOT_EXISTS;
|
||||
|
||||
/**
|
||||
* 用户配额 Service 实现类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Service
|
||||
@Validated
|
||||
@Slf4j
|
||||
public class TikUserQuotaServiceImpl implements TikUserQuotaService {
|
||||
|
||||
@Resource
|
||||
private TikUserQuotaMapper quotaMapper;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long initQuota(Long userId, Integer vipLevel) {
|
||||
// 检查是否已存在
|
||||
TikUserQuotaDO existing = quotaMapper.selectByUserId(userId);
|
||||
if (existing != null) {
|
||||
log.info("[initQuota][用户({})配额已存在,跳过初始化]", userId);
|
||||
return existing.getId();
|
||||
}
|
||||
|
||||
// 创建配额记录(默认值)
|
||||
TikUserQuotaDO quota = TikUserQuotaDO.builder()
|
||||
.userId(userId)
|
||||
.vipLevel(vipLevel != null ? vipLevel : 0)
|
||||
.totalStorage(0L)
|
||||
.usedStorage(0L)
|
||||
.totalQuota(0L)
|
||||
.usedQuota(0L)
|
||||
.build();
|
||||
quotaMapper.insert(quota);
|
||||
log.info("[initQuota][用户({})配额初始化成功,配额编号({})]", userId, quota.getId());
|
||||
return quota.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public TikUserQuotaDO getQuota(Long userId) {
|
||||
return quotaMapper.selectByUserId(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public TikUserQuotaDO getOrCreateQuota(Long userId) {
|
||||
TikUserQuotaDO quota = getQuota(userId);
|
||||
if (quota == null) {
|
||||
// 如果不存在,创建默认配额
|
||||
Long quotaId = initQuota(userId, 0);
|
||||
quota = quotaMapper.selectById(quotaId);
|
||||
}
|
||||
return quota;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateStorage(Long userId, Long fileSize) {
|
||||
TikUserQuotaDO quota = getOrCreateQuota(userId);
|
||||
if (quota == null) {
|
||||
throw exception(QUOTA_NOT_EXISTS);
|
||||
}
|
||||
|
||||
long remainingStorage = quota.getTotalStorage() - quota.getUsedStorage();
|
||||
if (remainingStorage < fileSize) {
|
||||
throw exception(QUOTA_NOT_ENOUGH);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void increaseUsedStorage(Long userId, Long fileSize) {
|
||||
TikUserQuotaDO quota = getOrCreateQuota(userId);
|
||||
quota.setUsedStorage(quota.getUsedStorage() + fileSize);
|
||||
quotaMapper.updateById(quota);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void decreaseUsedStorage(Long userId, Long fileSize) {
|
||||
TikUserQuotaDO quota = getQuota(userId);
|
||||
if (quota == null) {
|
||||
log.warn("[decreaseUsedStorage][用户({})配额不存在,跳过]", userId);
|
||||
return;
|
||||
}
|
||||
|
||||
long newUsedStorage = Math.max(0, quota.getUsedStorage() - fileSize);
|
||||
quota.setUsedStorage(newUsedStorage);
|
||||
quotaMapper.updateById(quota);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateVipLevel(Long userId, Integer vipLevel, Long totalStorage, Long totalQuota) {
|
||||
TikUserQuotaDO quota = getOrCreateQuota(userId);
|
||||
quota.setVipLevel(vipLevel);
|
||||
if (totalStorage != null) {
|
||||
quota.setTotalStorage(totalStorage);
|
||||
}
|
||||
if (totalQuota != null) {
|
||||
quota.setTotalQuota(totalQuota);
|
||||
}
|
||||
quotaMapper.updateById(quota);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
package cn.iocoder.yudao.module.tik.quota.vo.app;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 用户 App - 用户配额 Response VO
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Schema(description = "用户 App - 用户配额 Response VO")
|
||||
@Data
|
||||
public class AppTikUserQuotaRespVO {
|
||||
|
||||
@Schema(description = "配额编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "总存储空间(字节)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1073741824")
|
||||
private Long totalStorage;
|
||||
|
||||
@Schema(description = "已使用存储空间(字节)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024000")
|
||||
private Long usedStorage;
|
||||
|
||||
@Schema(description = "剩余存储空间(字节)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1072717824")
|
||||
private Long remainingStorage;
|
||||
|
||||
@Schema(description = "总配额(积分/额度)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000")
|
||||
private Long totalQuota;
|
||||
|
||||
@Schema(description = "已使用配额(积分/额度)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
|
||||
private Long usedQuota;
|
||||
|
||||
@Schema(description = "剩余配额(积分/额度)", requiredMode = Schema.RequiredMode.REQUIRED, example = "900")
|
||||
private Long remainingQuota;
|
||||
|
||||
@Schema(description = "VIP等级", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
|
||||
private Integer vipLevel;
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user