From 4ab1efbc12adc6bc27759ba8bb4f9fc605cd64dd Mon Sep 17 00:00:00 2001 From: sion <450702724@qq.com> Date: Sun, 15 Mar 2026 15:36:29 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8A=9F=E8=83=BD=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/web-gold/src/api/benchmarkTask.js | 39 +++ frontend/app/web-gold/src/api/userPrompt.js | 8 + .../src/components/PromptSelector.vue | 39 +-- frontend/app/web-gold/src/stores/prompt.js | 70 ++-- .../src/views/content-style/Benchmark.vue | 43 ++- .../src/views/content-style/Copywriting.vue | 126 +------- .../components/BenchmarkTable.vue | 22 +- .../views/task-center/BenchmarkTaskList.vue | 181 +++++++++++ sql/mysql/SQL建表语句.sql | 69 ++++ sql/mysql/benchmark_task_tables.sql | 41 +++ sql/mysql/redeem_code_menu.sql | 45 +++ sql/mysql/redeem_code_tables.sql | 58 ++++ .../AppBenchmarkTaskController.java | 77 +++++ .../dal/dataobject/BenchmarkTaskDO.java | 87 +++++ .../dal/mysql/BenchmarkTaskMapper.java | 23 ++ .../enums/BenchmarkTaskStatusEnum.java | 30 ++ .../benchmark/job/BenchmarkTaskExecutor.java | 194 +++++++++++ .../service/BenchmarkTaskService.java | 84 +++++ .../service/BenchmarkTaskServiceImpl.java | 162 ++++++++++ .../vo/BenchmarkTaskCreateReqVO.java | 30 ++ .../benchmark/vo/BenchmarkTaskPageReqVO.java | 22 ++ .../tik/benchmark/vo/BenchmarkTaskRespVO.java | 51 +++ .../module/tik/enums/ErrorCodeConstants.java | 17 + .../mapper/MemberUserProfileMapper.java | 13 + .../redeemcode/AppRedeemCodeController.java | 38 +++ .../muye/redeemcode/RedeemCodeController.java | 111 +++++++ .../tik/muye/redeemcode/dal/RedeemCodeDO.java | 103 ++++++ .../redeemcode/mapper/RedeemCodeMapper.java | 50 +++ .../redeemcode/service/RedeemCodeService.java | 97 ++++++ .../service/RedeemCodeServiceImpl.java | 302 ++++++++++++++++++ .../vo/RedeemCodeGenerateReqVO.java | 41 +++ .../redeemcode/vo/RedeemCodePageReqVO.java | 41 +++ .../redeemcode/vo/RedeemCodeRedeemReqVO.java | 16 + .../muye/redeemcode/vo/RedeemCodeRespVO.java | 56 ++++ .../redeemcode/vo/RedeemCodeSaveReqVO.java | 53 +++ .../AppRedeemRecordController.java | 42 +++ .../redeemrecord/RedeemRecordController.java | 42 +++ .../muye/redeemrecord/dal/RedeemRecordDO.java | 64 ++++ .../mapper/RedeemRecordMapper.java | 32 ++ .../service/RedeemRecordService.java | 31 ++ .../service/RedeemRecordServiceImpl.java | 34 ++ .../vo/RedeemRecordPageReqVO.java | 34 ++ .../redeemrecord/vo/RedeemRecordRespVO.java | 44 +++ .../AppRedemptionCodeController.java | 38 +++ .../RedemptionCodeController.java | 111 +++++++ .../redemptioncode/dal/RedemptionCodeDO.java | 92 ++++++ .../dal/RedemptionCodeMapper.java | 54 ++++ .../service/RedemptionCodeService.java | 87 +++++ .../service/RedemptionCodeServiceImpl.java | 216 +++++++++++++ .../vo/RedemptionCodeGenerateReqVO.java | 33 ++ .../vo/RedemptionCodePageReqVO.java | 34 ++ .../vo/RedemptionCodeRedeemReqVO.java | 16 + .../vo/RedemptionCodeRespVO.java | 48 +++ .../vo/RedemptionCodeSaveReqVO.java | 39 +++ .../controller/AppUserPromptController.java | 13 + .../userprompt/service/UserPromptService.java | 9 + .../service/UserPromptServiceImpl.java | 68 ++++ .../tik/userprompt/vo/UserPromptMyRespVO.java | 46 +++ .../src/api/muye/redeemcode/index.ts | 41 +++ .../src/api/muye/redeemrecord/index.ts | 26 ++ .../redeemcode/RedeemCodeGenerateForm.vue | 207 ++++++++++++ .../src/views/muye/redeemcode/index.vue | 253 +++++++++++++++ .../src/views/muye/redeemrecord/index.vue | 152 +++++++++ 63 files changed, 4175 insertions(+), 170 deletions(-) create mode 100644 frontend/app/web-gold/src/api/benchmarkTask.js create mode 100644 frontend/app/web-gold/src/views/task-center/BenchmarkTaskList.vue create mode 100644 sql/mysql/benchmark_task_tables.sql create mode 100644 sql/mysql/redeem_code_menu.sql create mode 100644 sql/mysql/redeem_code_tables.sql create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/controller/AppBenchmarkTaskController.java create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/dal/dataobject/BenchmarkTaskDO.java create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/dal/mysql/BenchmarkTaskMapper.java create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/enums/BenchmarkTaskStatusEnum.java create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/job/BenchmarkTaskExecutor.java create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/service/BenchmarkTaskService.java create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/service/BenchmarkTaskServiceImpl.java create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/vo/BenchmarkTaskCreateReqVO.java create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/vo/BenchmarkTaskPageReqVO.java create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/vo/BenchmarkTaskRespVO.java create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/AppRedeemCodeController.java create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/RedeemCodeController.java create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/dal/RedeemCodeDO.java create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/mapper/RedeemCodeMapper.java create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/service/RedeemCodeService.java create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/service/RedeemCodeServiceImpl.java create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/vo/RedeemCodeGenerateReqVO.java create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/vo/RedeemCodePageReqVO.java create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/vo/RedeemCodeRedeemReqVO.java create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/vo/RedeemCodeRespVO.java create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/vo/RedeemCodeSaveReqVO.java create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemrecord/AppRedeemRecordController.java create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemrecord/RedeemRecordController.java create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemrecord/dal/RedeemRecordDO.java create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemrecord/mapper/RedeemRecordMapper.java create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemrecord/service/RedeemRecordService.java create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemrecord/service/RedeemRecordServiceImpl.java create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemrecord/vo/RedeemRecordPageReqVO.java create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemrecord/vo/RedeemRecordRespVO.java create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/AppRedemptionCodeController.java create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/RedemptionCodeController.java create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/dal/RedemptionCodeDO.java create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/dal/RedemptionCodeMapper.java create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/service/RedemptionCodeService.java create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/service/RedemptionCodeServiceImpl.java create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/vo/RedemptionCodeGenerateReqVO.java create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/vo/RedemptionCodePageReqVO.java create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/vo/RedemptionCodeRedeemReqVO.java create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/vo/RedemptionCodeRespVO.java create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/vo/RedemptionCodeSaveReqVO.java create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/userprompt/vo/UserPromptMyRespVO.java create mode 100644 yudao-ui-admin-vue3/src/api/muye/redeemcode/index.ts create mode 100644 yudao-ui-admin-vue3/src/api/muye/redeemrecord/index.ts create mode 100644 yudao-ui-admin-vue3/src/views/muye/redeemcode/RedeemCodeGenerateForm.vue create mode 100644 yudao-ui-admin-vue3/src/views/muye/redeemcode/index.vue create mode 100644 yudao-ui-admin-vue3/src/views/muye/redeemrecord/index.vue diff --git a/frontend/app/web-gold/src/api/benchmarkTask.js b/frontend/app/web-gold/src/api/benchmarkTask.js new file mode 100644 index 0000000000..2062bc2ac6 --- /dev/null +++ b/frontend/app/web-gold/src/api/benchmarkTask.js @@ -0,0 +1,39 @@ +import http from '@/api/http' +import { API_BASE } from '@gold/config/api' + +const SERVER_BASE = API_BASE.APP_TIK + +/** + * 对标分析任务 API + */ +export const BenchmarkTaskApi = { + /** + * 创建对标分析任务 + */ + createTask: async (data) => { + return await http.post(`${SERVER_BASE}/benchmark-task/create`, data) + }, + + /** + * 获取任务详情 + */ + getTask: async (id) => { + return await http.get(`${SERVER_BASE}/benchmark-task/get`, { params: { id } }) + }, + + /** + * 获取任务分页列表 + */ + getTaskPage: async (params) => { + return await http.get(`${SERVER_BASE}/benchmark-task/page`, { params }) + }, + + /** + * 删除任务 + */ + deleteTask: async (id) => { + return await http.delete(`${SERVER_BASE}/benchmark-task/delete`, { params: { id } }) + }, +} + +export default BenchmarkTaskApi diff --git a/frontend/app/web-gold/src/api/userPrompt.js b/frontend/app/web-gold/src/api/userPrompt.js index 4972aead0f..c23edcccb6 100644 --- a/frontend/app/web-gold/src/api/userPrompt.js +++ b/frontend/app/web-gold/src/api/userPrompt.js @@ -9,6 +9,14 @@ const SERVER_BASE_AI = API_BASE.APP_AI * 用户提示词 API */ export const UserPromptApi = { + /** + * 获取用户可用提示词列表(自建 + 收藏的智能体) + * @returns {Promise} 响应数据 + */ + getMyPromptList: async () => { + return await http.get(`${SERVER_BASE_AI}/user-prompt/my-list`) + }, + /** * 创建用户提示词 * @param {Object} data - 提示词数据 diff --git a/frontend/app/web-gold/src/components/PromptSelector.vue b/frontend/app/web-gold/src/components/PromptSelector.vue index 90a05c0261..7c444cc18f 100644 --- a/frontend/app/web-gold/src/components/PromptSelector.vue +++ b/frontend/app/web-gold/src/components/PromptSelector.vue @@ -128,7 +128,7 @@ const props = defineProps({ // 展示模式:tags(标签)或 select(下拉选择) displayMode: { type: String, - default: 'select' // 默认为select模式,因为用户反馈标签模式不好看 + default: 'select' }, // 展示数量(仅标签模式有效) displayCount: { @@ -140,7 +140,7 @@ const props = defineProps({ type: Boolean, default: true }, - // 本地存储键名前缀,用于保存和恢复选择 + // 本地存储键名前缀 storageKey: { type: String, default: 'prompt_selector' @@ -150,7 +150,7 @@ const props = defineProps({ // Emits const emit = defineEmits(['update:modelValue', 'change']) -// Stores +// Stores - 单一数据源 const userStore = useUserStore() const promptStore = usePromptStore() @@ -159,7 +159,7 @@ const showAllPromptsModal = ref(false) const promptSearchKeyword = ref('') const selectedPromptId = ref(props.modelValue) -// 使用 store 中的数据 +// ===== 单一数据源:从 Store 获取 ===== const allPrompts = computed(() => promptStore.promptList) const loading = computed(() => promptStore.promptListLoading) @@ -197,28 +197,31 @@ watch(() => props.modelValue, (newValue) => { selectedPromptId.value = newValue }) -// 加载用户提示词 +// 加载用户提示词(通过 Store) async function loadUserPrompts() { - // 检查用户是否登录 if (!userStore.userId) { console.warn('用户未登录,无法加载提示词') return } try { - // 使用 store 加载数据 - const prompts = await promptStore.loadPromptList(userStore.userId) + // 使用 store 加载(自建 + 收藏的智能体) + await promptStore.loadPromptList() - // 如果有选中ID,但当前选中的提示词不在列表中,清空选择 - if (selectedPromptId.value && !prompts.find(p => p.id === selectedPromptId.value)) { - selectedPromptId.value = null - } - // 如果没有选中ID且有提示词,默认选中第一个 - else if (!selectedPromptId.value && prompts.length > 0) { - selectedPromptId.value = prompts[0].id + // 如果有选中ID,验证是否在列表中 + if (selectedPromptId.value) { + const exists = allPrompts.value.find(p => p.id === selectedPromptId.value) + if (!exists) { + selectedPromptId.value = null + } } - // 尝试从本地存储恢复选中状态 + // 如果没有选中且有提示词,默认选中第一个 + if (!selectedPromptId.value && allPrompts.value.length > 0) { + selectedPromptId.value = allPrompts.value[0].id + } + + // 恢复本地存储的选择 await restoreSelectedPromptId() } catch (error) { console.error('加载提示词失败:', error) @@ -272,10 +275,10 @@ function handleSelectChange(value) { } } -// 刷新用户提示词 +// 刷新用户提示词(强制重新加载) async function refreshUserPrompts() { try { - await promptStore.refreshPromptList(userStore.userId) + await promptStore.refreshPromptList() await restoreSelectedPromptId() } catch (error) { console.error('刷新提示词失败:', error) diff --git a/frontend/app/web-gold/src/stores/prompt.js b/frontend/app/web-gold/src/stores/prompt.js index d2700af85d..7ea05f4f2b 100644 --- a/frontend/app/web-gold/src/stores/prompt.js +++ b/frontend/app/web-gold/src/stores/prompt.js @@ -1,4 +1,4 @@ -import { ref } from 'vue' +import { ref, computed, watch } from 'vue' import { defineStore } from 'pinia' import localforage from 'localforage' import { UserPromptApi } from '@/api/userPrompt' @@ -10,11 +10,14 @@ export const usePromptStore = defineStore('prompt', () => { // 存储提示词相关的视频信息 const currentVideoInfo = ref(null) - // 存储提示词列表 + // 存储提示词列表(自建 + 收藏) const promptList = ref([]) const promptListLoading = ref(false) const promptListError = ref(null) + // 缓存标记:记录是否已加载过 + const hasLoaded = ref(false) + // 设置提示词 function setPrompt(prompt, videoInfo = null) { currentPrompt.value = prompt @@ -27,15 +30,29 @@ export const usePromptStore = defineStore('prompt', () => { currentVideoInfo.value = null } - // 加载提示词列表 - async function loadPromptList(userId) { - if (!userId) { - console.warn('用户未登录,无法加载提示词') - return + /** + * 加载用户可用提示词列表(自建 + 收藏的智能体) + * @param {Object} options + * @param {boolean} options.force - 是否强制刷新 + */ + async function loadPromptList(options = {}) { + const { force = false } = options + + // 如果已有数据且不强制刷新,直接返回缓存 + if (hasLoaded.value && !force && promptList.value.length > 0) { + return promptList.value } - // 如果已有数据且不在加载中,直接返回缓存数据 - if (promptList.value.length > 0 && !promptListLoading.value) { + // 防止重复请求 + if (promptListLoading.value) { + await new Promise(resolve => { + const unwatch = watch(promptListLoading, (loading) => { + if (!loading) { + unwatch() + resolve() + } + }) + }) return promptList.value } @@ -43,17 +60,16 @@ export const usePromptStore = defineStore('prompt', () => { promptListError.value = null try { - const response = await UserPromptApi.getUserPromptPage({ - pageNo: 1, - pageSize: 100, - status: undefined - }) + // 调用新接口:获取自建 + 收藏的提示词 + const response = await UserPromptApi.getMyPromptList() - if (response?.data?.list) { - promptList.value = response.data.list + if (response?.data) { + promptList.value = response.data } else { promptList.value = [] } + + hasLoaded.value = true return promptList.value } catch (error) { console.error('加载提示词列表失败:', error) @@ -68,10 +84,8 @@ export const usePromptStore = defineStore('prompt', () => { function addPromptToList(prompt) { const existingIndex = promptList.value.findIndex(p => p.id === prompt.id) if (existingIndex >= 0) { - // 更新已存在的提示词 promptList.value[existingIndex] = prompt } else { - // 添加新提示词 promptList.value.unshift(prompt) } } @@ -92,25 +106,35 @@ export const usePromptStore = defineStore('prompt', () => { } } - // 刷新提示词列表 - async function refreshPromptList(userId) { - promptList.value = [] // 清空缓存,强制重新加载 - return await loadPromptList(userId) + // 刷新提示词列表(强制重新加载) + async function refreshPromptList() { + hasLoaded.value = false + return await loadPromptList({ force: true }) + } + + // 根据ID获取提示词 + function getPromptById(id) { + return promptList.value.find(p => p.id === id) } return { + // State currentPrompt, currentVideoInfo, promptList, promptListLoading, promptListError, + hasLoaded, + + // Actions setPrompt, clearPrompt, loadPromptList, addPromptToList, removePromptFromList, updatePromptInList, - refreshPromptList + refreshPromptList, + getPromptById } }, { persist: { diff --git a/frontend/app/web-gold/src/views/content-style/Benchmark.vue b/frontend/app/web-gold/src/views/content-style/Benchmark.vue index eea5f3374e..ce567ef921 100644 --- a/frontend/app/web-gold/src/views/content-style/Benchmark.vue +++ b/frontend/app/web-gold/src/views/content-style/Benchmark.vue @@ -15,6 +15,7 @@ import BenchmarkForm from './components/BenchmarkForm.vue' import BenchmarkTable from './components/BenchmarkTable.vue' import BatchAnalyzeModal from './components/BatchAnalyzeModal.vue' import SavePromptModal from '@/components/SavePromptModal.vue' +import { BenchmarkTaskApi } from '@/api/benchmarkTask' const router = useRouter() const promptStore = usePromptStore() @@ -221,11 +222,9 @@ async function handleLoadMore() { } function validatePrompt(prompt, warningMsg = '没有提示词') { - if (!prompt?.trim()) { - message.warning(warningMsg) - return false - } - return true + const isValid = prompt?.trim() + if (!isValid) message.warning(warningMsg) + return !!isValid } async function handleCopyBatchPrompt(prompt) { @@ -250,6 +249,39 @@ function handleOpenSavePromptModal(batchPrompt = null) { savePromptModalVisible.value = true } +// 创建异步任务 +async function handleCreateAsyncTask() { + if (!selectedRowKeys.value.length) { + message.warning('请先选择要分析的视频') + return + } + + // 获取选中的视频 URL + const selectedRows = data.value.filter(item => selectedRowKeys.value.includes(item.id)) + const videoUrls = selectedRows.map(row => row.audio_url || row.share_url).filter(Boolean) + + if (!videoUrls.length) { + message.warning('选中的视频没有有效的URL') + return + } + + try { + const response = await BenchmarkTaskApi.createTask({ + taskName: `批量分析 ${videoUrls.length} 个视频`, + videoUrls: videoUrls, + }) + + if (response?.code === 0 || response?.data) { + message.success('任务创建成功!请到任务中心查看进度') + } else { + message.error(response?.message || '创建任务失败') + } + } catch (error) { + console.error('创建任务失败:', error) + message.error('创建任务失败') + } +} + onMounted(async () => { await loadTableDataFromSession() }) @@ -279,6 +311,7 @@ defineOptions({ name: 'ContentStyleBenchmark' }) @export="handleExportToExcel" @batch-analyze="handleBatchAnalyze" @load-more="handleLoadMore" + @create-async-task="handleCreateAsyncTask" />
diff --git a/frontend/app/web-gold/src/views/content-style/Copywriting.vue b/frontend/app/web-gold/src/views/content-style/Copywriting.vue index 2a85051d30..f36bfe5493 100644 --- a/frontend/app/web-gold/src/views/content-style/Copywriting.vue +++ b/frontend/app/web-gold/src/views/content-style/Copywriting.vue @@ -1,16 +1,14 @@ + + diff --git a/sql/mysql/SQL建表语句.sql b/sql/mysql/SQL建表语句.sql index ffb84696c4..9e066e88cf 100644 --- a/sql/mysql/SQL建表语句.sql +++ b/sql/mysql/SQL建表语句.sql @@ -294,3 +294,72 @@ CREATE TABLE `member_user_permission` ( KEY `idx_package_id` (`package_id`) USING BTREE, KEY `idx_validity_end` (`validity_end`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户权限表'; + +-- =============================================== +-- 5. 兑换码模块 +-- =============================================== + +-- =============================================== +-- 5.1 菜单配置(需要在后台手动添加或执行以下 SQL) +-- 注意:parent_id 需要根据实际情况调整,这里假设 muye 积分管理的 parent_id 为 5000 +-- =============================================== + +-- 兑换码管理菜单(目录) +-- INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) +-- VALUES (5010, '兑换码管理', '', 2, 10, 5000, 'redeemcode', 'ep:tickets', 'muye/redeemcode/index', 'RedeemCode', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'); + +-- 兑换码查询 +-- INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) +-- VALUES (5011, '兑换码查询', 'muye:redeem-code:query', 3, 1, 5010, '', '', '', NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'); + +-- 兑换码创建 +-- INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) +-- VALUES (5012, '兑换码创建', 'muye:redeem-code:create', 3, 2, 5010, '', '', '', NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'); + +-- 兑换码删除 +-- INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) +-- VALUES (5013, '兑换码删除', 'muye:redeem-code:delete', 3, 3, 5010, '', '', '', NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'); + +-- 兑换码导出 +-- INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) +-- VALUES (5014, '兑换码导出', 'muye:redeem-code:export', 3, 4, 5010, '', '', '', NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'); + +-- 兑换记录菜单 +-- INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) +-- VALUES (5020, '兑换记录', '', 2, 11, 5000, 'redeemrecord', 'ep:document', 'muye/redeemrecord/index', 'RedeemRecord', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'); + +-- 兑换记录查询 +-- INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) +-- VALUES (5021, '兑换记录查询', 'muye:redeem-record:query', 3, 1, 5020, '', '', '', NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'); + +-- 兑换记录导出 +-- INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) +-- VALUES (5022, '兑换记录导出', 'muye:redeem-record:export', 3, 2, 5020, '', '', '', NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0') + +-- 兑换码表 +CREATE TABLE `muye_redemption_code` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', + `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', + `code` varchar(32) NOT NULL COMMENT '兑换码', + `type` varchar(20) NOT NULL DEFAULT 'points' COMMENT '类型: points-积分', + `amount` int NOT NULL COMMENT '面额(积分数)', + `price` decimal(10,2) DEFAULT NULL COMMENT '原价金额(元)', + `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态: 0-未使用 1-已使用 2-已过期', + `batch_no` varchar(32) DEFAULT NULL COMMENT '批次号', + `batch_remark` varchar(200) DEFAULT NULL COMMENT '批次备注', + `user_id` bigint DEFAULT NULL COMMENT '使用者用户ID', + `used_time` datetime DEFAULT NULL COMMENT '使用时间', + `expire_time` datetime DEFAULT NULL COMMENT '过期时间', + `creator` varchar(64) DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit NOT NULL DEFAULT b'0' COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_code` (`code`) USING BTREE, + KEY `idx_tenant_id` (`tenant_id`) USING BTREE, + KEY `idx_batch_no` (`batch_no`) USING BTREE, + KEY `idx_status` (`status`) USING BTREE, + KEY `idx_user_id` (`user_id`) USING BTREE, + KEY `idx_expire_time` (`expire_time`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='兑换码表'; diff --git a/sql/mysql/benchmark_task_tables.sql b/sql/mysql/benchmark_task_tables.sql new file mode 100644 index 0000000000..9e0e64230e --- /dev/null +++ b/sql/mysql/benchmark_task_tables.sql @@ -0,0 +1,41 @@ +-- 对标分析异步任务表 +-- 执行前请确认数据库名称 + +CREATE TABLE IF NOT EXISTS `muye_benchmark_task` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户ID', + `user_id` bigint NOT NULL COMMENT '用户ID', + + -- 任务信息 + `task_name` varchar(200) NOT NULL COMMENT '任务名称', + `video_count` int NOT NULL DEFAULT 0 COMMENT '视频数量', + `video_urls` text COMMENT '视频URL列表(JSON)', + + -- 执行状态 + `status` tinyint NOT NULL DEFAULT 0 COMMENT '任务状态:0-待处理 1-处理中 2-成功 3-失败', + `progress` int NOT NULL DEFAULT 0 COMMENT '进度(0-100)', + + -- 执行结果 + `merged_text` longtext COMMENT '合并后的转写文本', + `generated_prompt` longtext COMMENT '生成的提示词', + `prompt_id` bigint DEFAULT NULL COMMENT '保存后的提示词ID', + + -- 错误信息 + `error_msg` varchar(1000) DEFAULT NULL COMMENT '错误信息', + + -- 时间戳 + `start_time` datetime DEFAULT NULL COMMENT '开始时间', + `finish_time` datetime DEFAULT NULL COMMENT '完成时间', + `creator` varchar(64) DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_status` (`status`), + KEY `idx_create_time` (`create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='对标分析异步任务表'; + +-- 同步到 DO 类的 @TableName diff --git a/sql/mysql/redeem_code_menu.sql b/sql/mysql/redeem_code_menu.sql new file mode 100644 index 0000000000..59bdd03b40 --- /dev/null +++ b/sql/mysql/redeem_code_menu.sql @@ -0,0 +1,45 @@ +-- =============================================== +-- 兑换码管理菜单配置 SQL +-- 注意:parent_id 需要根据实际情况调整 +-- 请先查询 muye 积分管理的 parent_id: SELECT id FROM system_menu WHERE name = '积分管理'; +-- =============================================== + +-- 假设 muye 积分管理的 parent_id 为 5000,请根据实际情况修改 + +-- 1. 兑换码管理菜单(目录) +INSERT INTO `system_menu` (`name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) +VALUES ('兑换码管理', '', 2, 20, 0, 'redeemcode', 'ep:tickets', 'muye/redeemcode/index', 'RedeemCode', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'); + +-- 获取刚插入的菜单ID(兑换码管理) +SET @redeem_code_menu_id = LAST_INSERT_ID(); + +-- 1.1 兑换码查询 +INSERT INTO `system_menu` (`name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) +VALUES ('兑换码查询', 'muye:redeem-code:query', 3, 1, @redeem_code_menu_id, '', '', '', NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'); + +-- 1.2 兑换码创建 +INSERT INTO `system_menu` (`name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) +VALUES ('兑换码创建', 'muye:redeem-code:create', 3, 2, @redeem_code_menu_id, '', '', '', NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'); + +-- 1.3 兑换码删除 +INSERT INTO `system_menu` (`name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) +VALUES ('兑换码删除', 'muye:redeem-code:delete', 3, 3, @redeem_code_menu_id, '', '', '', NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'); + +-- 1.4 兑换码导出 +INSERT INTO `system_menu` (`name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) +VALUES ('兑换码导出', 'muye:redeem-code:export', 3, 4, @redeem_code_menu_id, '', '', '', NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'); + +-- 2. 兑换记录菜单(目录) +INSERT INTO `system_menu` (`name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) +VALUES ('兑换记录', '', 2, 21, 0, 'redeemrecord', 'ep:document', 'muye/redeemrecord/index', 'RedeemRecord', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'); + +-- 获取刚插入的菜单ID(兑换记录) +SET @redeem_record_menu_id = LAST_INSERT_ID(); + +-- 2.1 兑换记录查询 +INSERT INTO `system_menu` (`name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) +VALUES ('兑换记录查询', 'muye:redeem-record:query', 3, 1, @redeem_record_menu_id, '', '', '', NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'); + +-- 2.2 兑换记录导出 +INSERT INTO `system_menu` (`name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) +VALUES ('兑换记录导出', 'muye:redeem-record:export', 3, 2, @redeem_record_menu_id, '', '', '', NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'); diff --git a/sql/mysql/redeem_code_tables.sql b/sql/mysql/redeem_code_tables.sql new file mode 100644 index 0000000000..794c1e9cb9 --- /dev/null +++ b/sql/mysql/redeem_code_tables.sql @@ -0,0 +1,58 @@ +-- =============================================== +-- 兑换码系统建表 SQL +-- 执行此文件前请确保数据库连接正确 +-- =============================================== + +-- 1. 兑换码表 +CREATE TABLE IF NOT EXISTS `muye_redeem_code` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', + `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', + `code` varchar(32) NOT NULL COMMENT '兑换码', + `code_type` tinyint NOT NULL DEFAULT 1 COMMENT '码类型: 1-单次 2-多次', + `points` int NOT NULL COMMENT '积分数', + `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态: 0-未使用 1-已使用 2-已过期 3-已禁用', + `batch_no` varchar(32) DEFAULT NULL COMMENT '批次号', + `source` varchar(50) DEFAULT NULL COMMENT '来源', + `max_use_count` int NOT NULL DEFAULT 1 COMMENT '最大使用次数', + `used_count` int NOT NULL DEFAULT 0 COMMENT '已使用次数', + `user_id` bigint DEFAULT NULL COMMENT '使用者用户ID', + `used_time` datetime DEFAULT NULL COMMENT '使用时间', + `expire_time` datetime DEFAULT NULL COMMENT '过期时间', + `remark` varchar(500) DEFAULT NULL COMMENT '备注', + `operator_id` bigint DEFAULT NULL COMMENT '操作人ID', + `operator_name` varchar(64) DEFAULT NULL COMMENT '操作人名称', + `creator` varchar(64) DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit NOT NULL DEFAULT b'0' COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_code` (`code`) USING BTREE, + KEY `idx_tenant_id` (`tenant_id`) USING BTREE, + KEY `idx_batch_no` (`batch_no`) USING BTREE, + KEY `idx_status` (`status`) USING BTREE, + KEY `idx_user_id` (`user_id`) USING BTREE, + KEY `idx_expire_time` (`expire_time`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='兑换码表'; + +-- 2. 兑换记录表 +CREATE TABLE IF NOT EXISTS `muye_redeem_record` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', + `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', + `user_id` bigint NOT NULL DEFAULT 0 COMMENT '用户编号', + `mobile` varchar(20) NOT NULL DEFAULT '' COMMENT '手机号', + `code_id` bigint NOT NULL DEFAULT 0 COMMENT '兑换码ID', + `code` varchar(32) NOT NULL COMMENT '兑换码', + `points` int NOT NULL DEFAULT 0 COMMENT '获得积分', + `balance_after` int NOT NULL DEFAULT 0 COMMENT '兑换后积分余额', + `source` varchar(50) NOT NULL DEFAULT '' COMMENT '来源', + `source_id` varchar(64) NOT NULL DEFAULT '' COMMENT '来源关联ID', + `remark` varchar(500) NOT NULL DEFAULT '' COMMENT '备注', + `creator` varchar(64) NOT NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_tenant_id` (`tenant_id`) USING BTREE, + KEY `idx_user_id` (`user_id`) USING BTREE, + KEY `idx_code_id` (`code_id`) USING BTREE, + KEY `idx_create_time` (`create_time`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='兑换记录表'; diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/controller/AppBenchmarkTaskController.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/controller/AppBenchmarkTaskController.java new file mode 100644 index 0000000000..2cf591b340 --- /dev/null +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/controller/AppBenchmarkTaskController.java @@ -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 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 getTask(@RequestParam("id") Long id) { + BenchmarkTaskDO task = benchmarkTaskService.getTask(id); + return success(BeanUtils.toBean(task, BenchmarkTaskRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获取任务分页列表") + public CommonResult> getTaskPage(@Valid BenchmarkTaskPageReqVO pageReqVO) { + Long userId = getLoginUserId(); + if (userId == null) { + return CommonResult.error(401, "用户未登录"); + } + pageReqVO.setUserId(userId); + PageResult 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 deleteTask(@RequestParam("id") Long id) { + Long userId = getLoginUserId(); + if (userId == null) { + return CommonResult.error(401, "用户未登录"); + } + benchmarkTaskService.deleteTask(userId, id); + return success(true); + } + +} diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/dal/dataobject/BenchmarkTaskDO.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/dal/dataobject/BenchmarkTaskDO.java new file mode 100644 index 0000000000..7c2488773a --- /dev/null +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/dal/dataobject/BenchmarkTaskDO.java @@ -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; + +} diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/dal/mysql/BenchmarkTaskMapper.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/dal/mysql/BenchmarkTaskMapper.java new file mode 100644 index 0000000000..56c484bfcc --- /dev/null +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/dal/mysql/BenchmarkTaskMapper.java @@ -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 { + + default PageResult selectPage(BenchmarkTaskPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(BenchmarkTaskDO::getUserId, reqVO.getUserId()) + .eqIfPresent(BenchmarkTaskDO::getStatus, reqVO.getStatus()) + .orderByDesc(BenchmarkTaskDO::getId)); + } + +} diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/enums/BenchmarkTaskStatusEnum.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/enums/BenchmarkTaskStatusEnum.java new file mode 100644 index 0000000000..c98142950d --- /dev/null +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/enums/BenchmarkTaskStatusEnum.java @@ -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; + } + +} diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/job/BenchmarkTaskExecutor.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/job/BenchmarkTaskExecutor.java new file mode 100644 index 0000000000..e5d2a06c98 --- /dev/null +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/job/BenchmarkTaskExecutor.java @@ -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 videoUrls = createReqVO.getVideoUrls(); + List 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); + } + +} diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/service/BenchmarkTaskService.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/service/BenchmarkTaskService.java new file mode 100644 index 0000000000..9d2a7192d9 --- /dev/null +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/service/BenchmarkTaskService.java @@ -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 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); + +} diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/service/BenchmarkTaskServiceImpl.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/service/BenchmarkTaskServiceImpl.java new file mode 100644 index 0000000000..47cf44b9e1 --- /dev/null +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/service/BenchmarkTaskServiceImpl.java @@ -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 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; + } + +} diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/vo/BenchmarkTaskCreateReqVO.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/vo/BenchmarkTaskCreateReqVO.java new file mode 100644 index 0000000000..ae2936cab2 --- /dev/null +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/vo/BenchmarkTaskCreateReqVO.java @@ -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 videoUrls; + + @Schema(description = "是否保存为提示词", example = "true") + private Boolean saveAsPrompt; + + @Schema(description = "提示词名称(保存时需要)", example = "小红书种草风") + private String promptName; + +} diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/vo/BenchmarkTaskPageReqVO.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/vo/BenchmarkTaskPageReqVO.java new file mode 100644 index 0000000000..2e6c988dc8 --- /dev/null +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/vo/BenchmarkTaskPageReqVO.java @@ -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; + +} diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/vo/BenchmarkTaskRespVO.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/vo/BenchmarkTaskRespVO.java new file mode 100644 index 0000000000..904795929d --- /dev/null +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/vo/BenchmarkTaskRespVO.java @@ -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; + +} diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/enums/ErrorCodeConstants.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/enums/ErrorCodeConstants.java index b623dded0c..7f84f72563 100644 --- a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/enums/ErrorCodeConstants.java +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/enums/ErrorCodeConstants.java @@ -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, "无权访问该任务"); + } diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/memberuserprofile/mapper/MemberUserProfileMapper.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/memberuserprofile/mapper/MemberUserProfileMapper.java index 7a66efda04..c7c7b5047e 100644 --- a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/memberuserprofile/mapper/MemberUserProfileMapper.java +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/memberuserprofile/mapper/MemberUserProfileMapper.java @@ -60,6 +60,19 @@ public interface MemberUserProfileMapper extends BaseMapperX= #{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 diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/AppRedeemCodeController.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/AppRedeemCodeController.java new file mode 100644 index 0000000000..9adb26a721 --- /dev/null +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/AppRedeemCodeController.java @@ -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 redeem(@Valid @RequestBody RedeemCodeRedeemReqVO reqVO) { + Long userId = SecurityFrameworkUtils.getLoginUserId(); + Long recordId = redeemCodeService.redeem(String.valueOf(userId), reqVO.getCode()); + return success(recordId); + } + +} diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/RedeemCodeController.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/RedeemCodeController.java new file mode 100644 index 0000000000..247cb1768b --- /dev/null +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/RedeemCodeController.java @@ -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 createRedeemCode(@Valid @RequestBody RedeemCodeSaveReqVO createReqVO) { + return success(redeemCodeService.createRedeemCode(createReqVO)); + } + + @PostMapping("/generate") + @Operation(summary = "批量生成兑换码") + @PreAuthorize("@ss.hasPermission('muye:redeem-code:create')") + public CommonResult> generateRedeemCodes(@Valid @RequestBody RedeemCodeGenerateReqVO generateReqVO) { + return success(redeemCodeService.generateRedeemCodes(generateReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新兑换码") + @PreAuthorize("@ss.hasPermission('muye:redeem-code:update')") + public CommonResult 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 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 deleteRedeemCodeList(@RequestParam("ids") List 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 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> getRedeemCodePage(@Valid RedeemCodePageReqVO pageReqVO) { + PageResult 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 list = redeemCodeService.getRedeemCodePage(pageReqVO).getList(); + ExcelUtils.write(response, "兑换码.xls", "数据", RedeemCodeRespVO.class, + BeanUtils.toBean(list, RedeemCodeRespVO.class)); + } + +} diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/dal/RedeemCodeDO.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/dal/RedeemCodeDO.java new file mode 100644 index 0000000000..490c2c0258 --- /dev/null +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/dal/RedeemCodeDO.java @@ -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"; + +} diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/mapper/RedeemCodeMapper.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/mapper/RedeemCodeMapper.java new file mode 100644 index 0000000000..e53dbc5956 --- /dev/null +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/mapper/RedeemCodeMapper.java @@ -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 { + + /** + * 根据兑换码查询 + */ + default RedeemCodeDO selectByCode(String code) { + return selectOne(new LambdaQueryWrapperX() + .eq(RedeemCodeDO::getCode, code)); + } + + /** + * 根据兑换码查询(带悲观锁) + */ + @Select("SELECT * FROM muye_redeem_code WHERE code = #{code} FOR UPDATE") + RedeemCodeDO selectByCodeForUpdate(@Param("code") String code); + + /** + * 分页查询 + */ + default PageResult selectPage(RedeemCodePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .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)); + } + +} diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/service/RedeemCodeService.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/service/RedeemCodeService.java new file mode 100644 index 0000000000..3068f60a73 --- /dev/null +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/service/RedeemCodeService.java @@ -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 generateRedeemCodes(RedeemCodeGenerateReqVO generateReqVO); + + /** + * 更新兑换码 + * + * @param updateReqVO 更新信息 + */ + void updateRedeemCode(RedeemCodeSaveReqVO updateReqVO); + + /** + * 删除兑换码 + * + * @param id 编号 + */ + void deleteRedeemCode(Long id); + + /** + * 批量删除兑换码 + * + * @param ids 编号列表 + */ + void deleteRedeemCodeListByIds(List ids); + + /** + * 获得兑换码 + * + * @param id 编号 + * @return 兑换码 + */ + RedeemCodeDO getRedeemCode(Long id); + + /** + * 根据兑换码字符串获取 + * + * @param code 兑换码 + * @return 兑换码 + */ + RedeemCodeDO getRedeemCodeByCode(String code); + + /** + * 获得兑换码分页 + * + * @param pageReqVO 分页查询 + * @return 兑换码分页 + */ + PageResult 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); + +} diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/service/RedeemCodeServiceImpl.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/service/RedeemCodeServiceImpl.java new file mode 100644 index 0000000000..52d569c593 --- /dev/null +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/service/RedeemCodeServiceImpl.java @@ -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 generateRedeemCodes(RedeemCodeGenerateReqVO generateReqVO) { + List 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 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 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); + } + } + +} diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/vo/RedeemCodeGenerateReqVO.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/vo/RedeemCodeGenerateReqVO.java new file mode 100644 index 0000000000..724bee4231 --- /dev/null +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/vo/RedeemCodeGenerateReqVO.java @@ -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; + +} diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/vo/RedeemCodePageReqVO.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/vo/RedeemCodePageReqVO.java new file mode 100644 index 0000000000..84ac5b18a8 --- /dev/null +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/vo/RedeemCodePageReqVO.java @@ -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; + +} diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/vo/RedeemCodeRedeemReqVO.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/vo/RedeemCodeRedeemReqVO.java new file mode 100644 index 0000000000..5168483fa5 --- /dev/null +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/vo/RedeemCodeRedeemReqVO.java @@ -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; + +} diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/vo/RedeemCodeRespVO.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/vo/RedeemCodeRespVO.java new file mode 100644 index 0000000000..6ae2bf08f1 --- /dev/null +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/vo/RedeemCodeRespVO.java @@ -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; + +} diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/vo/RedeemCodeSaveReqVO.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/vo/RedeemCodeSaveReqVO.java new file mode 100644 index 0000000000..f13033ca97 --- /dev/null +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemcode/vo/RedeemCodeSaveReqVO.java @@ -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; + +} diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemrecord/AppRedeemRecordController.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemrecord/AppRedeemRecordController.java new file mode 100644 index 0000000000..c830e5674e --- /dev/null +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemrecord/AppRedeemRecordController.java @@ -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> getMyRedeemRecordPage(@Valid RedeemRecordPageReqVO pageReqVO) { + Long userId = SecurityFrameworkUtils.getLoginUserId(); + PageResult pageResult = redeemRecordService.getRedeemRecordPageByUserId(userId, pageReqVO); + return success(BeanUtils.toBean(pageResult, RedeemRecordRespVO.class)); + } + +} diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemrecord/RedeemRecordController.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemrecord/RedeemRecordController.java new file mode 100644 index 0000000000..38fba51ed2 --- /dev/null +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemrecord/RedeemRecordController.java @@ -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> getRedeemRecordPage(@Valid RedeemRecordPageReqVO pageReqVO) { + PageResult pageResult = redeemRecordService.getRedeemRecordPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, RedeemRecordRespVO.class)); + } + +} diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemrecord/dal/RedeemRecordDO.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemrecord/dal/RedeemRecordDO.java new file mode 100644 index 0000000000..2cf12e4290 --- /dev/null +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemrecord/dal/RedeemRecordDO.java @@ -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; + +} diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemrecord/mapper/RedeemRecordMapper.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemrecord/mapper/RedeemRecordMapper.java new file mode 100644 index 0000000000..96fc543b1b --- /dev/null +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemrecord/mapper/RedeemRecordMapper.java @@ -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 { + + /** + * 分页查询 + */ + default PageResult selectPage(RedeemRecordPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .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)); + } + +} diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemrecord/service/RedeemRecordService.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemrecord/service/RedeemRecordService.java new file mode 100644 index 0000000000..d30728d960 --- /dev/null +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemrecord/service/RedeemRecordService.java @@ -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 getRedeemRecordPage(RedeemRecordPageReqVO pageReqVO); + + /** + * 获得用户的兑换记录分页 + * + * @param userId 用户ID + * @param pageReqVO 分页查询 + * @return 兑换记录分页 + */ + PageResult getRedeemRecordPageByUserId(Long userId, RedeemRecordPageReqVO pageReqVO); + +} diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemrecord/service/RedeemRecordServiceImpl.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemrecord/service/RedeemRecordServiceImpl.java new file mode 100644 index 0000000000..3b96f44ac3 --- /dev/null +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemrecord/service/RedeemRecordServiceImpl.java @@ -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 getRedeemRecordPage(RedeemRecordPageReqVO pageReqVO) { + return redeemRecordMapper.selectPage(pageReqVO); + } + + @Override + public PageResult getRedeemRecordPageByUserId(Long userId, RedeemRecordPageReqVO pageReqVO) { + pageReqVO.setUserId(userId); + return redeemRecordMapper.selectPage(pageReqVO); + } + +} diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemrecord/vo/RedeemRecordPageReqVO.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemrecord/vo/RedeemRecordPageReqVO.java new file mode 100644 index 0000000000..e42378d640 --- /dev/null +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemrecord/vo/RedeemRecordPageReqVO.java @@ -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; + +} diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemrecord/vo/RedeemRecordRespVO.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemrecord/vo/RedeemRecordRespVO.java new file mode 100644 index 0000000000..af00b987c8 --- /dev/null +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redeemrecord/vo/RedeemRecordRespVO.java @@ -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; + +} diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/AppRedemptionCodeController.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/AppRedemptionCodeController.java new file mode 100644 index 0000000000..9f5de89533 --- /dev/null +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/AppRedemptionCodeController.java @@ -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 redeem(@Valid @RequestBody RedemptionCodeRedeemReqVO reqVO) { + Long userId = SecurityFrameworkUtils.getLoginUserId(); + Integer amount = redemptionCodeService.redeem(userId, reqVO.getCode()); + return success(amount); + } + +} diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/RedemptionCodeController.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/RedemptionCodeController.java new file mode 100644 index 0000000000..80861c866b --- /dev/null +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/RedemptionCodeController.java @@ -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 createRedemptionCode(@Valid @RequestBody RedemptionCodeSaveReqVO createReqVO) { + return success(redemptionCodeService.createRedemptionCode(createReqVO)); + } + + @PostMapping("/generate") + @Operation(summary = "批量生成兑换码") + @PreAuthorize("@ss.hasPermission('muye:redemption-code:create')") + public CommonResult> generateRedemptionCodes(@Valid @RequestBody RedemptionCodeGenerateReqVO generateReqVO) { + return success(redemptionCodeService.generateRedemptionCodes(generateReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新兑换码") + @PreAuthorize("@ss.hasPermission('muye:redemption-code:update')") + public CommonResult 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 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 deleteRedemptionCodeList(@RequestParam("ids") List 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 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> getRedemptionCodePage(@Valid RedemptionCodePageReqVO pageReqVO) { + PageResult 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 list = redemptionCodeService.getRedemptionCodePage(pageReqVO).getList(); + ExcelUtils.write(response, "兑换码.xls", "数据", RedemptionCodeRespVO.class, + BeanUtils.toBean(list, RedemptionCodeRespVO.class)); + } + +} diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/dal/RedemptionCodeDO.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/dal/RedemptionCodeDO.java new file mode 100644 index 0000000000..7d6c040e08 --- /dev/null +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/dal/RedemptionCodeDO.java @@ -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"; + +} diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/dal/RedemptionCodeMapper.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/dal/RedemptionCodeMapper.java new file mode 100644 index 0000000000..659575e667 --- /dev/null +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/dal/RedemptionCodeMapper.java @@ -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 { + + /** + * 根据兑换码查询 + */ + default RedemptionCodeDO selectByCode(String code) { + return selectOne(new LambdaQueryWrapperX() + .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 selectPage(RedemptionCodePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .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)); + } + +} diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/service/RedemptionCodeService.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/service/RedemptionCodeService.java new file mode 100644 index 0000000000..48b0692c37 --- /dev/null +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/service/RedemptionCodeService.java @@ -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 generateRedemptionCodes(RedemptionCodeGenerateReqVO generateReqVO); + + /** + * 更新兑换码 + * + * @param updateReqVO 更新信息 + */ + void updateRedemptionCode(RedemptionCodeSaveReqVO updateReqVO); + + /** + * 删除兑换码 + * + * @param id 编号 + */ + void deleteRedemptionCode(Long id); + + /** + * 批量删除兑换码 + * + * @param ids 编号列表 + */ + void deleteRedemptionCodeListByIds(List ids); + + /** + * 获得兑换码 + * + * @param id 编号 + * @return 兑换码 + */ + RedemptionCodeDO getRedemptionCode(Long id); + + /** + * 根据兑换码字符串获取 + * + * @param code 兑换码 + * @return 兑换码 + */ + RedemptionCodeDO getRedemptionCodeByCode(String code); + + /** + * 获得兑换码分页 + * + * @param pageReqVO 分页查询 + * @return 兑换码分页 + */ + PageResult getRedemptionCodePage(RedemptionCodePageReqVO pageReqVO); + + /** + * 兑换码兑换(核心业务) + * 验证兑换码有效性,增加用户积分 + * + * @param userId 用户ID + * @param code 兑换码 + * @return 兑换码面额(积分数) + */ + Integer redeem(Long userId, String code); + +} diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/service/RedemptionCodeServiceImpl.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/service/RedemptionCodeServiceImpl.java new file mode 100644 index 0000000000..67bef663d5 --- /dev/null +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/service/RedemptionCodeServiceImpl.java @@ -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 generateRedemptionCodes(RedemptionCodeGenerateReqVO generateReqVO) { + List 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 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 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); + } + } + +} diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/vo/RedemptionCodeGenerateReqVO.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/vo/RedemptionCodeGenerateReqVO.java new file mode 100644 index 0000000000..4fdf61dd2c --- /dev/null +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/vo/RedemptionCodeGenerateReqVO.java @@ -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; + +} diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/vo/RedemptionCodePageReqVO.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/vo/RedemptionCodePageReqVO.java new file mode 100644 index 0000000000..7d3008cf17 --- /dev/null +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/vo/RedemptionCodePageReqVO.java @@ -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; + +} diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/vo/RedemptionCodeRedeemReqVO.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/vo/RedemptionCodeRedeemReqVO.java new file mode 100644 index 0000000000..5bc752efbe --- /dev/null +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/vo/RedemptionCodeRedeemReqVO.java @@ -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; + +} diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/vo/RedemptionCodeRespVO.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/vo/RedemptionCodeRespVO.java new file mode 100644 index 0000000000..383edf1bd0 --- /dev/null +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/vo/RedemptionCodeRespVO.java @@ -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; + +} diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/vo/RedemptionCodeSaveReqVO.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/vo/RedemptionCodeSaveReqVO.java new file mode 100644 index 0000000000..d845c87746 --- /dev/null +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/redemptioncode/vo/RedemptionCodeSaveReqVO.java @@ -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; + +} diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/userprompt/controller/AppUserPromptController.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/userprompt/controller/AppUserPromptController.java index 671d7ec89f..aa60ba0ec0 100644 --- a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/userprompt/controller/AppUserPromptController.java +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/userprompt/controller/AppUserPromptController.java @@ -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> getMyPromptList() { + Long userId = getLoginUserId(); + if (userId == null) { + return CommonResult.error(401, "用户未登录"); + } + return success(userPromptService.getMyPromptList(userId)); + } + @PostMapping("/create") @Operation(summary = "创建用户提示词") public CommonResult createUserPrompt(@Valid @RequestBody UserPromptSaveReqVO createReqVO) { diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/userprompt/service/UserPromptService.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/userprompt/service/UserPromptService.java index d63f2fdb62..27cb2f34bc 100644 --- a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/userprompt/service/UserPromptService.java +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/userprompt/service/UserPromptService.java @@ -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 getMyPromptList(Long userId); + /** * 创建用户提示词 * diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/userprompt/service/UserPromptServiceImpl.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/userprompt/service/UserPromptServiceImpl.java index 586f13e683..0217cb51e7 100644 --- a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/userprompt/service/UserPromptServiceImpl.java +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/userprompt/service/UserPromptServiceImpl.java @@ -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 getMyPromptList(Long userId) { + List result = new ArrayList<>(); + + // 1. 获取用户自建的提示词(status=1 启用) + List createdList = userPromptMapper.selectList( + new LambdaQueryWrapper() + .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 favoriteAgentIds = aiAgentFavoriteService.getFavoriteAgentIds(userId); + if (!favoriteAgentIds.isEmpty()) { + List 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); diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/userprompt/vo/UserPromptMyRespVO.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/userprompt/vo/UserPromptMyRespVO.java new file mode 100644 index 0000000000..5b00f2224e --- /dev/null +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/userprompt/vo/UserPromptMyRespVO.java @@ -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; + +} diff --git a/yudao-ui-admin-vue3/src/api/muye/redeemcode/index.ts b/yudao-ui-admin-vue3/src/api/muye/redeemcode/index.ts new file mode 100644 index 0000000000..46807afa2d --- /dev/null +++ b/yudao-ui-admin-vue3/src/api/muye/redeemcode/index.ts @@ -0,0 +1,41 @@ +import request from '@/config/axios' + +// 查询兑换码分页 +export const getRedeemCodePage = async (params) => { + return await request.get({ url: `/admin-api/muye/redeem-code/page`, params }) +} + +// 查询兑换码详情 +export const getRedeemCode = async (id: number) => { + return await request.get({ url: `/admin-api/muye/redeem-code/get?id=` + id }) +} + +// 新增兑换码 +export const createRedeemCode = async (data) => { + return await request.post({ url: `/admin-api/muye/redeem-code/create`, data }) +} + +// 修改兑换码 +export const updateRedeemCode = async (data) => { + return await request.put({ url: `/admin-api/muye/redeem-code/update`, data }) +} + +// 删除兑换码 +export const deleteRedeemCode = async (id: number) => { + return await request.delete({ url: `/admin-api/muye/redeem-code/delete?id=` + id }) +} + +// 批量删除兑换码 +export const deleteRedeemCodeList = async (ids: number[]) => { + return await request.delete({ url: `/admin-api/muye/redeem-code/delete-list?ids=` + ids.join(',') }) +} + +// 批量生成兑换码 +export const generateRedeemCodes = async (data) => { + return await request.post({ url: `/admin-api/muye/redeem-code/generate`, data }) +} + +// 导出兑换码 Excel +export const exportRedeemCode = async (params) => { + return await request.download({ url: `/admin-api/muye/redeem-code/export-excel`, params }) +} diff --git a/yudao-ui-admin-vue3/src/api/muye/redeemrecord/index.ts b/yudao-ui-admin-vue3/src/api/muye/redeemrecord/index.ts new file mode 100644 index 0000000000..b5d4798768 --- /dev/null +++ b/yudao-ui-admin-vue3/src/api/muye/redeemrecord/index.ts @@ -0,0 +1,26 @@ +import request from '@/config/axios' + +// 查询兑换记录分页 +export const getRedeemRecordPage = async (params) => { + return await request.get({ url: `/admin-api/muye/redeem-record/page`, params }) +} + +// 查询兑换记录详情 +export const getRedeemRecord = async (id: number) => { + return await request.get({ url: `/admin-api/muye/redeem-record/get?id=` + id }) +} + +// 查询我的兑换记录(用户端) +export const getMyRedeemRecords = async (params) => { + return await request.get({ url: `/app-api/muye/redeem-record/my-page`, params }) +} + +// 用户兑换 +export const redeemCode = async (code: string) => { + return await request.post({ url: `/app-api/muye/redeem-code/redeem`, data: { code } }) +} + +// 导出兑换记录 Excel +export const exportRedeemRecord = async (params) => { + return await request.download({ url: `/admin-api/muye/redeem-record/export-excel`, params }) +} diff --git a/yudao-ui-admin-vue3/src/views/muye/redeemcode/RedeemCodeGenerateForm.vue b/yudao-ui-admin-vue3/src/views/muye/redeemcode/RedeemCodeGenerateForm.vue new file mode 100644 index 0000000000..5b4d4319fe --- /dev/null +++ b/yudao-ui-admin-vue3/src/views/muye/redeemcode/RedeemCodeGenerateForm.vue @@ -0,0 +1,207 @@ + + + diff --git a/yudao-ui-admin-vue3/src/views/muye/redeemcode/index.vue b/yudao-ui-admin-vue3/src/views/muye/redeemcode/index.vue new file mode 100644 index 0000000000..293f7d62f2 --- /dev/null +++ b/yudao-ui-admin-vue3/src/views/muye/redeemcode/index.vue @@ -0,0 +1,253 @@ + + + diff --git a/yudao-ui-admin-vue3/src/views/muye/redeemrecord/index.vue b/yudao-ui-admin-vue3/src/views/muye/redeemrecord/index.vue new file mode 100644 index 0000000000..f05bb7f1b9 --- /dev/null +++ b/yudao-ui-admin-vue3/src/views/muye/redeemrecord/index.vue @@ -0,0 +1,152 @@ + + +