This commit is contained in:
2026-03-15 15:37:35 +08:00
63 changed files with 4175 additions and 170 deletions

View File

@@ -0,0 +1,77 @@
package cn.iocoder.yudao.module.tik.benchmark.controller;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.tik.benchmark.dal.dataobject.BenchmarkTaskDO;
import cn.iocoder.yudao.module.tik.benchmark.service.BenchmarkTaskService;
import cn.iocoder.yudao.module.tik.benchmark.vo.BenchmarkTaskCreateReqVO;
import cn.iocoder.yudao.module.tik.benchmark.vo.BenchmarkTaskPageReqVO;
import cn.iocoder.yudao.module.tik.benchmark.vo.BenchmarkTaskRespVO;
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 jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
/**
* 用户 App - 对标分析任务
*/
@Tag(name = "用户 App - 对标分析任务")
@RestController
@RequestMapping("/api/tik/benchmark-task")
@Validated
public class AppBenchmarkTaskController {
@Resource
private BenchmarkTaskService benchmarkTaskService;
@PostMapping("/create")
@Operation(summary = "创建对标分析任务")
public CommonResult<Long> createTask(@Valid @RequestBody BenchmarkTaskCreateReqVO createReqVO) {
Long userId = getLoginUserId();
if (userId == null) {
return CommonResult.error(401, "用户未登录");
}
return success(benchmarkTaskService.createTask(userId, createReqVO));
}
@GetMapping("/get")
@Operation(summary = "获取任务详情")
@Parameter(name = "id", description = "任务ID", required = true, example = "1024")
public CommonResult<BenchmarkTaskRespVO> getTask(@RequestParam("id") Long id) {
BenchmarkTaskDO task = benchmarkTaskService.getTask(id);
return success(BeanUtils.toBean(task, BenchmarkTaskRespVO.class));
}
@GetMapping("/page")
@Operation(summary = "获取任务分页列表")
public CommonResult<PageResult<BenchmarkTaskRespVO>> getTaskPage(@Valid BenchmarkTaskPageReqVO pageReqVO) {
Long userId = getLoginUserId();
if (userId == null) {
return CommonResult.error(401, "用户未登录");
}
pageReqVO.setUserId(userId);
PageResult<BenchmarkTaskDO> pageResult = benchmarkTaskService.getTaskPage(pageReqVO);
return success(BeanUtils.toBean(pageResult, BenchmarkTaskRespVO.class));
}
@DeleteMapping("/delete")
@Operation(summary = "删除任务")
@Parameter(name = "id", description = "任务ID", required = true, example = "1024")
public CommonResult<Boolean> deleteTask(@RequestParam("id") Long id) {
Long userId = getLoginUserId();
if (userId == null) {
return CommonResult.error(401, "用户未登录");
}
benchmarkTaskService.deleteTask(userId, id);
return success(true);
}
}

View File

@@ -0,0 +1,87 @@
package cn.iocoder.yudao.module.tik.benchmark.dal.dataobject;
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
import cn.iocoder.yudao.module.tik.benchmark.enums.BenchmarkTaskStatusEnum;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
* 对标分析异步任务 DO
*/
@TableName("muye_benchmark_task")
@KeySequence("tik_benchmark_task_seq")
@Data
@EqualsAndHashCode(callSuper = true)
public class BenchmarkTaskDO extends TenantBaseDO {
/**
* 主键ID
*/
@TableId
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 任务名称
*/
private String taskName;
/**
* 视频数量
*/
private Integer videoCount;
/**
* 视频URL列表(JSON)
*/
private String videoUrls;
/**
* 任务状态
* @see BenchmarkTaskStatusEnum
*/
private Integer status;
/**
* 进度(0-100)
*/
private Integer progress;
/**
* 合并后的转写文本
*/
private String mergedText;
/**
* 生成的提示词
*/
private String generatedPrompt;
/**
* 保存后的提示词ID
*/
private Long promptId;
/**
* 错误信息
*/
private String errorMsg;
/**
* 开始时间
*/
private LocalDateTime startTime;
/**
* 完成时间
*/
private LocalDateTime finishTime;
}

View File

@@ -0,0 +1,23 @@
package cn.iocoder.yudao.module.tik.benchmark.dal.mysql;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.tik.benchmark.dal.dataobject.BenchmarkTaskDO;
import cn.iocoder.yudao.module.tik.benchmark.vo.BenchmarkTaskPageReqVO;
import org.apache.ibatis.annotations.Mapper;
/**
* 对标分析异步任务 Mapper
*/
@Mapper
public interface BenchmarkTaskMapper extends BaseMapperX<BenchmarkTaskDO> {
default PageResult<BenchmarkTaskDO> selectPage(BenchmarkTaskPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<BenchmarkTaskDO>()
.eqIfPresent(BenchmarkTaskDO::getUserId, reqVO.getUserId())
.eqIfPresent(BenchmarkTaskDO::getStatus, reqVO.getStatus())
.orderByDesc(BenchmarkTaskDO::getId));
}
}

View File

@@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.tik.benchmark.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 对标分析任务状态枚举
*/
@Getter
@AllArgsConstructor
public enum BenchmarkTaskStatusEnum {
PENDING(0, "待处理"),
RUNNING(1, "处理中"),
SUCCESS(2, "成功"),
FAILED(3, "失败");
private final Integer status;
private final String name;
public static BenchmarkTaskStatusEnum valueOf(Integer status) {
for (BenchmarkTaskStatusEnum value : values()) {
if (value.getStatus().equals(status)) {
return value;
}
}
return null;
}
}

View File

@@ -0,0 +1,194 @@
package cn.iocoder.yudao.module.tik.benchmark.job;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.tik.benchmark.dal.dataobject.BenchmarkTaskDO;
import cn.iocoder.yudao.module.tik.benchmark.dal.mysql.BenchmarkTaskMapper;
import cn.iocoder.yudao.module.tik.benchmark.enums.BenchmarkTaskStatusEnum;
import cn.iocoder.yudao.module.tik.benchmark.vo.BenchmarkTaskCreateReqVO;
import cn.iocoder.yudao.module.tik.tikhup.service.TikHupService;
import cn.iocoder.yudao.module.tik.userprompt.service.UserPromptService;
import cn.iocoder.yudao.module.tik.userprompt.vo.UserPromptSaveReqVO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
/**
* 对标分析任务异步执行器
*/
@Slf4j
@Component
public class BenchmarkTaskExecutor {
@Resource
private BenchmarkTaskMapper benchmarkTaskMapper;
@Resource
private TikHupService tikHupService;
@Resource
private UserPromptService userPromptService;
/**
* 异步执行对标分析任务
*/
@Async("benchmarkTaskExecutor")
public void executeAsync(Long taskId, BenchmarkTaskCreateReqVO createReqVO) {
log.info("[executeAsync][taskId={}] 开始异步执行对标分析任务", taskId);
BenchmarkTaskDO task = benchmarkTaskMapper.selectById(taskId);
if (task == null) {
log.error("[executeAsync][taskId={}] 任务不存在", taskId);
return;
}
try {
// 1. 更新状态为执行中
updateTaskProgress(taskId, 0, BenchmarkTaskStatusEnum.RUNNING.getStatus());
// 2. 获取视频转写
updateTaskProgress(taskId, 10, BenchmarkTaskStatusEnum.RUNNING.getStatus());
List<String> videoUrls = createReqVO.getVideoUrls();
List<String> transcriptions = new ArrayList<>();
// 批量获取转写(使用 videoToCharacters2
try {
Object result = tikHupService.videoToCharacters2(videoUrls);
if (result instanceof List) {
List<?> resultList = (List<?>) result;
for (Object item : resultList) {
if (item != null && StrUtil.isNotBlank(item.toString())) {
transcriptions.add(item.toString());
}
}
}
} catch (Exception e) {
log.warn("[executeAsync][taskId={}] 批量转写失败,尝试逐个转写: {}", taskId, e.getMessage());
// 降级:逐个转写
for (String videoUrl : videoUrls) {
try {
Object result = tikHupService.videoToCharacters(videoUrl);
if (result != null && StrUtil.isNotBlank(result.toString())) {
transcriptions.add(result.toString());
}
} catch (Exception ex) {
log.warn("[executeAsync][taskId={}, videoUrl={}] 转写失败: {}", taskId, videoUrl, ex.getMessage());
}
}
}
updateTaskProgress(taskId, 60, BenchmarkTaskStatusEnum.RUNNING.getStatus());
if (CollUtil.isEmpty(transcriptions)) {
throw new RuntimeException("未能获取任何视频转写内容");
}
// 3. 合并转写文本
String mergedText = String.join("\n\n---\n\n", transcriptions);
// 4. 保存合并文本到任务
BenchmarkTaskDO update = new BenchmarkTaskDO();
update.setId(taskId);
update.setMergedText(mergedText);
benchmarkTaskMapper.updateById(update);
updateTaskProgress(taskId, 70, BenchmarkTaskStatusEnum.RUNNING.getStatus());
// 5. 调用 AI 生成提示词
String generatedPrompt;
try {
Object result = tikHupService.deepseekAnalysis("benchmark", mergedText);
generatedPrompt = result != null ? result.toString() : "";
} catch (Exception e) {
log.warn("[executeAsync][taskId={}] AI 生成失败,使用默认提示词: {}", taskId, e.getMessage());
// 降级使用合并文本的前2000字符
generatedPrompt = "请参考以下视频文案风格生成内容:\n\n" +
mergedText.substring(0, Math.min(2000, mergedText.length()));
}
updateTaskProgress(taskId, 90, BenchmarkTaskStatusEnum.RUNNING.getStatus());
// 6. 如果需要保存为提示词
Long promptId = null;
if (Boolean.TRUE.equals(createReqVO.getSaveAsPrompt()) && StrUtil.isNotBlank(createReqVO.getPromptName())) {
promptId = saveGeneratedPrompt(task.getUserId(), createReqVO.getPromptName(), generatedPrompt);
}
// 7. 标记任务成功
markTaskSuccess(taskId, mergedText, generatedPrompt, promptId);
log.info("[executeAsync][taskId={}] 对标分析任务执行成功", taskId);
} catch (Exception e) {
log.error("[executeAsync][taskId={}] 对标分析任务执行失败", taskId, e);
markTaskFailed(taskId, e.getMessage());
}
}
/**
* 更新任务进度
*/
private void updateTaskProgress(Long taskId, Integer progress, Integer status) {
BenchmarkTaskDO update = new BenchmarkTaskDO();
update.setId(taskId);
update.setProgress(progress);
update.setStatus(status);
if (BenchmarkTaskStatusEnum.RUNNING.getStatus().equals(status)) {
update.setStartTime(LocalDateTime.now());
}
benchmarkTaskMapper.updateById(update);
log.info("[updateTaskProgress][taskId={}, progress={}, status={}] 更新任务进度", taskId, progress, status);
}
/**
* 标记任务成功
*/
private void markTaskSuccess(Long taskId, String mergedText, String generatedPrompt, Long promptId) {
BenchmarkTaskDO update = new BenchmarkTaskDO();
update.setId(taskId);
update.setStatus(BenchmarkTaskStatusEnum.SUCCESS.getStatus());
update.setProgress(100);
update.setMergedText(mergedText);
update.setGeneratedPrompt(generatedPrompt);
update.setPromptId(promptId);
update.setFinishTime(LocalDateTime.now());
benchmarkTaskMapper.updateById(update);
log.info("[markTaskSuccess][taskId={}, promptId={}] 任务执行成功", taskId, promptId);
}
/**
* 标记任务失败
*/
private void markTaskFailed(Long taskId, String errorMsg) {
BenchmarkTaskDO update = new BenchmarkTaskDO();
update.setId(taskId);
update.setStatus(BenchmarkTaskStatusEnum.FAILED.getStatus());
update.setErrorMsg(StrUtil.sub(errorMsg, 0, 1000));
update.setFinishTime(LocalDateTime.now());
benchmarkTaskMapper.updateById(update);
log.error("[markTaskFailed][taskId={}, errorMsg={}] 任务执行失败", taskId, errorMsg);
}
/**
* 保存生成的提示词
*/
private Long saveGeneratedPrompt(Long userId, String promptName, String content) {
UserPromptSaveReqVO saveReqVO = new UserPromptSaveReqVO();
saveReqVO.setUserId(userId);
saveReqVO.setName(promptName);
saveReqVO.setContent(content);
saveReqVO.setStatus(1);
saveReqVO.setIsPublic(false);
return userPromptService.createUserPrompt(saveReqVO);
}
}

