Merge branch 'main' of http://8.155.172.147:3001/sion/sionrui
This commit is contained in:
39
frontend/app/web-gold/src/api/benchmarkTask.js
Normal file
39
frontend/app/web-gold/src/api/benchmarkTask.js
Normal file
@@ -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
|
||||
@@ -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 - 提示词数据
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
<section v-if="!data.length" class="card results-card empty-state">
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onActivated, computed, watch } from 'vue'
|
||||
import { usePromptStore } from '@/stores/prompt'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { CommonService } from '@/api/common'
|
||||
import useVoiceText from '@/hooks/web/useVoiceText'
|
||||
import GmIcon from '@/components/icons/Icon.vue'
|
||||
import { UserPromptApi } from '@/api/userPrompt'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import GradientButton from '@/components/GradientButton.vue'
|
||||
import PromptSelector from '@/components/PromptSelector.vue'
|
||||
import { setJSON, getJSON } from '@/utils/storage'
|
||||
import { copyToClipboard } from '@/utils/clipboard'
|
||||
import BasicLayout from '@/layouts/components/BasicLayout.vue'
|
||||
|
||||
@@ -18,11 +16,11 @@ const promptStore = usePromptStore()
|
||||
const userStore = useUserStore()
|
||||
const md = new MarkdownIt()
|
||||
|
||||
// 表单数据(合并为单一输入)
|
||||
// 表单数据
|
||||
const form = ref({
|
||||
prompt: '',
|
||||
userInput: '', // 用户输入的文本或视频链接
|
||||
amplitude: 50 // 幅度,默认50%
|
||||
userInput: '',
|
||||
amplitude: 50
|
||||
})
|
||||
|
||||
// 生成的文案内容
|
||||
@@ -37,52 +35,8 @@ const originalContent = ref('')
|
||||
const isLoading = ref(false)
|
||||
const { getVoiceText } = useVoiceText()
|
||||
|
||||
// 提示词相关状态
|
||||
const allPrompts = ref([])
|
||||
const loadingPrompts = ref(false)
|
||||
// ===== 使用 Store 作为单一数据源 =====
|
||||
const selectedPromptId = ref(null)
|
||||
const promptSearchKeyword = ref('')
|
||||
const DISPLAY_COUNT = 6 // 展示的提示词数量
|
||||
|
||||
/**
|
||||
* 加载用户提示词列表
|
||||
* 从服务器获取当前用户的提示词,并按创建时间倒序排列
|
||||
*/
|
||||
async function loadUserPrompts() {
|
||||
const userId = Number(userStore.userId)
|
||||
if (!userId) {
|
||||
console.warn('无法获取用户ID,跳过加载提示词')
|
||||
return
|
||||
}
|
||||
|
||||
loadingPrompts.value = true
|
||||
try {
|
||||
const response = await UserPromptApi.getUserPromptPage({
|
||||
userId: userId,
|
||||
status: 1, // 只加载启用的提示词
|
||||
pageNo: 1,
|
||||
pageSize: 100, // 加载前100个
|
||||
})
|
||||
|
||||
if (response && (response.code === 0 || response.code === 200)) {
|
||||
const list = response.data?.list || []
|
||||
// 按创建时间倒序排列(最新的在前)
|
||||
allPrompts.value = list.sort((a, b) => {
|
||||
const timeA = a.createTime ? new Date(a.createTime).getTime() : 0
|
||||
const timeB = b.createTime ? new Date(b.createTime).getTime() : 0
|
||||
return timeB - timeA
|
||||
})
|
||||
} else {
|
||||
throw new Error(response?.msg || response?.message || '加载失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载提示词列表失败:', error)
|
||||
// 不显示错误提示,避免影响用户体验
|
||||
} finally {
|
||||
loadingPrompts.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 处理提示词选择
|
||||
function handlePromptChange(prompt) {
|
||||
@@ -96,59 +50,23 @@ function handlePromptChange(prompt) {
|
||||
promptStore.setPrompt(prompt.content, prompt)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 等待用户信息初始化完成
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function waitForUserInfo() {
|
||||
// 等待 store 从本地存储恢复完成(最多等待 500ms)
|
||||
let waitCount = 0
|
||||
const maxWait = 50 // 50 * 10ms = 500ms
|
||||
while (!userStore.isHydrated && waitCount < maxWait) {
|
||||
await new Promise(resolve => setTimeout(resolve, 10))
|
||||
waitCount++
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保用户信息已加载
|
||||
* 如果已登录但 userId 为空,则从服务器获取用户信息
|
||||
*/
|
||||
async function ensureUserInfoLoaded() {
|
||||
const isLoggedIn = userStore.isLoggedIn
|
||||
const hasNoUserId = !userStore.userId
|
||||
|
||||
if (isLoggedIn && hasNoUserId) {
|
||||
try {
|
||||
await userStore.fetchUserInfo()
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化页面数据
|
||||
* 1. 恢复之前保存的提示词(如果有)
|
||||
* 2. 等待用户信息初始化
|
||||
* 3. 确保用户信息已加载
|
||||
* 4. 加载提示词列表
|
||||
* 通过 Store 加载提示词(Store 会自动缓存)
|
||||
*/
|
||||
async function initializePage() {
|
||||
// 1. 恢复之前保存的提示词
|
||||
// 恢复之前保存的提示词
|
||||
if (promptStore.currentPrompt) {
|
||||
form.value.prompt = promptStore.currentPrompt
|
||||
}
|
||||
|
||||
// 2. 等待用户信息初始化完成
|
||||
await waitForUserInfo()
|
||||
// 加载提示词列表(自建 + 收藏,Store 会自动缓存)
|
||||
await promptStore.loadPromptList()
|
||||
|
||||
// 3. 确保用户信息已加载
|
||||
await ensureUserInfoLoaded()
|
||||
|
||||
// 4. 加载提示词列表
|
||||
await loadUserPrompts()
|
||||
// 如果有选中的提示词,同步 ID
|
||||
if (promptStore.currentVideoInfo?.id) {
|
||||
selectedPromptId.value = promptStore.currentVideoInfo.id
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载时初始化
|
||||
@@ -156,16 +74,6 @@ onMounted(() => {
|
||||
initializePage()
|
||||
})
|
||||
|
||||
// 监听 userId 变化:如果之前没有 userId,现在有了,则自动加载提示词
|
||||
watch(() => userStore.userId, async (newUserId, oldUserId) => {
|
||||
const userIdChanged = newUserId && !oldUserId
|
||||
const hasNoPrompts = allPrompts.value.length === 0
|
||||
|
||||
if (userIdChanged && hasNoPrompts) {
|
||||
await loadUserPrompts()
|
||||
}
|
||||
})
|
||||
|
||||
// 生成文案(流式)
|
||||
async function generateCopywriting() {
|
||||
const inputContent = form.value.userInput || ''
|
||||
@@ -358,17 +266,11 @@ defineOptions({ name: 'ContentStyleCopywriting' })
|
||||
<a-card class="form-card" :bordered="false" title="创作设置">
|
||||
<a-form :model="form" layout="vertical" class="form-container">
|
||||
<a-form-item class="form-item">
|
||||
<!-- 使用 PromptSelector 组件 -->
|
||||
<!-- 使用 PromptSelector 组件(数据来自 Store) -->
|
||||
<PromptSelector
|
||||
v-model="selectedPromptId"
|
||||
:prompts="allPrompts"
|
||||
:loading="loadingPrompts"
|
||||
:search-keyword="promptSearchKeyword"
|
||||
@change="handlePromptChange"
|
||||
@update:searchKeyword="promptSearchKeyword = $event"
|
||||
/>
|
||||
|
||||
|
||||
</a-form-item>
|
||||
|
||||
<!-- 统一输入:文本或视频链接 -->
|
||||
|
||||
@@ -11,7 +11,7 @@ defineProps({
|
||||
hasMore: { type: Boolean, default: false },
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:selectedRowKeys', 'export', 'batchAnalyze', 'loadMore'])
|
||||
const emit = defineEmits(['update:selectedRowKeys', 'export', 'batchAnalyze', 'loadMore', 'createAsyncTask'])
|
||||
|
||||
const defaultColumns = [
|
||||
{ title: '封面', key: 'cover', dataIndex: 'cover', width: 100 },
|
||||
@@ -59,6 +59,13 @@ function formatNumber(value) {
|
||||
@click="$emit('batchAnalyze')"
|
||||
:disabled="data.length === 0 || selectedRowKeys.length === 0 || selectedRowKeys.length > 20"
|
||||
/>
|
||||
<GradientButton
|
||||
text="异步任务"
|
||||
size="small"
|
||||
@click="$emit('createAsyncTask')"
|
||||
:disabled="data.length === 0 || selectedRowKeys.length === 0"
|
||||
icon="clock-circle"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<a-table
|
||||
@@ -90,17 +97,8 @@ function formatNumber(value) {
|
||||
<template v-else-if="column.key === 'play_count'">
|
||||
{{ record.play_count ? (record.play_count / 10000).toFixed(1) + 'w' : '0' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'digg_count'">
|
||||
{{ formatNumber(record.digg_count) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'comment_count'">
|
||||
{{ formatNumber(record.comment_count) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'share_count'">
|
||||
{{ formatNumber(record.share_count) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'collect_count'">
|
||||
{{ formatNumber(record.collect_count) }}
|
||||
<template v-else-if="['digg_count', 'comment_count', 'share_count', 'collect_count'].includes(column.key)">
|
||||
{{ formatNumber(record[column.key]) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'create_time'">
|
||||
{{ formatTime(record.create_time) }}
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<BasicLayout title="对标分析任务">
|
||||
<div class="task-list-container">
|
||||
<!-- 筛选 -->
|
||||
<div class="filter-section">
|
||||
<a-select
|
||||
v-model:value="filterStatus"
|
||||
placeholder="全部状态"
|
||||
style="width: 150px"
|
||||
allowClear
|
||||
@change="handleFilterChange"
|
||||
>
|
||||
<a-select-option :value="0">待处理</a-select-option>
|
||||
<a-select-option :value="1">处理中</a-select-option>
|
||||
<a-select-option :value="2">成功</a-select-option>
|
||||
<a-select-option :value="3">失败</a-select-option>
|
||||
</a-select>
|
||||
<a-button type="primary" @click="handleRefresh" :loading="loading">刷新</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 列表 -->
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="taskList"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
@change="handleTableChange"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="getStatusColor(record.status)">{{ getStatusText(record.status) }}</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'progress'">
|
||||
<a-progress :percent="record.progress" :status="getProgressStatus(record.status)" size="small" />
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button v-if="record.status === 2 && record.generatedPrompt" type="link" size="small" @click="handleViewPrompt(record)">查看</a-button>
|
||||
<a-button v-if="record.status === 2 && record.generatedPrompt" type="link" size="small" @click="handleCopyPrompt(record)">复制</a-button>
|
||||
<a-popconfirm v-if="record.status !== 1" title="确定删除?" @confirm="handleDelete(record)">
|
||||
<a-button type="link" size="small" danger>删除</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 提示词弹窗 -->
|
||||
<a-modal v-model:open="promptModalVisible" title="生成的提示词" :footer="null" width="700px">
|
||||
<div class="prompt-content">{{ currentPrompt }}</div>
|
||||
<div class="prompt-actions">
|
||||
<a-button type="primary" @click="handleCopyCurrentPrompt">复制到剪贴板</a-button>
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</BasicLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { BenchmarkTaskApi } from '@/api/benchmarkTask'
|
||||
import { copyToClipboard } from '@/utils/clipboard'
|
||||
import BasicLayout from '@/layouts/components/BasicLayout.vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const taskList = ref([])
|
||||
const filterStatus = ref(undefined)
|
||||
const promptModalVisible = ref(false)
|
||||
const currentPrompt = ref('')
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{ title: '任务名称', dataIndex: 'taskName', key: 'taskName', ellipsis: true },
|
||||
{ title: '视频数量', dataIndex: 'videoCount', key: 'videoCount', width: 100 },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status', width: 100 },
|
||||
{ title: '进度', dataIndex: 'progress', key: 'progress', width: 150 },
|
||||
{ title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 180 },
|
||||
{ title: '操作', key: 'action', width: 180 },
|
||||
]
|
||||
|
||||
const STATUS_MAP = {
|
||||
color: { 0: 'default', 1: 'processing', 2: 'success', 3: 'error' },
|
||||
text: { 0: '待处理', 1: '处理中', 2: '成功', 3: '失败' }
|
||||
}
|
||||
|
||||
let refreshTimer = null
|
||||
|
||||
async function loadTaskList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await BenchmarkTaskApi.getTaskPage({
|
||||
pageNo: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
status: filterStatus.value,
|
||||
})
|
||||
if (response?.data) {
|
||||
taskList.value = response.data.list || []
|
||||
pagination.total = response.data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载失败:', error)
|
||||
message.error('加载任务列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleRefresh() { loadTaskList() }
|
||||
function handleFilterChange() { pagination.current = 1; loadTaskList() }
|
||||
function handleTableChange(page) { pagination.current = page.current; pagination.pageSize = page.pageSize; loadTaskList() }
|
||||
|
||||
function getStatusColor(status) {
|
||||
return STATUS_MAP.color[status] || 'default'
|
||||
}
|
||||
|
||||
function getStatusText(status) {
|
||||
return STATUS_MAP.text[status] || '未知'
|
||||
}
|
||||
|
||||
function getProgressStatus(status) {
|
||||
if (status === 3) return 'exception'
|
||||
if (status === 2) return 'success'
|
||||
return 'active'
|
||||
}
|
||||
|
||||
function handleViewPrompt(record) {
|
||||
currentPrompt.value = record.generatedPrompt
|
||||
promptModalVisible.value = true
|
||||
}
|
||||
|
||||
async function copyPromptText(text) {
|
||||
const success = await copyToClipboard(text)
|
||||
message[success ? 'success' : 'error'](success ? '已复制' : '复制失败')
|
||||
}
|
||||
|
||||
function handleCopyPrompt(record) {
|
||||
copyPromptText(record.generatedPrompt)
|
||||
}
|
||||
|
||||
function handleCopyCurrentPrompt() {
|
||||
copyPromptText(currentPrompt.value)
|
||||
}
|
||||
|
||||
async function handleDelete(record) {
|
||||
try {
|
||||
await BenchmarkTaskApi.deleteTask(record.id)
|
||||
message.success('删除成功')
|
||||
loadTaskList()
|
||||
} catch {
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadTaskList()
|
||||
refreshTimer = setInterval(() => {
|
||||
const hasRunning = taskList.value.some(t => t.status === 0 || t.status === 1)
|
||||
if (hasRunning) loadTaskList()
|
||||
}, 5000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (refreshTimer) clearInterval(refreshTimer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.task-list-container { padding: 24px; background: var(--bg-primary); border-radius: 8px; }
|
||||
.filter-section { display: flex; gap: 16px; margin-bottom: 16px; }
|
||||
.prompt-content { padding: 16px; background: var(--bg-secondary); border-radius: 6px; white-space: pre-wrap; max-height: 400px; overflow-y: auto; }
|
||||
.prompt-actions { margin-top: 16px; text-align: right; }
|
||||
</style>
|
||||
@@ -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='兑换码表';
|
||||
|
||||
41
sql/mysql/benchmark_task_tables.sql
Normal file
41
sql/mysql/benchmark_task_tables.sql
Normal file
@@ -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
|
||||
45
sql/mysql/redeem_code_menu.sql
Normal file
45
sql/mysql/redeem_code_menu.sql
Normal file
@@ -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');
|
||||
58
sql/mysql/redeem_code_tables.sql
Normal file
58
sql/mysql/redeem_code_tables.sql
Normal file
@@ -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='兑换记录表';
|
||||
@@ -0,0 +1,77 @@
|
||||
package cn.iocoder.yudao.module.tik.benchmark.controller;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.module.tik.benchmark.dal.dataobject.BenchmarkTaskDO;
|
||||
import cn.iocoder.yudao.module.tik.benchmark.service.BenchmarkTaskService;
|
||||
import cn.iocoder.yudao.module.tik.benchmark.vo.BenchmarkTaskCreateReqVO;
|
||||
import cn.iocoder.yudao.module.tik.benchmark.vo.BenchmarkTaskPageReqVO;
|
||||
import cn.iocoder.yudao.module.tik.benchmark.vo.BenchmarkTaskRespVO;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
|
||||
|
||||
/**
|
||||
* 用户 App - 对标分析任务
|
||||
*/
|
||||
@Tag(name = "用户 App - 对标分析任务")
|
||||
@RestController
|
||||
@RequestMapping("/api/tik/benchmark-task")
|
||||
@Validated
|
||||
public class AppBenchmarkTaskController {
|
||||
|
||||
@Resource
|
||||
private BenchmarkTaskService benchmarkTaskService;
|
||||
|
||||
@PostMapping("/create")
|
||||
@Operation(summary = "创建对标分析任务")
|
||||
public CommonResult<Long> createTask(@Valid @RequestBody BenchmarkTaskCreateReqVO createReqVO) {
|
||||
Long userId = getLoginUserId();
|
||||
if (userId == null) {
|
||||
return CommonResult.error(401, "用户未登录");
|
||||
}
|
||||
return success(benchmarkTaskService.createTask(userId, createReqVO));
|
||||
}
|
||||
|
||||
@GetMapping("/get")
|
||||
@Operation(summary = "获取任务详情")
|
||||
@Parameter(name = "id", description = "任务ID", required = true, example = "1024")
|
||||
public CommonResult<BenchmarkTaskRespVO> getTask(@RequestParam("id") Long id) {
|
||||
BenchmarkTaskDO task = benchmarkTaskService.getTask(id);
|
||||
return success(BeanUtils.toBean(task, BenchmarkTaskRespVO.class));
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "获取任务分页列表")
|
||||
public CommonResult<PageResult<BenchmarkTaskRespVO>> getTaskPage(@Valid BenchmarkTaskPageReqVO pageReqVO) {
|
||||
Long userId = getLoginUserId();
|
||||
if (userId == null) {
|
||||
return CommonResult.error(401, "用户未登录");
|
||||
}
|
||||
pageReqVO.setUserId(userId);
|
||||
PageResult<BenchmarkTaskDO> pageResult = benchmarkTaskService.getTaskPage(pageReqVO);
|
||||
return success(BeanUtils.toBean(pageResult, BenchmarkTaskRespVO.class));
|
||||
}
|
||||
|
||||
@DeleteMapping("/delete")
|
||||
@Operation(summary = "删除任务")
|
||||
@Parameter(name = "id", description = "任务ID", required = true, example = "1024")
|
||||
public CommonResult<Boolean> deleteTask(@RequestParam("id") Long id) {
|
||||
Long userId = getLoginUserId();
|
||||
if (userId == null) {
|
||||
return CommonResult.error(401, "用户未登录");
|
||||
}
|
||||
benchmarkTaskService.deleteTask(userId, id);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package cn.iocoder.yudao.module.tik.benchmark.dal.mysql;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import cn.iocoder.yudao.module.tik.benchmark.dal.dataobject.BenchmarkTaskDO;
|
||||
import cn.iocoder.yudao.module.tik.benchmark.vo.BenchmarkTaskPageReqVO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 对标分析异步任务 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface BenchmarkTaskMapper extends BaseMapperX<BenchmarkTaskDO> {
|
||||
|
||||
default PageResult<BenchmarkTaskDO> selectPage(BenchmarkTaskPageReqVO reqVO) {
|
||||
return selectPage(reqVO, new LambdaQueryWrapperX<BenchmarkTaskDO>()
|
||||
.eqIfPresent(BenchmarkTaskDO::getUserId, reqVO.getUserId())
|
||||
.eqIfPresent(BenchmarkTaskDO::getStatus, reqVO.getStatus())
|
||||
.orderByDesc(BenchmarkTaskDO::getId));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
package cn.iocoder.yudao.module.tik.benchmark.job;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.module.tik.benchmark.dal.dataobject.BenchmarkTaskDO;
|
||||
import cn.iocoder.yudao.module.tik.benchmark.dal.mysql.BenchmarkTaskMapper;
|
||||
import cn.iocoder.yudao.module.tik.benchmark.enums.BenchmarkTaskStatusEnum;
|
||||
import cn.iocoder.yudao.module.tik.benchmark.vo.BenchmarkTaskCreateReqVO;
|
||||
import cn.iocoder.yudao.module.tik.tikhup.service.TikHupService;
|
||||
import cn.iocoder.yudao.module.tik.userprompt.service.UserPromptService;
|
||||
import cn.iocoder.yudao.module.tik.userprompt.vo.UserPromptSaveReqVO;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 对标分析任务异步执行器
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class BenchmarkTaskExecutor {
|
||||
|
||||
@Resource
|
||||
private BenchmarkTaskMapper benchmarkTaskMapper;
|
||||
|
||||
@Resource
|
||||
private TikHupService tikHupService;
|
||||
|
||||
@Resource
|
||||
private UserPromptService userPromptService;
|
||||
|
||||
/**
|
||||
* 异步执行对标分析任务
|
||||
*/
|
||||
@Async("benchmarkTaskExecutor")
|
||||
public void executeAsync(Long taskId, BenchmarkTaskCreateReqVO createReqVO) {
|
||||
log.info("[executeAsync][taskId={}] 开始异步执行对标分析任务", taskId);
|
||||
|
||||
BenchmarkTaskDO task = benchmarkTaskMapper.selectById(taskId);
|
||||
if (task == null) {
|
||||
log.error("[executeAsync][taskId={}] 任务不存在", taskId);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. 更新状态为执行中
|
||||
updateTaskProgress(taskId, 0, BenchmarkTaskStatusEnum.RUNNING.getStatus());
|
||||
|
||||
// 2. 获取视频转写
|
||||
updateTaskProgress(taskId, 10, BenchmarkTaskStatusEnum.RUNNING.getStatus());
|
||||
List<String> videoUrls = createReqVO.getVideoUrls();
|
||||
List<String> transcriptions = new ArrayList<>();
|
||||
|
||||
// 批量获取转写(使用 videoToCharacters2)
|
||||
try {
|
||||
Object result = tikHupService.videoToCharacters2(videoUrls);
|
||||
if (result instanceof List) {
|
||||
List<?> resultList = (List<?>) result;
|
||||
for (Object item : resultList) {
|
||||
if (item != null && StrUtil.isNotBlank(item.toString())) {
|
||||
transcriptions.add(item.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[executeAsync][taskId={}] 批量转写失败,尝试逐个转写: {}", taskId, e.getMessage());
|
||||
// 降级:逐个转写
|
||||
for (String videoUrl : videoUrls) {
|
||||
try {
|
||||
Object result = tikHupService.videoToCharacters(videoUrl);
|
||||
if (result != null && StrUtil.isNotBlank(result.toString())) {
|
||||
transcriptions.add(result.toString());
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.warn("[executeAsync][taskId={}, videoUrl={}] 转写失败: {}", taskId, videoUrl, ex.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateTaskProgress(taskId, 60, BenchmarkTaskStatusEnum.RUNNING.getStatus());
|
||||
|
||||
if (CollUtil.isEmpty(transcriptions)) {
|
||||
throw new RuntimeException("未能获取任何视频转写内容");
|
||||
}
|
||||
|
||||
// 3. 合并转写文本
|
||||
String mergedText = String.join("\n\n---\n\n", transcriptions);
|
||||
|
||||
// 4. 保存合并文本到任务
|
||||
BenchmarkTaskDO update = new BenchmarkTaskDO();
|
||||
update.setId(taskId);
|
||||
update.setMergedText(mergedText);
|
||||
benchmarkTaskMapper.updateById(update);
|
||||
|
||||
updateTaskProgress(taskId, 70, BenchmarkTaskStatusEnum.RUNNING.getStatus());
|
||||
|
||||
// 5. 调用 AI 生成提示词
|
||||
String generatedPrompt;
|
||||
try {
|
||||
Object result = tikHupService.deepseekAnalysis("benchmark", mergedText);
|
||||
generatedPrompt = result != null ? result.toString() : "";
|
||||
} catch (Exception e) {
|
||||
log.warn("[executeAsync][taskId={}] AI 生成失败,使用默认提示词: {}", taskId, e.getMessage());
|
||||
// 降级:使用合并文本的前2000字符
|
||||
generatedPrompt = "请参考以下视频文案风格生成内容:\n\n" +
|
||||
mergedText.substring(0, Math.min(2000, mergedText.length()));
|
||||
}
|
||||
|
||||
updateTaskProgress(taskId, 90, BenchmarkTaskStatusEnum.RUNNING.getStatus());
|
||||
|
||||
// 6. 如果需要保存为提示词
|
||||
Long promptId = null;
|
||||
if (Boolean.TRUE.equals(createReqVO.getSaveAsPrompt()) && StrUtil.isNotBlank(createReqVO.getPromptName())) {
|
||||
promptId = saveGeneratedPrompt(task.getUserId(), createReqVO.getPromptName(), generatedPrompt);
|
||||
}
|
||||
|
||||
// 7. 标记任务成功
|
||||
markTaskSuccess(taskId, mergedText, generatedPrompt, promptId);
|
||||
|
||||
log.info("[executeAsync][taskId={}] 对标分析任务执行成功", taskId);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[executeAsync][taskId={}] 对标分析任务执行失败", taskId, e);
|
||||
markTaskFailed(taskId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新任务进度
|
||||
*/
|
||||
private void updateTaskProgress(Long taskId, Integer progress, Integer status) {
|
||||
BenchmarkTaskDO update = new BenchmarkTaskDO();
|
||||
update.setId(taskId);
|
||||
update.setProgress(progress);
|
||||
update.setStatus(status);
|
||||
|
||||
if (BenchmarkTaskStatusEnum.RUNNING.getStatus().equals(status)) {
|
||||
update.setStartTime(LocalDateTime.now());
|
||||
}
|
||||
|
||||
benchmarkTaskMapper.updateById(update);
|
||||
log.info("[updateTaskProgress][taskId={}, progress={}, status={}] 更新任务进度", taskId, progress, status);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记任务成功
|
||||
*/
|
||||
private void markTaskSuccess(Long taskId, String mergedText, String generatedPrompt, Long promptId) {
|
||||
BenchmarkTaskDO update = new BenchmarkTaskDO();
|
||||
update.setId(taskId);
|
||||
update.setStatus(BenchmarkTaskStatusEnum.SUCCESS.getStatus());
|
||||
update.setProgress(100);
|
||||
update.setMergedText(mergedText);
|
||||
update.setGeneratedPrompt(generatedPrompt);
|
||||
update.setPromptId(promptId);
|
||||
update.setFinishTime(LocalDateTime.now());
|
||||
|
||||
benchmarkTaskMapper.updateById(update);
|
||||
log.info("[markTaskSuccess][taskId={}, promptId={}] 任务执行成功", taskId, promptId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记任务失败
|
||||
*/
|
||||
private void markTaskFailed(Long taskId, String errorMsg) {
|
||||
BenchmarkTaskDO update = new BenchmarkTaskDO();
|
||||
update.setId(taskId);
|
||||
update.setStatus(BenchmarkTaskStatusEnum.FAILED.getStatus());
|
||||
update.setErrorMsg(StrUtil.sub(errorMsg, 0, 1000));
|
||||
update.setFinishTime(LocalDateTime.now());
|
||||
|
||||
benchmarkTaskMapper.updateById(update);
|
||||
log.error("[markTaskFailed][taskId={}, errorMsg={}] 任务执行失败", taskId, errorMsg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存生成的提示词
|
||||
*/
|
||||
private Long saveGeneratedPrompt(Long userId, String promptName, String content) {
|
||||
UserPromptSaveReqVO saveReqVO = new UserPromptSaveReqVO();
|
||||
saveReqVO.setUserId(userId);
|
||||
saveReqVO.setName(promptName);
|
||||
saveReqVO.setContent(content);
|
||||
saveReqVO.setStatus(1);
|
||||
saveReqVO.setIsPublic(false);
|
||||
return userPromptService.createUserPrompt(saveReqVO);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package cn.iocoder.yudao.module.tik.benchmark.service;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.module.tik.benchmark.dal.dataobject.BenchmarkTaskDO;
|
||||
import cn.iocoder.yudao.module.tik.benchmark.vo.BenchmarkTaskCreateReqVO;
|
||||
import cn.iocoder.yudao.module.tik.benchmark.vo.BenchmarkTaskPageReqVO;
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
/**
|
||||
* 对标分析任务 Service 接口
|
||||
*/
|
||||
public interface BenchmarkTaskService {
|
||||
|
||||
/**
|
||||
* 创建对标分析任务
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param createReqVO 创建请求
|
||||
* @return 任务ID
|
||||
*/
|
||||
Long createTask(Long userId, @Valid BenchmarkTaskCreateReqVO createReqVO);
|
||||
|
||||
/**
|
||||
* 获取任务详情
|
||||
*
|
||||
* @param id 任务ID
|
||||
* @return 任务详情
|
||||
*/
|
||||
BenchmarkTaskDO getTask(Long id);
|
||||
|
||||
/**
|
||||
* 获取任务分页列表
|
||||
*
|
||||
* @param pageReqVO 分页请求
|
||||
* @return 分页列表
|
||||
*/
|
||||
PageResult<BenchmarkTaskDO> getTaskPage(BenchmarkTaskPageReqVO pageReqVO);
|
||||
|
||||
/**
|
||||
* 删除任务
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param id 任务ID
|
||||
*/
|
||||
void deleteTask(Long userId, Long id);
|
||||
|
||||
/**
|
||||
* 更新任务进度
|
||||
*
|
||||
* @param taskId 任务ID
|
||||
* @param progress 进度
|
||||
* @param status 状态
|
||||
*/
|
||||
void updateTaskProgress(Long taskId, Integer progress, Integer status);
|
||||
|
||||
/**
|
||||
* 标记任务成功
|
||||
*
|
||||
* @param taskId 任务ID
|
||||
* @param mergedText 合并文本
|
||||
* @param generatedPrompt 生成的提示词
|
||||
* @param promptId 保存的提示词ID(可选)
|
||||
*/
|
||||
void markTaskSuccess(Long taskId, String mergedText, String generatedPrompt, Long promptId);
|
||||
|
||||
/**
|
||||
* 标记任务失败
|
||||
*
|
||||
* @param taskId 任务ID
|
||||
* @param errorMsg 错误信息
|
||||
*/
|
||||
void markTaskFailed(Long taskId, String errorMsg);
|
||||
|
||||
/**
|
||||
* 保存生成的提示词
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param promptName 提示词名称
|
||||
* @param content 提示词内容
|
||||
* @return 提示词ID
|
||||
*/
|
||||
Long saveGeneratedPrompt(Long userId, String promptName, String content);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package cn.iocoder.yudao.module.tik.benchmark.service;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.module.tik.benchmark.dal.dataobject.BenchmarkTaskDO;
|
||||
import cn.iocoder.yudao.module.tik.benchmark.dal.mysql.BenchmarkTaskMapper;
|
||||
import cn.iocoder.yudao.module.tik.benchmark.enums.BenchmarkTaskStatusEnum;
|
||||
import cn.iocoder.yudao.module.tik.benchmark.job.BenchmarkTaskExecutor;
|
||||
import cn.iocoder.yudao.module.tik.benchmark.vo.BenchmarkTaskCreateReqVO;
|
||||
import cn.iocoder.yudao.module.tik.benchmark.vo.BenchmarkTaskPageReqVO;
|
||||
import cn.iocoder.yudao.module.tik.userprompt.service.UserPromptService;
|
||||
import cn.iocoder.yudao.module.tik.userprompt.vo.UserPromptSaveReqVO;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.module.tik.enums.ErrorCodeConstants.*;
|
||||
|
||||
/**
|
||||
* 对标分析任务 Service 实现类
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@Validated
|
||||
public class BenchmarkTaskServiceImpl implements BenchmarkTaskService {
|
||||
|
||||
@Resource
|
||||
private BenchmarkTaskMapper benchmarkTaskMapper;
|
||||
|
||||
@Resource
|
||||
private BenchmarkTaskExecutor benchmarkTaskExecutor;
|
||||
|
||||
@Resource
|
||||
private UserPromptService userPromptService;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long createTask(Long userId, BenchmarkTaskCreateReqVO createReqVO) {
|
||||
// 1. 创建任务记录
|
||||
BenchmarkTaskDO task = new BenchmarkTaskDO();
|
||||
task.setUserId(userId);
|
||||
task.setTaskName(createReqVO.getTaskName());
|
||||
task.setVideoCount(createReqVO.getVideoUrls().size());
|
||||
task.setVideoUrls(JsonUtils.toJsonString(createReqVO.getVideoUrls()));
|
||||
task.setStatus(BenchmarkTaskStatusEnum.PENDING.getStatus());
|
||||
task.setProgress(0);
|
||||
|
||||
benchmarkTaskMapper.insert(task);
|
||||
Long taskId = task.getId();
|
||||
|
||||
log.info("[createTask][userId={}, taskId={}] 创建对标分析任务成功", userId, taskId);
|
||||
|
||||
// 2. 异步执行任务
|
||||
benchmarkTaskExecutor.executeAsync(taskId, createReqVO);
|
||||
|
||||
return taskId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BenchmarkTaskDO getTask(Long id) {
|
||||
return benchmarkTaskMapper.selectById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<BenchmarkTaskDO> getTaskPage(BenchmarkTaskPageReqVO pageReqVO) {
|
||||
return benchmarkTaskMapper.selectPage(pageReqVO);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteTask(Long userId, Long id) {
|
||||
// 1. 校验任务存在
|
||||
BenchmarkTaskDO task = validateTaskExists(id);
|
||||
|
||||
// 2. 校验是否属于当前用户
|
||||
if (!task.getUserId().equals(userId)) {
|
||||
throw exception(BENCHMARK_TASK_NOT_EXISTS);
|
||||
}
|
||||
|
||||
// 3. 只能删除已完成或失败的任务
|
||||
if (BenchmarkTaskStatusEnum.RUNNING.getStatus().equals(task.getStatus())) {
|
||||
throw exception(BENCHMARK_TASK_RUNNING);
|
||||
}
|
||||
|
||||
// 4. 删除任务
|
||||
benchmarkTaskMapper.deleteById(id);
|
||||
log.info("[deleteTask][userId={}, taskId={}] 删除任务成功", userId, id);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateTaskProgress(Long taskId, Integer progress, Integer status) {
|
||||
BenchmarkTaskDO update = new BenchmarkTaskDO();
|
||||
update.setId(taskId);
|
||||
update.setProgress(progress);
|
||||
update.setStatus(status);
|
||||
|
||||
// 如果是开始执行,记录开始时间
|
||||
if (BenchmarkTaskStatusEnum.RUNNING.getStatus().equals(status)) {
|
||||
update.setStartTime(LocalDateTime.now());
|
||||
}
|
||||
|
||||
benchmarkTaskMapper.updateById(update);
|
||||
log.info("[updateTaskProgress][taskId={}, progress={}, status={}] 更新任务进度", taskId, progress, status);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void markTaskSuccess(Long taskId, String mergedText, String generatedPrompt, Long promptId) {
|
||||
BenchmarkTaskDO update = new BenchmarkTaskDO();
|
||||
update.setId(taskId);
|
||||
update.setStatus(BenchmarkTaskStatusEnum.SUCCESS.getStatus());
|
||||
update.setProgress(100);
|
||||
update.setMergedText(mergedText);
|
||||
update.setGeneratedPrompt(generatedPrompt);
|
||||
update.setPromptId(promptId);
|
||||
update.setFinishTime(LocalDateTime.now());
|
||||
|
||||
benchmarkTaskMapper.updateById(update);
|
||||
log.info("[markTaskSuccess][taskId={}, promptId={}] 任务执行成功", taskId, promptId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void markTaskFailed(Long taskId, String errorMsg) {
|
||||
BenchmarkTaskDO update = new BenchmarkTaskDO();
|
||||
update.setId(taskId);
|
||||
update.setStatus(BenchmarkTaskStatusEnum.FAILED.getStatus());
|
||||
update.setErrorMsg(StrUtil.sub(errorMsg, 0, 1000));
|
||||
update.setFinishTime(LocalDateTime.now());
|
||||
|
||||
benchmarkTaskMapper.updateById(update);
|
||||
log.error("[markTaskFailed][taskId={}, errorMsg={}] 任务执行失败", taskId, errorMsg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long saveGeneratedPrompt(Long userId, String promptName, String content) {
|
||||
UserPromptSaveReqVO saveReqVO = new UserPromptSaveReqVO();
|
||||
saveReqVO.setUserId(userId);
|
||||
saveReqVO.setName(promptName);
|
||||
saveReqVO.setContent(content);
|
||||
saveReqVO.setStatus(1); // 启用
|
||||
saveReqVO.setIsPublic(false);
|
||||
|
||||
return userPromptService.createUserPrompt(saveReqVO);
|
||||
}
|
||||
|
||||
private BenchmarkTaskDO validateTaskExists(Long id) {
|
||||
BenchmarkTaskDO task = benchmarkTaskMapper.selectById(id);
|
||||
if (task == null) {
|
||||
throw exception(BENCHMARK_TASK_NOT_EXISTS);
|
||||
}
|
||||
return task;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package cn.iocoder.yudao.module.tik.benchmark.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 创建对标分析任务 Request VO
|
||||
*/
|
||||
@Schema(description = "用户 App - 创建对标分析任务 Request VO")
|
||||
@Data
|
||||
public class BenchmarkTaskCreateReqVO {
|
||||
|
||||
@Schema(description = "任务名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "小红书种草风格分析")
|
||||
@NotEmpty(message = "任务名称不能为空")
|
||||
private String taskName;
|
||||
|
||||
@Schema(description = "视频URL列表", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotEmpty(message = "视频URL列表不能为空")
|
||||
private List<String> videoUrls;
|
||||
|
||||
@Schema(description = "是否保存为提示词", example = "true")
|
||||
private Boolean saveAsPrompt;
|
||||
|
||||
@Schema(description = "提示词名称(保存时需要)", example = "小红书种草风")
|
||||
private String promptName;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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, "无权访问该任务");
|
||||
|
||||
}
|
||||
|
||||
@@ -60,6 +60,19 @@ public interface MemberUserProfileMapper extends BaseMapperX<MemberUserProfileDO
|
||||
"WHERE user_id = #{userId} AND remaining_points >= #{points}")
|
||||
int updatePointsDeduct(@Param("userId") String userId, @Param("points") Integer points);
|
||||
|
||||
/**
|
||||
* 原子增加积分
|
||||
* @param userId 用户ID
|
||||
* @param points 增加积分数量(正数)
|
||||
* @return 影响行数
|
||||
*/
|
||||
@Update("UPDATE muye_member_user_profile " +
|
||||
"SET remaining_points = remaining_points + #{points}, " +
|
||||
" total_points = total_points + #{points}, " +
|
||||
" update_time = NOW() " +
|
||||
"WHERE user_id = #{userId}")
|
||||
int updatePointsIncrease(@Param("userId") String userId, @Param("points") Integer points);
|
||||
|
||||
/**
|
||||
* 原子增加已用存储(乐观锁)
|
||||
* @param userId 用户ID
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package cn.iocoder.yudao.module.tik.muye.redeemcode;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import cn.iocoder.yudao.module.tik.muye.redeemcode.service.RedeemCodeService;
|
||||
import cn.iocoder.yudao.module.tik.muye.redeemcode.vo.RedeemCodeRedeemReqVO;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* 用户 App - 兑换码
|
||||
*
|
||||
* @author muye
|
||||
*/
|
||||
@Tag(name = "用户 App - 兑换码")
|
||||
@RestController
|
||||
@RequestMapping("/api/tik/redeem-code")
|
||||
@Validated
|
||||
public class AppRedeemCodeController {
|
||||
|
||||
@Resource
|
||||
private RedeemCodeService redeemCodeService;
|
||||
|
||||
@PostMapping("/redeem")
|
||||
@Operation(summary = "兑换码兑换")
|
||||
public CommonResult<Long> redeem(@Valid @RequestBody RedeemCodeRedeemReqVO reqVO) {
|
||||
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
||||
Long recordId = redeemCodeService.redeem(String.valueOf(userId), reqVO.getCode());
|
||||
return success(recordId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package cn.iocoder.yudao.module.tik.muye.redeemcode;
|
||||
|
||||
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
|
||||
import cn.iocoder.yudao.module.tik.muye.redeemcode.dal.RedeemCodeDO;
|
||||
import cn.iocoder.yudao.module.tik.muye.redeemcode.service.RedeemCodeService;
|
||||
import cn.iocoder.yudao.module.tik.muye.redeemcode.vo.*;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* 管理后台 - 兑换码
|
||||
*
|
||||
* @author muye
|
||||
*/
|
||||
@Tag(name = "管理后台 - 兑换码")
|
||||
@RestController
|
||||
@RequestMapping("/admin-api/muye/redeem-code")
|
||||
@Validated
|
||||
public class RedeemCodeController {
|
||||
|
||||
@Resource
|
||||
private RedeemCodeService redeemCodeService;
|
||||
|
||||
@PostMapping("/create")
|
||||
@Operation(summary = "创建兑换码")
|
||||
@PreAuthorize("@ss.hasPermission('muye:redeem-code:create')")
|
||||
public CommonResult<Long> createRedeemCode(@Valid @RequestBody RedeemCodeSaveReqVO createReqVO) {
|
||||
return success(redeemCodeService.createRedeemCode(createReqVO));
|
||||
}
|
||||
|
||||
@PostMapping("/generate")
|
||||
@Operation(summary = "批量生成兑换码")
|
||||
@PreAuthorize("@ss.hasPermission('muye:redeem-code:create')")
|
||||
public CommonResult<List<String>> generateRedeemCodes(@Valid @RequestBody RedeemCodeGenerateReqVO generateReqVO) {
|
||||
return success(redeemCodeService.generateRedeemCodes(generateReqVO));
|
||||
}
|
||||
|
||||
@PutMapping("/update")
|
||||
@Operation(summary = "更新兑换码")
|
||||
@PreAuthorize("@ss.hasPermission('muye:redeem-code:update')")
|
||||
public CommonResult<Boolean> updateRedeemCode(@Valid @RequestBody RedeemCodeSaveReqVO updateReqVO) {
|
||||
redeemCodeService.updateRedeemCode(updateReqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/delete")
|
||||
@Operation(summary = "删除兑换码")
|
||||
@Parameter(name = "id", description = "编号", required = true)
|
||||
@PreAuthorize("@ss.hasPermission('muye:redeem-code:delete')")
|
||||
public CommonResult<Boolean> deleteRedeemCode(@RequestParam("id") Long id) {
|
||||
redeemCodeService.deleteRedeemCode(id);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/delete-list")
|
||||
@Operation(summary = "批量删除兑换码")
|
||||
@Parameter(name = "ids", description = "编号", required = true)
|
||||
@PreAuthorize("@ss.hasPermission('muye:redeem-code:delete')")
|
||||
public CommonResult<Boolean> deleteRedeemCodeList(@RequestParam("ids") List<Long> ids) {
|
||||
redeemCodeService.deleteRedeemCodeListByIds(ids);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/get")
|
||||
@Operation(summary = "获得兑换码")
|
||||
@Parameter(name = "id", description = "编号", required = true)
|
||||
@PreAuthorize("@ss.hasPermission('muye:redeem-code:query')")
|
||||
public CommonResult<RedeemCodeRespVO> getRedeemCode(@RequestParam("id") Long id) {
|
||||
RedeemCodeDO redeemCode = redeemCodeService.getRedeemCode(id);
|
||||
return success(BeanUtils.toBean(redeemCode, RedeemCodeRespVO.class));
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "获得兑换码分页")
|
||||
@PreAuthorize("@ss.hasPermission('muye:redeem-code:query')")
|
||||
public CommonResult<PageResult<RedeemCodeRespVO>> getRedeemCodePage(@Valid RedeemCodePageReqVO pageReqVO) {
|
||||
PageResult<RedeemCodeDO> pageResult = redeemCodeService.getRedeemCodePage(pageReqVO);
|
||||
return success(BeanUtils.toBean(pageResult, RedeemCodeRespVO.class));
|
||||
}
|
||||
|
||||
@GetMapping("/export-excel")
|
||||
@Operation(summary = "导出兑换码 Excel")
|
||||
@PreAuthorize("@ss.hasPermission('muye:redeem-code:export')")
|
||||
@ApiAccessLog(operateType = EXPORT)
|
||||
public void exportRedeemCodeExcel(@Valid RedeemCodePageReqVO pageReqVO,
|
||||
HttpServletResponse response) throws IOException {
|
||||
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
|
||||
List<RedeemCodeDO> list = redeemCodeService.getRedeemCodePage(pageReqVO).getList();
|
||||
ExcelUtils.write(response, "兑换码.xls", "数据", RedeemCodeRespVO.class,
|
||||
BeanUtils.toBean(list, RedeemCodeRespVO.class));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package cn.iocoder.yudao.module.tik.muye.redeemcode.mapper;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import cn.iocoder.yudao.module.tik.muye.redeemcode.dal.RedeemCodeDO;
|
||||
import cn.iocoder.yudao.module.tik.muye.redeemcode.vo.RedeemCodePageReqVO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
/**
|
||||
* 兑换码 Mapper
|
||||
*
|
||||
* @author muye
|
||||
*/
|
||||
@Mapper
|
||||
public interface RedeemCodeMapper extends BaseMapperX<RedeemCodeDO> {
|
||||
|
||||
/**
|
||||
* 根据兑换码查询
|
||||
*/
|
||||
default RedeemCodeDO selectByCode(String code) {
|
||||
return selectOne(new LambdaQueryWrapperX<RedeemCodeDO>()
|
||||
.eq(RedeemCodeDO::getCode, code));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据兑换码查询(带悲观锁)
|
||||
*/
|
||||
@Select("SELECT * FROM muye_redeem_code WHERE code = #{code} FOR UPDATE")
|
||||
RedeemCodeDO selectByCodeForUpdate(@Param("code") String code);
|
||||
|
||||
/**
|
||||
* 分页查询
|
||||
*/
|
||||
default PageResult<RedeemCodeDO> selectPage(RedeemCodePageReqVO reqVO) {
|
||||
return selectPage(reqVO, new LambdaQueryWrapperX<RedeemCodeDO>()
|
||||
.eqIfPresent(RedeemCodeDO::getCode, reqVO.getCode())
|
||||
.eqIfPresent(RedeemCodeDO::getCodeType, reqVO.getCodeType())
|
||||
.eqIfPresent(RedeemCodeDO::getPoints, reqVO.getPoints())
|
||||
.eqIfPresent(RedeemCodeDO::getBatchNo, reqVO.getBatchNo())
|
||||
.eqIfPresent(RedeemCodeDO::getSource, reqVO.getSource())
|
||||
.eqIfPresent(RedeemCodeDO::getStatus, reqVO.getStatus())
|
||||
.betweenIfPresent(RedeemCodeDO::getCreateTime, reqVO.getCreateTime())
|
||||
.betweenIfPresent(RedeemCodeDO::getExpireTime, reqVO.getExpireTime())
|
||||
.orderByDesc(RedeemCodeDO::getId));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package cn.iocoder.yudao.module.tik.muye.redeemcode.service;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.module.tik.muye.redeemcode.dal.RedeemCodeDO;
|
||||
import cn.iocoder.yudao.module.tik.muye.redeemcode.vo.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 兑换码 Service 接口
|
||||
*
|
||||
* @author muye
|
||||
*/
|
||||
public interface RedeemCodeService {
|
||||
|
||||
/**
|
||||
* 创建兑换码(单个)
|
||||
*
|
||||
* @param createReqVO 创建信息
|
||||
* @return 编号
|
||||
*/
|
||||
Long createRedeemCode(RedeemCodeSaveReqVO createReqVO);
|
||||
|
||||
/**
|
||||
* 批量生成兑换码
|
||||
*
|
||||
* @param generateReqVO 生成信息
|
||||
* @return 生成的兑换码列表
|
||||
*/
|
||||
List<String> generateRedeemCodes(RedeemCodeGenerateReqVO generateReqVO);
|
||||
|
||||
/**
|
||||
* 更新兑换码
|
||||
*
|
||||
* @param updateReqVO 更新信息
|
||||
*/
|
||||
void updateRedeemCode(RedeemCodeSaveReqVO updateReqVO);
|
||||
|
||||
/**
|
||||
* 删除兑换码
|
||||
*
|
||||
* @param id 编号
|
||||
*/
|
||||
void deleteRedeemCode(Long id);
|
||||
|
||||
/**
|
||||
* 批量删除兑换码
|
||||
*
|
||||
* @param ids 编号列表
|
||||
*/
|
||||
void deleteRedeemCodeListByIds(List<Long> ids);
|
||||
|
||||
/**
|
||||
* 获得兑换码
|
||||
*
|
||||
* @param id 编号
|
||||
* @return 兑换码
|
||||
*/
|
||||
RedeemCodeDO getRedeemCode(Long id);
|
||||
|
||||
/**
|
||||
* 根据兑换码字符串获取
|
||||
*
|
||||
* @param code 兑换码
|
||||
* @return 兑换码
|
||||
*/
|
||||
RedeemCodeDO getRedeemCodeByCode(String code);
|
||||
|
||||
/**
|
||||
* 获得兑换码分页
|
||||
*
|
||||
* @param pageReqVO 分页查询
|
||||
* @return 兑换码分页
|
||||
*/
|
||||
PageResult<RedeemCodeDO> getRedeemCodePage(RedeemCodePageReqVO pageReqVO);
|
||||
|
||||
/**
|
||||
* 兑换码兑换(核心业务)
|
||||
* 验证兑换码有效性,增加用户积分,记录兑换流水
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param code 兑换码
|
||||
* @return 兑换记录ID
|
||||
*/
|
||||
Long redeem(String userId, String code);
|
||||
|
||||
/**
|
||||
* 支付成功后发放兑换码(支付集成用)
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param points 积分数量
|
||||
* @param sourceId 支付订单ID
|
||||
* @return 兑换码
|
||||
*/
|
||||
String issueCodeAfterPayment(Long userId, Integer points, String sourceId);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
package cn.iocoder.yudao.module.tik.muye.redeemcode.service;
|
||||
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
|
||||
import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
|
||||
import cn.iocoder.yudao.module.tik.muye.memberuserprofile.dal.MemberUserProfileDO;
|
||||
import cn.iocoder.yudao.module.tik.muye.memberuserprofile.mapper.MemberUserProfileMapper;
|
||||
import cn.iocoder.yudao.module.tik.muye.pointrecord.dal.PointRecordDO;
|
||||
import cn.iocoder.yudao.module.tik.muye.pointrecord.mapper.PointRecordMapper;
|
||||
import cn.iocoder.yudao.module.tik.muye.redeemcode.dal.RedeemCodeDO;
|
||||
import cn.iocoder.yudao.module.tik.muye.redeemcode.mapper.RedeemCodeMapper;
|
||||
import cn.iocoder.yudao.module.tik.muye.redeemcode.vo.*;
|
||||
import cn.iocoder.yudao.module.tik.muye.redeemrecord.dal.RedeemRecordDO;
|
||||
import cn.iocoder.yudao.module.tik.muye.redeemrecord.mapper.RedeemRecordMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.module.tik.enums.ErrorCodeConstants.*;
|
||||
|
||||
/**
|
||||
* 兑换码 Service 实现类
|
||||
*
|
||||
* @author muye
|
||||
*/
|
||||
@Service
|
||||
@Validated
|
||||
@Slf4j
|
||||
public class RedeemCodeServiceImpl implements RedeemCodeService {
|
||||
|
||||
@Resource
|
||||
private RedeemCodeMapper redeemCodeMapper;
|
||||
|
||||
@Resource
|
||||
private RedeemRecordMapper redeemRecordMapper;
|
||||
|
||||
@Resource
|
||||
private MemberUserProfileMapper memberUserProfileMapper;
|
||||
|
||||
@Resource
|
||||
private PointRecordMapper pointRecordMapper;
|
||||
|
||||
@Resource
|
||||
private MemberUserApi memberUserApi;
|
||||
|
||||
@Override
|
||||
public Long createRedeemCode(RedeemCodeSaveReqVO createReqVO) {
|
||||
// 生成唯一兑换码
|
||||
String code = createReqVO.getCode() != null ? createReqVO.getCode() : generateUniqueCode();
|
||||
|
||||
RedeemCodeDO redeemCode = BeanUtils.toBean(createReqVO, RedeemCodeDO.class);
|
||||
redeemCode.setCode(code);
|
||||
redeemCode.setUsedCount(0);
|
||||
|
||||
// 设置默认值
|
||||
if (redeemCode.getCodeType() == null) {
|
||||
redeemCode.setCodeType(RedeemCodeDO.CODE_TYPE_SINGLE);
|
||||
}
|
||||
if (redeemCode.getMaxUseCount() == null) {
|
||||
redeemCode.setMaxUseCount(1);
|
||||
}
|
||||
if (redeemCode.getSource() == null) {
|
||||
redeemCode.setSource(RedeemCodeDO.SOURCE_ADMIN);
|
||||
}
|
||||
if (redeemCode.getStatus() == null) {
|
||||
redeemCode.setStatus(RedeemCodeDO.STATUS_ENABLED);
|
||||
}
|
||||
|
||||
redeemCodeMapper.insert(redeemCode);
|
||||
return redeemCode.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> generateRedeemCodes(RedeemCodeGenerateReqVO generateReqVO) {
|
||||
List<String> codes = new ArrayList<>();
|
||||
String batchNo = IdUtil.fastSimpleUUID().substring(0, 16);
|
||||
|
||||
for (int i = 0; i < generateReqVO.getCount(); i++) {
|
||||
String code = generateUniqueCode();
|
||||
codes.add(code);
|
||||
|
||||
RedeemCodeDO redeemCode = RedeemCodeDO.builder()
|
||||
.code(code)
|
||||
.codeType(generateReqVO.getCodeType() != null ? generateReqVO.getCodeType() : RedeemCodeDO.CODE_TYPE_SINGLE)
|
||||
.points(generateReqVO.getPoints())
|
||||
.maxUseCount(generateReqVO.getMaxUseCount() != null ? generateReqVO.getMaxUseCount() : 1)
|
||||
.usedCount(0)
|
||||
.batchNo(batchNo)
|
||||
.source(RedeemCodeDO.SOURCE_ADMIN)
|
||||
.expireTime(generateReqVO.getExpireTime())
|
||||
.status(RedeemCodeDO.STATUS_ENABLED)
|
||||
.remark(generateReqVO.getRemark())
|
||||
.operatorId(generateReqVO.getOperatorId())
|
||||
.operatorName(generateReqVO.getOperatorName())
|
||||
.build();
|
||||
|
||||
redeemCodeMapper.insert(redeemCode);
|
||||
}
|
||||
|
||||
log.info("[generateRedeemCodes] 批量生成 {} 个兑换码,批次号 {}", generateReqVO.getCount(), batchNo);
|
||||
return codes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateRedeemCode(RedeemCodeSaveReqVO updateReqVO) {
|
||||
validateRedeemCodeExists(updateReqVO.getId());
|
||||
RedeemCodeDO updateObj = BeanUtils.toBean(updateReqVO, RedeemCodeDO.class);
|
||||
redeemCodeMapper.updateById(updateObj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteRedeemCode(Long id) {
|
||||
validateRedeemCodeExists(id);
|
||||
redeemCodeMapper.deleteById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteRedeemCodeListByIds(List<Long> ids) {
|
||||
redeemCodeMapper.deleteByIds(ids);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RedeemCodeDO getRedeemCode(Long id) {
|
||||
return redeemCodeMapper.selectById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RedeemCodeDO getRedeemCodeByCode(String code) {
|
||||
return redeemCodeMapper.selectByCode(code);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<RedeemCodeDO> getRedeemCodePage(RedeemCodePageReqVO pageReqVO) {
|
||||
return redeemCodeMapper.selectPage(pageReqVO);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long redeem(String userId, String code) {
|
||||
// 1. 查询兑换码(带悲观锁)
|
||||
RedeemCodeDO redeemCode = redeemCodeMapper.selectByCodeForUpdate(code);
|
||||
if (redeemCode == null) {
|
||||
throw exception(REDEEM_CODE_NOT_EXISTS);
|
||||
}
|
||||
|
||||
// 2. 校验状态
|
||||
if (RedeemCodeDO.STATUS_DISABLED == redeemCode.getStatus()) {
|
||||
throw exception(REDEEM_CODE_DISABLED);
|
||||
}
|
||||
if (RedeemCodeDO.STATUS_EXHAUSTED == redeemCode.getStatus()) {
|
||||
throw exception(REDEEM_CODE_EXHAUSTED);
|
||||
}
|
||||
|
||||
// 3. 校验过期时间
|
||||
if (redeemCode.getExpireTime() != null && LocalDateTime.now().isAfter(redeemCode.getExpireTime())) {
|
||||
throw exception(REDEEM_CODE_EXPIRED);
|
||||
}
|
||||
|
||||
// 4. 校验使用次数
|
||||
if (redeemCode.getUsedCount() >= redeemCode.getMaxUseCount()) {
|
||||
throw exception(REDEEM_CODE_EXHAUSTED);
|
||||
}
|
||||
|
||||
// 5. 获取用户信息
|
||||
MemberUserRespDTO user = memberUserApi.getUser(Long.parseLong(userId));
|
||||
String mobile = user != null ? user.getMobile() : "";
|
||||
|
||||
// 6. 原子增加积分
|
||||
int affectedRows = memberUserProfileMapper.updatePointsIncrease(userId, redeemCode.getPoints());
|
||||
if (affectedRows == 0) {
|
||||
// 用户档案不存在,创建后重试
|
||||
createProfileIfAbsent(Long.parseLong(userId), mobile);
|
||||
affectedRows = memberUserProfileMapper.updatePointsIncrease(userId, redeemCode.getPoints());
|
||||
if (affectedRows == 0) {
|
||||
throw exception(REDEEM_CODE_REDEEM_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
// 7. 查询兑换后余额
|
||||
MemberUserProfileDO profile = memberUserProfileMapper.selectByUserId(userId);
|
||||
|
||||
// 8. 创建积分记录
|
||||
PointRecordDO pointRecord = PointRecordDO.builder()
|
||||
.userId(Long.parseLong(userId))
|
||||
.mobile(mobile)
|
||||
.type("increase")
|
||||
.pointAmount(redeemCode.getPoints())
|
||||
.balance(profile.getRemainingPoints())
|
||||
.reason("兑换码兑换")
|
||||
.bizType("redeem")
|
||||
.bizId(String.valueOf(redeemCode.getId()))
|
||||
.status("confirmed")
|
||||
.build();
|
||||
pointRecordMapper.insert(pointRecord);
|
||||
|
||||
// 9. 创建兑换记录
|
||||
RedeemRecordDO redeemRecord = RedeemRecordDO.builder()
|
||||
.userId(Long.parseLong(userId))
|
||||
.mobile(mobile)
|
||||
.codeId(redeemCode.getId())
|
||||
.code(redeemCode.getCode())
|
||||
.points(redeemCode.getPoints())
|
||||
.balanceAfter(profile.getRemainingPoints())
|
||||
.source(redeemCode.getSource())
|
||||
.sourceId(redeemCode.getSourceId())
|
||||
.remark("兑换码兑换")
|
||||
.build();
|
||||
redeemRecordMapper.insert(redeemRecord);
|
||||
|
||||
// 10. 更新兑换码使用次数和状态
|
||||
redeemCode.setUsedCount(redeemCode.getUsedCount() + 1);
|
||||
if (redeemCode.getUsedCount() >= redeemCode.getMaxUseCount()) {
|
||||
redeemCode.setStatus(RedeemCodeDO.STATUS_EXHAUSTED);
|
||||
}
|
||||
redeemCodeMapper.updateById(redeemCode);
|
||||
|
||||
log.info("[redeem] 用户 {} 使用兑换码 {} 兑换 {} 积分", userId, code, redeemCode.getPoints());
|
||||
return redeemRecord.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public String issueCodeAfterPayment(Long userId, Integer points, String sourceId) {
|
||||
String code = generateUniqueCode();
|
||||
|
||||
RedeemCodeDO redeemCode = RedeemCodeDO.builder()
|
||||
.code(code)
|
||||
.codeType(RedeemCodeDO.CODE_TYPE_SINGLE)
|
||||
.points(points)
|
||||
.maxUseCount(1)
|
||||
.usedCount(0)
|
||||
.source(RedeemCodeDO.SOURCE_PAY)
|
||||
.sourceId(sourceId)
|
||||
.status(RedeemCodeDO.STATUS_ENABLED)
|
||||
.remark("支付成功自动发放")
|
||||
.build();
|
||||
|
||||
redeemCodeMapper.insert(redeemCode);
|
||||
log.info("[issueCodeAfterPayment] 支付订单 {} 发放兑换码 {},积分 {}", sourceId, code, points);
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成唯一兑换码
|
||||
*/
|
||||
private String generateUniqueCode() {
|
||||
String code;
|
||||
int maxAttempts = 10;
|
||||
int attempts = 0;
|
||||
|
||||
do {
|
||||
// 生成 16 位大写字母+数字组合
|
||||
code = IdUtil.fastSimpleUUID().substring(0, 16).toUpperCase();
|
||||
attempts++;
|
||||
|
||||
if (attempts >= maxAttempts) {
|
||||
throw new RuntimeException("无法生成唯一兑换码");
|
||||
}
|
||||
} while (redeemCodeMapper.selectByCode(code) != null);
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验兑换码是否存在
|
||||
*/
|
||||
private void validateRedeemCodeExists(Long id) {
|
||||
if (redeemCodeMapper.selectById(id) == null) {
|
||||
throw exception(REDEEM_CODE_NOT_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果用户档案不存在则创建
|
||||
*/
|
||||
private void createProfileIfAbsent(Long userId, String mobile) {
|
||||
MemberUserProfileDO profile = memberUserProfileMapper.selectByUserId(userId);
|
||||
if (profile == null) {
|
||||
profile = MemberUserProfileDO.builder()
|
||||
.userId(String.valueOf(userId))
|
||||
.mobile(mobile)
|
||||
.registerTime(LocalDateTime.now())
|
||||
.totalPoints(0)
|
||||
.usedPoints(0)
|
||||
.remainingPoints(0)
|
||||
.status(1)
|
||||
.build();
|
||||
memberUserProfileMapper.insert(profile);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package cn.iocoder.yudao.module.tik.muye.redeemrecord;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import cn.iocoder.yudao.module.tik.muye.redeemrecord.dal.RedeemRecordDO;
|
||||
import cn.iocoder.yudao.module.tik.muye.redeemrecord.service.RedeemRecordService;
|
||||
import cn.iocoder.yudao.module.tik.muye.redeemrecord.vo.RedeemRecordPageReqVO;
|
||||
import cn.iocoder.yudao.module.tik.muye.redeemrecord.vo.RedeemRecordRespVO;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* 用户 App - 兑换记录
|
||||
*
|
||||
* @author muye
|
||||
*/
|
||||
@Tag(name = "用户 App - 兑换记录")
|
||||
@RestController
|
||||
@RequestMapping("/api/tik/redeem-record")
|
||||
@Validated
|
||||
public class AppRedeemRecordController {
|
||||
|
||||
@Resource
|
||||
private RedeemRecordService redeemRecordService;
|
||||
|
||||
@GetMapping("/my-page")
|
||||
@Operation(summary = "获得我的兑换记录分页")
|
||||
public CommonResult<PageResult<RedeemRecordRespVO>> getMyRedeemRecordPage(@Valid RedeemRecordPageReqVO pageReqVO) {
|
||||
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
||||
PageResult<RedeemRecordDO> pageResult = redeemRecordService.getRedeemRecordPageByUserId(userId, pageReqVO);
|
||||
return success(BeanUtils.toBean(pageResult, RedeemRecordRespVO.class));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package cn.iocoder.yudao.module.tik.muye.redeemrecord;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.module.tik.muye.redeemrecord.dal.RedeemRecordDO;
|
||||
import cn.iocoder.yudao.module.tik.muye.redeemrecord.service.RedeemRecordService;
|
||||
import cn.iocoder.yudao.module.tik.muye.redeemrecord.vo.RedeemRecordPageReqVO;
|
||||
import cn.iocoder.yudao.module.tik.muye.redeemrecord.vo.RedeemRecordRespVO;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* 管理后台 - 兑换记录
|
||||
*
|
||||
* @author muye
|
||||
*/
|
||||
@Tag(name = "管理后台 - 兑换记录")
|
||||
@RestController
|
||||
@RequestMapping("/admin-api/muye/redeem-record")
|
||||
@Validated
|
||||
public class RedeemRecordController {
|
||||
|
||||
@Resource
|
||||
private RedeemRecordService redeemRecordService;
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "获得兑换记录分页")
|
||||
@PreAuthorize("@ss.hasPermission('muye:redeem-record:query')")
|
||||
public CommonResult<PageResult<RedeemRecordRespVO>> getRedeemRecordPage(@Valid RedeemRecordPageReqVO pageReqVO) {
|
||||
PageResult<RedeemRecordDO> pageResult = redeemRecordService.getRedeemRecordPage(pageReqVO);
|
||||
return success(BeanUtils.toBean(pageResult, RedeemRecordRespVO.class));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package cn.iocoder.yudao.module.tik.muye.redeemrecord.mapper;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import cn.iocoder.yudao.module.tik.muye.redeemrecord.dal.RedeemRecordDO;
|
||||
import cn.iocoder.yudao.module.tik.muye.redeemrecord.vo.RedeemRecordPageReqVO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 兑换记录 Mapper
|
||||
*
|
||||
* @author muye
|
||||
*/
|
||||
@Mapper
|
||||
public interface RedeemRecordMapper extends BaseMapperX<RedeemRecordDO> {
|
||||
|
||||
/**
|
||||
* 分页查询
|
||||
*/
|
||||
default PageResult<RedeemRecordDO> selectPage(RedeemRecordPageReqVO reqVO) {
|
||||
return selectPage(reqVO, new LambdaQueryWrapperX<RedeemRecordDO>()
|
||||
.eqIfPresent(RedeemRecordDO::getUserId, reqVO.getUserId())
|
||||
.eqIfPresent(RedeemRecordDO::getMobile, reqVO.getMobile())
|
||||
.eqIfPresent(RedeemRecordDO::getCodeId, reqVO.getCodeId())
|
||||
.eqIfPresent(RedeemRecordDO::getCode, reqVO.getCode())
|
||||
.eqIfPresent(RedeemRecordDO::getSource, reqVO.getSource())
|
||||
.betweenIfPresent(RedeemRecordDO::getCreateTime, reqVO.getCreateTime())
|
||||
.orderByDesc(RedeemRecordDO::getId));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package cn.iocoder.yudao.module.tik.muye.redeemrecord.service;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.module.tik.muye.redeemrecord.dal.RedeemRecordDO;
|
||||
import cn.iocoder.yudao.module.tik.muye.redeemrecord.vo.RedeemRecordPageReqVO;
|
||||
|
||||
/**
|
||||
* 兑换记录 Service 接口
|
||||
*
|
||||
* @author muye
|
||||
*/
|
||||
public interface RedeemRecordService {
|
||||
|
||||
/**
|
||||
* 获得兑换记录分页
|
||||
*
|
||||
* @param pageReqVO 分页查询
|
||||
* @return 兑换记录分页
|
||||
*/
|
||||
PageResult<RedeemRecordDO> getRedeemRecordPage(RedeemRecordPageReqVO pageReqVO);
|
||||
|
||||
/**
|
||||
* 获得用户的兑换记录分页
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param pageReqVO 分页查询
|
||||
* @return 兑换记录分页
|
||||
*/
|
||||
PageResult<RedeemRecordDO> getRedeemRecordPageByUserId(Long userId, RedeemRecordPageReqVO pageReqVO);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package cn.iocoder.yudao.module.tik.muye.redeemrecord.service;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.module.tik.muye.redeemrecord.dal.RedeemRecordDO;
|
||||
import cn.iocoder.yudao.module.tik.muye.redeemrecord.mapper.RedeemRecordMapper;
|
||||
import cn.iocoder.yudao.module.tik.muye.redeemrecord.vo.RedeemRecordPageReqVO;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
/**
|
||||
* 兑换记录 Service 实现类
|
||||
*
|
||||
* @author muye
|
||||
*/
|
||||
@Service
|
||||
@Validated
|
||||
public class RedeemRecordServiceImpl implements RedeemRecordService {
|
||||
|
||||
@Resource
|
||||
private RedeemRecordMapper redeemRecordMapper;
|
||||
|
||||
@Override
|
||||
public PageResult<RedeemRecordDO> getRedeemRecordPage(RedeemRecordPageReqVO pageReqVO) {
|
||||
return redeemRecordMapper.selectPage(pageReqVO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<RedeemRecordDO> getRedeemRecordPageByUserId(Long userId, RedeemRecordPageReqVO pageReqVO) {
|
||||
pageReqVO.setUserId(userId);
|
||||
return redeemRecordMapper.selectPage(pageReqVO);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package cn.iocoder.yudao.module.tik.muye.redemptioncode;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import cn.iocoder.yudao.module.tik.muye.redemptioncode.service.RedemptionCodeService;
|
||||
import cn.iocoder.yudao.module.tik.muye.redemptioncode.vo.RedemptionCodeRedeemReqVO;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* 用户 App - 兑换码
|
||||
*
|
||||
* @author muye
|
||||
*/
|
||||
@Tag(name = "用户 App - 兑换码")
|
||||
@RestController
|
||||
@RequestMapping("/app-api/muye/redemption-code")
|
||||
@Validated
|
||||
public class AppRedemptionCodeController {
|
||||
|
||||
@Resource
|
||||
private RedemptionCodeService redemptionCodeService;
|
||||
|
||||
@PostMapping("/redeem")
|
||||
@Operation(summary = "兑换码兑换")
|
||||
public CommonResult<Integer> redeem(@Valid @RequestBody RedemptionCodeRedeemReqVO reqVO) {
|
||||
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
||||
Integer amount = redemptionCodeService.redeem(userId, reqVO.getCode());
|
||||
return success(amount);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package cn.iocoder.yudao.module.tik.muye.redemptioncode;
|
||||
|
||||
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
|
||||
import cn.iocoder.yudao.module.tik.muye.redemptioncode.dal.RedemptionCodeDO;
|
||||
import cn.iocoder.yudao.module.tik.muye.redemptioncode.service.RedemptionCodeService;
|
||||
import cn.iocoder.yudao.module.tik.muye.redemptioncode.vo.*;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* 管理后台 - 兑换码
|
||||
*
|
||||
* @author muye
|
||||
*/
|
||||
@Tag(name = "管理后台 - 兑换码")
|
||||
@RestController
|
||||
@RequestMapping("/admin-api/muye/redemption-code")
|
||||
@Validated
|
||||
public class RedemptionCodeController {
|
||||
|
||||
@Resource
|
||||
private RedemptionCodeService redemptionCodeService;
|
||||
|
||||
@PostMapping("/create")
|
||||
@Operation(summary = "创建兑换码")
|
||||
@PreAuthorize("@ss.hasPermission('muye:redemption-code:create')")
|
||||
public CommonResult<Long> createRedemptionCode(@Valid @RequestBody RedemptionCodeSaveReqVO createReqVO) {
|
||||
return success(redemptionCodeService.createRedemptionCode(createReqVO));
|
||||
}
|
||||
|
||||
@PostMapping("/generate")
|
||||
@Operation(summary = "批量生成兑换码")
|
||||
@PreAuthorize("@ss.hasPermission('muye:redemption-code:create')")
|
||||
public CommonResult<List<String>> generateRedemptionCodes(@Valid @RequestBody RedemptionCodeGenerateReqVO generateReqVO) {
|
||||
return success(redemptionCodeService.generateRedemptionCodes(generateReqVO));
|
||||
}
|
||||
|
||||
@PutMapping("/update")
|
||||
@Operation(summary = "更新兑换码")
|
||||
@PreAuthorize("@ss.hasPermission('muye:redemption-code:update')")
|
||||
public CommonResult<Boolean> updateRedemptionCode(@Valid @RequestBody RedemptionCodeSaveReqVO updateReqVO) {
|
||||
redemptionCodeService.updateRedemptionCode(updateReqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/delete")
|
||||
@Operation(summary = "删除兑换码")
|
||||
@Parameter(name = "id", description = "编号", required = true)
|
||||
@PreAuthorize("@ss.hasPermission('muye:redemption-code:delete')")
|
||||
public CommonResult<Boolean> deleteRedemptionCode(@RequestParam("id") Long id) {
|
||||
redemptionCodeService.deleteRedemptionCode(id);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/delete-list")
|
||||
@Operation(summary = "批量删除兑换码")
|
||||
@Parameter(name = "ids", description = "编号列表", required = true)
|
||||
@PreAuthorize("@ss.hasPermission('muye:redemption-code:delete')")
|
||||
public CommonResult<Boolean> deleteRedemptionCodeList(@RequestParam("ids") List<Long> ids) {
|
||||
redemptionCodeService.deleteRedemptionCodeListByIds(ids);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/get")
|
||||
@Operation(summary = "获得兑换码")
|
||||
@Parameter(name = "id", description = "编号", required = true)
|
||||
@PreAuthorize("@ss.hasPermission('muye:redemption-code:query')")
|
||||
public CommonResult<RedemptionCodeRespVO> getRedemptionCode(@RequestParam("id") Long id) {
|
||||
RedemptionCodeDO redemptionCode = redemptionCodeService.getRedemptionCode(id);
|
||||
return success(BeanUtils.toBean(redemptionCode, RedemptionCodeRespVO.class));
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "获得兑换码分页")
|
||||
@PreAuthorize("@ss.hasPermission('muye:redemption-code:query')")
|
||||
public CommonResult<PageResult<RedemptionCodeRespVO>> getRedemptionCodePage(@Valid RedemptionCodePageReqVO pageReqVO) {
|
||||
PageResult<RedemptionCodeDO> pageResult = redemptionCodeService.getRedemptionCodePage(pageReqVO);
|
||||
return success(BeanUtils.toBean(pageResult, RedemptionCodeRespVO.class));
|
||||
}
|
||||
|
||||
@GetMapping("/export-excel")
|
||||
@Operation(summary = "导出兑换码 Excel")
|
||||
@PreAuthorize("@ss.hasPermission('muye:redemption-code:export')")
|
||||
@ApiAccessLog(operateType = EXPORT)
|
||||
public void exportRedemptionCodeExcel(@Valid RedemptionCodePageReqVO pageReqVO,
|
||||
HttpServletResponse response) throws IOException {
|
||||
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
|
||||
List<RedemptionCodeDO> list = redemptionCodeService.getRedemptionCodePage(pageReqVO).getList();
|
||||
ExcelUtils.write(response, "兑换码.xls", "数据", RedemptionCodeRespVO.class,
|
||||
BeanUtils.toBean(list, RedemptionCodeRespVO.class));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package cn.iocoder.yudao.module.tik.muye.redemptioncode.dal;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import cn.iocoder.yudao.module.tik.muye.redemptioncode.vo.RedemptionCodePageReqVO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
/**
|
||||
* 兑换码 Mapper
|
||||
*
|
||||
* @author muye
|
||||
*/
|
||||
@Mapper
|
||||
public interface RedemptionCodeMapper extends BaseMapperX<RedemptionCodeDO> {
|
||||
|
||||
/**
|
||||
* 根据兑换码查询
|
||||
*/
|
||||
default RedemptionCodeDO selectByCode(String code) {
|
||||
return selectOne(new LambdaQueryWrapperX<RedemptionCodeDO>()
|
||||
.eq(RedemptionCodeDO::getCode, code));
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用兑换码(乐观锁)
|
||||
* 只有未使用状态才能更新为已使用
|
||||
*
|
||||
* @param code 兑换码
|
||||
* @param userId 用户ID
|
||||
* @return 影响行数, */
|
||||
@Update("UPDATE muye_redemption_code " +
|
||||
"SET status = 1, user_id = #{userId}, used_time = NOW(), update_time = NOW() " +
|
||||
"WHERE code = #{code} AND status = 0 AND deleted = 0 " +
|
||||
"AND (expire_time IS NULL OR expire_time > NOW())")
|
||||
int useRedemptionCode(@Param("code") String code, @Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 分页查询
|
||||
*/
|
||||
default PageResult<RedemptionCodeDO> selectPage(RedemptionCodePageReqVO reqVO) {
|
||||
return selectPage(reqVO, new LambdaQueryWrapperX<RedemptionCodeDO>()
|
||||
.eqIfPresent(RedemptionCodeDO::getCode, reqVO.getCode())
|
||||
.eqIfPresent(RedemptionCodeDO::getType, reqVO.getType())
|
||||
.eqIfPresent(RedemptionCodeDO::getStatus, reqVO.getStatus())
|
||||
.eqIfPresent(RedemptionCodeDO::getBatchNo, reqVO.getBatchNo())
|
||||
.eqIfPresent(RedemptionCodeDO::getUserId, reqVO.getUserId())
|
||||
.betweenIfPresent(RedemptionCodeDO::getCreateTime, reqVO.getCreateTime())
|
||||
.orderByDesc(RedemptionCodeDO::getId));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package cn.iocoder.yudao.module.tik.muye.redemptioncode.service;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.module.tik.muye.redemptioncode.dal.RedemptionCodeDO;
|
||||
import cn.iocoder.yudao.module.tik.muye.redemptioncode.vo.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 兑换码 Service 接口
|
||||
*
|
||||
* @author muye
|
||||
*/
|
||||
public interface RedemptionCodeService {
|
||||
|
||||
/**
|
||||
* 创建兑换码(单个)
|
||||
*
|
||||
* @param createReqVO 创建信息
|
||||
* @return 编号
|
||||
*/
|
||||
Long createRedemptionCode(RedemptionCodeSaveReqVO createReqVO);
|
||||
|
||||
/**
|
||||
* 批量生成兑换码
|
||||
*
|
||||
* @param generateReqVO 生成信息
|
||||
* @return 生成的兑换码列表
|
||||
*/
|
||||
List<String> generateRedemptionCodes(RedemptionCodeGenerateReqVO generateReqVO);
|
||||
|
||||
/**
|
||||
* 更新兑换码
|
||||
*
|
||||
* @param updateReqVO 更新信息
|
||||
*/
|
||||
void updateRedemptionCode(RedemptionCodeSaveReqVO updateReqVO);
|
||||
|
||||
/**
|
||||
* 删除兑换码
|
||||
*
|
||||
* @param id 编号
|
||||
*/
|
||||
void deleteRedemptionCode(Long id);
|
||||
|
||||
/**
|
||||
* 批量删除兑换码
|
||||
*
|
||||
* @param ids 编号列表
|
||||
*/
|
||||
void deleteRedemptionCodeListByIds(List<Long> ids);
|
||||
|
||||
/**
|
||||
* 获得兑换码
|
||||
*
|
||||
* @param id 编号
|
||||
* @return 兑换码
|
||||
*/
|
||||
RedemptionCodeDO getRedemptionCode(Long id);
|
||||
|
||||
/**
|
||||
* 根据兑换码字符串获取
|
||||
*
|
||||
* @param code 兑换码
|
||||
* @return 兑换码
|
||||
*/
|
||||
RedemptionCodeDO getRedemptionCodeByCode(String code);
|
||||
|
||||
/**
|
||||
* 获得兑换码分页
|
||||
*
|
||||
* @param pageReqVO 分页查询
|
||||
* @return 兑换码分页
|
||||
*/
|
||||
PageResult<RedemptionCodeDO> getRedemptionCodePage(RedemptionCodePageReqVO pageReqVO);
|
||||
|
||||
/**
|
||||
* 兑换码兑换(核心业务)
|
||||
* 验证兑换码有效性,增加用户积分
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param code 兑换码
|
||||
* @return 兑换码面额(积分数)
|
||||
*/
|
||||
Integer redeem(Long userId, String code);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
package cn.iocoder.yudao.module.tik.muye.redemptioncode.service;
|
||||
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
|
||||
import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
|
||||
import cn.iocoder.yudao.module.tik.muye.memberuserprofile.dal.MemberUserProfileDO;
|
||||
import cn.iocoder.yudao.module.tik.muye.memberuserprofile.mapper.MemberUserProfileMapper;
|
||||
import cn.iocoder.yudao.module.tik.muye.pointrecord.dal.PointRecordDO;
|
||||
import cn.iocoder.yudao.module.tik.muye.pointrecord.mapper.PointRecordMapper;
|
||||
import cn.iocoder.yudao.module.tik.muye.redemptioncode.dal.RedemptionCodeDO;
|
||||
import cn.iocoder.yudao.module.tik.muye.redemptioncode.dal.RedemptionCodeMapper;
|
||||
import cn.iocoder.yudao.module.tik.muye.redemptioncode.vo.*;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.module.tik.enums.ErrorCodeConstants.*;
|
||||
|
||||
/**
|
||||
* 兑换码 Service 实现类
|
||||
*
|
||||
* @author muye
|
||||
*/
|
||||
@Service
|
||||
@Validated
|
||||
@Slf4j
|
||||
public class RedemptionCodeServiceImpl implements RedemptionCodeService {
|
||||
|
||||
@Resource
|
||||
private RedemptionCodeMapper redemptionCodeMapper;
|
||||
|
||||
@Resource
|
||||
private MemberUserProfileMapper memberUserProfileMapper;
|
||||
|
||||
@Resource
|
||||
private PointRecordMapper pointRecordMapper;
|
||||
|
||||
@Resource
|
||||
private MemberUserApi memberUserApi;
|
||||
|
||||
@Override
|
||||
public Long createRedemptionCode(RedemptionCodeSaveReqVO createReqVO) {
|
||||
String code = createReqVO.getCode() != null ? createReqVO.getCode() : generateUniqueCode();
|
||||
|
||||
RedemptionCodeDO redemptionCode = BeanUtils.toBean(createReqVO, RedemptionCodeDO.class);
|
||||
redemptionCode.setCode(code);
|
||||
redemptionCode.setType(RedemptionCodeDO.TYPE_POINTS);
|
||||
redemptionCode.setStatus(RedemptionCodeDO.STATUS_UNUSED);
|
||||
|
||||
redemptionCodeMapper.insert(redemptionCode);
|
||||
return redemptionCode.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> generateRedemptionCodes(RedemptionCodeGenerateReqVO generateReqVO) {
|
||||
List<String> codes = new ArrayList<>();
|
||||
String batchNo = "BATCH" + IdUtil.fastSimpleUUID().substring(0, 12).toUpperCase();
|
||||
|
||||
for (int i = 0; i < generateReqVO.getCount(); i++) {
|
||||
String code = generateUniqueCode();
|
||||
codes.add(code);
|
||||
|
||||
RedemptionCodeDO redemptionCode = RedemptionCodeDO.builder()
|
||||
.code(code)
|
||||
.type(RedemptionCodeDO.TYPE_POINTS)
|
||||
.amount(generateReqVO.getAmount())
|
||||
.price(generateReqVO.getPrice())
|
||||
.status(RedemptionCodeDO.STATUS_UNUSED)
|
||||
.batchNo(batchNo)
|
||||
.batchRemark(generateReqVO.getBatchRemark())
|
||||
.expireTime(generateReqVO.getExpireTime())
|
||||
.build();
|
||||
|
||||
redemptionCodeMapper.insert(redemptionCode);
|
||||
}
|
||||
|
||||
log.info("[generateRedemptionCodes] 批量生成 {} 个兑换码,批次号 {}", generateReqVO.getCount(), batchNo);
|
||||
return codes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateRedemptionCode(RedemptionCodeSaveReqVO updateReqVO) {
|
||||
validateRedemptionCodeExists(updateReqVO.getId());
|
||||
RedemptionCodeDO updateObj = BeanUtils.toBean(updateReqVO, RedemptionCodeDO.class);
|
||||
redemptionCodeMapper.updateById(updateObj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteRedemptionCode(Long id) {
|
||||
validateRedemptionCodeExists(id);
|
||||
redemptionCodeMapper.deleteById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteRedemptionCodeListByIds(List<Long> ids) {
|
||||
redemptionCodeMapper.deleteByIds(ids);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RedemptionCodeDO getRedemptionCode(Long id) {
|
||||
return redemptionCodeMapper.selectById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RedemptionCodeDO getRedemptionCodeByCode(String code) {
|
||||
return redemptionCodeMapper.selectByCode(code);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<RedemptionCodeDO> getRedemptionCodePage(RedemptionCodePageReqVO pageReqVO) {
|
||||
return redemptionCodeMapper.selectPage(pageReqVO);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Integer redeem(Long userId, String code) {
|
||||
// 1. 使用乐观锁更新兑换码状态
|
||||
int affectedRows = redemptionCodeMapper.useRedemptionCode(code, userId);
|
||||
if (affectedRows == 0) {
|
||||
RedemptionCodeDO redemptionCode = redemptionCodeMapper.selectByCode(code);
|
||||
if (redemptionCode == null) {
|
||||
throw exception(REDEMPTION_CODE_NOT_FOUND);
|
||||
}
|
||||
if (RedemptionCodeDO.STATUS_USED == redemptionCode.getStatus()) {
|
||||
throw exception(REDEMPTION_CODE_ALREADY_USED);
|
||||
}
|
||||
if (RedemptionCodeDO.STATUS_EXPIRED == redemptionCode.getStatus() ||
|
||||
(redemptionCode.getExpireTime() != null && LocalDateTime.now().isAfter(redemptionCode.getExpireTime()))) {
|
||||
throw exception(REDEMPTION_CODE_EXPIRED);
|
||||
}
|
||||
throw exception(REDEMPTION_CODE_NOT_FOUND);
|
||||
}
|
||||
|
||||
// 2. 查询兑换码信息
|
||||
RedemptionCodeDO redemptionCode = redemptionCodeMapper.selectByCode(code);
|
||||
|
||||
// 3. 获取用户信息
|
||||
MemberUserRespDTO user = memberUserApi.getUser(userId);
|
||||
String mobile = user != null ? user.getMobile() : "";
|
||||
|
||||
// 4. 原子增加积分
|
||||
int rows = memberUserProfileMapper.updatePointsIncrease(String.valueOf(userId), redemptionCode.getAmount());
|
||||
if (rows == 0) {
|
||||
createProfileIfAbsent(userId, mobile);
|
||||
memberUserProfileMapper.updatePointsIncrease(String.valueOf(userId), redemptionCode.getAmount());
|
||||
}
|
||||
|
||||
// 5. 查询兑换后余额
|
||||
MemberUserProfileDO profile = memberUserProfileMapper.selectByUserId(userId);
|
||||
|
||||
// 6. 创建积分记录
|
||||
PointRecordDO pointRecord = PointRecordDO.builder()
|
||||
.userId(userId)
|
||||
.mobile(mobile)
|
||||
.type("increase")
|
||||
.pointAmount(redemptionCode.getAmount())
|
||||
.balance(profile.getRemainingPoints())
|
||||
.reason("兑换码兑换")
|
||||
.bizType("redemption")
|
||||
.bizId(String.valueOf(redemptionCode.getId()))
|
||||
.status("confirmed")
|
||||
.build();
|
||||
pointRecordMapper.insert(pointRecord);
|
||||
|
||||
log.info("[redeem] 用户 {} 使用兑换码 {} 兑换 {} 积分", userId, code, redemptionCode.getAmount());
|
||||
return redemptionCode.getAmount();
|
||||
}
|
||||
|
||||
private String generateUniqueCode() {
|
||||
String code;
|
||||
int maxAttempts = 10;
|
||||
int attempts = 0;
|
||||
|
||||
do {
|
||||
code = IdUtil.fastSimpleUUID().substring(0, 16).toUpperCase();
|
||||
attempts++;
|
||||
if (attempts >= maxAttempts) {
|
||||
throw new RuntimeException("无法生成唯一兑换码");
|
||||
}
|
||||
} while (redemptionCodeMapper.selectByCode(code) != null);
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
private void validateRedemptionCodeExists(Long id) {
|
||||
if (redemptionCodeMapper.selectById(id) == null) {
|
||||
throw exception(REDEMPTION_CODE_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
private void createProfileIfAbsent(Long userId, String mobile) {
|
||||
MemberUserProfileDO profile = memberUserProfileMapper.selectByUserId(userId);
|
||||
if (profile == null) {
|
||||
profile = MemberUserProfileDO.builder()
|
||||
.userId(String.valueOf(userId))
|
||||
.mobile(mobile)
|
||||
.registerTime(LocalDateTime.now())
|
||||
.totalPoints(0)
|
||||
.usedPoints(0)
|
||||
.remainingPoints(0)
|
||||
.status(1)
|
||||
.build();
|
||||
memberUserProfileMapper.insert(profile);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.module.tik.userprompt.service.UserPromptService;
|
||||
import cn.iocoder.yudao.module.tik.userprompt.vo.UserPromptDO;
|
||||
import cn.iocoder.yudao.module.tik.userprompt.vo.UserPromptMyRespVO;
|
||||
import cn.iocoder.yudao.module.tik.userprompt.vo.UserPromptPageReqVO;
|
||||
import cn.iocoder.yudao.module.tik.userprompt.vo.UserPromptRespVO;
|
||||
import cn.iocoder.yudao.module.tik.userprompt.vo.UserPromptSaveReqVO;
|
||||
@@ -16,6 +17,8 @@ import jakarta.validation.Valid;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
|
||||
@@ -30,6 +33,16 @@ public class AppUserPromptController {
|
||||
@Resource
|
||||
private UserPromptService userPromptService;
|
||||
|
||||
@GetMapping("/my-list")
|
||||
@Operation(summary = "获取用户可用提示词列表(自建 + 收藏的智能体)")
|
||||
public CommonResult<List<UserPromptMyRespVO>> getMyPromptList() {
|
||||
Long userId = getLoginUserId();
|
||||
if (userId == null) {
|
||||
return CommonResult.error(401, "用户未登录");
|
||||
}
|
||||
return success(userPromptService.getMyPromptList(userId));
|
||||
}
|
||||
|
||||
@PostMapping("/create")
|
||||
@Operation(summary = "创建用户提示词")
|
||||
public CommonResult<Long> createUserPrompt(@Valid @RequestBody UserPromptSaveReqVO createReqVO) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.tik.userprompt.service;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.module.tik.userprompt.vo.UserPromptDO;
|
||||
import cn.iocoder.yudao.module.tik.userprompt.vo.UserPromptMyRespVO;
|
||||
import cn.iocoder.yudao.module.tik.userprompt.vo.UserPromptPageReqVO;
|
||||
import cn.iocoder.yudao.module.tik.userprompt.vo.UserPromptSaveReqVO;
|
||||
import jakarta.validation.Valid;
|
||||
@@ -15,6 +16,14 @@ import java.util.List;
|
||||
*/
|
||||
public interface UserPromptService {
|
||||
|
||||
/**
|
||||
* 获取用户可用提示词列表(自建 + 收藏的智能体)
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 提示词列表
|
||||
*/
|
||||
List<UserPromptMyRespVO> getMyPromptList(Long userId);
|
||||
|
||||
/**
|
||||
* 创建用户提示词
|
||||
*
|
||||
|
||||
@@ -2,15 +2,22 @@ package cn.iocoder.yudao.module.tik.userprompt.service;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.module.tik.muye.aiagent.dal.AiAgentDO;
|
||||
import cn.iocoder.yudao.module.tik.muye.aiagent.service.AiAgentFavoriteService;
|
||||
import cn.iocoder.yudao.module.tik.muye.aiagent.service.AiAgentService;
|
||||
import cn.iocoder.yudao.module.tik.userprompt.mapper.UserPromptMapper;
|
||||
import cn.iocoder.yudao.module.tik.userprompt.vo.UserPromptDO;
|
||||
import cn.iocoder.yudao.module.tik.userprompt.vo.UserPromptMyRespVO;
|
||||
import cn.iocoder.yudao.module.tik.userprompt.vo.UserPromptPageReqVO;
|
||||
import cn.iocoder.yudao.module.tik.userprompt.vo.UserPromptSaveReqVO;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.module.tik.enums.ErrorCodeConstants.USER_PROMPT_NOT_EXISTS;
|
||||
@@ -27,6 +34,67 @@ public class UserPromptServiceImpl implements UserPromptService {
|
||||
@Resource
|
||||
private UserPromptMapper userPromptMapper;
|
||||
|
||||
@Resource
|
||||
private AiAgentFavoriteService aiAgentFavoriteService;
|
||||
|
||||
@Resource
|
||||
private AiAgentService aiAgentService;
|
||||
|
||||
@Override
|
||||
public List<UserPromptMyRespVO> getMyPromptList(Long userId) {
|
||||
List<UserPromptMyRespVO> result = new ArrayList<>();
|
||||
|
||||
// 1. 获取用户自建的提示词(status=1 启用)
|
||||
List<UserPromptDO> createdList = userPromptMapper.selectList(
|
||||
new LambdaQueryWrapper<UserPromptDO>()
|
||||
.eq(UserPromptDO::getUserId, userId)
|
||||
.eq(UserPromptDO::getStatus, 1)
|
||||
.orderByDesc(UserPromptDO::getSort)
|
||||
.orderByDesc(UserPromptDO::getCreateTime)
|
||||
);
|
||||
createdList.forEach(p -> {
|
||||
UserPromptMyRespVO vo = BeanUtils.toBean(p, UserPromptMyRespVO.class);
|
||||
vo.setSource("created");
|
||||
result.add(vo);
|
||||
});
|
||||
|
||||
// 2. 获取用户收藏的智能体
|
||||
Set<Long> favoriteAgentIds = aiAgentFavoriteService.getFavoriteAgentIds(userId);
|
||||
if (!favoriteAgentIds.isEmpty()) {
|
||||
List<AiAgentDO> allAgents = aiAgentService.getEnabledAgentList();
|
||||
allAgents.stream()
|
||||
.filter(agent -> favoriteAgentIds.contains(agent.getId()))
|
||||
.forEach(agent -> {
|
||||
UserPromptMyRespVO vo = new UserPromptMyRespVO();
|
||||
vo.setId(-agent.getId()); // 负数表示收藏的智能体
|
||||
vo.setName(agent.getAgentName());
|
||||
vo.setContent(agent.getSystemPrompt());
|
||||
vo.setCategory(agent.getCategoryName());
|
||||
vo.setIcon(agent.getIcon());
|
||||
vo.setDescription(agent.getDescription());
|
||||
vo.setSource("favorite");
|
||||
vo.setOriginalAuthor(agent.getOperatorName());
|
||||
vo.setAgentId(agent.getAgentId());
|
||||
result.add(vo);
|
||||
});
|
||||
}
|
||||
|
||||
// 3. 排序:自建优先 > 排序值降序 > 使用次数降序
|
||||
result.sort((a, b) -> {
|
||||
int sourceCompare = a.getSource().compareTo(b.getSource());
|
||||
if (sourceCompare != 0) return sourceCompare;
|
||||
int sortCompare = Integer.compare(
|
||||
b.getSort() != null ? b.getSort() : 0,
|
||||
a.getSort() != null ? a.getSort() : 0);
|
||||
if (sortCompare != 0) return sortCompare;
|
||||
return Integer.compare(
|
||||
b.getUseCount() != null ? b.getUseCount() : 0,
|
||||
a.getUseCount() != null ? a.getUseCount() : 0);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long createUserPrompt(UserPromptSaveReqVO createReqVO) {
|
||||
UserPromptDO userPrompt = BeanUtils.toBean(createReqVO, UserPromptDO.class);
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
41
yudao-ui-admin-vue3/src/api/muye/redeemcode/index.ts
Normal file
41
yudao-ui-admin-vue3/src/api/muye/redeemcode/index.ts
Normal file
@@ -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 })
|
||||
}
|
||||
26
yudao-ui-admin-vue3/src/api/muye/redeemrecord/index.ts
Normal file
26
yudao-ui-admin-vue3/src/api/muye/redeemrecord/index.ts
Normal file
@@ -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 })
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<Dialog :title="dialogTitle" v-model="dialogVisible" width="600px">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="120px"
|
||||
v-loading="formLoading"
|
||||
>
|
||||
<el-form-item label="面额类型" prop="amountType">
|
||||
<el-radio-group v-model="formData.amountType" @change="handleAmountTypeChange">
|
||||
<el-radio label="points">按积分</el-radio>
|
||||
<el-radio label="money">按金额</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="formData.amountType === 'points'" label="积分面额" prop="points">
|
||||
<el-input-number v-model="formData.points" :min="1" :max="100000" placeholder="请输入积分数" />
|
||||
<span class="ml-10px text-gray-500">积分</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-else label="金额" prop="money">
|
||||
<el-input-number v-model="formData.money" :min="0.01" :max="10000" :precision="2" placeholder="请输入金额" @change="calculatePoints" />
|
||||
<span class="ml-10px text-gray-500">元</span>
|
||||
<span class="ml-10px text-orange-500" v-if="calculatedPoints > 0">
|
||||
= {{ calculatedPoints }} 积分(比例 1:{{ exchangeRate }})
|
||||
</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="码类型" prop="codeType">
|
||||
<el-radio-group v-model="formData.codeType">
|
||||
<el-radio :label="1">单次使用</el-radio>
|
||||
<el-radio :label="2">多次使用</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="text-gray-400 text-12px mt-5px">
|
||||
单次使用:只能被兑换一次;多次使用:可被多人兑换
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="formData.codeType === 2" label="最大使用次数" prop="maxUseCount">
|
||||
<el-input-number v-model="formData.maxUseCount" :min="1" :max="10000" placeholder="最大使用次数" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="生成数量" prop="count">
|
||||
<el-input-number v-model="formData.count" :min="1" :max="10000" placeholder="请输入生成数量" />
|
||||
<span class="ml-10px text-gray-500">个</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="有效期" prop="expireDays">
|
||||
<el-input-number v-model="formData.expireDays" :min="1" :max="3650" placeholder="请输入有效期" />
|
||||
<span class="ml-10px text-gray-500">天</span>
|
||||
<div class="text-gray-400 text-12px mt-5px">
|
||||
从生成时间开始计算
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="批次备注" prop="remark">
|
||||
<el-input
|
||||
v-model="formData.remark"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="请输入批次备注"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="来源" prop="source">
|
||||
<el-select v-model="formData.source" placeholder="请选择来源" class="!w-full">
|
||||
<el-option label="后台生成" value="admin" />
|
||||
<el-option label="活动发放" value="activity" />
|
||||
<el-option label="支付发放" value="payment" />
|
||||
<el-option label="其他" value="other" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="submitForm" :loading="formLoading">确 定</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import * as RedeemCodeApi from '@/api/muye/redeemcode'
|
||||
import request from '@/config/axios'
|
||||
|
||||
defineOptions({ name: 'RedeemCodeGenerateForm' })
|
||||
|
||||
const message = useMessage()
|
||||
const { t } = useI18n()
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('批量生成兑换码')
|
||||
const formLoading = ref(false)
|
||||
const formRef = ref()
|
||||
|
||||
const formData = ref({
|
||||
amountType: 'points',
|
||||
points: 100,
|
||||
money: 10,
|
||||
codeType: 1,
|
||||
maxUseCount: 1,
|
||||
count: 100,
|
||||
expireDays: 30,
|
||||
remark: '',
|
||||
source: 'admin'
|
||||
})
|
||||
|
||||
const exchangeRate = ref(10) // 默认 1元=10积分
|
||||
const calculatedPoints = ref(0)
|
||||
|
||||
const formRules = {
|
||||
amountType: [{ required: true, message: '请选择面额类型', trigger: 'change' }],
|
||||
points: [{ required: true, message: '请输入积分数', trigger: 'blur' }],
|
||||
money: [{ required: true, message: '请输入金额', trigger: 'blur' }],
|
||||
codeType: [{ required: true, message: '请选择码类型', trigger: 'change' }],
|
||||
count: [{ required: true, message: '请输入生成数量', trigger: 'blur' }],
|
||||
expireDays: [{ required: true, message: '请输入有效期', trigger: 'blur' }],
|
||||
source: [{ required: true, message: '请选择来源', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const emit = defineEmits(['success'])
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = async () => {
|
||||
dialogVisible.value = true
|
||||
resetForm()
|
||||
// 获取兑换比例
|
||||
await getExchangeRate()
|
||||
}
|
||||
|
||||
/** 获取兑换比例 */
|
||||
const getExchangeRate = async () => {
|
||||
try {
|
||||
const data = await request.get({ url: '/admin-api/muye/point-exchange-config/get' })
|
||||
if (data && data.exchangeRate) {
|
||||
exchangeRate.value = data.exchangeRate
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取兑换比例失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
/** 计算积分 */
|
||||
const calculatePoints = () => {
|
||||
if (formData.value.amountType === 'money' && formData.value.money) {
|
||||
calculatedPoints.value = Math.floor(formData.value.money * exchangeRate.value)
|
||||
} else {
|
||||
calculatedPoints.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
/** 面额类型改变 */
|
||||
const handleAmountTypeChange = () => {
|
||||
calculatePoints()
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
const submitForm = async () => {
|
||||
if (!formRef.value) return
|
||||
const valid = await formRef.value.validate()
|
||||
if (!valid) return
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
const data = {
|
||||
points: formData.value.amountType === 'points'
|
||||
? formData.value.points
|
||||
: calculatedPoints.value,
|
||||
codeType: formData.value.codeType,
|
||||
maxUseCount: formData.value.codeType === 2 ? formData.value.maxUseCount : 1,
|
||||
count: formData.value.count,
|
||||
expireDays: formData.value.expireDays,
|
||||
remark: formData.value.remark,
|
||||
source: formData.value.source
|
||||
}
|
||||
|
||||
const codes = await RedeemCodeApi.generateRedeemCodes(data)
|
||||
message.success(`成功生成 ${codes.length} 个兑换码`)
|
||||
dialogVisible.value = false
|
||||
emit('success')
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置表单 */
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
amountType: 'points',
|
||||
points: 100,
|
||||
money: 10,
|
||||
codeType: 1,
|
||||
maxUseCount: 1,
|
||||
count: 100,
|
||||
expireDays: 30,
|
||||
remark: '',
|
||||
source: 'admin'
|
||||
}
|
||||
calculatedPoints.value = 0
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
253
yudao-ui-admin-vue3/src/views/muye/redeemcode/index.vue
Normal file
253
yudao-ui-admin-vue3/src/views/muye/redeemcode/index.vue
Normal file
@@ -0,0 +1,253 @@
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
label-width="68px"
|
||||
>
|
||||
<el-form-item label="兑换码" prop="code">
|
||||
<el-input
|
||||
v-model="queryParams.code"
|
||||
placeholder="请输入兑换码"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select
|
||||
v-model="queryParams.status"
|
||||
placeholder="请选择状态"
|
||||
clearable
|
||||
class="!w-240px"
|
||||
>
|
||||
<el-option label="未使用" :value="0" />
|
||||
<el-option label="已使用" :value="1" />
|
||||
<el-option label="已过期" :value="2" />
|
||||
<el-option label="已禁用" :value="3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="批次号" prop="batchNo">
|
||||
<el-input
|
||||
v-model="queryParams.batchNo"
|
||||
placeholder="请输入批次号"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="创建时间" prop="createTime">
|
||||
<el-date-picker
|
||||
v-model="queryParams.createTime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="daterange"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
@click="openGenerateForm"
|
||||
v-hasPermi="['muye:redeem-code:create']"
|
||||
>
|
||||
<Icon icon="ep:plus" class="mr-5px" /> 批量生成
|
||||
</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
plain
|
||||
@click="handleExport"
|
||||
:loading="exportLoading"
|
||||
v-hasPermi="['muye:redeem-code:export']"
|
||||
>
|
||||
<Icon icon="ep:download" class="mr-5px" /> 导出
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
:disabled="checkedIds.length === 0"
|
||||
@click="handleDeleteBatch"
|
||||
v-hasPermi="['muye:redeem-code:delete']"
|
||||
>
|
||||
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table
|
||||
row-key="id"
|
||||
v-loading="loading"
|
||||
:data="list"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="40" />
|
||||
<el-table-column label="兑换码" align="center" prop="code" width="180" />
|
||||
<el-table-column label="积分面额" align="center" prop="points" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag type="warning">{{ scope.row.points }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="码类型" align="center" prop="codeType" width="100">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.REDEEM_CODE_TYPE" :value="scope.row.codeType" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" align="center" prop="status" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.status === 0" type="success">未使用</el-tag>
|
||||
<el-tag v-else-if="scope.row.status === 1" type="info">已使用</el-tag>
|
||||
<el-tag v-else-if="scope.row.status === 2" type="danger">已过期</el-tag>
|
||||
<el-tag v-else type="warning">已禁用</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="批次号" align="center" prop="batchNo" width="160" />
|
||||
<el-table-column label="使用者ID" align="center" prop="userId" width="100" />
|
||||
<el-table-column label="使用次数" align="center" width="100">
|
||||
<template #default="scope">
|
||||
{{ scope.row.usedCount }} / {{ scope.row.maxUseCount }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="过期时间" align="center" prop="expireTime" width="180" />
|
||||
<el-table-column
|
||||
label="创建时间"
|
||||
align="center"
|
||||
prop="createTime"
|
||||
:formatter="dateFormatter"
|
||||
width="180"
|
||||
/>
|
||||
<el-table-column label="操作" align="center" fixed="right" width="120">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row.id)"
|
||||
v-hasPermi="['muye:redeem-code:delete']"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 批量生成表单 -->
|
||||
<RedeemCodeGenerateForm ref="generateFormRef" @success="getList" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import download from '@/utils/download'
|
||||
import * as RedeemCodeApi from '@/api/muye/redeemcode'
|
||||
import RedeemCodeGenerateForm from './RedeemCodeGenerateForm.vue'
|
||||
|
||||
defineOptions({ name: 'RedeemCode' })
|
||||
|
||||
const message = useMessage()
|
||||
const { t } = useI18n()
|
||||
|
||||
const loading = ref(true)
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
code: undefined,
|
||||
status: undefined,
|
||||
batchNo: undefined,
|
||||
createTime: []
|
||||
})
|
||||
const queryFormRef = ref()
|
||||
const exportLoading = ref(false)
|
||||
const checkedIds = ref<number[]>([])
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await RedeemCodeApi.getRedeemCodePage(queryParams)
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 选择改变 */
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
checkedIds.value = selection.map((item) => item.id)
|
||||
}
|
||||
|
||||
/** 打开批量生成表单 */
|
||||
const generateFormRef = ref()
|
||||
const openGenerateForm = () => {
|
||||
generateFormRef.value.open()
|
||||
}
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
await message.delConfirm()
|
||||
await RedeemCodeApi.deleteRedeemCode(id)
|
||||
message.success(t('common.delSuccess'))
|
||||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 批量删除按钮操作 */
|
||||
const handleDeleteBatch = async () => {
|
||||
try {
|
||||
await message.delConfirm()
|
||||
await RedeemCodeApi.deleteRedeemCodeList(checkedIds.value)
|
||||
message.success(t('common.delSuccess'))
|
||||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 导出按钮操作 */
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
await message.exportConfirm()
|
||||
exportLoading.value = true
|
||||
const data = await RedeemCodeApi.exportRedeemCode(queryParams)
|
||||
download.excel(data, '兑换码列表.xls')
|
||||
} catch {
|
||||
} finally {
|
||||
exportLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
152
yudao-ui-admin-vue3/src/views/muye/redeemrecord/index.vue
Normal file
152
yudao-ui-admin-vue3/src/views/muye/redeemrecord/index.vue
Normal file
@@ -0,0 +1,152 @@
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
label-width="68px"
|
||||
>
|
||||
<el-form-item label="用户编号" prop="userId">
|
||||
<el-input
|
||||
v-model="queryParams.userId"
|
||||
placeholder="请输入用户编号"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="兑换码" prop="code">
|
||||
<el-input
|
||||
v-model="queryParams.code"
|
||||
placeholder="请输入兑换码"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="兑换时间" prop="createTime">
|
||||
<el-date-picker
|
||||
v-model="queryParams.createTime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="daterange"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
plain
|
||||
@click="handleExport"
|
||||
:loading="exportLoading"
|
||||
v-hasPermi="['muye:redeem-record:export']"
|
||||
>
|
||||
<Icon icon="ep:download" class="mr-5px" /> 导出
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list">
|
||||
<el-table-column label="用户编号" align="center" prop="userId" width="100" />
|
||||
<el-table-column label="手机号" align="center" prop="mobile" width="120" />
|
||||
<el-table-column label="兑换码" align="center" prop="code" width="180" />
|
||||
<el-table-column label="获得积分" align="center" prop="points" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag type="warning">+{{ scope.row.points }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="兑换后余额" align="center" prop="balanceAfter" width="120" />
|
||||
<el-table-column label="来源" align="center" prop="source" width="100" />
|
||||
<el-table-column label="备注" align="center" prop="remark" show-overflow-tooltip />
|
||||
<el-table-column
|
||||
label="兑换时间"
|
||||
align="center"
|
||||
prop="createTime"
|
||||
:formatter="dateFormatter"
|
||||
width="180"
|
||||
/>
|
||||
</el-table>
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import download from '@/utils/download'
|
||||
import * as RedeemRecordApi from '@/api/muye/redeemrecord'
|
||||
|
||||
defineOptions({ name: 'RedeemRecord' })
|
||||
|
||||
const message = useMessage()
|
||||
const { t } = useI18n()
|
||||
|
||||
const loading = ref(true)
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
userId: undefined,
|
||||
code: undefined,
|
||||
createTime: []
|
||||
})
|
||||
const queryFormRef = ref()
|
||||
const exportLoading = ref(false)
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await RedeemRecordApi.getRedeemRecordPage(queryParams)
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 导出按钮操作 */
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
await message.exportConfirm()
|
||||
exportLoading.value = true
|
||||
const data = await RedeemRecordApi.exportRedeemRecord(queryParams)
|
||||
download.excel(data, '兑换记录.xls')
|
||||
} catch {
|
||||
} finally {
|
||||
exportLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user