feat: 配额优化

This commit is contained in:
2026-02-25 21:30:24 +08:00
parent 2e93211697
commit 79a5c1f3ed
29 changed files with 1225 additions and 489 deletions

View File

@@ -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);
}
}

View File

@@ -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();
}

View File

@@ -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());

View File

@@ -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);
}

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -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));
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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;
}