View File

@@ -0,0 +1,84 @@
package cn.iocoder.yudao.module.tik.benchmark.service;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.tik.benchmark.dal.dataobject.BenchmarkTaskDO;
import cn.iocoder.yudao.module.tik.benchmark.vo.BenchmarkTaskCreateReqVO;
import cn.iocoder.yudao.module.tik.benchmark.vo.BenchmarkTaskPageReqVO;
import jakarta.validation.Valid;
/**
* 对标分析任务 Service 接口
*/
public interface BenchmarkTaskService {
/**
* 创建对标分析任务
*
* @param userId 用户ID
* @param createReqVO 创建请求
* @return 任务ID
*/
Long createTask(Long userId, @Valid BenchmarkTaskCreateReqVO createReqVO);
/**
* 获取任务详情
*
* @param id 任务ID
* @return 任务详情
*/
BenchmarkTaskDO getTask(Long id);
/**
* 获取任务分页列表
*
* @param pageReqVO 分页请求
* @return 分页列表
*/
PageResult<BenchmarkTaskDO> getTaskPage(BenchmarkTaskPageReqVO pageReqVO);
/**
* 删除任务
*
* @param userId 用户ID
* @param id 任务ID
*/
void deleteTask(Long userId, Long id);
/**
* 更新任务进度
*
* @param taskId 任务ID
* @param progress 进度
* @param status 状态
*/
void updateTaskProgress(Long taskId, Integer progress, Integer status);
/**
* 标记任务成功
*
* @param taskId 任务ID
* @param mergedText 合并文本
* @param generatedPrompt 生成的提示词
* @param promptId 保存的提示词ID可选
*/
void markTaskSuccess(Long taskId, String mergedText, String generatedPrompt, Long promptId);
/**
* 标记任务失败
*
* @param taskId 任务ID
* @param errorMsg 错误信息
*/
void markTaskFailed(Long taskId, String errorMsg);
/**
* 保存生成的提示词
*
* @param userId 用户ID
* @param promptName 提示词名称
* @param content 提示词内容
* @return 提示词ID
*/
Long saveGeneratedPrompt(Long userId, String promptName, String content);
}

View File

@@ -0,0 +1,162 @@
package cn.iocoder.yudao.module.tik.benchmark.service;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.tik.benchmark.dal.dataobject.BenchmarkTaskDO;
import cn.iocoder.yudao.module.tik.benchmark.dal.mysql.BenchmarkTaskMapper;
import cn.iocoder.yudao.module.tik.benchmark.enums.BenchmarkTaskStatusEnum;
import cn.iocoder.yudao.module.tik.benchmark.job.BenchmarkTaskExecutor;
import cn.iocoder.yudao.module.tik.benchmark.vo.BenchmarkTaskCreateReqVO;
import cn.iocoder.yudao.module.tik.benchmark.vo.BenchmarkTaskPageReqVO;
import cn.iocoder.yudao.module.tik.userprompt.service.UserPromptService;
import cn.iocoder.yudao.module.tik.userprompt.vo.UserPromptSaveReqVO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.tik.enums.ErrorCodeConstants.*;
/**
* 对标分析任务 Service 实现类
*/
@Slf4j
@Service
@Validated
public class BenchmarkTaskServiceImpl implements BenchmarkTaskService {
@Resource
private BenchmarkTaskMapper benchmarkTaskMapper;
@Resource
private BenchmarkTaskExecutor benchmarkTaskExecutor;
@Resource
private UserPromptService userPromptService;
@Override
@Transactional(rollbackFor = Exception.class)
public Long createTask(Long userId, BenchmarkTaskCreateReqVO createReqVO) {
// 1. 创建任务记录
BenchmarkTaskDO task = new BenchmarkTaskDO();
task.setUserId(userId);
task.setTaskName(createReqVO.getTaskName());
task.setVideoCount(createReqVO.getVideoUrls().size());
task.setVideoUrls(JsonUtils.toJsonString(createReqVO.getVideoUrls()));
task.setStatus(BenchmarkTaskStatusEnum.PENDING.getStatus());
task.setProgress(0);
benchmarkTaskMapper.insert(task);
Long taskId = task.getId();
log.info("[createTask][userId={}, taskId={}] 创建对标分析任务成功", userId, taskId);
// 2. 异步执行任务
benchmarkTaskExecutor.executeAsync(taskId, createReqVO);
return taskId;
}
@Override
public BenchmarkTaskDO getTask(Long id) {
return benchmarkTaskMapper.selectById(id);
}
@Override
public PageResult<BenchmarkTaskDO> getTaskPage(BenchmarkTaskPageReqVO pageReqVO) {
return benchmarkTaskMapper.selectPage(pageReqVO);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteTask(Long userId, Long id) {
// 1. 校验任务存在
BenchmarkTaskDO task = validateTaskExists(id);
// 2. 校验是否属于当前用户
if (!task.getUserId().equals(userId)) {
throw exception(BENCHMARK_TASK_NOT_EXISTS);
}
// 3. 只能删除已完成或失败的任务
if (BenchmarkTaskStatusEnum.RUNNING.getStatus().equals(task.getStatus())) {
throw exception(BENCHMARK_TASK_RUNNING);
}
// 4. 删除任务
benchmarkTaskMapper.deleteById(id);
log.info("[deleteTask][userId={}, taskId={}] 删除任务成功", userId, id);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateTaskProgress(Long taskId, Integer progress, Integer status) {
BenchmarkTaskDO update = new BenchmarkTaskDO();
update.setId(taskId);
update.setProgress(progress);
update.setStatus(status);
// 如果是开始执行,记录开始时间
if (BenchmarkTaskStatusEnum.RUNNING.getStatus().equals(status)) {
update.setStartTime(LocalDateTime.now());
}
benchmarkTaskMapper.updateById(update);
log.info("[updateTaskProgress][taskId={}, progress={}, status={}] 更新任务进度", taskId, progress, status);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void markTaskSuccess(Long taskId, String mergedText, String generatedPrompt, Long promptId) {
BenchmarkTaskDO update = new BenchmarkTaskDO();
update.setId(taskId);
update.setStatus(BenchmarkTaskStatusEnum.SUCCESS.getStatus());
update.setProgress(100);
update.setMergedText(mergedText);
update.setGeneratedPrompt(generatedPrompt);
update.setPromptId(promptId);
update.setFinishTime(LocalDateTime.now());
benchmarkTaskMapper.updateById(update);
log.info("[markTaskSuccess][taskId={}, promptId={}] 任务执行成功", taskId, promptId);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void markTaskFailed(Long taskId, String errorMsg) {
BenchmarkTaskDO update = new BenchmarkTaskDO();
update.setId(taskId);
update.setStatus(BenchmarkTaskStatusEnum.FAILED.getStatus());
update.setErrorMsg(StrUtil.sub(errorMsg, 0, 1000));
update.setFinishTime(LocalDateTime.now());
benchmarkTaskMapper.updateById(update);
log.error("[markTaskFailed][taskId={}, errorMsg={}] 任务执行失败", taskId, errorMsg);
}
@Override
public Long saveGeneratedPrompt(Long userId, String promptName, String content) {
UserPromptSaveReqVO saveReqVO = new UserPromptSaveReqVO();
saveReqVO.setUserId(userId);
saveReqVO.setName(promptName);
saveReqVO.setContent(content);
saveReqVO.setStatus(1); // 启用
saveReqVO.setIsPublic(false);
return userPromptService.createUserPrompt(saveReqVO);
}
private BenchmarkTaskDO validateTaskExists(Long id) {
BenchmarkTaskDO task = benchmarkTaskMapper.selectById(id);
if (task == null) {
throw exception(BENCHMARK_TASK_NOT_EXISTS);
}
return task;
}
}

View File

@@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.tik.benchmark.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import java.util.List;
/**
* 创建对标分析任务 Request VO
*/
@Schema(description = "用户 App - 创建对标分析任务 Request VO")
@Data
public class BenchmarkTaskCreateReqVO {
@Schema(description = "任务名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "小红书种草风格分析")
@NotEmpty(message = "任务名称不能为空")
private String taskName;
@Schema(description = "视频URL列表", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty(message = "视频URL列表不能为空")
private List<String> videoUrls;
@Schema(description = "是否保存为提示词", example = "true")
private Boolean saveAsPrompt;
@Schema(description = "提示词名称(保存时需要)", example = "小红书种草风")
private String promptName;
}

View File

@@ -0,0 +1,22 @@
package cn.iocoder.yudao.module.tik.benchmark.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 对标分析任务分页 Request VO
*/
@Schema(description = "用户 App - 对标分析任务分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class BenchmarkTaskPageReqVO extends PageParam {
@Schema(description = "用户ID内部使用", hidden = true)
private Long userId;
@Schema(description = "任务状态", example = "1")
private Integer status;
}

View File

@@ -0,0 +1,51 @@
package cn.iocoder.yudao.module.tik.benchmark.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 对标分析任务 Response VO
*/
@Schema(description = "用户 App - 对标分析任务 Response VO")
@Data
public class BenchmarkTaskRespVO {
@Schema(description = "任务ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "任务名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "小红书种草风格分析")
private String taskName;
@Schema(description = "视频数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "5")
private Integer videoCount;
@Schema(description = "任务状态0-待处理 1-处理中 2-成功 3-失败", requiredMode = Schema.RequiredMode.REQUIRED)
private Integer status;
@Schema(description = "状态名称", example = "处理中")
private String statusName;
@Schema(description = "进度(0-100)", example = "50")
private Integer progress;
@Schema(description = "生成的提示词")
private String generatedPrompt;
@Schema(description = "保存后的提示词ID", example = "2048")
private Long promptId;
@Schema(description = "错误信息")
private String errorMsg;
@Schema(description = "开始时间")
private LocalDateTime startTime;
@Schema(description = "完成时间")
private LocalDateTime finishTime;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
}

View File

@@ -52,4 +52,21 @@ public interface ErrorCodeConstants {
// ========== TikToken相关错误码 1-030-004-000 ==========
ErrorCode TIK_TOKEN_NOT_EXISTS = new ErrorCode(1_030_004_001, "TikToken不存在");
// ========== 兑换码相关错误码 1-030-005-000 ==========
ErrorCode REDEEM_CODE_NOT_EXISTS = new ErrorCode(1_030_005_001, "兑换码不存在");
ErrorCode REDEEM_CODE_DISABLED = new ErrorCode(1_030_005_002, "兑换码已禁用");
ErrorCode REDEEM_CODE_EXHAUSTED = new ErrorCode(1_030_005_003, "兑换码已用完");
ErrorCode REDEEM_CODE_EXPIRED = new ErrorCode(1_030_005_004, "兑换码已过期");
ErrorCode REDEEM_CODE_REDEEM_FAILED = new ErrorCode(1_030_005_005, "兑换码兑换失败");
// ========== 兑换码 V2 相关错误码 1-030-006-000 ==========
ErrorCode REDEMPTION_CODE_NOT_FOUND = new ErrorCode(1_030_006_001, "兑换码不存在");
ErrorCode REDEMPTION_CODE_ALREADY_USED = new ErrorCode(1_030_006_002, "兑换码已被使用");
ErrorCode REDEMPTION_CODE_EXPIRED = new ErrorCode(1_030_006_003, "兑换码已过期");
// ========== 对标分析任务 1-030-007-000 ==========
ErrorCode BENCHMARK_TASK_NOT_EXISTS = new ErrorCode(1_030_007_001, "对标分析任务不存在");
ErrorCode BENCHMARK_TASK_RUNNING = new ErrorCode(1_030_007_002, "任务正在执行中,无法删除");
ErrorCode BENCHMARK_TASK_NO_PERMISSION = new ErrorCode(1_030_007_003, "无权访问该任务");
}

View File

@@ -60,6 +60,19 @@ 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 points 增加积分数量(正数)
* @return 影响行数
*/
@Update("UPDATE muye_member_user_profile " +
"SET remaining_points = remaining_points + #{points}, " +
" total_points = total_points + #{points}, " +
" update_time = NOW() " +
"WHERE user_id = #{userId}")
int updatePointsIncrease(@Param("userId") String userId, @Param("points") Integer points);
/**
* 原子增加已用存储(乐观锁)
* @param userId 用户ID

View File

@@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.tik.muye.redeemcode;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.module.tik.muye.redeemcode.service.RedeemCodeService;
import cn.iocoder.yudao.module.tik.muye.redeemcode.vo.RedeemCodeRedeemReqVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
* 用户 App - 兑换码
*
* @author muye
*/
@Tag(name = "用户 App - 兑换码")
@RestController
@RequestMapping("/api/tik/redeem-code")
@Validated
public class AppRedeemCodeController {
@Resource
private RedeemCodeService redeemCodeService;
@PostMapping("/redeem")
@Operation(summary = "兑换码兑换")
public CommonResult<Long> redeem(@Valid @RequestBody RedeemCodeRedeemReqVO reqVO) {
Long userId = SecurityFrameworkUtils.getLoginUserId();
Long recordId = redeemCodeService.redeem(String.valueOf(userId), reqVO.getCode());
return success(recordId);
}
}

View File

@@ -0,0 +1,111 @@
package cn.iocoder.yudao.module.tik.muye.redeemcode;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.tik.muye.redeemcode.dal.RedeemCodeDO;
import cn.iocoder.yudao.module.tik.muye.redeemcode.service.RedeemCodeService;
import cn.iocoder.yudao.module.tik.muye.redeemcode.vo.*;
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 jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.List;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
* 管理后台 - 兑换码
*
* @author muye
*/
@Tag(name = "管理后台 - 兑换码")
@RestController
@RequestMapping("/admin-api/muye/redeem-code")
@Validated
public class RedeemCodeController {
@Resource
private RedeemCodeService redeemCodeService;
@PostMapping("/create")
@Operation(summary = "创建兑换码")
@PreAuthorize("@ss.hasPermission('muye:redeem-code:create')")
public CommonResult<Long> createRedeemCode(@Valid @RequestBody RedeemCodeSaveReqVO createReqVO) {
return success(redeemCodeService.createRedeemCode(createReqVO));
}
@PostMapping("/generate")
@Operation(summary = "批量生成兑换码")
@PreAuthorize("@ss.hasPermission('muye:redeem-code:create')")
public CommonResult<List<String>> generateRedeemCodes(@Valid @RequestBody RedeemCodeGenerateReqVO generateReqVO) {
return success(redeemCodeService.generateRedeemCodes(generateReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新兑换码")
@PreAuthorize("@ss.hasPermission('muye:redeem-code:update')")
public CommonResult<Boolean> updateRedeemCode(@Valid @RequestBody RedeemCodeSaveReqVO updateReqVO) {
redeemCodeService.updateRedeemCode(updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除兑换码")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('muye:redeem-code:delete')")
public CommonResult<Boolean> deleteRedeemCode(@RequestParam("id") Long id) {
redeemCodeService.deleteRedeemCode(id);
return success(true);
}
@DeleteMapping("/delete-list")
@Operation(summary = "批量删除兑换码")
@Parameter(name = "ids", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('muye:redeem-code:delete')")
public CommonResult<Boolean> deleteRedeemCodeList(@RequestParam("ids") List<Long> ids) {
redeemCodeService.deleteRedeemCodeListByIds(ids);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得兑换码")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('muye:redeem-code:query')")
public CommonResult<RedeemCodeRespVO> getRedeemCode(@RequestParam("id") Long id) {
RedeemCodeDO redeemCode = redeemCodeService.getRedeemCode(id);
return success(BeanUtils.toBean(redeemCode, RedeemCodeRespVO.class));
}
@GetMapping("/page")
@Operation(summary = "获得兑换码分页")
@PreAuthorize("@ss.hasPermission('muye:redeem-code:query')")
public CommonResult<PageResult<RedeemCodeRespVO>> getRedeemCodePage(@Valid RedeemCodePageReqVO pageReqVO) {
PageResult<RedeemCodeDO> pageResult = redeemCodeService.getRedeemCodePage(pageReqVO);
return success(BeanUtils.toBean(pageResult, RedeemCodeRespVO.class));
}
@GetMapping("/export-excel")
@Operation(summary = "导出兑换码 Excel")
@PreAuthorize("@ss.hasPermission('muye:redeem-code:export')")
@ApiAccessLog(operateType = EXPORT)
public void exportRedeemCodeExcel(@Valid RedeemCodePageReqVO pageReqVO,
HttpServletResponse response) throws IOException {
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
List<RedeemCodeDO> list = redeemCodeService.getRedeemCodePage(pageReqVO).getList();
ExcelUtils.write(response, "兑换码.xls", "数据", RedeemCodeRespVO.class,
BeanUtils.toBean(list, RedeemCodeRespVO.class));
}
}

View File

@@ -0,0 +1,103 @@
package cn.iocoder.yudao.module.tik.muye.redeemcode.dal;
import lombok.*;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.*;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
/**
* 兑换码 DO
*
* @author muye
*/
@TableName("muye_redeem_code")
@KeySequence("muye_redeem_code_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RedeemCodeDO extends BaseDO {
/**
* 主键
*/
@TableId
private Long id;
/**
* 兑换码(唯一)
*/
private String code;
/**
* 兑换码类型(single-单次使用 multi-多次使用)
*/
private String codeType;
/**
* 积分数量
*/
private Integer points;
/**
* 最大使用次数(单次为1)
*/
private Integer maxUseCount;
/**
* 已使用次数
*/
private Integer usedCount;
/**
* 批次号(用于批量生成)
*/
private String batchNo;
/**
* 来源(admin-后台生成 pay-支付发放 gift-礼包赠送)
*/
private String source;
/**
* 来源关联ID(支付订单ID/礼包ID等)
*/
private String sourceId;
/**
* 过期时间(NULL表示永不过期)
*/
private LocalDateTime expireTime;
/**
* 状态(0-禁用 1-启用 2-已用完)
*/
private Integer status;
/**
* 备注
*/
private String remark;
/**
* 操作人用户编号
*/
private Long operatorId;
/**
* 操作人账号
*/
private String operatorName;
// ========== 状态常量 ==========
/** 状态:禁用 */
public static final int STATUS_DISABLED = 0;
/** 状态:启用 */
public static final int STATUS_ENABLED = 1;
/** 状态:已用完 */
public static final int STATUS_EXHAUSTED = 2;
// ========== 兑换码类型常量 ==========
/** 类型:单次使用 */
public static final String CODE_TYPE_SINGLE = "single";
/** 类型:多次使用 */
public static final String CODE_TYPE_MULTI = "multi";
// ========== 来源常量 ==========
/** 来源:后台生成 */
public static final String SOURCE_ADMIN = "admin";
/** 来源:支付发放 */
public static final String SOURCE_PAY = "pay";
/** 来源:礼包赠送 */
public static final String SOURCE_GIFT = "gift";
}

View File

@@ -0,0 +1,50 @@
package cn.iocoder.yudao.module.tik.muye.redeemcode.mapper;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.tik.muye.redeemcode.dal.RedeemCodeDO;
import cn.iocoder.yudao.module.tik.muye.redeemcode.vo.RedeemCodePageReqVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
/**
* 兑换码 Mapper
*
* @author muye
*/
@Mapper
public interface RedeemCodeMapper extends BaseMapperX<RedeemCodeDO> {
/**
* 根据兑换码查询
*/
default RedeemCodeDO selectByCode(String code) {
return selectOne(new LambdaQueryWrapperX<RedeemCodeDO>()
.eq(RedeemCodeDO::getCode, code));
}
/**
* 根据兑换码查询(带悲观锁)
*/
@Select("SELECT * FROM muye_redeem_code WHERE code = #{code} FOR UPDATE")
RedeemCodeDO selectByCodeForUpdate(@Param("code") String code);
/**
* 分页查询
*/
default PageResult<RedeemCodeDO> selectPage(RedeemCodePageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<RedeemCodeDO>()
.eqIfPresent(RedeemCodeDO::getCode, reqVO.getCode())
.eqIfPresent(RedeemCodeDO::getCodeType, reqVO.getCodeType())
.eqIfPresent(RedeemCodeDO::getPoints, reqVO.getPoints())
.eqIfPresent(RedeemCodeDO::getBatchNo, reqVO.getBatchNo())
.eqIfPresent(RedeemCodeDO::getSource, reqVO.getSource())
.eqIfPresent(RedeemCodeDO::getStatus, reqVO.getStatus())
.betweenIfPresent(RedeemCodeDO::getCreateTime, reqVO.getCreateTime())
.betweenIfPresent(RedeemCodeDO::getExpireTime, reqVO.getExpireTime())
.orderByDesc(RedeemCodeDO::getId));
}
}

View File

@@ -0,0 +1,97 @@
package cn.iocoder.yudao.module.tik.muye.redeemcode.service;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.tik.muye.redeemcode.dal.RedeemCodeDO;
import cn.iocoder.yudao.module.tik.muye.redeemcode.vo.*;
import java.util.List;
/**
* 兑换码 Service 接口
*
* @author muye
*/
public interface RedeemCodeService {
/**
* 创建兑换码(单个)
*
* @param createReqVO 创建信息
* @return 编号
*/
Long createRedeemCode(RedeemCodeSaveReqVO createReqVO);
/**
* 批量生成兑换码
*
* @param generateReqVO 生成信息
* @return 生成的兑换码列表
*/
List<String> generateRedeemCodes(RedeemCodeGenerateReqVO generateReqVO);
/**
* 更新兑换码
*
* @param updateReqVO 更新信息
*/
void updateRedeemCode(RedeemCodeSaveReqVO updateReqVO);
/**
* 删除兑换码
*
* @param id 编号
*/
void deleteRedeemCode(Long id);
/**
* 批量删除兑换码
*
* @param ids 编号列表
*/
void deleteRedeemCodeListByIds(List<Long> ids);
/**
* 获得兑换码
*
* @param id 编号
* @return 兑换码
*/
RedeemCodeDO getRedeemCode(Long id);
/**
* 根据兑换码字符串获取
*
* @param code 兑换码
* @return 兑换码
*/
RedeemCodeDO getRedeemCodeByCode(String code);
/**
* 获得兑换码分页
*
* @param pageReqVO 分页查询
* @return 兑换码分页
*/
PageResult<RedeemCodeDO> getRedeemCodePage(RedeemCodePageReqVO pageReqVO);
/**
* 兑换码兑换(核心业务)
* 验证兑换码有效性,增加用户积分,记录兑换流水
*
* @param userId 用户ID
* @param code 兑换码
* @return 兑换记录ID
*/
Long redeem(String userId, String code);
/**
* 支付成功后发放兑换码(支付集成用)
*
* @param userId 用户ID
* @param points 积分数量
* @param sourceId 支付订单ID
* @return 兑换码
*/
String issueCodeAfterPayment(Long userId, Integer points, String sourceId);
}

View File

@@ -0,0 +1,302 @@
package cn.iocoder.yudao.module.tik.muye.redeemcode.service;
import cn.hutool.core.util.IdUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
import cn.iocoder.yudao.module.tik.muye.memberuserprofile.dal.MemberUserProfileDO;
import cn.iocoder.yudao.module.tik.muye.memberuserprofile.mapper.MemberUserProfileMapper;
import cn.iocoder.yudao.module.tik.muye.pointrecord.dal.PointRecordDO;
import cn.iocoder.yudao.module.tik.muye.pointrecord.mapper.PointRecordMapper;
import cn.iocoder.yudao.module.tik.muye.redeemcode.dal.RedeemCodeDO;
import cn.iocoder.yudao.module.tik.muye.redeemcode.mapper.RedeemCodeMapper;
import cn.iocoder.yudao.module.tik.muye.redeemcode.vo.*;
import cn.iocoder.yudao.module.tik.muye.redeemrecord.dal.RedeemRecordDO;
import cn.iocoder.yudao.module.tik.muye.redeemrecord.mapper.RedeemRecordMapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.tik.enums.ErrorCodeConstants.*;
/**
* 兑换码 Service 实现类
*
* @author muye
*/
@Service
@Validated
@Slf4j
public class RedeemCodeServiceImpl implements RedeemCodeService {
@Resource
private RedeemCodeMapper redeemCodeMapper;
@Resource
private RedeemRecordMapper redeemRecordMapper;
@Resource
private MemberUserProfileMapper memberUserProfileMapper;
@Resource
private PointRecordMapper pointRecordMapper;
@Resource
private MemberUserApi memberUserApi;
@Override
public Long createRedeemCode(RedeemCodeSaveReqVO createReqVO) {
// 生成唯一兑换码
String code = createReqVO.getCode() != null ? createReqVO.getCode() : generateUniqueCode();
RedeemCodeDO redeemCode = BeanUtils.toBean(createReqVO, RedeemCodeDO.class);
redeemCode.setCode(code);
redeemCode.setUsedCount(0);
// 设置默认值
if (redeemCode.getCodeType() == null) {
redeemCode.setCodeType(RedeemCodeDO.CODE_TYPE_SINGLE);
}
if (redeemCode.getMaxUseCount() == null) {
redeemCode.setMaxUseCount(1);
}
if (redeemCode.getSource() == null) {
redeemCode.setSource(RedeemCodeDO.SOURCE_ADMIN);
}
if (redeemCode.getStatus() == null) {
redeemCode.setStatus(RedeemCodeDO.STATUS_ENABLED);
}
redeemCodeMapper.insert(redeemCode);
return redeemCode.getId();
}
@Override
public List<String> generateRedeemCodes(RedeemCodeGenerateReqVO generateReqVO) {
List<String> codes = new ArrayList<>();
String batchNo = IdUtil.fastSimpleUUID().substring(0, 16);
for (int i = 0; i < generateReqVO.getCount(); i++) {
String code = generateUniqueCode();
codes.add(code);
RedeemCodeDO redeemCode = RedeemCodeDO.builder()
.code(code)
.codeType(generateReqVO.getCodeType() != null ? generateReqVO.getCodeType() : RedeemCodeDO.CODE_TYPE_SINGLE)
.points(generateReqVO.getPoints())
.maxUseCount(generateReqVO.getMaxUseCount() != null ? generateReqVO.getMaxUseCount() : 1)
.usedCount(0)
.batchNo(batchNo)
.source(RedeemCodeDO.SOURCE_ADMIN)
.expireTime(generateReqVO.getExpireTime())
.status(RedeemCodeDO.STATUS_ENABLED)
.remark(generateReqVO.getRemark())
.operatorId(generateReqVO.getOperatorId())
.operatorName(generateReqVO.getOperatorName())
.build();
redeemCodeMapper.insert(redeemCode);
}
log.info("[generateRedeemCodes] 批量生成 {} 个兑换码,批次号 {}", generateReqVO.getCount(), batchNo);
return codes;
}
@Override
public void updateRedeemCode(RedeemCodeSaveReqVO updateReqVO) {
validateRedeemCodeExists(updateReqVO.getId());
RedeemCodeDO updateObj = BeanUtils.toBean(updateReqVO, RedeemCodeDO.class);
redeemCodeMapper.updateById(updateObj);
}
@Override
public void deleteRedeemCode(Long id) {
validateRedeemCodeExists(id);
redeemCodeMapper.deleteById(id);
}
@Override
public void deleteRedeemCodeListByIds(List<Long> ids) {
redeemCodeMapper.deleteByIds(ids);
}
@Override
public RedeemCodeDO getRedeemCode(Long id) {
return redeemCodeMapper.selectById(id);
}
@Override
public RedeemCodeDO getRedeemCodeByCode(String code) {
return redeemCodeMapper.selectByCode(code);
}
@Override
public PageResult<RedeemCodeDO> getRedeemCodePage(RedeemCodePageReqVO pageReqVO) {
return redeemCodeMapper.selectPage(pageReqVO);
}
@Override
@Transactional(rollbackFor = Exception.class)
public Long redeem(String userId, String code) {
// 1. 查询兑换码(带悲观锁)
RedeemCodeDO redeemCode = redeemCodeMapper.selectByCodeForUpdate(code);
if (redeemCode == null) {
throw exception(REDEEM_CODE_NOT_EXISTS);
}
// 2. 校验状态
if (RedeemCodeDO.STATUS_DISABLED == redeemCode.getStatus()) {
throw exception(REDEEM_CODE_DISABLED);
}
if (RedeemCodeDO.STATUS_EXHAUSTED == redeemCode.getStatus()) {
throw exception(REDEEM_CODE_EXHAUSTED);
}
// 3. 校验过期时间
if (redeemCode.getExpireTime() != null && LocalDateTime.now().isAfter(redeemCode.getExpireTime())) {
throw exception(REDEEM_CODE_EXPIRED);
}
// 4. 校验使用次数
if (redeemCode.getUsedCount() >= redeemCode.getMaxUseCount()) {
throw exception(REDEEM_CODE_EXHAUSTED);
}
// 5. 获取用户信息
MemberUserRespDTO user = memberUserApi.getUser(Long.parseLong(userId));
String mobile = user != null ? user.getMobile() : "";
// 6. 原子增加积分
int affectedRows = memberUserProfileMapper.updatePointsIncrease(userId, redeemCode.getPoints());
if (affectedRows == 0) {
// 用户档案不存在,创建后重试
createProfileIfAbsent(Long.parseLong(userId), mobile);
affectedRows = memberUserProfileMapper.updatePointsIncrease(userId, redeemCode.getPoints());
if (affectedRows == 0) {
throw exception(REDEEM_CODE_REDEEM_FAILED);
}
}
// 7. 查询兑换后余额
MemberUserProfileDO profile = memberUserProfileMapper.selectByUserId(userId);
// 8. 创建积分记录
PointRecordDO pointRecord = PointRecordDO.builder()
.userId(Long.parseLong(userId))
.mobile(mobile)
.type("increase")
.pointAmount(redeemCode.getPoints())
.balance(profile.getRemainingPoints())
.reason("兑换码兑换")
.bizType("redeem")
.bizId(String.valueOf(redeemCode.getId()))
.status("confirmed")
.build();
pointRecordMapper.insert(pointRecord);
// 9. 创建兑换记录
RedeemRecordDO redeemRecord = RedeemRecordDO.builder()
.userId(Long.parseLong(userId))
.mobile(mobile)
.codeId(redeemCode.getId())
.code(redeemCode.getCode())
.points(redeemCode.getPoints())
.balanceAfter(profile.getRemainingPoints())
.source(redeemCode.getSource())
.sourceId(redeemCode.getSourceId())
.remark("兑换码兑换")
.build();
redeemRecordMapper.insert(redeemRecord);
// 10. 更新兑换码使用次数和状态
redeemCode.setUsedCount(redeemCode.getUsedCount() + 1);
if (redeemCode.getUsedCount() >= redeemCode.getMaxUseCount()) {
redeemCode.setStatus(RedeemCodeDO.STATUS_EXHAUSTED);
}
redeemCodeMapper.updateById(redeemCode);
log.info("[redeem] 用户 {} 使用兑换码 {} 兑换 {} 积分", userId, code, redeemCode.getPoints());
return redeemRecord.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public String issueCodeAfterPayment(Long userId, Integer points, String sourceId) {
String code = generateUniqueCode();
RedeemCodeDO redeemCode = RedeemCodeDO.builder()
.code(code)
.codeType(RedeemCodeDO.CODE_TYPE_SINGLE)
.points(points)
.maxUseCount(1)
.usedCount(0)
.source(RedeemCodeDO.SOURCE_PAY)
.sourceId(sourceId)
.status(RedeemCodeDO.STATUS_ENABLED)
.remark("支付成功自动发放")
.build();
redeemCodeMapper.insert(redeemCode);
log.info("[issueCodeAfterPayment] 支付订单 {} 发放兑换码 {},积分 {}", sourceId, code, points);
return code;
}
/**
* 生成唯一兑换码
*/
private String generateUniqueCode() {
String code;
int maxAttempts = 10;
int attempts = 0;
do {
// 生成 16 位大写字母+数字组合
code = IdUtil.fastSimpleUUID().substring(0, 16).toUpperCase();
attempts++;
if (attempts >= maxAttempts) {
throw new RuntimeException("无法生成唯一兑换码");
}
} while (redeemCodeMapper.selectByCode(code) != null);
return code;
}
/**
* 校验兑换码是否存在
*/
private void validateRedeemCodeExists(Long id) {
if (redeemCodeMapper.selectById(id) == null) {
throw exception(REDEEM_CODE_NOT_EXISTS);
}
}
/**
* 如果用户档案不存在则创建
*/
private void createProfileIfAbsent(Long userId, String mobile) {
MemberUserProfileDO profile = memberUserProfileMapper.selectByUserId(userId);
if (profile == null) {
profile = MemberUserProfileDO.builder()
.userId(String.valueOf(userId))
.mobile(mobile)
.registerTime(LocalDateTime.now())
.totalPoints(0)
.usedPoints(0)
.remainingPoints(0)
.status(1)
.build();
memberUserProfileMapper.insert(profile);
}
}
}

View File

@@ -0,0 +1,41 @@
package cn.iocoder.yudao.module.tik.muye.redeemcode.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.*;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 兑换码批量生成 Request VO")
@Data
public class RedeemCodeGenerateReqVO {
@Schema(description = "生成数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
@NotNull(message = "生成数量不能为空")
@Min(value = 1, message = "生成数量必须大于0")
@Max(value = 10000, message = "单次最多生成10000个兑换码")
private Integer count;
@Schema(description = "积分数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
@NotNull(message = "积分数量不能为空")
@Min(value = 1, message = "积分数量必须大于0")
private Integer points;
@Schema(description = "兑换码类型", example = "single")
private String codeType;
@Schema(description = "最大使用次数(多次使用时有效)", example = "100")
private Integer maxUseCount;
@Schema(description = "过期时间")
private LocalDateTime expireTime;
@Schema(description = "备注", example = "新年活动兑换码")
private String remark;
@Schema(description = "操作人用户编号", example = "1")
private Long operatorId;
@Schema(description = "操作人账号", example = "admin")
private String operatorName;
}

View File

@@ -0,0 +1,41 @@
package cn.iocoder.yudao.module.tik.muye.redeemcode.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - 兑换码分页 Request VO")
@Data
public class RedeemCodePageReqVO extends PageParam {
@Schema(description = "兑换码", example = "ABCD1234EFGH5678")
private String code;
@Schema(description = "兑换码类型", example = "single")
private String codeType;
@Schema(description = "积分数量", example = "100")
private Integer points;
@Schema(description = "批次号", example = "batch20250101")
private String batchNo;
@Schema(description = "来源", example = "admin")
private String source;
@Schema(description = "状态", example = "1")
private Integer status;
@Schema(description = "创建时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
@Schema(description = "过期时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] expireTime;
}

View File

@@ -0,0 +1,16 @@
package cn.iocoder.yudao.module.tik.muye.redeemcode.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.*;
@Schema(description = "用户 App - 兑换码兑换 Request VO")
@Data
public class RedeemCodeRedeemReqVO {
@Schema(description = "兑换码", requiredMode = Schema.RequiredMode.REQUIRED, example = "ABCD1234EFGH5678")
@NotBlank(message = "兑换码不能为空")
@Size(min = 8, max = 32, message = "兑换码长度必须在8-32位之间")
private String code;
}

View File

@@ -0,0 +1,56 @@
package cn.iocoder.yudao.module.tik.muye.redeemcode.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 兑换码 Response VO")
@Data
public class RedeemCodeRespVO {
@Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id;
@Schema(description = "兑换码", requiredMode = Schema.RequiredMode.REQUIRED, example = "ABCD1234EFGH5678")
private String code;
@Schema(description = "兑换码类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "single")
private String codeType;
@Schema(description = "积分数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
private Integer points;
@Schema(description = "最大使用次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer maxUseCount;
@Schema(description = "已使用次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
private Integer usedCount;
@Schema(description = "批次号", example = "batch20250101")
private String batchNo;
@Schema(description = "来源", example = "admin")
private String source;
@Schema(description = "来源关联ID", example = "123456")
private String sourceId;
@Schema(description = "过期时间")
private LocalDateTime expireTime;
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer status;
@Schema(description = "备注", example = "新年活动兑换码")
private String remark;
@Schema(description = "操作人用户编号", example = "1")
private Long operatorId;
@Schema(description = "操作人账号", example = "admin")
private String operatorName;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,53 @@
package cn.iocoder.yudao.module.tik.muye.redeemcode.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.*;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 兑换码新增/修改 Request VO")
@Data
public class RedeemCodeSaveReqVO {
@Schema(description = "主键", example = "1")
private Long id;
@Schema(description = "兑换码(为空则自动生成)", example = "ABCD1234EFGH5678")
private String code;
@Schema(description = "兑换码类型", example = "single")
private String codeType;
@Schema(description = "积分数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
@NotNull(message = "积分数量不能为空")
@Min(value = 1, message = "积分数量必须大于0")
private Integer points;
@Schema(description = "最大使用次数", example = "1")
private Integer maxUseCount;
@Schema(description = "批次号", example = "batch20250101")
private String batchNo;
@Schema(description = "来源", example = "admin")
private String source;
@Schema(description = "来源关联ID", example = "123456")
private String sourceId;
@Schema(description = "过期时间")
private LocalDateTime expireTime;
@Schema(description = "状态", example = "1")
private Integer status;
@Schema(description = "备注", example = "新年活动兑换码")
private String remark;
@Schema(description = "操作人用户编号", example = "1")
private Long operatorId;
@Schema(description = "操作人账号", example = "admin")
private String operatorName;
}

View File

@@ -0,0 +1,42 @@
package cn.iocoder.yudao.module.tik.muye.redeemrecord;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.module.tik.muye.redeemrecord.dal.RedeemRecordDO;
import cn.iocoder.yudao.module.tik.muye.redeemrecord.service.RedeemRecordService;
import cn.iocoder.yudao.module.tik.muye.redeemrecord.vo.RedeemRecordPageReqVO;
import cn.iocoder.yudao.module.tik.muye.redeemrecord.vo.RedeemRecordRespVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
* 用户 App - 兑换记录
*
* @author muye
*/
@Tag(name = "用户 App - 兑换记录")
@RestController
@RequestMapping("/api/tik/redeem-record")
@Validated
public class AppRedeemRecordController {
@Resource
private RedeemRecordService redeemRecordService;
@GetMapping("/my-page")
@Operation(summary = "获得我的兑换记录分页")
public CommonResult<PageResult<RedeemRecordRespVO>> getMyRedeemRecordPage(@Valid RedeemRecordPageReqVO pageReqVO) {
Long userId = SecurityFrameworkUtils.getLoginUserId();
PageResult<RedeemRecordDO> pageResult = redeemRecordService.getRedeemRecordPageByUserId(userId, pageReqVO);
return success(BeanUtils.toBean(pageResult, RedeemRecordRespVO.class));
}
}

View File

@@ -0,0 +1,42 @@
package cn.iocoder.yudao.module.tik.muye.redeemrecord;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.tik.muye.redeemrecord.dal.RedeemRecordDO;
import cn.iocoder.yudao.module.tik.muye.redeemrecord.service.RedeemRecordService;
import cn.iocoder.yudao.module.tik.muye.redeemrecord.vo.RedeemRecordPageReqVO;
import cn.iocoder.yudao.module.tik.muye.redeemrecord.vo.RedeemRecordRespVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
* 管理后台 - 兑换记录
*
* @author muye
*/
@Tag(name = "管理后台 - 兑换记录")
@RestController
@RequestMapping("/admin-api/muye/redeem-record")
@Validated
public class RedeemRecordController {
@Resource
private RedeemRecordService redeemRecordService;
@GetMapping("/page")
@Operation(summary = "获得兑换记录分页")
@PreAuthorize("@ss.hasPermission('muye:redeem-record:query')")
public CommonResult<PageResult<RedeemRecordRespVO>> getRedeemRecordPage(@Valid RedeemRecordPageReqVO pageReqVO) {
PageResult<RedeemRecordDO> pageResult = redeemRecordService.getRedeemRecordPage(pageReqVO);
return success(BeanUtils.toBean(pageResult, RedeemRecordRespVO.class));
}
}

View File

@@ -0,0 +1,64 @@
package cn.iocoder.yudao.module.tik.muye.redeemrecord.dal;
import lombok.*;
import com.baomidou.mybatisplus.annotation.*;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
/**
* 兑换记录 DO
*
* @author muye
*/
@TableName("muye_redeem_record")
@KeySequence("muye_redeem_record_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RedeemRecordDO extends BaseDO {
/**
* 主键
*/
@TableId
private Long id;
/**
* 用户编号
*/
private Long userId;
/**
* 手机号
*/
private String mobile;
/**
* 兑换码ID
*/
private Long codeId;
/**
* 兑换码
*/
private String code;
/**
* 获得积分
*/
private Integer points;
/**
* 兑换后积分余额
*/
private Integer balanceAfter;
/**
* 来源(继承自兑换码)
*/
private String source;
/**
* 来源关联ID
*/
private String sourceId;
/**
* 备注
*/
private String remark;
}

View File

@@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.tik.muye.redeemrecord.mapper;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.tik.muye.redeemrecord.dal.RedeemRecordDO;
import cn.iocoder.yudao.module.tik.muye.redeemrecord.vo.RedeemRecordPageReqVO;
import org.apache.ibatis.annotations.Mapper;
/**
* 兑换记录 Mapper
*
* @author muye
*/
@Mapper
public interface RedeemRecordMapper extends BaseMapperX<RedeemRecordDO> {
/**
* 分页查询
*/
default PageResult<RedeemRecordDO> selectPage(RedeemRecordPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<RedeemRecordDO>()
.eqIfPresent(RedeemRecordDO::getUserId, reqVO.getUserId())
.eqIfPresent(RedeemRecordDO::getMobile, reqVO.getMobile())
.eqIfPresent(RedeemRecordDO::getCodeId, reqVO.getCodeId())
.eqIfPresent(RedeemRecordDO::getCode, reqVO.getCode())
.eqIfPresent(RedeemRecordDO::getSource, reqVO.getSource())
.betweenIfPresent(RedeemRecordDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(RedeemRecordDO::getId));
}
}

View File

@@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.tik.muye.redeemrecord.service;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.tik.muye.redeemrecord.dal.RedeemRecordDO;
import cn.iocoder.yudao.module.tik.muye.redeemrecord.vo.RedeemRecordPageReqVO;
/**
* 兑换记录 Service 接口
*
* @author muye
*/
public interface RedeemRecordService {
/**
* 获得兑换记录分页
*
* @param pageReqVO 分页查询
* @return 兑换记录分页
*/
PageResult<RedeemRecordDO> getRedeemRecordPage(RedeemRecordPageReqVO pageReqVO);
/**
* 获得用户的兑换记录分页
*
* @param userId 用户ID
* @param pageReqVO 分页查询
* @return 兑换记录分页
*/
PageResult<RedeemRecordDO> getRedeemRecordPageByUserId(Long userId, RedeemRecordPageReqVO pageReqVO);
}

View File

@@ -0,0 +1,34 @@
package cn.iocoder.yudao.module.tik.muye.redeemrecord.service;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.tik.muye.redeemrecord.dal.RedeemRecordDO;
import cn.iocoder.yudao.module.tik.muye.redeemrecord.mapper.RedeemRecordMapper;
import cn.iocoder.yudao.module.tik.muye.redeemrecord.vo.RedeemRecordPageReqVO;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
/**
* 兑换记录 Service 实现类
*
* @author muye
*/
@Service
@Validated
public class RedeemRecordServiceImpl implements RedeemRecordService {
@Resource
private RedeemRecordMapper redeemRecordMapper;
@Override
public PageResult<RedeemRecordDO> getRedeemRecordPage(RedeemRecordPageReqVO pageReqVO) {
return redeemRecordMapper.selectPage(pageReqVO);
}
@Override
public PageResult<RedeemRecordDO> getRedeemRecordPageByUserId(Long userId, RedeemRecordPageReqVO pageReqVO) {
pageReqVO.setUserId(userId);
return redeemRecordMapper.selectPage(pageReqVO);
}
}

View File

@@ -0,0 +1,34 @@
package cn.iocoder.yudao.module.tik.muye.redeemrecord.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - 兑换记录分页 Request VO")
@Data
public class RedeemRecordPageReqVO extends PageParam {
@Schema(description = "用户编号", example = "1")
private Long userId;
@Schema(description = "手机号", example = "13800138000")
private String mobile;
@Schema(description = "兑换码ID", example = "1")
private Long codeId;
@Schema(description = "兑换码", example = "ABCD1234EFGH5678")
private String code;
@Schema(description = "来源", example = "admin")
private String source;
@Schema(description = "创建时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
}

View File

@@ -0,0 +1,44 @@
package cn.iocoder.yudao.module.tik.muye.redeemrecord.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 兑换记录 Response VO")
@Data
public class RedeemRecordRespVO {
@Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id;
@Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long userId;
@Schema(description = "手机号", example = "13800138000")
private String mobile;
@Schema(description = "兑换码ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long codeId;
@Schema(description = "兑换码", requiredMode = Schema.RequiredMode.REQUIRED, example = "ABCD1234EFGH5678")
private String code;
@Schema(description = "获得积分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
private Integer points;
@Schema(description = "兑换后积分余额", requiredMode = Schema.RequiredMode.REQUIRED, example = "500")
private Integer balanceAfter;
@Schema(description = "来源", example = "admin")
private String source;
@Schema(description = "来源关联ID", example = "123456")
private String sourceId;
@Schema(description = "备注", example = "兑换成功")
private String remark;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.tik.muye.redemptioncode;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.module.tik.muye.redemptioncode.service.RedemptionCodeService;
import cn.iocoder.yudao.module.tik.muye.redemptioncode.vo.RedemptionCodeRedeemReqVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
* 用户 App - 兑换码
*
* @author muye
*/
@Tag(name = "用户 App - 兑换码")
@RestController
@RequestMapping("/app-api/muye/redemption-code")
@Validated
public class AppRedemptionCodeController {
@Resource
private RedemptionCodeService redemptionCodeService;
@PostMapping("/redeem")
@Operation(summary = "兑换码兑换")
public CommonResult<Integer> redeem(@Valid @RequestBody RedemptionCodeRedeemReqVO reqVO) {
Long userId = SecurityFrameworkUtils.getLoginUserId();
Integer amount = redemptionCodeService.redeem(userId, reqVO.getCode());
return success(amount);
}
}

View File

@@ -0,0 +1,111 @@
package cn.iocoder.yudao.module.tik.muye.redemptioncode;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.tik.muye.redemptioncode.dal.RedemptionCodeDO;
import cn.iocoder.yudao.module.tik.muye.redemptioncode.service.RedemptionCodeService;
import cn.iocoder.yudao.module.tik.muye.redemptioncode.vo.*;
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 jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.List;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
* 管理后台 - 兑换码
*
* @author muye
*/
@Tag(name = "管理后台 - 兑换码")
@RestController
@RequestMapping("/admin-api/muye/redemption-code")
@Validated
public class RedemptionCodeController {
@Resource
private RedemptionCodeService redemptionCodeService;
@PostMapping("/create")
@Operation(summary = "创建兑换码")
@PreAuthorize("@ss.hasPermission('muye:redemption-code:create')")
public CommonResult<Long> createRedemptionCode(@Valid @RequestBody RedemptionCodeSaveReqVO createReqVO) {
return success(redemptionCodeService.createRedemptionCode(createReqVO));
}
@PostMapping("/generate")
@Operation(summary = "批量生成兑换码")
@PreAuthorize("@ss.hasPermission('muye:redemption-code:create')")
public CommonResult<List<String>> generateRedemptionCodes(@Valid @RequestBody RedemptionCodeGenerateReqVO generateReqVO) {
return success(redemptionCodeService.generateRedemptionCodes(generateReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新兑换码")
@PreAuthorize("@ss.hasPermission('muye:redemption-code:update')")
public CommonResult<Boolean> updateRedemptionCode(@Valid @RequestBody RedemptionCodeSaveReqVO updateReqVO) {
redemptionCodeService.updateRedemptionCode(updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除兑换码")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('muye:redemption-code:delete')")
public CommonResult<Boolean> deleteRedemptionCode(@RequestParam("id") Long id) {
redemptionCodeService.deleteRedemptionCode(id);
return success(true);
}
@DeleteMapping("/delete-list")
@Operation(summary = "批量删除兑换码")
@Parameter(name = "ids", description = "编号列表", required = true)
@PreAuthorize("@ss.hasPermission('muye:redemption-code:delete')")
public CommonResult<Boolean> deleteRedemptionCodeList(@RequestParam("ids") List<Long> ids) {
redemptionCodeService.deleteRedemptionCodeListByIds(ids);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得兑换码")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('muye:redemption-code:query')")
public CommonResult<RedemptionCodeRespVO> getRedemptionCode(@RequestParam("id") Long id) {
RedemptionCodeDO redemptionCode = redemptionCodeService.getRedemptionCode(id);
return success(BeanUtils.toBean(redemptionCode, RedemptionCodeRespVO.class));
}
@GetMapping("/page")
@Operation(summary = "获得兑换码分页")
@PreAuthorize("@ss.hasPermission('muye:redemption-code:query')")
public CommonResult<PageResult<RedemptionCodeRespVO>> getRedemptionCodePage(@Valid RedemptionCodePageReqVO pageReqVO) {
PageResult<RedemptionCodeDO> pageResult = redemptionCodeService.getRedemptionCodePage(pageReqVO);
return success(BeanUtils.toBean(pageResult, RedemptionCodeRespVO.class));
}
@GetMapping("/export-excel")
@Operation(summary = "导出兑换码 Excel")
@PreAuthorize("@ss.hasPermission('muye:redemption-code:export')")
@ApiAccessLog(operateType = EXPORT)
public void exportRedemptionCodeExcel(@Valid RedemptionCodePageReqVO pageReqVO,
HttpServletResponse response) throws IOException {
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
List<RedemptionCodeDO> list = redemptionCodeService.getRedemptionCodePage(pageReqVO).getList();
ExcelUtils.write(response, "兑换码.xls", "数据", RedemptionCodeRespVO.class,
BeanUtils.toBean(list, RedemptionCodeRespVO.class));
}
}

View File

@@ -0,0 +1,92 @@
package cn.iocoder.yudao.module.tik.muye.redemptioncode.dal;
import lombok.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.*;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
/**
* 兑换码 DO
*
* @author muye
*/
@TableName("muye_redemption_code")
@KeySequence("muye_redemption_code_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RedemptionCodeDO extends BaseDO {
/**
* 主键
*/
@TableId
private Long id;
/**
* 兑换码
*/
private String code;
/**
* 类型: points-积分
*/
private String type;
/**
* 面额(积分数)
*/
private Integer amount;
/**
* 原价金额(元)
*/
private BigDecimal price;
/**
* 状态: 0-未使用 1-已使用 2-已过期
*/
private Integer status;
/**
* 批次号
*/
private String batchNo;
/**
* 批次备注
*/
private String batchRemark;
/**
* 使用者用户ID
*/
private Long userId;
/**
* 使用时间
*/
private LocalDateTime usedTime;
/**
* 过期时间
*/
private LocalDateTime expireTime;
// ========== 状态常量 ==========
/** 状态:未使用 */
public static final int STATUS_UNUSED = 0;
/** 状态:已使用 */
public static final int STATUS_USED = 1;
/** 状态:已过期 */
public static final int STATUS_EXPIRED = 2;
// ========== 类型常量 ==========
/** 类型:积分 */
public static final String TYPE_POINTS = "points";
}

View File

@@ -0,0 +1,54 @@
package cn.iocoder.yudao.module.tik.muye.redemptioncode.dal;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.tik.muye.redemptioncode.vo.RedemptionCodePageReqVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
/**
* 兑换码 Mapper
*
* @author muye
*/
@Mapper
public interface RedemptionCodeMapper extends BaseMapperX<RedemptionCodeDO> {
/**
* 根据兑换码查询
*/
default RedemptionCodeDO selectByCode(String code) {
return selectOne(new LambdaQueryWrapperX<RedemptionCodeDO>()
.eq(RedemptionCodeDO::getCode, code));
}
/**
* 使用兑换码(乐观锁)
* 只有未使用状态才能更新为已使用
*
* @param code 兑换码
* @param userId 用户ID
* @return 影响行数, */
@Update("UPDATE muye_redemption_code " +
"SET status = 1, user_id = #{userId}, used_time = NOW(), update_time = NOW() " +
"WHERE code = #{code} AND status = 0 AND deleted = 0 " +
"AND (expire_time IS NULL OR expire_time > NOW())")
int useRedemptionCode(@Param("code") String code, @Param("userId") Long userId);
/**
* 分页查询
*/
default PageResult<RedemptionCodeDO> selectPage(RedemptionCodePageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<RedemptionCodeDO>()
.eqIfPresent(RedemptionCodeDO::getCode, reqVO.getCode())
.eqIfPresent(RedemptionCodeDO::getType, reqVO.getType())
.eqIfPresent(RedemptionCodeDO::getStatus, reqVO.getStatus())
.eqIfPresent(RedemptionCodeDO::getBatchNo, reqVO.getBatchNo())
.eqIfPresent(RedemptionCodeDO::getUserId, reqVO.getUserId())
.betweenIfPresent(RedemptionCodeDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(RedemptionCodeDO::getId));
}
}

View File

@@ -0,0 +1,87 @@
package cn.iocoder.yudao.module.tik.muye.redemptioncode.service;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.tik.muye.redemptioncode.dal.RedemptionCodeDO;
import cn.iocoder.yudao.module.tik.muye.redemptioncode.vo.*;
import java.util.List;
/**
* 兑换码 Service 接口
*
* @author muye
*/
public interface RedemptionCodeService {
/**
* 创建兑换码(单个)
*
* @param createReqVO 创建信息
* @return 编号
*/
Long createRedemptionCode(RedemptionCodeSaveReqVO createReqVO);
/**
* 批量生成兑换码
*
* @param generateReqVO 生成信息
* @return 生成的兑换码列表
*/
List<String> generateRedemptionCodes(RedemptionCodeGenerateReqVO generateReqVO);
/**
* 更新兑换码
*
* @param updateReqVO 更新信息
*/
void updateRedemptionCode(RedemptionCodeSaveReqVO updateReqVO);
/**
* 删除兑换码
*
* @param id 编号
*/
void deleteRedemptionCode(Long id);
/**
* 批量删除兑换码
*
* @param ids 编号列表
*/
void deleteRedemptionCodeListByIds(List<Long> ids);
/**
* 获得兑换码
*
* @param id 编号
* @return 兑换码
*/
RedemptionCodeDO getRedemptionCode(Long id);
/**
* 根据兑换码字符串获取
*
* @param code 兑换码
* @return 兑换码
*/
RedemptionCodeDO getRedemptionCodeByCode(String code);
/**
* 获得兑换码分页
*
* @param pageReqVO 分页查询
* @return 兑换码分页
*/
PageResult<RedemptionCodeDO> getRedemptionCodePage(RedemptionCodePageReqVO pageReqVO);
/**
* 兑换码兑换(核心业务)
* 验证兑换码有效性,增加用户积分
*
* @param userId 用户ID
* @param code 兑换码
* @return 兑换码面额(积分数)
*/
Integer redeem(Long userId, String code);
}

View File

@@ -0,0 +1,216 @@
package cn.iocoder.yudao.module.tik.muye.redemptioncode.service;
import cn.hutool.core.util.IdUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
import cn.iocoder.yudao.module.tik.muye.memberuserprofile.dal.MemberUserProfileDO;
import cn.iocoder.yudao.module.tik.muye.memberuserprofile.mapper.MemberUserProfileMapper;
import cn.iocoder.yudao.module.tik.muye.pointrecord.dal.PointRecordDO;
import cn.iocoder.yudao.module.tik.muye.pointrecord.mapper.PointRecordMapper;
import cn.iocoder.yudao.module.tik.muye.redemptioncode.dal.RedemptionCodeDO;
import cn.iocoder.yudao.module.tik.muye.redemptioncode.dal.RedemptionCodeMapper;
import cn.iocoder.yudao.module.tik.muye.redemptioncode.vo.*;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.tik.enums.ErrorCodeConstants.*;
/**
* 兑换码 Service 实现类
*
* @author muye
*/
@Service
@Validated
@Slf4j
public class RedemptionCodeServiceImpl implements RedemptionCodeService {
@Resource
private RedemptionCodeMapper redemptionCodeMapper;
@Resource
private MemberUserProfileMapper memberUserProfileMapper;
@Resource
private PointRecordMapper pointRecordMapper;
@Resource
private MemberUserApi memberUserApi;
@Override
public Long createRedemptionCode(RedemptionCodeSaveReqVO createReqVO) {
String code = createReqVO.getCode() != null ? createReqVO.getCode() : generateUniqueCode();
RedemptionCodeDO redemptionCode = BeanUtils.toBean(createReqVO, RedemptionCodeDO.class);
redemptionCode.setCode(code);
redemptionCode.setType(RedemptionCodeDO.TYPE_POINTS);
redemptionCode.setStatus(RedemptionCodeDO.STATUS_UNUSED);
redemptionCodeMapper.insert(redemptionCode);
return redemptionCode.getId();
}
@Override
public List<String> generateRedemptionCodes(RedemptionCodeGenerateReqVO generateReqVO) {
List<String> codes = new ArrayList<>();
String batchNo = "BATCH" + IdUtil.fastSimpleUUID().substring(0, 12).toUpperCase();
for (int i = 0; i < generateReqVO.getCount(); i++) {
String code = generateUniqueCode();
codes.add(code);
RedemptionCodeDO redemptionCode = RedemptionCodeDO.builder()
.code(code)
.type(RedemptionCodeDO.TYPE_POINTS)
.amount(generateReqVO.getAmount())
.price(generateReqVO.getPrice())
.status(RedemptionCodeDO.STATUS_UNUSED)
.batchNo(batchNo)
.batchRemark(generateReqVO.getBatchRemark())
.expireTime(generateReqVO.getExpireTime())
.build();
redemptionCodeMapper.insert(redemptionCode);
}
log.info("[generateRedemptionCodes] 批量生成 {} 个兑换码,批次号 {}", generateReqVO.getCount(), batchNo);
return codes;
}
@Override
public void updateRedemptionCode(RedemptionCodeSaveReqVO updateReqVO) {
validateRedemptionCodeExists(updateReqVO.getId());
RedemptionCodeDO updateObj = BeanUtils.toBean(updateReqVO, RedemptionCodeDO.class);
redemptionCodeMapper.updateById(updateObj);
}
@Override
public void deleteRedemptionCode(Long id) {
validateRedemptionCodeExists(id);
redemptionCodeMapper.deleteById(id);
}
@Override
public void deleteRedemptionCodeListByIds(List<Long> ids) {
redemptionCodeMapper.deleteByIds(ids);
}
@Override
public RedemptionCodeDO getRedemptionCode(Long id) {
return redemptionCodeMapper.selectById(id);
}
@Override
public RedemptionCodeDO getRedemptionCodeByCode(String code) {
return redemptionCodeMapper.selectByCode(code);
}
@Override
public PageResult<RedemptionCodeDO> getRedemptionCodePage(RedemptionCodePageReqVO pageReqVO) {
return redemptionCodeMapper.selectPage(pageReqVO);
}
@Override
@Transactional(rollbackFor = Exception.class)
public Integer redeem(Long userId, String code) {
// 1. 使用乐观锁更新兑换码状态
int affectedRows = redemptionCodeMapper.useRedemptionCode(code, userId);
if (affectedRows == 0) {
RedemptionCodeDO redemptionCode = redemptionCodeMapper.selectByCode(code);
if (redemptionCode == null) {
throw exception(REDEMPTION_CODE_NOT_FOUND);
}
if (RedemptionCodeDO.STATUS_USED == redemptionCode.getStatus()) {
throw exception(REDEMPTION_CODE_ALREADY_USED);
}
if (RedemptionCodeDO.STATUS_EXPIRED == redemptionCode.getStatus() ||
(redemptionCode.getExpireTime() != null && LocalDateTime.now().isAfter(redemptionCode.getExpireTime()))) {
throw exception(REDEMPTION_CODE_EXPIRED);
}
throw exception(REDEMPTION_CODE_NOT_FOUND);
}
// 2. 查询兑换码信息
RedemptionCodeDO redemptionCode = redemptionCodeMapper.selectByCode(code);
// 3. 获取用户信息
MemberUserRespDTO user = memberUserApi.getUser(userId);
String mobile = user != null ? user.getMobile() : "";
// 4. 原子增加积分
int rows = memberUserProfileMapper.updatePointsIncrease(String.valueOf(userId), redemptionCode.getAmount());
if (rows == 0) {
createProfileIfAbsent(userId, mobile);
memberUserProfileMapper.updatePointsIncrease(String.valueOf(userId), redemptionCode.getAmount());
}
// 5. 查询兑换后余额
MemberUserProfileDO profile = memberUserProfileMapper.selectByUserId(userId);
// 6. 创建积分记录
PointRecordDO pointRecord = PointRecordDO.builder()
.userId(userId)
.mobile(mobile)
.type("increase")
.pointAmount(redemptionCode.getAmount())
.balance(profile.getRemainingPoints())
.reason("兑换码兑换")
.bizType("redemption")
.bizId(String.valueOf(redemptionCode.getId()))
.status("confirmed")
.build();
pointRecordMapper.insert(pointRecord);
log.info("[redeem] 用户 {} 使用兑换码 {} 兑换 {} 积分", userId, code, redemptionCode.getAmount());
return redemptionCode.getAmount();
}
private String generateUniqueCode() {
String code;
int maxAttempts = 10;
int attempts = 0;
do {
code = IdUtil.fastSimpleUUID().substring(0, 16).toUpperCase();
attempts++;
if (attempts >= maxAttempts) {
throw new RuntimeException("无法生成唯一兑换码");
}
} while (redemptionCodeMapper.selectByCode(code) != null);
return code;
}
private void validateRedemptionCodeExists(Long id) {
if (redemptionCodeMapper.selectById(id) == null) {
throw exception(REDEMPTION_CODE_NOT_FOUND);
}
}
private void createProfileIfAbsent(Long userId, String mobile) {
MemberUserProfileDO profile = memberUserProfileMapper.selectByUserId(userId);
if (profile == null) {
profile = MemberUserProfileDO.builder()
.userId(String.valueOf(userId))
.mobile(mobile)
.registerTime(LocalDateTime.now())
.totalPoints(0)
.usedPoints(0)
.remainingPoints(0)
.status(1)
.build();
memberUserProfileMapper.insert(profile);
}
}
}

View File

@@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.tik.muye.redemptioncode.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 兑换码批量生成 Request VO")
@Data
public class RedemptionCodeGenerateReqVO {
@Schema(description = "生成数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
@NotNull(message = "生成数量不能为空")
@Min(value = 1, message = "生成数量必须大于0")
@Max(value = 10000, message = "单次最多生成10000个兑换码")
private Integer count;
@Schema(description = "面额(积分数)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
@NotNull(message = "面额不能为空")
@Min(value = 1, message = "面额必须大于0")
private Integer amount;
@Schema(description = "原价金额(元)", example = "10.00")
private BigDecimal price;
@Schema(description = "过期时间")
private LocalDateTime expireTime;
@Schema(description = "批次备注", example = "新年活动")
private String batchRemark;
}

View File

@@ -0,0 +1,34 @@
package cn.iocoder.yudao.module.tik.muye.redemptioncode.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - 兑换码分页 Request VO")
@Data
public class RedemptionCodePageReqVO extends PageParam {
@Schema(description = "兑换码", example = "ABCD1234")
private String code;
@Schema(description = "类型", example = "points")
private String type;
@Schema(description = "状态: 0-未使用 1-已使用 2-已过期", example = "0")
private Integer status;
@Schema(description = "批次号", example = "BATCH20250101")
private String batchNo;
@Schema(description = "使用者用户ID", example = "1")
private Long userId;
@Schema(description = "创建时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
}

View File

@@ -0,0 +1,16 @@
package cn.iocoder.yudao.module.tik.muye.redemptioncode.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.*;
@Schema(description = "用户 App - 兑换码兑换 Request VO")
@Data
public class RedemptionCodeRedeemReqVO {
@Schema(description = "兑换码", requiredMode = Schema.RequiredMode.REQUIRED, example = "ABCD1234EFGH5678")
@NotBlank(message = "兑换码不能为空")
@Size(min = 4, max = 32, message = "兑换码长度必须在4-32位之间")
private String code;
}

View File

@@ -0,0 +1,48 @@
package cn.iocoder.yudao.module.tik.muye.redemptioncode.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 兑换码 Response VO")
@Data
public class RedemptionCodeRespVO {
@Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id;
@Schema(description = "兑换码", requiredMode = Schema.RequiredMode.REQUIRED, example = "ABCD1234EFGH5678")
private String code;
@Schema(description = "类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "points")
private String type;
@Schema(description = "面额(积分数)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
private Integer amount;
@Schema(description = "原价金额(元)", example = "10.00")
private BigDecimal price;
@Schema(description = "状态: 0-未使用 1-已使用 2-已过期", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
private Integer status;
@Schema(description = "批次号", example = "BATCH20250101")
private String batchNo;
@Schema(description = "批次备注", example = "新年活动")
private String batchRemark;
@Schema(description = "使用者用户ID", example = "1")
private Long userId;
@Schema(description = "使用时间")
private LocalDateTime usedTime;
@Schema(description = "过期时间")
private LocalDateTime expireTime;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.tik.muye.redemptioncode.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 兑换码新增/修改 Request VO")
@Data
public class RedemptionCodeSaveReqVO {
@Schema(description = "主键", example = "1")
private Long id;
@Schema(description = "兑换码(为空则自动生成)", example = "ABCD1234EFGH5678")
private String code;
@Schema(description = "类型", example = "points")
private String type;
@Schema(description = "面额(积分数)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
@NotNull(message = "面额不能为空")
@Min(value = 1, message = "面额必须大于0")
private Integer amount;
@Schema(description = "原价金额(元)", example = "10.00")
private BigDecimal price;
@Schema(description = "批次号", example = "BATCH20250101")
private String batchNo;
@Schema(description = "批次备注", example = "新年活动")
private String batchRemark;
@Schema(description = "过期时间")
private LocalDateTime expireTime;
}

View File

@@ -5,6 +5,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.tik.userprompt.service.UserPromptService;
import cn.iocoder.yudao.module.tik.userprompt.vo.UserPromptDO;
import cn.iocoder.yudao.module.tik.userprompt.vo.UserPromptMyRespVO;
import cn.iocoder.yudao.module.tik.userprompt.vo.UserPromptPageReqVO;
import cn.iocoder.yudao.module.tik.userprompt.vo.UserPromptRespVO;
import cn.iocoder.yudao.module.tik.userprompt.vo.UserPromptSaveReqVO;
@@ -16,6 +17,8 @@ import jakarta.validation.Valid;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@@ -30,6 +33,16 @@ public class AppUserPromptController {
@Resource
private UserPromptService userPromptService;
@GetMapping("/my-list")
@Operation(summary = "获取用户可用提示词列表(自建 + 收藏的智能体)")
public CommonResult<List<UserPromptMyRespVO>> getMyPromptList() {
Long userId = getLoginUserId();
if (userId == null) {
return CommonResult.error(401, "用户未登录");
}
return success(userPromptService.getMyPromptList(userId));
}
@PostMapping("/create")
@Operation(summary = "创建用户提示词")
public CommonResult<Long> createUserPrompt(@Valid @RequestBody UserPromptSaveReqVO createReqVO) {

View File

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.tik.userprompt.service;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.tik.userprompt.vo.UserPromptDO;
import cn.iocoder.yudao.module.tik.userprompt.vo.UserPromptMyRespVO;
import cn.iocoder.yudao.module.tik.userprompt.vo.UserPromptPageReqVO;
import cn.iocoder.yudao.module.tik.userprompt.vo.UserPromptSaveReqVO;
import jakarta.validation.Valid;
@@ -15,6 +16,14 @@ import java.util.List;
*/
public interface UserPromptService {
/**
* 获取用户可用提示词列表(自建 + 收藏的智能体)
*
* @param userId 用户ID
* @return 提示词列表
*/
List<UserPromptMyRespVO> getMyPromptList(Long userId);
/**
* 创建用户提示词
*

View File

@@ -2,15 +2,22 @@ package cn.iocoder.yudao.module.tik.userprompt.service;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.tik.muye.aiagent.dal.AiAgentDO;
import cn.iocoder.yudao.module.tik.muye.aiagent.service.AiAgentFavoriteService;
import cn.iocoder.yudao.module.tik.muye.aiagent.service.AiAgentService;
import cn.iocoder.yudao.module.tik.userprompt.mapper.UserPromptMapper;
import cn.iocoder.yudao.module.tik.userprompt.vo.UserPromptDO;
import cn.iocoder.yudao.module.tik.userprompt.vo.UserPromptMyRespVO;
import cn.iocoder.yudao.module.tik.userprompt.vo.UserPromptPageReqVO;
import cn.iocoder.yudao.module.tik.userprompt.vo.UserPromptSaveReqVO;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.tik.enums.ErrorCodeConstants.USER_PROMPT_NOT_EXISTS;
@@ -27,6 +34,67 @@ public class UserPromptServiceImpl implements UserPromptService {
@Resource
private UserPromptMapper userPromptMapper;
@Resource
private AiAgentFavoriteService aiAgentFavoriteService;
@Resource
private AiAgentService aiAgentService;
@Override
public List<UserPromptMyRespVO> getMyPromptList(Long userId) {
List<UserPromptMyRespVO> result = new ArrayList<>();
// 1. 获取用户自建的提示词status=1 启用)
List<UserPromptDO> createdList = userPromptMapper.selectList(
new LambdaQueryWrapper<UserPromptDO>()
.eq(UserPromptDO::getUserId, userId)
.eq(UserPromptDO::getStatus, 1)
.orderByDesc(UserPromptDO::getSort)
.orderByDesc(UserPromptDO::getCreateTime)
);
createdList.forEach(p -> {
UserPromptMyRespVO vo = BeanUtils.toBean(p, UserPromptMyRespVO.class);
vo.setSource("created");
result.add(vo);
});
// 2. 获取用户收藏的智能体
Set<Long> favoriteAgentIds = aiAgentFavoriteService.getFavoriteAgentIds(userId);
if (!favoriteAgentIds.isEmpty()) {
List<AiAgentDO> allAgents = aiAgentService.getEnabledAgentList();
allAgents.stream()
.filter(agent -> favoriteAgentIds.contains(agent.getId()))
.forEach(agent -> {
UserPromptMyRespVO vo = new UserPromptMyRespVO();
vo.setId(-agent.getId()); // 负数表示收藏的智能体
vo.setName(agent.getAgentName());
vo.setContent(agent.getSystemPrompt());
vo.setCategory(agent.getCategoryName());
vo.setIcon(agent.getIcon());
vo.setDescription(agent.getDescription());
vo.setSource("favorite");
vo.setOriginalAuthor(agent.getOperatorName());
vo.setAgentId(agent.getAgentId());
result.add(vo);
});
}
// 3. 排序:自建优先 > 排序值降序 > 使用次数降序
result.sort((a, b) -> {
int sourceCompare = a.getSource().compareTo(b.getSource());
if (sourceCompare != 0) return sourceCompare;
int sortCompare = Integer.compare(
b.getSort() != null ? b.getSort() : 0,
a.getSort() != null ? a.getSort() : 0);
if (sortCompare != 0) return sortCompare;
return Integer.compare(
b.getUseCount() != null ? b.getUseCount() : 0,
a.getUseCount() != null ? a.getUseCount() : 0);
});
return result;
}
@Override
public Long createUserPrompt(UserPromptSaveReqVO createReqVO) {
UserPromptDO userPrompt = BeanUtils.toBean(createReqVO, UserPromptDO.class);

View File

@@ -0,0 +1,46 @@
package cn.iocoder.yudao.module.tik.userprompt.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 用户可用提示词 Response VO自建 + 收藏的智能体)
*/
@Schema(description = "用户 App - 用户可用提示词 Response VO")
@Data
public class UserPromptMyRespVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "提示词名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "小红书种草风")
private String name;
@Schema(description = "提示词内容", requiredMode = Schema.RequiredMode.REQUIRED)
private String content;
@Schema(description = "分类", example = "文案")
private String category;
@Schema(description = "图标URL", example = "https://xxx.com/icon.png")
private String icon;
@Schema(description = "描述", example = "适用于小红书种草笔记")
private String description;
@Schema(description = "来源created-自建favorite-收藏", requiredMode = Schema.RequiredMode.REQUIRED, example = "created")
private String source;
@Schema(description = "原作者(仅收藏时返回)", example = "系统官方")
private String originalAuthor;
@Schema(description = "排序", example = "0")
private Integer sort;
@Schema(description = "使用次数", example = "100")
private Integer useCount;
@Schema(description = "智能体ID仅收藏时返回用于跳转", example = "agent_001")
private String agentId;
}