Compare commits

..

63 Commits

Author SHA1 Message Date
ovo d25063164b feature: [评论的屏蔽字-DFA算法] 2025-05-09 00:18:37 +08:00
ovo 1b4f330af6 feature: [定时任务统计评分和评论功能实现] 2025-05-08 23:48:32 +08:00
ovo 46adeb5622 fix: [1+66] 2025-05-08 20:16:09 +08:00
ovo 676cf30e44 fix: [临时提交] 2025-05-07 23:33:31 +08:00
ovo 6d80269b2c fix: [临时提交] 2025-05-07 18:47:17 +08:00
ovo bebac77236 fix: [临时提交] 2025-04-29 00:18:39 +08:00
ovo 37a7af487b fix: [临时提交] 2025-04-17 00:12:33 +08:00
ovo a3decaedb4 fix: [临时提交] 2025-04-15 23:50:51 +08:00
ovo e01fe45e8b fix: [临时提交] 2025-04-15 23:47:50 +08:00
ovo 8a81b6bae3 fix: [临时提交] 2025-04-15 00:55:27 +08:00
ovo 52f7f2c1c7 fix: [临时提交] 2025-04-14 21:11:11 +08:00
ovo 36b70e9450 fix: [日志管理simple] 2025-03-30 22:52:54 +08:00
ovo bc26f13802 fix: [日志管理simple] 2025-03-30 22:18:26 +08:00
ovo 80275af210 fix: [xxljob开关] 2025-03-30 22:11:59 +08:00
ovo 0ce8b27eea fix: [xxljob开关] 2025-03-30 22:04:59 +08:00
ovo b2be9c9058 fix: [xxljob开关] 2025-03-30 21:33:50 +08:00
ovo 1917337775 fix: [xxljob开关] 2025-03-30 21:11:52 +08:00
ovo 6b3a6217b8 fix: [临时提交] 2025-03-30 20:32:16 +08:00
ovo 470d3caf21 fix: [修改readme] 2025-03-13 23:56:13 +08:00
ovo 53b536f2fc fix: [毕设课程demo] 2025-03-13 23:55:12 +08:00
ovo 8b8e5fa497 更新.gitignore 2025-03-05 23:14:47 +08:00
ovo e1a782fc44 fix: [mongo测试文件存储] 2025-02-27 22:00:01 +08:00
ovo 7fe4cda97b fix: [mongo测试文件存储] 2025-02-19 23:06:52 +08:00
ovo 1c6b2f75b6 删除与该项目无关代码 2025-01-11 19:31:24 +08:00
ovo 0c6404829d 1.删除一部分代码
2.新增多选判断题算分简单逻辑
2025-01-11 19:13:39 +08:00
ovo 756dea6abf 1.删除某部分图书代码
2.试卷部分
3.修改配置类 使mybatis分页生效
补充提交
2025-01-11 15:10:13 +08:00
ovo 2863e90f07 1.删除某部分图书代码
2.试卷部分
3.修改配置类 使mybatis分页生效
2025-01-11 14:37:54 +08:00
ovo 627b12f099 feat(加入docker-java): 加入docker-java 2024-12-27 18:24:11 +08:00
ovo 45b2eea3d2 feat(加入docker-java): 加入docker-java 2024-12-27 16:07:04 +08:00
ovo 9afa1cb1c4 feat(加入docker-java): 加入docker-java 2024-12-27 16:06:20 +08:00
ovo 38a72d5ea2 Merge remote-tracking branch 'origin/book-ct' into book-ct 2024-12-25 15:11:26 +08:00
ovo 3738110395 feat(修改 RequestPart 问题): 修改 RequestPart 问题 2024-12-25 15:11:04 +08:00
Guwan 629678241f feat(docker部署文档): docker部署文档 2024-12-24 23:17:30 +08:00
ovo deed1bb13a feat(集成xxljob): 集成xxljob 2024-12-24 17:08:04 +08:00
ovo ce9823c369 feat(集成xxljob): 集成xxljob 2024-12-24 17:04:39 +08:00
ovo f6651db2b8 feat(读书听书准备): 读书听书准备 2024-12-24 16:29:53 +08:00
ovo 17c571c61d feat(读书听书准备): 读书听书准备 2024-12-24 16:29:26 +08:00
ovo 2423e27b34 feat(java查询elk日志接口): java查询elk日志接口
java查询elk日志接口
2024-12-24 13:39:36 +08:00
Guwan a9212be666 feat(加点文档): 加点文档
加点文档
2024-12-23 22:41:53 +08:00
Guwan d82bf584bb feat(测试连接logstash): 测试连接logstash
测试连接logstash
2024-12-23 22:26:39 +08:00
ovo a6a12a3b04 feat(本地缓存): 加入本地缓存功能
1.项目第一次启动 查数据库 加入redis 加入本地缓存
2.第二次启动 查询redis 加入本地缓存
2024-12-23 17:25:04 +08:00
ovo 89f8550dff feat(本地缓存): 加入本地缓存功能
1.项目第一次启动 查数据库 加入redis 加入本地缓存
2.第二次启动 查询redis 加入本地缓存
2024-12-23 17:07:44 +08:00
ovo f51134ea29 feat(本地缓存): 加入本地缓存功能
1.项目第一次启动 查数据库 加入redis 加入本地缓存
2.第二次启动 查询redis 加入本地缓存
2024-12-23 17:04:02 +08:00
ovo 556802f406 feat(用户): 修改密码 2024-12-23 16:08:26 +08:00
ovo 9693cfc524 feat(用户): 修改密码 2024-12-23 14:57:08 +08:00
ovo 7f94911665 feat(用户): 修改密码 2024-12-23 14:55:04 +08:00
ovo 91b482d0bc feat(图书初步): 图书初步
图书初步
2024-12-20 17:55:45 +08:00
ovo fc7524589c feat(图书初步): 图书初步
图书初步
2024-12-20 17:54:01 +08:00
ovo 736b824d7f feat(图书初步): 图书初步
图书初步
2024-12-20 17:47:26 +08:00
ovo 4b5a912784 feat(图书初步): 图书初步
图书初步
2024-12-20 16:33:45 +08:00
ovo f0e417f1d7 feat(图书初步): 图书初步
图书初步
2024-12-20 16:28:48 +08:00
ovo 4deba9f86c feat(图书初步): 图书初步
图书初步
2024-12-20 15:42:38 +08:00
ovo 102c9c4a38 feat(图书初步): 图书初步
图书初步
2024-12-20 15:10:17 +08:00
ovo 45512694b6 feat(图书初步): 图书初步
图书初步
2024-12-20 14:14:26 +08:00
ovo 1551cc1263 feat(图书初步): 图书初步
图书初步
2024-12-18 14:30:27 +08:00
ovo 9551f322f0 feat(图书初步): 图书初步
图书初步
2024-12-17 14:47:27 +08:00
ovo 1ffa7c4a75 feat(图书初步): 图书初步
图书初步
2024-12-17 14:46:59 +08:00
ovo 8154807f90 feat(图书初步): 图书初步
图书初步
2024-12-15 22:44:49 +08:00
Guwan 256743d3b9 `
feat(图书初步): 图书初步
2024-12-15 22:29:53 +08:00
Guwan bc378ee9ec `
feat(图书初步): 图书初步
2024-12-15 22:24:00 +08:00
Guwan 0350786d00 `
feat(图书初步): 图书初步
2024-12-15 21:24:44 +08:00
ovo 378c4cbdf8 feat(图书初步): 图书初步
图书初步
2024-12-13 18:00:32 +08:00
ovo 85b2729a04 feat(图书初步): 图书初步
图书初步
2024-12-13 17:56:54 +08:00
395 changed files with 398832 additions and 3163 deletions

View File

@ -1 +1 @@
EWEPEPEOGMGTELIZJUGECKIUJDBCJTCNISGPBNHLJTJUBHEWGNAKGEGAIOHJDQAJGNCFDRFZJEDMJTGFGBEAAWGLBZAUCNHCCPBZCIIKBJATGYARHRAZHXFRBIEBCHIXAFDLFQBZFVJQGTCEHDIMIVGQEJAJIYHXHTISIVETBACMCCGFDPEKDQHYGGAPFXIOCJAAGOHYIFHNHZHWIGCZIGHHDMDXHQGTFLJOHGAYIVBGIHIQHHHJDJFWHMCRHJAMHZESGWGGAUJRGDHAGHIQITIJIUAXAEIZGBGZCCHJAHASGNCVIIJAIYFOEQGFELEIEDECJTCXBCAPIKHTFTHBAGJTERHQGSAFEUDIHLDDITDPIMACAOGTIIHFEMHLHIHYJTFDFDGWAKHJEIEIJFGABQJCIHIADYCXAHIMJTHOASFLGFFIBJJEHDHLHOCMDIGGDOJDHNBQCJENFUBAGOAZITIRJSFBBUCUHABLHRFVIUBWCADJCMDXDPATBPJSJRJLBRABGVFTDNFOAQDOARDEBRHJAQGYHIGMBDCJCSJKFBBLGECAEFEYCVCHAEAZJRIOFEHLCJILEHJVGYIVCWGHCMGJGLBTFMHFCAEAAUJQJLAEARDHDFHDBJJGALEHFNGSAIHOJUBOEAJDDFFYFTINITHTBNIJFDHLEAFGBFHFFQGHGFGREVFHFDCZGYEVBWAZDSCAGLDMIAAEFOAXIXFECSFQDWHFFHCFASFSGAHVJSDBBZJQAZBXARILBAJFJEHCANAIABBMECBJJFIOGYGHBXCUCVBDJOCYBZDZAJEXAXEPFRFOGVHQAOJLCYBOHFEKJFIJBDHDDCEAAUJVDRIGGGGCJOFVECAHAQFSBSGYJVGKCQDDHPGUCIARFAIEJGGDITAUDIIVBBJUEFCIDTGJGYJODRDEJPBMFNCXAKCPAIGOJQGHBZHQJUCOBKCKDPJSGGCVCAIWFVHIIEAJJMEFGRHZDEDACFBQJODGDVJBAUBXGKGCFZERAHIOALGKGAILDNHQGGAZDEIGATBTCWHMDKGSIWFMHAIZHREBJBEFENDFBRBLGLCMERJAEOBXCNDBHVCSJBDMEHCLJLCFFOGVGWATBOJBFJEQETHGESEXFDIIFDAGJPDNHEDSFNBRIVFMFPGOEEIHEFCOCKJGJAIZJIFTIGAWITGWDXGBEFDTJHFXBF
EWEPEPEOGMGTELIZJUGECKIUJDBCJTCNISGPBNHLJTJUBHEWGNAKGEGAIOHJDQAJGNCFDRFZJEDMJTGKGSHREQHMATEHBXBBELGFHLHTHOEGBAEOIHCLEOHMBGAAGYCLCYAXCDJTCCEAIOJNEBAXFCBLJQCMJPFBHWAADIFPHXAFDQDXEHCODEIXBWDBCOEZIAFNBYGJFLJEFIHFIDETGZDMJAAYBVBWDKGOFEJDDXITHJCOJTHIHOIAETIBFLAWIZCBDNJSCBGXGQFLCOAEGXIHEKGXFPCAACCFGOGJIZISFMACEFJJAYDTIWCYHQFTDDGVANDNBICDBRJIBBIHHWFZHRHWJCBACHHSFPCKINHXHPEVDMAHAJBHHNBTFLEQEPDQIWCXBEGVEPCYECIFEHAMBQJIGUELIAAWHABFDTGYIPGDGOHVCJDFDDGUBTBECFFLIGJJBJJOGNJDHKIUBVHFIRAVHQGKJLBQIUAVFTICHLHFEAAGGAJODVIRINBWHXEDAWCMGOIHDZCNAFIEEEGXJMHXBAFXCJGNDSCOFPGLFNBQDRATGQEMAMDHHCEDJRDAITICJNJSJADSBAAVHNERBZJIAOIUAVIHFODFJDBQJBIPEXJOCXDSEAJUBXECHVCQGJGGFRIPIVGVHPBRAUIADJEWBPHAHXBICDBKELCIGEIBFLHHGFHNDDDMIRBYIIDODBDYBAIPBQAWHFAVFMJRGHHCDYJGDQJACYHGAXGSIPAGIWFJDRBRGXHJESHZHPAGDKGWFUIKJOEPITFCEXELAVITCPBFGXHNEDANGLDLCMDRHBEAEHEYIKAJDSDFCMGQIJAWHAGOJNFYIHDWACBTGBIHENHJFNITEWCIBPCEIYDVFCJQGZHDILIUGYFJBAGTICJVDYJLGIBMJAAUFIJNIEERJOIMDCFAANCEEXHBBBGNESJKCMDYDBGOBCBVEDAFCBIQAKHVHUIUEOHVBPCJIRDGIFCQBWAFACELFSGYEMENEUITANIAIMBLBAAQGCHOGFDSENIFEWFVEYHZIVEUGXAXGCHSCXEOALHQGPCYDIBRHYHCEFAFHNAMCVHBBUFPJMHPDXIHBXGXJPFRCHBPGPFWABCKIEHOIZFLJECTERFOHAHZGXGLIJAAFBCRDCGOFPBCIGJPBXJMEYGQDVAXHMJGBIIOCLDYJQHNJMECBK

24
admin-frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

5
admin-frontend/README.md Normal file
View File

@ -0,0 +1,5 @@
# Vue 3 + TypeScript + Vite
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).

13
admin-frontend/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue + TS</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

2057
admin-frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,26 @@
{
"name": "admin-frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"axios": "^1.8.4",
"element-plus": "^2.9.7",
"pinia": "^3.0.2",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"@vue/tsconfig": "^0.7.0",
"typescript": "~5.7.2",
"vite": "^6.2.0",
"vue-tsc": "^2.2.4"
}
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,10 @@
<template>
<router-view />
</template>
<script setup lang="ts">
</script>
<style scoped>
/* 可以移除或保留样式 */
</style>

View File

@ -0,0 +1,67 @@
import request from '@/utils/request'
import type { Course } from '@/types/api' // Assuming you have this type defined
// Define interface for query parameters
export interface CourseQueryParams {
page?: number
size?: number
title?: string // Example filter
categoryId?: string // Example filter
// Add other filter parameters as needed
}
// Define interface for the paginated response (adjust based on your backend)
// IMPORTANT: Ensure this matches how your backend structures the paginated list response!
export interface CourseListResponse {
list: Course[]
total: number
pageNum?: number // Common alternative names
pageSize?: number
pages?: number
}
// Get course list (paginated)
export function getCourseList(params: CourseQueryParams) {
// The return type here should ideally match the structure your interceptor returns
// If your interceptor returns the full AxiosResponse, use AxiosResponse<CourseListResponse>
// If it returns the data directly (like res or res.data), use CourseListResponse or adjust accordingly.
return request<CourseListResponse>({
url: '/admin/courses', // Adjust API path as needed
method: 'get',
params
})
}
// Get single course details
export function getCourse(id: string) {
return request<Course>({
url: `/admin/courses/${id}`,
method: 'get'
})
}
// Create a new course
export function createCourse(data: Partial<Course>) {
return request({
url: '/admin/courses',
method: 'post',
data
})
}
// Update an existing course
export function updateCourse(id: string, data: Partial<Course>) {
return request({
url: `/admin/courses/${id}`,
method: 'put',
data
})
}
// Delete a course
export function deleteCourse(id: string) {
return request({
url: `/admin/courses/${id}`,
method: 'delete'
})
}

View File

@ -0,0 +1,61 @@
import request from '@/utils/request'
import type { Exam } from '@/types/api'
// Define interface for query parameters
export interface ExamQueryParams {
page?: number
size?: number
title?: string
timeLimit?: number
}
// Define interface for the paginated response
export interface ExamListResponse {
list: Exam[]
total: number
pageNum?: number
pageSize?: number
}
// Get exam list (paginated)
export function getExamList(params: ExamQueryParams) {
return request<ExamListResponse>({
url: '/admin/exams',
method: 'get',
params
})
}
// Get single exam details
export function getExam(id: string) {
return request<Exam>({
url: `/admin/exams/${id}`,
method: 'get'
})
}
// Create a new exam
export function createExam(data: Partial<Exam>) {
return request({
url: '/admin/exams',
method: 'post',
data
})
}
// Update an existing exam
export function updateExam(id: string, data: Partial<Exam>) {
return request({
url: `/admin/exams/${id}`,
method: 'put',
data
})
}
// Delete an exam
export function deleteExam(id: string) {
return request({
url: `/admin/exams/${id}`,
method: 'delete'
})
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@ -0,0 +1,41 @@
<script setup lang="ts">
import { ref } from 'vue'
defineProps<{ msg: string }>()
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test HMR
</p>
</div>
<p>
Check out
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
>create-vue</a
>, the official Vue + Vite starter
</p>
<p>
Learn more about IDE Support for Vue in the
<a
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
target="_blank"
>Vue Docs Scaling up Guide</a
>.
</p>
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>
<style scoped>
.read-the-docs {
color: #888;
}
</style>

View File

@ -0,0 +1,96 @@
<template>
<el-container class="layout-container">
<el-aside width="200px" class="aside">
<el-scrollbar>
<div class="logo">管理后台</div>
<el-menu :default-openeds="['1']" router>
<template v-for="route in menuRoutes" :key="route.path">
<el-menu-item v-if="!route.children || route.children.length === 0" :index="route.path">
<el-icon v-if="route.meta?.icon"><component :is="route.meta.icon" /></el-icon>
<span>{{ route.meta?.title }}</span>
</el-menu-item>
<el-sub-menu v-else :index="route.path">
<template #title>
<el-icon v-if="route.meta?.icon"><component :is="route.meta.icon" /></el-icon>
<span>{{ route.meta?.title }}</span>
</template>
<el-menu-item v-for="child in route.children" :key="child.path" :index="route.path + '/' + child.path">
<el-icon v-if="child.meta?.icon"><component :is="child.meta.icon" /></el-icon>
<span>{{ child.meta?.title }}</span>
</el-menu-item>
</el-sub-menu>
</template>
</el-menu>
</el-scrollbar>
</el-aside>
<el-container>
<el-header class="header">
<div>面包屑/用户信息</div>
</el-header>
<el-main class="main-content">
<router-view />
</el-main>
</el-container>
</el-container>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
//
const menuRoutes = computed(() => {
const rootRoute = router.options.routes.find(r => r.path === '/')
return rootRoute?.children || []
})
</script>
<style scoped>
.layout-container {
height: 100vh;
}
.aside {
background-color: #545c64;
color: #fff;
}
.logo {
height: 60px;
line-height: 60px;
text-align: center;
font-size: 18px;
font-weight: bold;
background-color: #434a50;
}
.el-menu {
border-right: none;
background-color: #545c64;
}
.el-menu-item, .el-sub-menu__title {
color: #fff;
}
.el-menu-item:hover, .el-sub-menu__title:hover {
background-color: #434a50;
}
.el-menu-item.is-active {
background-color: #409EFF !important; /* Element Plus might override, use !important */
color: #fff !important;
}
.header {
background-color: #fff;
line-height: 60px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
}
.main-content {
padding: 20px;
background-color: #f0f2f5;
}
</style>

View File

@ -0,0 +1,24 @@
// import './assets/main.css' // Comment out or remove this line
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import { router } from './router' // Use named import
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
const app = createApp(App)
// Register Element Plus Icons
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(createPinia())
app.use(router) // Use the imported router
app.use(ElementPlus)
app.mount('#app')

View File

@ -0,0 +1,54 @@
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
import AdminLayout from '@/layouts/AdminLayout.vue'
import DashboardView from '@/views/DashboardView.vue'
import CategoryListView from '@/views/category/CategoryListView.vue'
import CourseListView from '@/views/course/CourseListView.vue'
import ExamListView from '@/views/exam/index.vue'
// Restore the routes array definition
const routes: Array<RouteRecordRaw> = [
{
path: '/',
component: AdminLayout,
redirect: '/dashboard',
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: DashboardView,
meta: { title: '仪表盘', icon: 'DataAnalysis' }
},
{
path: 'categories',
name: 'CategoryList',
component: CategoryListView,
meta: { title: '类别管理', icon: 'CollectionTag' }
},
{
path: 'courses',
name: 'CourseList',
component: CourseListView,
meta: { title: '课程管理', icon: 'Reading' }
},
{
path: 'exams',
name: 'ExamList',
component: ExamListView,
meta: { title: '考试管理', icon: 'Document' }
}
]
},
// {
// path: '/login',
// name: 'Login',
// component: () => import('../views/LoginView.vue')
// }
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes // Now routes is defined
})
export { router }

View File

@ -0,0 +1,79 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.card {
padding: 2em;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

11
admin-frontend/src/types/api.d.ts vendored Normal file
View File

@ -0,0 +1,11 @@
// Exam entity type definition
export interface Exam {
id: string
title: string
timeLimit: number
startTime: string
endTime: string
totalTime: number
totalScore: number
qualifyScore: number
}

View File

@ -0,0 +1,66 @@
import axios from 'axios'
import { ElMessage } from 'element-plus'
// 创建 axios 实例
const service = axios.create({
baseURL: 'http://localhost:8084' || '/api', // API 基础路径,稍后在 .env 文件中配置
timeout: 10000, // 请求超时时间
})
// 请求拦截器
service.interceptors.request.use(
(config) => {
// 在发送请求之前做些什么,例如添加 token
// const token = localStorage.getItem('token')
// if (token) {
// config.headers['Authorization'] = `Bearer ${token}`
// }
return config
},
(error) => {
// 对请求错误做些什么
console.error('Request Error:', error) // for debug
return Promise.reject(error)
}
)
// 响应拦截器
service.interceptors.response.use(
(response) => {
// 对响应数据做点什么
const res = response.data
// 这里假设后端接口返回的数据结构是 { code: number, message: string, data: any }
// 如果 code 不是成功代码 (例如 200), 就判断为错误。
// 这个判断需要根据你的后端实际返回格式调整
if (res.code !== 200 && response.status === 200) {
ElMessage({
message: res.message || 'Error',
type: 'error',
duration: 5 * 1000,
})
// 可以根据后端状态码做进一步处理,例如 401 跳转登录页
// if (res.code === 401) {
// // to re-login
// }
return Promise.reject(new Error(res.message || 'Error'))
} else {
// 如果后端直接返回了数据,没有包裹 code/message/data则直接返回 response.data
// 或者根据你的后端结构调整返回 res.data
return res // 或者 return res.data
}
},
(error) => {
// 对响应错误做点什么
console.error('Response Error:', error) // for debug
ElMessage({
message: error.message,
type: 'error',
duration: 5 * 1000,
})
return Promise.reject(error)
}
)
export default service

View File

@ -0,0 +1,10 @@
<template>
<div>
<h1>仪表盘</h1>
<p>欢迎来到管理后台</p>
</div>
</template>
<script lang="ts" setup>
//
</script>

View File

@ -0,0 +1,3 @@
<template>
11
</template>

View File

@ -0,0 +1,275 @@
<template>
<div class="course-list-view">
<h1>课程管理</h1>
<!-- 搜索/过滤区域 (Placeholder) -->
<el-card class="filter-card" shadow="never">
<el-form :inline="true" :model="queryParams" @submit.prevent="handleSearch">
<el-form-item label="课程标题">
<el-input v-model="queryParams.title" placeholder="输入课程标题" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 操作按钮 -->
<el-row class="action-row">
<el-button type="primary" :icon="Edit" @click="handleAdd">新增课程</el-button>
</el-row>
<!-- 课程表格 -->
<el-card shadow="never">
<el-table v-loading="loading" :data="courseList" style="width: 100%">
<el-table-column prop="id" label="ID" width="180" />
<el-table-column prop="title" label="课程标题" show-overflow-tooltip />
<el-table-column prop="categoryName" label="类别" width="150" />
<el-table-column prop="price" label="价格" width="100" />
<el-table-column prop="studentCount" label="学习人数" width="120" />
<el-table-column prop="createdAt" label="创建时间" width="180">
<template #default="scope">
{{ formatDateTime(scope.row.createdAt) }}
</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right">
<template #default="scope">
<el-button size="small" type="primary" link :icon="EditPen" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="small" type="danger" link :icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-if="total > 0"
v-model:current-page="queryParams.page"
v-model:page-size="queryParams.size"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="fetchCourseList"
@current-change="fetchCourseList"
class="pagination-container"
/>
</el-card>
<!-- 新增/编辑对话框 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px" @close="handleDialogClose">
<el-form ref="courseFormRef" :model="currentCourse" :rules="courseRules" label-width="100px">
<el-form-item label="课程标题" prop="title">
<el-input v-model="currentCourse.title" placeholder="请输入课程标题" />
</el-form-item>
<el-form-item label="课程描述" prop="description">
<el-input type="textarea" v-model="currentCourse.description" placeholder="请输入课程描述" />
</el-form-item>
<el-form-item label="类别ID" prop="categoryId">
<el-input v-model="currentCourse.categoryId" placeholder="请输入类别ID" />
<!-- TODO: Replace with category selector -->
</el-form-item>
<el-form-item label="价格" prop="price">
<el-input-number v-model="currentCourse.price" :precision="2" :step="0.1" :min="0" />
</el-form-item>
<!-- TODO: Add other form fields based on Course entity:
teacherId, coverImg, coursrTeacherId? etc.
Consider using selectors for teacher/category if data is available
-->
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox, FormInstance, FormRules } from 'element-plus'
import { Edit, Delete, EditPen } from '@element-plus/icons-vue'
import { getCourseList, createCourse, updateCourse, deleteCourse } from '@/api/course'
import type { Course, CourseQueryParams } from '@/types/api'
const loading = ref(false)
const courseList = ref<Course[]>([])
const total = ref(0)
const queryParams = reactive<CourseQueryParams>({
page: 1,
size: 10,
title: undefined,
})
// State for Dialog/Form
const dialogVisible = ref(false)
const dialogTitle = ref('')
const courseFormRef = ref<FormInstance | null>(null)
const currentCourse = ref<Partial<Course>>({})
const isEditMode = ref(false)
// --- Form Validation Rules ---
const courseRules = reactive<FormRules>({
title: [{ required: true, message: '请输入课程标题', trigger: 'blur' }],
categoryId: [{ required: true, message: '请输入或选择类别ID', trigger: 'blur' }],
price: [{ type: 'number', required: true, message: '请输入有效的价格', trigger: 'blur' }],
// Add more rules as needed
})
const fetchCourseList = async () => {
loading.value = true
try {
// --- IMPORTANT: Adjust based on your backend response structure ---
// Option 1: If backend returns { list: [], total: number }
const response = await getCourseList(queryParams)
courseList.value = response.list || []
total.value = response.total || 0
// Option 2: If backend returns { code: 200, data: { list: [], total: number }, message: '' }
// const response = await getCourseList(queryParams)
// if (response.code === 200 && response.data) {
// courseList.value = response.data.list || []
// total.value = response.data.total || 0
// } else {
// throw new Error(response.message || 'Failed to fetch data')
// }
// Option 3: If backend returns the list directly
// const response = await getCourseList(queryParams) // Assuming it returns Course[]
// courseList.value = response || []
// total.value = courseList.value.length // Manual total for client-side pagination (not ideal)
} catch (error) {
console.error('Failed to fetch courses:', error)
ElMessage.error('获取课程列表失败')
courseList.value = []
total.value = 0
} finally {
loading.value = false
}
}
const formatDateTime = (date: string | Date | undefined): string => {
if (!date) return ''
return new Date(date).toLocaleString()
}
const handleSearch = () => {
queryParams.page = 1
fetchCourseList()
}
const resetQuery = () => {
queryParams.page = 1
queryParams.title = undefined
fetchCourseList()
}
// Handle add button click
const handleAdd = () => {
isEditMode.value = false
dialogTitle.value = '新增课程'
currentCourse.value = { price: 0, categoryId: '' }
dialogVisible.value = true
courseFormRef.value?.resetFields()
}
// Handle edit button click
const handleEdit = (row: Course) => {
isEditMode.value = true
dialogTitle.value = '编辑课程'
currentCourse.value = { ...row }
dialogVisible.value = true
courseFormRef.value?.resetFields()
}
// Handle dialog close
const handleDialogClose = () => {
// Optional: Reset form fields if needed upon explicit close
// courseFormRef.value?.resetFields()
}
// Submit form (Create or Update)
const submitForm = async () => {
if (!courseFormRef.value) return
await courseFormRef.value.validate(async (valid) => {
if (valid) {
loading.value = true
try {
if (isEditMode.value) {
await updateCourse(currentCourse.value.id!, currentCourse.value)
ElMessage.success('更新成功')
} else {
await createCourse(currentCourse.value)
ElMessage.success('新增成功')
}
dialogVisible.value = false
fetchCourseList()
} catch (error) {
console.error('Failed to save course:', error)
ElMessage.error('保存失败')
} finally {
loading.value = false
}
} else {
console.log('Form validation failed')
return false
}
})
}
// Handle delete button click (Implement confirmation)
const handleDelete = (row: Course) => {
if (!row.id) {
ElMessage.warning('无法删除缺少ID的课程')
return
}
ElMessageBox.confirm(
`确定要删除课程 "${row.title}" 吗? 此操作不可撤销。`,
'警告',
{
confirmButtonText: '确定删除',
cancelButtonText: '取消',
type: 'warning',
}
).then(async () => {
loading.value = true // Add loading state
try {
await deleteCourse(row.id!) // Call the delete API
ElMessage.success('删除成功')
fetchCourseList() // Refresh list after delete
} catch (error) {
console.error('Failed to delete course:', error)
ElMessage.error('删除失败')
} finally {
loading.value = false
}
}).catch(() => {
ElMessage.info('取消删除')
})
}
onMounted(() => {
fetchCourseList()
})
</script>
<style scoped>
.course-list-view {
padding: 20px;
}
.filter-card {
margin-bottom: 20px;
}
.action-row {
margin-bottom: 20px;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
</style>

View File

@ -0,0 +1,306 @@
<template>
<div class="exam-container">
<div class="filter-container">
<el-form :inline="true" :model="queryParams" class="demo-form-inline">
<el-form-item label="考试名称">
<el-input v-model="queryParams.title" placeholder="请输入考试名称" clearable />
</el-form-item>
<el-form-item label="是否限时">
<el-select v-model="queryParams.timeLimit" placeholder="请选择" clearable>
<el-option :value="1" label="是" />
<el-option :value="0" label="否" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</div>
<div class="action-container">
<el-button type="primary" @click="handleAdd">新增考试</el-button>
</div>
<el-table v-loading="loading" :data="examList" border style="width: 100%">
<el-table-column prop="id" label="ID" width="150" />
<el-table-column prop="title" label="考试名称" min-width="120" />
<el-table-column label="是否限时" width="100">
<template #default="scope">
{{ scope.row.timeLimit === 1 ? '是' : '否' }}
</template>
</el-table-column>
<el-table-column label="考试时间" min-width="280">
<template #default="scope">
<span v-if="scope.row.timeLimit === 1">
{{ formatDateTime(scope.row.startTime) }} {{ formatDateTime(scope.row.endTime) }}
</span>
<span v-else>
{{ scope.row.totalTime }} 分钟
</span>
</template>
</el-table-column>
<el-table-column prop="totalScore" label="考试总分" width="100" />
<el-table-column prop="qualifyScore" label="及格分数" width="100" />
<el-table-column label="操作" width="180" fixed="right">
<template #default="scope">
<el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination
v-model:current-page="queryParams.page"
v-model:page-size="queryParams.size"
:page-sizes="[10, 20, 30, 50]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
<!-- 添加/编辑对话框 -->
<el-dialog
:title="dialogTitle"
v-model="dialogVisible"
width="600px"
append-to-body
>
<el-form
ref="examFormRef"
:model="formData"
:rules="rules"
label-width="120px"
>
<el-form-item label="考试名称" prop="title">
<el-input v-model="formData.title" placeholder="请输入考试名称" />
</el-form-item>
<el-form-item label="是否限时" prop="timeLimit">
<el-radio-group v-model="formData.timeLimit">
<el-radio :label="1"></el-radio>
<el-radio :label="0"></el-radio>
</el-radio-group>
</el-form-item>
<template v-if="formData.timeLimit === 1">
<el-form-item label="开始时间" prop="startTime">
<el-date-picker
v-model="formData.startTime"
type="datetime"
placeholder="选择开始时间"
/>
</el-form-item>
<el-form-item label="结束时间" prop="endTime">
<el-date-picker
v-model="formData.endTime"
type="datetime"
placeholder="选择结束时间"
/>
</el-form-item>
</template>
<template v-else>
<el-form-item label="考试时长(分钟)" prop="totalTime">
<el-input-number v-model="formData.totalTime" :min="1" />
</el-form-item>
</template>
<el-form-item label="考试总分" prop="totalScore">
<el-input-number v-model="formData.totalScore" :min="1" />
</el-form-item>
<el-form-item label="及格分数" prop="qualifyScore">
<el-input-number
v-model="formData.qualifyScore"
:min="1"
:max="formData.totalScore || 100"
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox, FormInstance } from 'element-plus'
import { getExamList, getExam, createExam, updateExam, deleteExam } from '@/api/exam'
import type { Exam, ExamQueryParams } from '@/api/exam'
//
const loading = ref(false)
const examList = ref<Exam[]>([])
const total = ref(0)
const dialogVisible = ref(false)
const dialogType = ref<'add' | 'edit'>('add')
const examFormRef = ref<FormInstance>()
//
const queryParams = reactive<ExamQueryParams>({
page: 1,
size: 10,
title: '',
timeLimit: undefined
})
//
const defaultFormData = {
id: '',
title: '',
timeLimit: 1,
startTime: '',
endTime: '',
totalTime: 90,
totalScore: 100,
qualifyScore: 60
}
const formData = reactive<Partial<Exam>>({ ...defaultFormData })
//
const rules = reactive({
title: [{ required: true, message: '请输入考试名称', trigger: 'blur' }],
timeLimit: [{ required: true, message: '请选择是否限时', trigger: 'change' }],
startTime: [{ required: true, message: '请选择开始时间', trigger: 'change' }],
endTime: [{ required: true, message: '请选择结束时间', trigger: 'change' }],
totalTime: [{ required: true, message: '请输入考试时长', trigger: 'blur' }],
totalScore: [{ required: true, message: '请输入考试总分', trigger: 'blur' }],
qualifyScore: [{ required: true, message: '请输入及格分数', trigger: 'blur' }]
})
//
const dialogTitle = computed(() => {
return dialogType.value === 'add' ? '添加考试' : '编辑考试'
})
//
onMounted(() => {
fetchExamList()
})
//
const fetchExamList = async () => {
loading.value = true
try {
const res = await getExamList(queryParams)
examList.value = res.list
total.value = res.total
} catch (error) {
console.error('获取考试列表失败', error)
} finally {
loading.value = false
}
}
const handleQuery = () => {
queryParams.page = 1
fetchExamList()
}
const resetQuery = () => {
queryParams.title = ''
queryParams.timeLimit = undefined
queryParams.page = 1
fetchExamList()
}
const handleSizeChange = (size: number) => {
queryParams.size = size
fetchExamList()
}
const handleCurrentChange = (page: number) => {
queryParams.page = page
fetchExamList()
}
const handleAdd = () => {
dialogType.value = 'add'
Object.assign(formData, defaultFormData)
dialogVisible.value = true
}
const handleEdit = async (row: Exam) => {
dialogType.value = 'edit'
dialogVisible.value = true
try {
loading.value = true
const res = await getExam(row.id)
Object.assign(formData, res)
} catch (error) {
console.error('获取考试详情失败', error)
} finally {
loading.value = false
}
}
const handleDelete = (row: Exam) => {
ElMessageBox.confirm(
`确认删除考试 "${row.title}" 吗?`,
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
.then(async () => {
try {
await deleteExam(row.id)
ElMessage.success('删除成功')
fetchExamList()
} catch (error) {
console.error('删除失败', error)
}
})
.catch(() => {})
}
const submitForm = async () => {
if (!examFormRef.value) return
await examFormRef.value.validate(async (valid, fields) => {
if (valid) {
try {
if (dialogType.value === 'add') {
await createExam(formData)
ElMessage.success('添加成功')
} else {
await updateExam(formData.id as string, formData)
ElMessage.success('更新成功')
}
dialogVisible.value = false
fetchExamList()
} catch (error) {
console.error('保存失败', error)
}
}
})
}
const formatDateTime = (time: string) => {
if (!time) return ''
const date = new Date(time)
return date.toLocaleString()
}
</script>
<style scoped>
.exam-container {
padding: 20px;
}
.filter-container {
margin-bottom: 20px;
}
.action-container {
margin-bottom: 20px;
}
.pagination-container {
margin-top: 20px;
text-align: right;
}
</style>

1
admin-frontend/src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@ -0,0 +1,20 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
}
}

View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

View File

@ -0,0 +1,23 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
server: {
proxy: {
'/api': {
target: 'http://localhost:8084', // 你的后端API地址
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
},
}
}
})

View File

@ -1,28 +0,0 @@
listen 1935;
max_connections 1000;
daemon off;
http_api {
enabled on;
listen 1985;
}
http_server {
enabled on;
listen 8080;
}
vhost __defaultVhost__ {
hls {
enabled on;
hls_path ./objs/nginx/html;
hls_fragment 10;
hls_window 60;
}
http_remux {
enabled on;
mount [vhost]/[app]/[stream].flv;
}
dvr {
enabled on;
dvr_path ./recordings/[app]/[stream].flv;
dvr_plan session;
}
}

455
deps.txt Normal file
View File

@ -0,0 +1,455 @@
[INFO] Scanning for projects...
[WARNING]
[WARNING] Some problems were encountered while building the effective model for com.guwan:backend:jar:0.0.1-SNAPSHOT
[WARNING] 'dependencies.dependency.(groupId:artifactId:type:classifier)' must be unique: com.aliyun:dysmsapi20170525:jar -> version 2.0.24 vs 3.0.0 @ line 139, column 21
[WARNING] 'dependencies.dependency.(groupId:artifactId:type:classifier)' must be unique: commons-io:commons-io:jar -> version 2.11.0 vs 2.18.0 @ line 436, column 21
[WARNING]
[WARNING] It is highly recommended to fix these problems because they threaten the stability of your build.
[WARNING]
[WARNING] For this reason, future Maven versions might no longer support building such malformed projects.
[WARNING]
[INFO]
[INFO] -------------------------< com.guwan:backend >--------------------------
[INFO] Building backend 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[WARNING] The POM for com.arcsoft.face:arcsoft-sdk-face:jar:4.1.1.0 is missing, no dependency information available
[INFO]
[INFO] --- maven-dependency-plugin:3.6.1:tree (default-cli) @ backend ---
[INFO] com.guwan:backend:jar:0.0.1-SNAPSHOT
[INFO] +- org.springframework.boot:spring-boot-starter-web:jar:3.2.1:compile
[INFO] | +- org.springframework.boot:spring-boot-starter:jar:3.2.1:compile
[INFO] | | +- org.springframework.boot:spring-boot:jar:3.2.1:compile
[INFO] | | \- jakarta.annotation:jakarta.annotation-api:jar:2.1.1:compile
[INFO] | +- org.springframework.boot:spring-boot-starter-json:jar:3.2.1:compile
[INFO] | | +- com.fasterxml.jackson.datatype:jackson-datatype-jdk8:jar:2.15.3:compile
[INFO] | | \- com.fasterxml.jackson.module:jackson-module-parameter-names:jar:2.15.3:compile
[INFO] | +- org.springframework.boot:spring-boot-starter-tomcat:jar:3.2.1:compile
[INFO] | | +- org.apache.tomcat.embed:tomcat-embed-core:jar:10.1.17:compile
[INFO] | | \- org.apache.tomcat.embed:tomcat-embed-websocket:jar:10.1.17:compile
[INFO] | +- org.springframework:spring-web:jar:6.1.2:compile
[INFO] | | \- org.springframework:spring-beans:jar:6.1.2:compile
[INFO] | \- org.springframework:spring-webmvc:jar:6.1.2:compile
[INFO] | \- org.springframework:spring-expression:jar:6.1.2:compile
[INFO] +- com.mysql:mysql-connector-j:jar:8.1.0:runtime
[INFO] +- org.projectlombok:lombok:jar:1.18.30:compile
[INFO] +- org.springframework.boot:spring-boot-starter-test:jar:3.2.1:test
[INFO] | +- org.springframework.boot:spring-boot-test:jar:3.2.1:test
[INFO] | +- org.springframework.boot:spring-boot-test-autoconfigure:jar:3.2.1:test
[INFO] | +- com.jayway.jsonpath:json-path:jar:2.8.0:test
[INFO] | +- jakarta.xml.bind:jakarta.xml.bind-api:jar:4.0.1:compile
[INFO] | | \- jakarta.activation:jakarta.activation-api:jar:2.1.2:compile
[INFO] | +- net.minidev:json-smart:jar:2.5.0:test
[INFO] | | \- net.minidev:accessors-smart:jar:2.5.0:test
[INFO] | | \- org.ow2.asm:asm:jar:9.3:test
[INFO] | +- org.assertj:assertj-core:jar:3.24.2:test
[INFO] | | \- net.bytebuddy:byte-buddy:jar:1.14.10:test
[INFO] | +- org.awaitility:awaitility:jar:4.2.0:test
[INFO] | +- org.hamcrest:hamcrest:jar:2.2:test
[INFO] | +- org.junit.jupiter:junit-jupiter:jar:5.10.1:test
[INFO] | | +- org.junit.jupiter:junit-jupiter-api:jar:5.10.1:test
[INFO] | | | +- org.opentest4j:opentest4j:jar:1.3.0:test
[INFO] | | | +- org.junit.platform:junit-platform-commons:jar:1.10.1:test
[INFO] | | | \- org.apiguardian:apiguardian-api:jar:1.1.2:test
[INFO] | | +- org.junit.jupiter:junit-jupiter-params:jar:5.10.1:test
[INFO] | | \- org.junit.jupiter:junit-jupiter-engine:jar:5.10.1:test
[INFO] | | \- org.junit.platform:junit-platform-engine:jar:1.10.1:test
[INFO] | +- org.mockito:mockito-core:jar:5.7.0:test
[INFO] | | +- net.bytebuddy:byte-buddy-agent:jar:1.14.10:test
[INFO] | | \- org.objenesis:objenesis:jar:3.3:test
[INFO] | +- org.mockito:mockito-junit-jupiter:jar:5.7.0:test
[INFO] | +- org.skyscreamer:jsonassert:jar:1.5.1:test
[INFO] | | \- com.vaadin.external.google:android-json:jar:0.0.20131108.vaadin1:test
[INFO] | +- org.springframework:spring-core:jar:6.1.2:compile
[INFO] | | \- org.springframework:spring-jcl:jar:6.1.2:compile
[INFO] | +- org.springframework:spring-test:jar:6.1.2:test
[INFO] | \- org.xmlunit:xmlunit-core:jar:2.9.1:test
[INFO] +- com.baomidou:mybatis-plus-boot-starter:jar:3.5.5:compile
[INFO] | +- com.baomidou:mybatis-plus:jar:3.5.5:compile
[INFO] | | +- com.baomidou:mybatis-plus-core:jar:3.5.5:compile
[INFO] | | +- com.baomidou:mybatis-plus-annotation:jar:3.5.5:compile
[INFO] | | +- com.baomidou:mybatis-plus-extension:jar:3.5.5:compile
[INFO] | | +- org.mybatis:mybatis:jar:3.5.15:compile
[INFO] | | \- com.github.jsqlparser:jsqlparser:jar:4.6:compile
[INFO] | +- com.baomidou:mybatis-plus-spring-boot-autoconfigure:jar:3.5.5:compile
[INFO] | +- org.springframework.boot:spring-boot-autoconfigure:jar:3.2.1:compile
[INFO] | \- org.springframework.boot:spring-boot-starter-jdbc:jar:3.2.1:compile
[INFO] | +- com.zaxxer:HikariCP:jar:5.0.1:compile
[INFO] | \- org.springframework:spring-jdbc:jar:6.1.2:compile
[INFO] +- org.mybatis:mybatis-spring:jar:3.0.3:compile
[INFO] +- org.springframework.boot:spring-boot-starter-data-redis:jar:3.2.1:compile
[INFO] | +- io.lettuce:lettuce-core:jar:6.3.0.RELEASE:compile
[INFO] | | \- io.projectreactor:reactor-core:jar:3.6.1:compile
[INFO] | | \- org.reactivestreams:reactive-streams:jar:1.0.4:compile
[INFO] | \- org.springframework.data:spring-data-redis:jar:3.2.1:compile
[INFO] | +- org.springframework.data:spring-data-keyvalue:jar:3.2.1:compile
[INFO] | \- org.springframework:spring-oxm:jar:6.1.2:compile
[INFO] +- org.apache.commons:commons-pool2:jar:2.12.0:compile
[INFO] +- org.springframework.boot:spring-boot-starter-validation:jar:3.2.1:compile
[INFO] | +- org.apache.tomcat.embed:tomcat-embed-el:jar:10.1.17:compile
[INFO] | \- org.hibernate.validator:hibernate-validator:jar:8.0.1.Final:compile
[INFO] | +- jakarta.validation:jakarta.validation-api:jar:3.0.2:compile
[INFO] | +- org.jboss.logging:jboss-logging:jar:3.5.3.Final:compile
[INFO] | \- com.fasterxml:classmate:jar:1.6.0:compile
[INFO] +- com.fasterxml.jackson.core:jackson-databind:jar:2.15.3:compile
[INFO] | +- com.fasterxml.jackson.core:jackson-annotations:jar:2.15.3:compile
[INFO] | \- com.fasterxml.jackson.core:jackson-core:jar:2.15.3:compile
[INFO] +- org.springframework.boot:spring-boot-starter-security:jar:3.2.1:compile
[INFO] | +- org.springframework:spring-aop:jar:6.1.2:compile
[INFO] | +- org.springframework.security:spring-security-config:jar:6.2.1:compile
[INFO] | | \- org.springframework.security:spring-security-core:jar:6.2.1:compile
[INFO] | \- org.springframework.security:spring-security-web:jar:6.2.1:compile
[INFO] +- io.jsonwebtoken:jjwt-api:jar:0.11.5:compile
[INFO] +- io.jsonwebtoken:jjwt-impl:jar:0.11.5:runtime
[INFO] +- io.jsonwebtoken:jjwt-jackson:jar:0.11.5:runtime
[INFO] +- com.aliyun:dysmsapi20170525:jar:3.0.0:compile
[INFO] | +- com.aliyun:tea-util:jar:0.2.22:compile
[INFO] | +- com.aliyun:endpoint-util:jar:0.0.7:compile
[INFO] | +- com.aliyun:tea:jar:1.2.7:compile
[INFO] | | \- org.jacoco:org.jacoco.agent:jar:runtime:0.8.4:compile
[INFO] | +- com.aliyun:tea-openapi:jar:0.3.4:compile
[INFO] | | +- com.aliyun:credentials-java:jar:0.3.3:compile
[INFO] | | | +- org.ini4j:ini4j:jar:0.5.4:compile
[INFO] | | | +- com.sun.xml.bind:jaxb-core:jar:4.0.4:compile
[INFO] | | | \- com.sun.xml.bind:jaxb-impl:jar:4.0.4:compile
[INFO] | | +- com.aliyun:alibabacloud-gateway-spi:jar:0.0.1:compile
[INFO] | | \- com.aliyun:tea-xml:jar:0.1.6:compile
[INFO] | | \- org.dom4j:dom4j:jar:2.0.3:compile
[INFO] | \- com.aliyun:openapiutil:jar:0.2.1:compile
[INFO] | \- org.bouncycastle:bcpkix-jdk15on:jar:1.70:compile
[INFO] | +- org.bouncycastle:bcprov-jdk15on:jar:1.70:compile
[INFO] | \- org.bouncycastle:bcutil-jdk15on:jar:1.70:compile
[INFO] +- org.springframework.boot:spring-boot-starter-mail:jar:3.2.1:compile
[INFO] | +- org.springframework:spring-context-support:jar:6.1.2:compile
[INFO] | \- org.eclipse.angus:jakarta.mail:jar:2.0.2:compile
[INFO] | \- org.eclipse.angus:angus-activation:jar:2.0.1:runtime
[INFO] +- org.springframework.boot:spring-boot-starter-thymeleaf:jar:3.2.1:compile
[INFO] | \- org.thymeleaf:thymeleaf-spring6:jar:3.1.2.RELEASE:compile
[INFO] | \- org.thymeleaf:thymeleaf:jar:3.1.2.RELEASE:compile
[INFO] | +- org.attoparser:attoparser:jar:2.0.7.RELEASE:compile
[INFO] | \- org.unbescape:unbescape:jar:1.1.6.RELEASE:compile
[INFO] +- org.apache.commons:commons-lang3:jar:3.13.0:compile
[INFO] +- cn.hutool:hutool-all:jar:5.8.18:compile
[INFO] +- com.fasterxml.jackson.datatype:jackson-datatype-jsr310:jar:2.15.3:compile
[INFO] +- com.alibaba:fastjson:jar:2.0.53:compile
[INFO] | \- com.alibaba.fastjson2:fastjson2-extension:jar:2.0.53:compile
[INFO] | \- com.alibaba.fastjson2:fastjson2:jar:2.0.53:compile
[INFO] +- org.springframework.boot:spring-boot-starter-aop:jar:3.2.1:compile
[INFO] +- org.aspectj:aspectjweaver:jar:1.9.21:compile
[INFO] +- io.minio:minio:jar:8.5.7:compile
[INFO] | +- com.carrotsearch.thirdparty:simple-xml-safe:jar:2.7.1:compile
[INFO] | +- com.google.guava:guava:jar:32.1.3-jre:compile
[INFO] | | +- com.google.guava:failureaccess:jar:1.0.1:compile
[INFO] | | +- com.google.guava:listenablefuture:jar:9999.0-empty-to-avoid-conflict-with-guava:compile
[INFO] | | \- com.google.j2objc:j2objc-annotations:jar:2.8:compile
[INFO] | +- org.bouncycastle:bcprov-jdk18on:jar:1.76:compile
[INFO] | +- org.apache.commons:commons-compress:jar:1.24.0:compile
[INFO] | \- org.xerial.snappy:snappy-java:jar:1.1.10.5:compile
[INFO] +- commons-io:commons-io:jar:2.18.0:compile
[INFO] +- ai.djl:api:jar:0.25.0:compile
[INFO] | +- com.google.code.gson:gson:jar:2.10.1:compile
[INFO] | +- net.java.dev.jna:jna:jar:5.13.0:compile
[INFO] | \- org.slf4j:slf4j-api:jar:2.0.9:compile
[INFO] +- ai.djl.pytorch:pytorch-engine:jar:0.25.0:compile
[INFO] +- ai.djl.pytorch:pytorch-model-zoo:jar:0.25.0:compile
[INFO] +- org.apache.hadoop:hadoop-client:jar:3.3.6:compile
[INFO] | +- org.apache.hadoop:hadoop-common:jar:3.3.6:compile
[INFO] | | +- org.apache.hadoop.thirdparty:hadoop-shaded-protobuf_3_7:jar:1.1.1:compile
[INFO] | | +- commons-net:commons-net:jar:3.9.0:compile
[INFO] | | +- commons-collections:commons-collections:jar:3.2.2:compile
[INFO] | | +- org.eclipse.jetty:jetty-servlet:jar:9.4.51.v20230217:compile
[INFO] | | | \- org.eclipse.jetty:jetty-security:jar:12.0.5:compile
[INFO] | | +- org.eclipse.jetty:jetty-webapp:jar:9.4.51.v20230217:compile
[INFO] | | | \- org.eclipse.jetty:jetty-xml:jar:12.0.5:compile
[INFO] | | +- javax.servlet.jsp:jsp-api:jar:2.1:runtime
[INFO] | | +- com.sun.jersey:jersey-servlet:jar:1.19.4:compile
[INFO] | | +- org.apache.commons:commons-configuration2:jar:2.8.0:compile
[INFO] | | +- org.apache.commons:commons-text:jar:1.10.0:compile
[INFO] | | +- org.apache.avro:avro:jar:1.7.7:compile
[INFO] | | | +- org.codehaus.jackson:jackson-core-asl:jar:1.9.13:compile
[INFO] | | | +- org.codehaus.jackson:jackson-mapper-asl:jar:1.9.13:compile
[INFO] | | | \- com.thoughtworks.paranamer:paranamer:jar:2.3:compile
[INFO] | | +- com.google.re2j:re2j:jar:1.1:compile
[INFO] | | +- org.apache.hadoop:hadoop-auth:jar:3.3.6:compile
[INFO] | | | +- com.nimbusds:nimbus-jose-jwt:jar:9.8.1:compile
[INFO] | | | | \- com.github.stephenc.jcip:jcip-annotations:jar:1.0-1:compile
[INFO] | | | +- org.apache.curator:curator-framework:jar:5.2.0:compile
[INFO] | | | \- org.apache.kerby:kerb-simplekdc:jar:1.0.1:compile
[INFO] | | | +- org.apache.kerby:kerb-client:jar:1.0.1:compile
[INFO] | | | | +- org.apache.kerby:kerby-config:jar:1.0.1:compile
[INFO] | | | | +- org.apache.kerby:kerb-common:jar:1.0.1:compile
[INFO] | | | | | \- org.apache.kerby:kerb-crypto:jar:1.0.1:compile
[INFO] | | | | +- org.apache.kerby:kerb-util:jar:1.0.1:compile
[INFO] | | | | \- org.apache.kerby:token-provider:jar:1.0.1:compile
[INFO] | | | \- org.apache.kerby:kerb-admin:jar:1.0.1:compile
[INFO] | | | +- org.apache.kerby:kerb-server:jar:1.0.1:compile
[INFO] | | | | \- org.apache.kerby:kerb-identity:jar:1.0.1:compile
[INFO] | | | \- org.apache.kerby:kerby-xdr:jar:1.0.1:compile
[INFO] | | +- org.apache.curator:curator-client:jar:5.2.0:compile
[INFO] | | +- org.apache.curator:curator-recipes:jar:5.2.0:compile
[INFO] | | +- io.dropwizard.metrics:metrics-core:jar:4.2.23:compile
[INFO] | | +- org.apache.kerby:kerb-core:jar:1.0.1:compile
[INFO] | | | \- org.apache.kerby:kerby-pkix:jar:1.0.1:compile
[INFO] | | | +- org.apache.kerby:kerby-asn1:jar:1.0.1:compile
[INFO] | | | \- org.apache.kerby:kerby-util:jar:1.0.1:compile
[INFO] | | +- org.codehaus.woodstox:stax2-api:jar:4.2.1:compile
[INFO] | | +- com.fasterxml.woodstox:woodstox-core:jar:5.4.0:compile
[INFO] | | \- dnsjava:dnsjava:jar:2.1.7:compile
[INFO] | +- org.apache.hadoop:hadoop-hdfs-client:jar:3.3.6:compile
[INFO] | +- org.apache.hadoop:hadoop-yarn-api:jar:3.3.6:compile
[INFO] | | \- javax.xml.bind:jaxb-api:jar:2.2.11:compile
[INFO] | +- org.apache.hadoop:hadoop-yarn-client:jar:3.3.6:compile
[INFO] | | +- org.eclipse.jetty.websocket:websocket-client:jar:9.4.51.v20230217:compile
[INFO] | | | +- org.eclipse.jetty:jetty-client:jar:12.0.5:compile
[INFO] | | | | \- org.eclipse.jetty:jetty-alpn-client:jar:12.0.5:compile
[INFO] | | | \- org.eclipse.jetty.websocket:websocket-common:jar:9.4.51.v20230217:compile
[INFO] | | | \- org.eclipse.jetty.websocket:websocket-api:jar:9.4.51.v20230217:compile
[INFO] | | \- org.jline:jline:jar:3.9.0:compile
[INFO] | +- org.apache.hadoop:hadoop-mapreduce-client-core:jar:3.3.6:compile
[INFO] | | \- org.apache.hadoop:hadoop-yarn-common:jar:3.3.6:compile
[INFO] | | +- com.sun.jersey:jersey-client:jar:1.19.4:compile
[INFO] | | \- com.fasterxml.jackson.module:jackson-module-jaxb-annotations:jar:2.15.3:compile
[INFO] | +- org.apache.hadoop:hadoop-mapreduce-client-jobclient:jar:3.3.6:compile
[INFO] | | \- org.apache.hadoop:hadoop-mapreduce-client-common:jar:3.3.6:compile
[INFO] | \- org.apache.hadoop:hadoop-annotations:jar:3.3.6:compile
[INFO] +- org.apache.hadoop:hadoop-hdfs:jar:3.3.6:compile
[INFO] | +- org.apache.hadoop.thirdparty:hadoop-shaded-guava:jar:1.1.1:compile
[INFO] | +- org.eclipse.jetty:jetty-server:jar:12.0.5:compile
[INFO] | | +- org.eclipse.jetty:jetty-http:jar:12.0.5:compile
[INFO] | | \- org.eclipse.jetty:jetty-io:jar:12.0.5:compile
[INFO] | +- org.eclipse.jetty:jetty-util:jar:12.0.5:compile
[INFO] | +- org.eclipse.jetty:jetty-util-ajax:jar:12.0.5:compile
[INFO] | +- com.sun.jersey:jersey-core:jar:1.19.4:compile
[INFO] | | \- javax.ws.rs:jsr311-api:jar:1.1.1:compile
[INFO] | +- com.sun.jersey:jersey-server:jar:1.19.4:compile
[INFO] | +- commons-cli:commons-cli:jar:1.2:compile
[INFO] | +- commons-codec:commons-codec:jar:1.16.0:compile
[INFO] | +- commons-logging:commons-logging:jar:1.1.3:compile
[INFO] | +- commons-daemon:commons-daemon:jar:1.0.13:compile
[INFO] | +- ch.qos.reload4j:reload4j:jar:1.2.22:compile
[INFO] | +- com.google.protobuf:protobuf-java:jar:2.5.0:compile
[INFO] | +- javax.servlet:javax.servlet-api:jar:3.1.0:compile
[INFO] | +- io.netty:netty:jar:3.10.6.Final:compile
[INFO] | \- org.fusesource.leveldbjni:leveldbjni-all:jar:1.8:compile
[INFO] +- org.springdoc:springdoc-openapi-starter-webmvc-ui:jar:2.3.0:compile
[INFO] | +- org.springdoc:springdoc-openapi-starter-webmvc-api:jar:2.3.0:compile
[INFO] | | \- org.springdoc:springdoc-openapi-starter-common:jar:2.3.0:compile
[INFO] | | \- io.swagger.core.v3:swagger-core-jakarta:jar:2.2.19:compile
[INFO] | | +- io.swagger.core.v3:swagger-annotations-jakarta:jar:2.2.19:compile
[INFO] | | \- io.swagger.core.v3:swagger-models-jakarta:jar:2.2.19:compile
[INFO] | \- org.webjars:swagger-ui:jar:5.10.3:compile
[INFO] +- com.arcsoft.face:arcsoft-sdk-face:jar:4.1.1.0:compile
[INFO] +- org.springframework.boot:spring-boot-starter-websocket:jar:3.2.1:compile
[INFO] | +- org.springframework:spring-messaging:jar:6.1.2:compile
[INFO] | \- org.springframework:spring-websocket:jar:6.1.2:compile
[INFO] +- io.netty:netty-all:jar:4.1.94.Final:compile
[INFO] | +- io.netty:netty-buffer:jar:4.1.104.Final:compile
[INFO] | +- io.netty:netty-codec:jar:4.1.104.Final:compile
[INFO] | +- io.netty:netty-codec-dns:jar:4.1.104.Final:compile
[INFO] | +- io.netty:netty-codec-haproxy:jar:4.1.104.Final:compile
[INFO] | +- io.netty:netty-codec-http:jar:4.1.104.Final:compile
[INFO] | +- io.netty:netty-codec-http2:jar:4.1.104.Final:compile
[INFO] | +- io.netty:netty-codec-memcache:jar:4.1.104.Final:compile
[INFO] | +- io.netty:netty-codec-mqtt:jar:4.1.104.Final:compile
[INFO] | +- io.netty:netty-codec-redis:jar:4.1.104.Final:compile
[INFO] | +- io.netty:netty-codec-smtp:jar:4.1.104.Final:compile
[INFO] | +- io.netty:netty-codec-socks:jar:4.1.104.Final:compile
[INFO] | +- io.netty:netty-codec-stomp:jar:4.1.104.Final:compile
[INFO] | +- io.netty:netty-codec-xml:jar:4.1.104.Final:compile
[INFO] | +- io.netty:netty-common:jar:4.1.104.Final:compile
[INFO] | +- io.netty:netty-handler:jar:4.1.104.Final:compile
[INFO] | +- io.netty:netty-transport-native-unix-common:jar:4.1.104.Final:compile
[INFO] | +- io.netty:netty-handler-proxy:jar:4.1.104.Final:compile
[INFO] | +- io.netty:netty-handler-ssl-ocsp:jar:4.1.104.Final:compile
[INFO] | +- io.netty:netty-resolver:jar:4.1.104.Final:compile
[INFO] | +- io.netty:netty-resolver-dns:jar:4.1.104.Final:compile
[INFO] | +- io.netty:netty-transport:jar:4.1.104.Final:compile
[INFO] | +- io.netty:netty-transport-rxtx:jar:4.1.104.Final:compile
[INFO] | +- io.netty:netty-transport-sctp:jar:4.1.104.Final:compile
[INFO] | +- io.netty:netty-transport-udt:jar:4.1.104.Final:compile
[INFO] | +- io.netty:netty-transport-classes-epoll:jar:4.1.104.Final:compile
[INFO] | +- io.netty:netty-transport-classes-kqueue:jar:4.1.104.Final:compile
[INFO] | +- io.netty:netty-resolver-dns-classes-macos:jar:4.1.104.Final:compile
[INFO] | +- io.netty:netty-transport-native-epoll:jar:linux-x86_64:4.1.104.Final:compile
[INFO] | +- io.netty:netty-transport-native-epoll:jar:linux-aarch_64:4.1.104.Final:runtime
[INFO] | +- io.netty:netty-transport-native-kqueue:jar:osx-x86_64:4.1.104.Final:compile
[INFO] | +- io.netty:netty-transport-native-kqueue:jar:osx-aarch_64:4.1.104.Final:runtime
[INFO] | +- io.netty:netty-resolver-dns-native-macos:jar:osx-x86_64:4.1.104.Final:runtime
[INFO] | \- io.netty:netty-resolver-dns-native-macos:jar:osx-aarch_64:4.1.104.Final:runtime
[INFO] +- cn.easy-es:easy-es-boot-starter:jar:1.1.1:compile
[INFO] | \- cn.easy-es:easy-es-core:jar:1.1.1:compile
[INFO] | \- cn.easy-es:easy-es-extension:jar:1.1.1:compile
[INFO] | +- cn.easy-es:easy-es-annotation:jar:1.1.1:compile
[INFO] | \- cn.easy-es:easy-es-common:jar:1.1.1:compile
[INFO] +- org.elasticsearch.client:elasticsearch-rest-high-level-client:jar:7.14.0:compile
[INFO] | +- org.elasticsearch.plugin:mapper-extras-client:jar:7.14.0:compile
[INFO] | +- org.elasticsearch.plugin:parent-join-client:jar:7.14.0:compile
[INFO] | +- org.elasticsearch.plugin:aggs-matrix-stats-client:jar:7.14.0:compile
[INFO] | +- org.elasticsearch.plugin:rank-eval-client:jar:7.14.0:compile
[INFO] | \- org.elasticsearch.plugin:lang-mustache-client:jar:7.14.0:compile
[INFO] | \- com.github.spullara.mustache.java:compiler:jar:0.9.6:compile
[INFO] +- org.elasticsearch:elasticsearch:jar:7.14.0:compile
[INFO] | +- org.elasticsearch:elasticsearch-core:jar:7.14.0:compile
[INFO] | +- org.elasticsearch:elasticsearch-secure-sm:jar:7.14.0:compile
[INFO] | +- org.elasticsearch:elasticsearch-x-content:jar:7.14.0:compile
[INFO] | | +- com.fasterxml.jackson.dataformat:jackson-dataformat-smile:jar:2.15.3:compile
[INFO] | | +- com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:jar:2.15.3:compile
[INFO] | | \- com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:jar:2.15.3:compile
[INFO] | +- org.elasticsearch:elasticsearch-geo:jar:7.14.0:compile
[INFO] | +- org.apache.lucene:lucene-core:jar:8.9.0:compile
[INFO] | +- org.apache.lucene:lucene-analyzers-common:jar:8.9.0:compile
[INFO] | +- org.apache.lucene:lucene-backward-codecs:jar:8.9.0:compile
[INFO] | +- org.apache.lucene:lucene-grouping:jar:8.9.0:compile
[INFO] | +- org.apache.lucene:lucene-highlighter:jar:8.9.0:compile
[INFO] | +- org.apache.lucene:lucene-join:jar:8.9.0:compile
[INFO] | +- org.apache.lucene:lucene-memory:jar:8.9.0:compile
[INFO] | +- org.apache.lucene:lucene-misc:jar:8.9.0:compile
[INFO] | +- org.apache.lucene:lucene-queries:jar:8.9.0:compile
[INFO] | +- org.apache.lucene:lucene-queryparser:jar:8.9.0:compile
[INFO] | +- org.apache.lucene:lucene-sandbox:jar:8.9.0:compile
[INFO] | +- org.apache.lucene:lucene-spatial-extras:jar:8.9.0:compile
[INFO] | +- org.apache.lucene:lucene-spatial3d:jar:8.9.0:compile
[INFO] | +- org.apache.lucene:lucene-suggest:jar:8.9.0:compile
[INFO] | +- org.elasticsearch:elasticsearch-cli:jar:7.14.0:compile
[INFO] | | \- net.sf.jopt-simple:jopt-simple:jar:5.0.2:compile
[INFO] | +- com.carrotsearch:hppc:jar:0.8.1:compile
[INFO] | +- org.lz4:lz4-java:jar:1.8.0:compile
[INFO] | +- joda-time:joda-time:jar:2.10.10:compile
[INFO] | +- com.tdunning:t-digest:jar:3.2:compile
[INFO] | +- org.hdrhistogram:HdrHistogram:jar:2.1.9:compile
[INFO] | +- org.elasticsearch:jna:jar:5.7.0-1:compile
[INFO] | \- org.elasticsearch:elasticsearch-plugin-classloader:jar:7.14.0:runtime
[INFO] +- org.elasticsearch.client:elasticsearch-rest-client:jar:7.14.0:compile
[INFO] | +- org.apache.httpcomponents:httpclient:jar:4.5.10:compile
[INFO] | +- org.apache.httpcomponents:httpcore:jar:4.4.16:compile
[INFO] | +- org.apache.httpcomponents:httpasyncclient:jar:4.1.5:compile
[INFO] | \- org.apache.httpcomponents:httpcore-nio:jar:4.4.16:compile
[INFO] +- com.squareup.okhttp3:okhttp:jar:4.12.0:compile
[INFO] | +- com.squareup.okio:okio:jar:3.6.0:compile
[INFO] | | \- com.squareup.okio:okio-jvm:jar:3.6.0:compile
[INFO] | | \- org.jetbrains.kotlin:kotlin-stdlib-common:jar:1.9.21:compile
[INFO] | \- org.jetbrains.kotlin:kotlin-stdlib-jdk8:jar:1.9.21:compile
[INFO] | +- org.jetbrains.kotlin:kotlin-stdlib:jar:1.9.21:compile
[INFO] | | \- org.jetbrains:annotations:jar:13.0:compile
[INFO] | \- org.jetbrains.kotlin:kotlin-stdlib-jdk7:jar:1.9.21:compile
[INFO] +- com.mongoplus:mongo-plus-boot-starter:jar:2.1.6.1:compile
[INFO] | +- com.mongoplus:mongo-plus-core:jar:2.1.6:compile
[INFO] | | \- com.mongoplus:mongo-plus-annotation:jar:2.1.6:compile
[INFO] | \- org.springframework:spring-tx:jar:6.1.2:compile
[INFO] +- com.mongoplus:mongo-plus-solon-plugin:jar:2.1.6.1:compile
[INFO] +- org.springframework.boot:spring-boot-starter-data-mongodb:jar:3.2.1:compile
[INFO] | +- org.mongodb:mongodb-driver-sync:jar:4.11.1:compile
[INFO] | | +- org.mongodb:bson:jar:4.11.1:compile
[INFO] | | \- org.mongodb:mongodb-driver-core:jar:4.11.1:compile
[INFO] | | \- org.mongodb:bson-record-codec:jar:4.11.1:runtime
[INFO] | \- org.springframework.data:spring-data-mongodb:jar:4.2.1:compile
[INFO] | \- org.springframework.data:spring-data-commons:jar:3.2.1:compile
[INFO] +- org.springframework.kafka:spring-kafka:jar:3.0.9:compile
[INFO] | +- org.springframework:spring-context:jar:6.1.2:compile
[INFO] | +- org.springframework.retry:spring-retry:jar:2.0.5:compile
[INFO] | +- org.apache.kafka:kafka-clients:jar:3.6.1:compile
[INFO] | | \- com.github.luben:zstd-jni:jar:1.5.5-1:runtime
[INFO] | +- io.micrometer:micrometer-observation:jar:1.12.1:compile
[INFO] | | \- io.micrometer:micrometer-commons:jar:1.12.1:compile
[INFO] | \- com.google.code.findbugs:jsr305:jar:3.0.2:compile
[INFO] +- org.codehaus.janino:janino:jar:3.1.11:compile
[INFO] | \- org.codehaus.janino:commons-compiler:jar:3.1.11:compile
[INFO] +- com.github.ben-manes.caffeine:caffeine:jar:3.1.8:compile
[INFO] | +- org.checkerframework:checker-qual:jar:3.37.0:compile
[INFO] | \- com.google.errorprone:error_prone_annotations:jar:2.21.1:compile
[INFO] +- net.logstash.logback:logstash-logback-encoder:jar:6.6:compile
[INFO] +- org.springframework.cloud:spring-cloud-starter-openfeign:jar:4.1.2:compile
[INFO] | +- org.springframework.cloud:spring-cloud-starter:jar:4.1.3:compile
[INFO] | | +- org.springframework.cloud:spring-cloud-context:jar:4.1.3:compile
[INFO] | | \- org.springframework.security:spring-security-rsa:jar:1.1.3:compile
[INFO] | +- org.springframework.cloud:spring-cloud-openfeign-core:jar:4.1.2:compile
[INFO] | | +- io.github.openfeign.form:feign-form-spring:jar:3.8.0:compile
[INFO] | | | \- io.github.openfeign.form:feign-form:jar:3.8.0:compile
[INFO] | | \- commons-fileupload:commons-fileupload:jar:1.5:compile
[INFO] | +- org.springframework.cloud:spring-cloud-commons:jar:4.1.3:compile
[INFO] | | \- org.springframework.security:spring-security-crypto:jar:6.2.1:compile
[INFO] | +- io.github.openfeign:feign-core:jar:13.2.1:compile
[INFO] | \- io.github.openfeign:feign-slf4j:jar:13.2.1:compile
[INFO] +- com.xuxueli:xxl-job-core:jar:2.4.0:compile
[INFO] | \- org.apache.groovy:groovy:jar:4.0.16:compile
[INFO] +- com.github.docker-java:docker-java:jar:3.2.13:compile
[INFO] | +- com.github.docker-java:docker-java-core:jar:3.2.13:compile
[INFO] | | \- com.github.docker-java:docker-java-api:jar:3.2.13:compile
[INFO] | +- com.github.docker-java:docker-java-transport-jersey:jar:3.2.13:compile
[INFO] | | +- com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider:jar:2.15.3:compile
[INFO] | | | \- com.fasterxml.jackson.jaxrs:jackson-jaxrs-base:jar:2.15.3:compile
[INFO] | | +- org.glassfish.jersey.connectors:jersey-apache-connector:jar:3.1.5:compile
[INFO] | | | +- org.glassfish.jersey.core:jersey-common:jar:3.1.5:compile
[INFO] | | | | \- org.glassfish.hk2:osgi-resource-locator:jar:1.0.3:compile
[INFO] | | | \- jakarta.ws.rs:jakarta.ws.rs-api:jar:3.1.0:compile
[INFO] | | +- org.glassfish.jersey.core:jersey-client:jar:3.1.5:compile
[INFO] | | | \- jakarta.inject:jakarta.inject-api:jar:2.0.1:compile
[INFO] | | +- org.glassfish.jersey.inject:jersey-hk2:jar:3.1.5:compile
[INFO] | | | \- org.glassfish.hk2:hk2-locator:jar:3.0.5:compile
[INFO] | | | +- org.glassfish.hk2.external:aopalliance-repackaged:jar:3.0.5:compile
[INFO] | | | +- org.glassfish.hk2:hk2-api:jar:3.0.5:compile
[INFO] | | | \- org.glassfish.hk2:hk2-utils:jar:3.0.5:compile
[INFO] | | +- com.kohlschutter.junixsocket:junixsocket-common:jar:2.3.2:compile
[INFO] | | \- com.kohlschutter.junixsocket:junixsocket-native-common:jar:2.3.2:compile
[INFO] | +- com.github.docker-java:docker-java-transport-netty:jar:3.2.13:compile
[INFO] | \- org.slf4j:jcl-over-slf4j:jar:2.0.9:compile
[INFO] +- com.github.docker-java:docker-java-transport-httpclient5:jar:3.2.13:compile
[INFO] | \- com.github.docker-java:docker-java-transport:jar:3.2.13:compile
[INFO] +- org.apache.httpcomponents.client5:httpclient5:jar:5.2:compile
[INFO] | \- org.apache.httpcomponents.core5:httpcore5-h2:jar:5.2.4:compile
[INFO] +- org.apache.httpcomponents.core5:httpcore5:jar:5.2:compile
[INFO] +- org.reflections:reflections:jar:0.10.2:compile
[INFO] | \- org.javassist:javassist:jar:3.28.0-GA:compile
[INFO] +- io.swagger.parser.v3:swagger-parser:jar:2.1.25:compile
[INFO] | +- io.swagger.parser.v3:swagger-parser-v2-converter:jar:2.1.25:compile
[INFO] | | +- io.swagger:swagger-core:jar:1.6.15:compile
[INFO] | | | \- io.swagger:swagger-models:jar:1.6.15:compile
[INFO] | | | \- io.swagger:swagger-annotations:jar:1.6.15:compile
[INFO] | | +- io.swagger:swagger-parser:jar:1.0.73:compile
[INFO] | | | \- io.swagger:swagger-parser-safe-url-resolver:jar:1.0.73:compile
[INFO] | | +- io.swagger:swagger-compat-spec-parser:jar:1.0.73:compile
[INFO] | | | +- com.github.java-json-tools:json-schema-validator:jar:2.2.14:compile
[INFO] | | | | +- com.github.java-json-tools:jackson-coreutils-equivalence:jar:1.0:compile
[INFO] | | | | +- com.github.java-json-tools:json-schema-core:jar:1.2.14:compile
[INFO] | | | | | +- com.github.java-json-tools:uri-template:jar:0.10:compile
[INFO] | | | | | \- org.mozilla:rhino:jar:1.7.7.2:compile
[INFO] | | | | \- com.googlecode.libphonenumber:libphonenumber:jar:8.11.1:compile
[INFO] | | | \- com.github.java-json-tools:json-patch:jar:1.13:compile
[INFO] | | | +- com.github.java-json-tools:msg-simple:jar:1.2:compile
[INFO] | | | | \- com.github.java-json-tools:btf:jar:1.3:compile
[INFO] | | | \- com.github.java-json-tools:jackson-coreutils:jar:2.0:compile
[INFO] | | +- io.swagger.core.v3:swagger-models:jar:2.2.28:compile
[INFO] | | \- io.swagger.parser.v3:swagger-parser-core:jar:2.1.25:compile
[INFO] | +- io.swagger.parser.v3:swagger-parser-v3:jar:2.1.25:compile
[INFO] | | +- io.swagger.core.v3:swagger-core:jar:2.2.28:compile
[INFO] | | | \- io.swagger.core.v3:swagger-annotations:jar:2.2.28:compile
[INFO] | | \- io.swagger.parser.v3:swagger-parser-safe-url-resolver:jar:2.1.25:compile
[INFO] | \- org.yaml:snakeyaml:jar:2.2:compile
[INFO] +- org.apache.poi:poi-ooxml:jar:5.2.3:compile
[INFO] | +- org.apache.poi:poi:jar:5.2.3:compile
[INFO] | | \- com.zaxxer:SparseBitSet:jar:1.2:compile
[INFO] | +- org.apache.poi:poi-ooxml-lite:jar:5.2.3:compile
[INFO] | +- org.apache.xmlbeans:xmlbeans:jar:5.1.1:compile
[INFO] | | \- xml-apis:xml-apis:jar:1.4.01:compile
[INFO] | +- com.github.virtuald:curvesapi:jar:1.07:compile
[INFO] | \- org.apache.commons:commons-collections4:jar:4.4:compile
[INFO] +- org.apache.poi:poi-scratchpad:jar:5.2.3:compile
[INFO] | \- org.apache.commons:commons-math3:jar:3.6.1:compile
[INFO] +- net.sf.dozer:dozer:jar:5.5.1:compile
[INFO] | \- commons-beanutils:commons-beanutils:jar:1.9.1:compile
[INFO] \- dev.langchain4j:langchain4j-community-dashscope-spring-boot-starter:jar:1.0.0-beta2:compile
[INFO] +- dev.langchain4j:langchain4j-community-dashscope:jar:1.0.0-beta2:compile
[INFO] | +- dev.langchain4j:langchain4j-core:jar:1.0.0-beta2:compile
[INFO] | | \- org.jspecify:jspecify:jar:1.0.0:compile
[INFO] | \- com.alibaba:dashscope-sdk-java:jar:2.18.3:compile
[INFO] | +- io.reactivex.rxjava2:rxjava:jar:2.2.21:compile
[INFO] | +- com.squareup.okhttp3:logging-interceptor:jar:4.12.0:compile
[INFO] | +- com.squareup.okhttp3:okhttp-sse:jar:4.12.0:compile
[INFO] | \- com.github.victools:jsonschema-generator:jar:4.31.1:compile
[INFO] \- ch.qos.logback:logback-classic:jar:1.4.14:compile
[INFO] \- ch.qos.logback:logback-core:jar:1.4.14:compile
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.098 s
[INFO] Finished at: 2025-03-20T23:57:43+08:00
[INFO] ------------------------------------------------------------------------

View File

@ -1,11 +0,0 @@
version: '3'
services:
srs:
image: ossrs/srs:4
ports:
- "1935:1935" # RTMP
- "8080:8080" # HTTP-FLV
- "8088:8088" # HLS
volumes:
- ./conf/srs.conf:/usr/local/srs/conf/srs.conf
- ./recordings:/usr/local/srs/recordings

View File

@ -1,378 +0,0 @@
\# 视频模块接口文档
\## 基础信息
- 基础路径: `/api/videos`
- 请求头: 需要携带 `Authorization: Bearer {token}` (除了获取视频列表和视频详情)
\## 接口列表
\### 1. 上传视频
POST /api/videos/upload
// 请求参数 (multipart/form-data)
{
file: File, // 视频文件
title: string, // 视频标题
description: string, // 视频描述
tags?: string // 视频标签,可选,多个标签用逗号分隔
}
// 响应
{
code: number, // 200表示成功
message: string, // 响应消息
data: {
id: number, // 视频ID
title: string, // 视频标题
description: string, // 视频描述
url: string, // 视频URL
coverUrl: string, // 封面URL
duration: number, // 视频时长(秒)
size: number, // 文件大小(字节)
status: string, // 状态DRAFT-草稿PUBLISHED-已发布DELETED-已删除
userId: number, // 上传用户ID
username: string, // 上传用户名
createdTime: string, // 创建时间
updatedTime: string, // 更新时间
viewCount: number, // 观看次数
likeCount: number, // 点赞次数
tags: string, // 标签
hasLiked: boolean // 当前用户是否已点赞
}
}
\### 2. 获取视频列表
GET /api/videos?pageNum=1&pageSize=10&keyword=xxx
// 请求参数 (query)
{
pageNum?: number, // 页码默认1
pageSize?: number, // 每页条数默认10
keyword?: string // 搜索关键词,可选
}
// 响应
{
code: number,
message: string,
data: {
records: Array<{ // 视频列表
id: number,
title: string,
description: string,
url: string,
coverUrl: string,
duration: number,
size: number,
status: string,
userId: number,
username: string,
createdTime: string,
updatedTime: string,
viewCount: number,
likeCount: number,
tags: string,
hasLiked: boolean
}>,
total: number, // 总记录数
size: number, // 每页条数
current: number, // 当前页码
pages: number // 总页数
}
}
\### 3. 获取视频详情
GET /api/videos/{id}
// 响应
{
code: number,
message: string,
data: {
id: number,
title: string,
description: string,
url: string,
coverUrl: string,
duration: number,
size: number,
status: string,
userId: number,
username: string,
createdTime: string,
updatedTime: string,
viewCount: number,
likeCount: number,
tags: string,
hasLiked: boolean
}
}
\### 4. 更新视频信息
PUT /api/videos/{id}
// 请求体
{
title: string,
description: string,
tags?: string
}
// 响应
{
code: number,
message: string,
data: VideoDTO // 同上面的视频详情
}
\### 5. 删除视频
DELETE /api/videos/{id}
// 响应
{
code: number,
message: string,
data: null
}
\### 6. 视频点赞/取消点赞
POST /api/videos/{id}/like
// 响应
{
code: number,
message: string,
data: null
}
\### 7. 增加观看次数
POST /api/videos/{id}/view
// 响应
{
code: number,
message: string,
data: null
}
\### 8. 获取推荐视频
GET /api/videos/recommend?limit=10
// 请求参数 (query)
{
limit?: number // 返回数量默认10
}
// 响应
{
code: number,
message: string,
data: Array<VideoDTO> // 视频列表
}
\### 9. 获取相似视频
GET /api/videos/{id}/similar?limit=10
// 请求参数 (query)
{
limit?: number // 返回数量默认10
}
// 响应
{
code: number,
message: string,
data: Array<VideoDTO> // 视频列表
}
\## 错误码说明
{
200: "操作成功",
400: "请求参数错误",
401: "未登录或token已过期",
403: "无权限执行此操作",
404: "资源不存在",
500: "服务器内部错误"
}
\## 数据结构
\### VideoDTO
{
id: number; // 视频ID
title: string; // 视频标题
description: string; // 视频描述
url: string; // 视频URL
coverUrl: string; // 封面URL
duration: number; // 视频时长(秒)
size: number; // 文件大小(字节)
status: string; // 状态DRAFT-草稿PUBLISHED-已发布DELETED-已删除
userId: number; // 上传用户ID
username: string; // 上传用户名
createdTime: string; // 创建时间
updatedTime: string; // 更新时间
viewCount: number; // 观看次数
likeCount: number; // 点赞次数
tags: string; // 标签,多个用逗号分隔
hasLiked: boolean; // 当前用户是否已点赞
}

View File

@ -1,218 +0,0 @@
# 视频模块接口文档
## 基础信息
- 基础路径: `/api/videos`
- 请求头: 需要携带 `Authorization: Bearer {token}` (除了获取视频列表和视频详情)
## 接口列表
### 1. 上传视频
POST /api/videos/upload
// 请求参数 (multipart/form-data)
{
file: File, // 视频文件
title: string, // 视频标题
description: string, // 视频描述
tags?: string // 视频标签,可选,多个标签用逗号分隔
}
// 响应
{
code: number, // 200表示成功
message: string, // 响应消息
data: {
id: number, // 视频ID
title: string, // 视频标题
description: string, // 视频描述
url: string, // 视频URL
coverUrl: string, // 封面URL
duration: number, // 视频时长(秒)
size: number, // 文件大小(字节)
status: string, // 状态DRAFT-草稿PUBLISHED-已发布DELETED-已删除
userId: number, // 上传用户ID
username: string, // 上传用户名
createdTime: string, // 创建时间
updatedTime: string, // 更新时间
viewCount: number, // 观看次数
likeCount: number, // 点赞次数
tags: string, // 标签
hasLiked: boolean // 当前用户是否已点赞
}
}
### 2. 获取视频列表
GET /api/videos?pageNum=1&pageSize=10&keyword=xxx
// 请求参数 (query)
{
pageNum?: number, // 页码默认1
pageSize?: number, // 每页条数默认10
keyword?: string // 搜索关键词,可选
}
// 响应
{
code: number,
message: string,
data: {
records: Array<{ // 视频列表
id: number,
title: string,
description: string,
url: string,
coverUrl: string,
duration: number,
size: number,
status: string,
userId: number,
username: string,
createdTime: string,
updatedTime: string,
viewCount: number,
likeCount: number,
tags: string,
hasLiked: boolean
}>,
total: number, // 总记录数
size: number, // 每页条数
current: number, // 当前页码
pages: number // 总页数
}
}
### 3. 获取视频详情
GET /api/videos/{id}
// 响应
{
code: number,
message: string,
data: {
id: number,
title: string,
description: string,
url: string,
coverUrl: string,
duration: number,
size: number,
status: string,
userId: number,
username: string,
createdTime: string,
updatedTime: string,
viewCount: number,
likeCount: number,
tags: string,
hasLiked: boolean
}
}
### 4. 更新视频信息
PUT /api/videos/{id}
// 请求体
{
title: string,
description: string,
tags?: string
}
// 响应
{
code: number,
message: string,
data: VideoDTO // 同上面的视频详情
}
### 5. 删除视频
DELETE /api/videos/{id}
// 响应
{
code: number,
message: string,
data: null
}
### 6. 视频点赞/取消点赞
POST /api/videos/{id}/like
// 响应
{
code: number,
message: string,
data: null
}
### 7. 增加观看次数
POST /api/videos/{id}/view
// 响应
{
code: number,
message: string,
data: null
}
### 8. 获取推荐视频
GET /api/videos/recommend?limit=10
// 请求参数 (query)
{
limit?: number // 返回数量默认10
}
// 响应
{
code: number,
message: string,
data: Array<VideoDTO> // 视频列表
}
### 9. 获取相似视频
GET /api/videos/{id}/similar?limit=10
// 请求参数 (query)
{
limit?: number // 返回数量默认10
}
// 响应
{
code: number,
message: string,
data: Array<VideoDTO> // 视频列表
}
## 错误码说明
{
200: "操作成功",
400: "请求参数错误",
401: "未登录或token已过期",
403: "无权限执行此操作",
404: "资源不存在",
500: "服务器内部错误"
}
## 数据结构
### VideoDTO
{
id: number; // 视频ID
title: string; // 视频标题
description: string; // 视频描述
url: string; // 视频URL
coverUrl: string; // 封面URL
duration: number; // 视频时长(秒)
size: number; // 文件大小(字节)
status: string; // 状态DRAFT-草稿PUBLISHED-已发布DELETED-已删除
userId: number; // 上传用户ID
username: string; // 上传用户名
createdTime: string; // 创建时间
updatedTime: string; // 更新时间
viewCount: number; // 观看次数
likeCount: number; // 点赞次数
tags: string; // 标签,多个用逗号分隔
hasLiked: boolean; // 当前用户是否已点赞
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -1 +0,0 @@
Field client in cn.easyes.starter.register.MapperFactoryBean required a bean of type 'org.elasticsearch.client.RestHighLevelClient' that could not be found.

View File

@ -1,83 +0,0 @@
很久以前,人类以为只有神仙可以听到、看到千里之外的声音和景象,称之为千里眼和顺风耳,短短几百年里,人类的技术革命实现了质的飞跃。
1876 年,贝尔电话的发明,使人类可以听到千里之外声音的梦想终于成真。
此后,音视频技术不断发展。一方面,视频压缩技术从 H.261 到 H.264,再到现在的 H.265 及AV1视频压缩率越来越高音频压缩技术也从电话使用的 G.711、G.722 等窄带音频压缩技术,发展到现代的 AAC、OPUS 等宽带音频压缩技术。
另一方面,从中国 3G 网络正式商用开始,移动网络也发生了翻天覆地的变化。从 3G 到 4G ,再到马上要落地的 5G移动网络的带宽和质量越来越高为音视频数据传输打下了坚实的基础。
尤其是 2011 年 Google 推出 WebRTC 技术后,大大降低了音视频技术的门槛,可以在浏览器上快速开发出各种音视频应用。
如今在疫情的三年里视频会议远程会诊线上教学等需求将RTC技术推向高潮成为影响社会发展不可或缺的技术之一。
2023年从用3W法学习解构RTC开始笔者也开启了RTC分享之路。
1.什么是RTC?
RTC是Real-Time Communication的缩写译为实时通信目的是在设备端实时的转发音视频多媒体数据让用户能实时的进行音频和视频的会话即基于 IP 技术实现的实时交互的音视频通信技术。
具体涵义如下:
▪ 实时音视频数据传输的延迟要达到“实时”的标准也就是说延时要小于400ms能够实现低延时和无卡顿在正常通信过程中基本感受不到延迟的存在。
▪ 音视频:音视频数据传输,实时音视频通信通过服务端为中转节点,即时采集、渲染、处理、传输终端用户的图像、视频、音频数据进行,实现音视频流数据在终端节点间完成通信的过程。
▪ 实时音视频服务商一般以SDK的形式提供一整套解决方案。
2.为什么选择RTC技术
痛点:
基础音视频流程复杂且广泛:涵盖音视频收集、音视频压缩/解码、数据传输、终端适配、视频分发等系列环节,每个环节展开,都是复杂技术点。企业若想打造自主实时音视频方案,不仅要养一定规模软硬研发团队,还要花费一定时间沉淀,对于该企业来说,成本太高。
RTC技术优势
高音质
基于专有回声消除&降噪技术,可在嘈杂的环境下实现高音质通话,让对话里语音听得比较清晰,没有回声、啸叫的状况出现。
高画质
视频支持超高清晰度画面,一路视频提供多种分辨率,大屏幕可订阅更高分辨率提升视频通话体验,分分钟感受面对面交流感。
低延迟
全球通信节点支持支持实时性更好的UDP协议端到端延时低
抗弱网
自动增益控制&弱网丢包补偿技术 ,在丢包下保持音视频通话流畅。
当然基于一些特定行业的应用场景比如多人数直播时的高并发医疗行业的网闸透传成熟RTC服务商都有着良好的技术沉淀让越来越多的企业使用这项技术。
3.RTC有哪些使用场景
随着移动互联网的普及和智能终端设备的广泛应用,实时音视频正逐渐成为主流互动方式已在在线教育、社交娱乐、互动电商等热门领域得到广泛应用也赋能于更多创新场景如金融、政企服务、loT、医疗等帮助人们享受更便捷和更人性化的生活服务。
协同办公-视频会议
丰富的会议场景,轻松实现远程办公系统,打通团队沟通渠道,帮助企业充分挖掘和整合隐形资源。
典型应用Zoom腾讯会议
社交沟通-聊天室
支持 1v1 通话或群聊功能,频道内用户可自由发言,适用于语音通话、语音群聊、语音聊天室等场景。
典型应用微信语音通话YY语音
游戏&娱乐
玩家可通过语音/视频聊天推进游戏进程,团战作战、协同作战,及时分享游戏信息,一起连麦开黑,拉近玩家距离。
典型应用:网易游戏,虎牙直播
电商直播
通过IM+音视频拓展多样化电商直播玩法,增强购物体验,提升获客率,让购物更有趣,促进电商平台交易转化,实现全球购物零距离。
典型应用:淘宝直播
在线教育
视频面对面教学真实还原线下教学场景支持1V1教学、1对多教学、双师课堂等多种互动教学模式。
典型应用:小鹅通
远程医疗
基于IM及实时音视频RTC通过实施互动技术实现优质医疗资源和知识共享满足远程会诊、手术示教多种场景需求。
典型应用:微医
视频双录
根据金融监管要求为客户提供多场景的双录服务提供柜面双录、远程双录、移动双录、AI自助双录帮助金融机构实现业务回溯。
典型应用招商银行app

View File

@ -0,0 +1,84 @@
mysql57
docker run -p 3308:3306 --name mysql57 -v /d/05_docker/DockerData/mysql57/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=root -d mysql:5.7
mysql83
docker run ^
-p 3306:3306 ^
--name mysql8.3 ^
--privileged=true ^
-v /d/05_docker/DockerData/mysql83/log:/var/log/mysql ^
-v /d/05_docker/DockerData/mysql83/data:/var/lib/mysql ^
-v /d/05_docker/DockerData/mysql83/conf/my.cnf:/etc/mysql/my.cnf ^
-v /d/05_docker/DockerData/mysql83/sql:/var/sql ^
-e MYSQL_ROOT_PASSWORD=root ^
-d mysql:8.3.0
redis
docker run -d --name redis -p 6379:6379 redis --requirepass "123456"
minio
docker run -p 9000:9000 -p 9090:9090 ^
--name minio ^
-d ^
-e "MINIO_ROOT_USER=admin" ^
-e "MINIO_ROOT_PASSWORD=admin123456" ^
-v /d/05_docker/DockerData/minio/data:/data ^
-v /d/05_docker/DockerData/minio/config:/root/.minio ^
minio/minio:latest server /data ^
--console-address ":9090" -address ":9000"
es
docker network create es-net
docker run -d ^
--name es ^
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m" ^
-e "discovery.type=single-node" ^
-v es-data:/usr/share/elasticsearch/data ^
-v es-plugins:/usr/share/elasticsearch/plugins ^
--privileged ^
--network es-net ^
-p 9200:9200 ^
-p 9300:9300 ^
elasticsearch:7.12.1
docker cp es:/usr/share/elasticsearch/data D:\05_docker\DockerData\elasticsearch\
docker cp es:/usr/share/elasticsearch/plugins D:\05_docker\DockerData\elasticsearch\
docker run -d ^
--name es ^
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m" ^
-e "discovery.type=single-node" ^
-v /d/05_docker/DockerData/elasticsearch/data:/usr/share/elasticsearch/data ^
-v /d/05_docker/DockerData/elasticsearch/plugins:/usr/share/elasticsearch/plugins ^
--privileged ^
--network es-net ^
-p 9200:9200 ^
-p 9300:9300 ^
elasticsearch:7.12.1
Kibana
docker run -d ^
--name kibana ^
-e ELASTICSEARCH_HOSTS=http://es:9200 ^
--network es-net ^
-p 5601:5601 ^
kibana:7.12.1
logstash
docker run -it -d --name logstash ^
--network es-net ^
-p 5044:5044 ^
-v /d/05_docker/DockerData/logstash/pipeline/logstash.conf:/usr/share/logstash/pipeline/logstash.conf ^
-v /d/05_docker/DockerData/logstash/config/logstash.yml:/usr/share/logstash/config/logstash.yml ^
logstash:7.12.1

16
docs/初步.md Normal file
View File

@ -0,0 +1,16 @@
## 角色
管理员<br>
用户<br>
教师<br>
只要有多角色 就要设计 <br>
RBAC——基于角色权限的模型
角色表 用户表 用户角色表 权限表 角色权限表 <br>
要做全控制
表单 按钮 视图
merge和rebase

250
pom.xml
View File

@ -29,6 +29,8 @@
<properties>
<java.version>21</java.version>
<mybatis-plus.version>3.5.5</mybatis-plus.version>
<poi.version>3.9</poi.version>
<dozer.version>5.5.1</dozer.version>
</properties>
<dependencies>
<dependency>
@ -200,11 +202,23 @@
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>3.3.6</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-reload4j</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-hdfs</artifactId>
<version>3.3.6</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-reload4j</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Swagger UI -->
@ -264,6 +278,12 @@
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>7.14.0</version>
<exclusions>
<exclusion>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
@ -271,9 +291,239 @@
<version>7.14.0</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>com.agentsflex</groupId>-->
<!-- <artifactId>agents-flex-bom</artifactId>-->
<!-- <version>1.0.0-rc.3</version>-->
<!-- </dependency>-->
<!-- springboot -->
<dependency>
<groupId>com.mongoplus</groupId>
<artifactId>mongo-plus-boot-starter</artifactId>
<version>2.1.6.1</version>
</dependency>
<!-- solon -->
<dependency>
<groupId>com.mongoplus</groupId>
<artifactId>mongo-plus-solon-plugin</artifactId>
<version>2.1.6.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
<version>3.0.9</version>
</dependency>
<!-- Janino 依赖,用于 logback 条件判断 -->
<dependency>
<groupId>org.codehaus.janino</groupId>
<artifactId>janino</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version>
</dependency>
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>6.6</version> <!-- 请使用最新版本 -->
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>4.1.2</version>
</dependency>
<!--xxljob 许雪里 job-->
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>2.4.0</version>
</dependency>
<!-- docker java -->
<dependency>
<groupId>com.github.docker-java</groupId>
<artifactId>docker-java</artifactId>
<version>3.2.13</version>
</dependency>
<!-- docker java httpclient -->
<dependency>
<groupId>com.github.docker-java</groupId>
<artifactId>docker-java-transport-httpclient5</artifactId>
<version>3.2.13</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.2</version> <!-- 或者适合你项目的版本 -->
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.core5</groupId>
<artifactId>httpcore5</artifactId>
<version>5.2</version> <!-- 或者适合你项目的版本 -->
</dependency>
<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
<version>0.10.2</version>
</dependency>
<dependency>
<groupId>io.swagger.parser.v3</groupId>
<artifactId>swagger-parser</artifactId>
<version>2.1.25</version>
</dependency>
<!-- Apache POI -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.3</version>
<exclusions>
<exclusion>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-scratchpad</artifactId>
<version>5.2.3</version>
<exclusions>
<exclusion>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 图片处理库 -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.18.0</version>
</dependency>
<!-- poi office -->
<!-- <dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>${poi.version}</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>${poi.version}</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml-schemas</artifactId>
<version>${poi.version}</version>
</dependency>-->
<dependency>
<groupId>net.sf.dozer</groupId>
<artifactId>dozer</artifactId>
<version>${dozer.version}</version>
<exclusions>
<exclusion>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-dashscope-spring-boot-starter</artifactId>
<version>1.0.0-beta2</version>
<!-- <exclusions>
&lt;!&ndash; 排除新依赖中的 logback-classic &ndash;&gt;
<exclusion>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</exclusion>
</exclusions>-->
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>1.0.0-beta2</version>
</dependency>
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-parsers-standard-package</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>4.0.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>

11
readme.md Normal file
View File

@ -0,0 +1,11 @@
毕设后端
<br>
如果运行 暂时屏蔽face包
mvn dependency:tree > deps.txt
<br>
分析依赖
这次主要是日志的
slf4j-reload4
log4j-api

View File

@ -1,15 +1,26 @@
package com.guwan.backend;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@Slf4j
@SpringBootApplication
@MapperScan("com.guwan.backend.mapper")
@MapperScan({"com.guwan.backend.mapper", "com.guwan.backend.model.exam.mapper"})
@EnableFeignClients
public class BackendApplication {
public static void main(String[] args) {
SpringApplication.run(BackendApplication.class, args);
}
@PostConstruct
public void init() {
System.out.println("SLF4J Implementation: " + org.slf4j.LoggerFactory.getILoggerFactory().getClass().getName());
log.info("大爱仙尊: http://localhost:8084/daxz.html?id=1");
}
}

View File

@ -0,0 +1,100 @@
package com.guwan.backend.Handler;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.guwan.backend.service.CourseService;
import com.guwan.backend.service.WebSocketService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.io.IOException;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Component
public class CourseWebSocketHandler extends TextWebSocketHandler {
@Autowired
private WebSocketService webSocketService;
@Autowired
private CourseService courseService;
// 存储每个课程的在线用户
private static final Map<String, Set<WebSocketSession>> courseSessions = new ConcurrentHashMap<>();
@Override
public void afterConnectionEstablished(WebSocketSession session) {
log.info("Course detail WebSocket connection established: {}", session.getId());
String courseId = getCourseIdFromSession(session);
courseSessions.computeIfAbsent(courseId, k -> ConcurrentHashMap.newKeySet()).add(session);
broadcastStudentCount(courseId);
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
log.info("Course detail received message from {}: {}", session.getId(), message.getPayload());
try {
JsonNode jsonNode = new ObjectMapper().readTree(message.getPayload());
String type = jsonNode.get("type").asText();
String courseId = jsonNode.get("courseId").asText();
if ("JOIN_COURSE".equals(type)) {
courseSessions.computeIfAbsent(courseId, k -> ConcurrentHashMap.newKeySet()).add(session);
broadcastStudentCount(courseId);
} else if ("LEAVE_COURSE".equals(type)) {
courseSessions.getOrDefault(courseId, Collections.emptySet()).remove(session);
broadcastStudentCount(courseId);
}
} catch (Exception e) {
log.error("Error handling course detail message", e);
}
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
log.info("Course detail WebSocket connection closed: {}, status: {}", session.getId(), status);
String courseId = getCourseIdFromSession(session);
courseSessions.getOrDefault(courseId, Collections.emptySet()).remove(session);
broadcastStudentCount(courseId);
}
private String getCourseIdFromSession(WebSocketSession session) {
String path = session.getUri().getPath();
return path.substring(path.lastIndexOf('/') + 1);
}
private void broadcastStudentCount(String courseId) {
int count = courseSessions.getOrDefault(courseId, Collections.emptySet()).size();
// 更新持久化的学习人数
courseService.updateStudentCount(courseId, count);
// 广播给课程详情页
String message = String.format(
"{\"type\":\"STUDENT_COUNT_UPDATE\",\"courseId\":\"%s\",\"count\":%d}",
courseId, count
);
courseSessions.getOrDefault(courseId, Collections.emptySet())
.forEach(session -> {
try {
if (session.isOpen()) {
session.sendMessage(new TextMessage(message));
}
} catch (IOException e) {
log.error("Error broadcasting to course detail", e);
}
});
// 通过WebSocketService广播给首页
webSocketService.broadcastStudentCount(courseId, count);
}
}

View File

@ -0,0 +1,93 @@
package com.guwan.backend.Handler;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.guwan.backend.service.CourseService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Component
public class CoursesWebSocketHandler extends TextWebSocketHandler {
@Autowired
private CourseService courseService;
private static final Set<WebSocketSession> sessions = ConcurrentHashMap.newKeySet();
@Override
public void afterConnectionEstablished(WebSocketSession session) {
log.info("Home page WebSocket connection established: {}", session.getId());
sessions.add(session);
// 连接建立时发送所有课程的当前学习人数
sendAllCourseCounts(session);
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
log.info("Home page received message from {}: {}", session.getId(), message.getPayload());
try {
JsonNode jsonNode = new ObjectMapper().readTree(message.getPayload());
String type = jsonNode.get("type").asText();
if ("STUDENT_COUNT_UPDATE".equals(type)) {
// 转发消息给所有首页连接的客户端
broadcastMessage(message.getPayload());
}
} catch (Exception e) {
log.error("Error handling home page message", e);
}
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
log.info("Home page WebSocket connection closed: {}, status: {}", session.getId(), status);
sessions.remove(session);
}
public void broadcastMessage(String message) {
sessions.forEach(session -> {
try {
if (session.isOpen()) {
session.sendMessage(new TextMessage(message));
}
} catch (IOException e) {
log.error("Error broadcasting to home page", e);
}
});
}
private void sendAllCourseCounts(WebSocketSession session) {
try {
// 获取所有课程的学习人数
Map<String, Integer> allCounts = courseService.getAllCourseCounts();
// 发送每个课程的学习人数
allCounts.forEach((courseId, count) -> {
String message = String.format(
"{\"type\":\"STUDENT_COUNT_UPDATE\",\"courseId\":\"%s\",\"count\":%d}",
courseId, count
);
try {
if (session.isOpen()) {
session.sendMessage(new TextMessage(message));
}
} catch (IOException e) {
log.error("Error sending course count", e);
}
});
} catch (Exception e) {
log.error("Error sending all course counts", e);
}
}
}

View File

@ -1,14 +1,25 @@
package com.guwan.backend.Handler;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.core.toolkit.ReflectionKit;
import com.guwan.backend.annotation.RecoverIfDeleted;
import com.guwan.backend.util.ReflectUtil;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.SqlSession;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
System.out.println("metaObject = " + metaObject);
this.strictInsertFill(metaObject, "createdTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "updatedTime", LocalDateTime.class, LocalDateTime.now());
}
@ -16,5 +27,8 @@ public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "lastLoginTime", LocalDateTime.class, LocalDateTime.now());
this.strictUpdateFill(metaObject, "updatedTime", LocalDateTime.class, LocalDateTime.now());
}
}

View File

@ -0,0 +1,57 @@
package com.guwan.backend;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.lang.reflect.Method;
import java.util.*;
public class JudgeEngine {
// 假设用户代码已编译好类名为 Solution
public static void main(String[] args) throws Exception {
// 1. 取出测试用例
String inputJson = "{\"nums\": [2,7,11,15], \"target\": 9}";
String outputJson = "[0,1]";
List<String> paramList = Arrays.asList("nums", "target");
// 2. 反序列化参数
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> paramMap = mapper.readValue(inputJson, Map.class);
// 3. 构造参数数组 paramList 顺序
Object[] params = new Object[paramList.size()];
for (int i = 0; i < paramList.size(); i++) {
String key = paramList.get(i);
Object value = paramMap.get(key);
// 类型转换如果是数组需转为 int[]
if (key.equals("nums")) {
// value List<Integer>转为 int[]
List<Integer> list = (List<Integer>) value;
int[] arr = list.stream().mapToInt(Integer::intValue).toArray();
params[i] = arr;
} else if (key.equals("target")) {
params[i] = ((Number) value).intValue();
}
// 其他类型可按需扩展
}
// 4. 反射调用用户代码
Class<?> clazz = Class.forName("Solution");
Method method = clazz.getMethod("twoSum", int[].class, int.class);
Object instance = clazz.getDeclaredConstructor().newInstance();
Object result = method.invoke(instance, params);
// 5. 反序列化期望输出
int[] expected = mapper.readValue(outputJson, int[].class);
// 6. 比对结果
if (Arrays.equals((int[]) result, expected)) {
System.out.println("通过");
} else {
System.out.println("不通过");
System.out.println("期望: " + Arrays.toString(expected));
System.out.println("实际: " + Arrays.toString((int[]) result));
}
}
}

View File

@ -0,0 +1,52 @@
package com.guwan.backend;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.AutoDetectParser;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.parser.Parser;
import org.apache.tika.sax.BodyContentHandler;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.URL;
public class VideoDuration {
// public static void main(String[] args) {
// /* String preSignedUrl = "http://localhost:9000/videos/ffffad37-9804-4765-ae18-3f8dcda9bea8.mp4"; // Minio 预签名 URL
//
// try (InputStream stream = new URL(preSignedUrl).openStream()) {
// Metadata metadata = new Metadata();
// Parser parser = new AutoDetectParser();
// parser.parse(stream, new BodyContentHandler(), metadata, new ParseContext());
//
// String duration = metadata.get("duration");
// System.out.println("视频时长(毫秒): " + duration);
// } catch (Exception e) {
// e.printStackTrace();
// }*/
// int i = 0;
// while (i< 5){
// if (i ==3){
// i++;
// continue;
// }
// System.out.println(i);
// i++;
// }
//
// }
static boolean foo(char x) {
System.out.print(x);
return true;
}
public static void main(String[] args) {
int i = 0;
for (foo('A'); foo('B') && (i < 2); foo('C')) {
i++;
foo('D');
}
}
}

View File

@ -0,0 +1,12 @@
package com.guwan.backend.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD) // 这个注解应用于字段
@Retention(RetentionPolicy.RUNTIME) // 注解在运行时有效
public @interface CheckExistence {
String message() default "Field value already exists"; // 错误提示消息
}

View File

@ -0,0 +1,63 @@
package com.guwan.backend.annotation;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.IService;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.guwan.backend.annotation.CheckExistence;
import java.lang.reflect.Field;
public class CheckExistenceProcessor {
// 检查该字段值是否在数据库中存在
public static <T> boolean checkFieldExistence(T entity, IService<T> service) throws Exception {
// 获取实体类的所有字段
Field[] fields = entity.getClass().getDeclaredFields();
for (Field field : fields) {
// 如果字段上有 @CheckExistence 注解
if (field.isAnnotationPresent(CheckExistence.class)) {
field.setAccessible(true); // 设置字段可访问
Object value = field.get(entity); // 获取字段值
// 如果字段值为空不进行检查
if (value == null) {
continue;
}
// 获取字段的名称
String fieldName = field.getName();
// 使用 MyBatis-Plus 查询是否已存在该值
LambdaQueryWrapper<T> queryWrapper = new LambdaQueryWrapper<>();
// 将字段通过 SFunction 引入
SFunction<T, ?> fieldExpression = getFieldExpression(entity, fieldName);
if (fieldExpression != null) {
queryWrapper.eq(fieldExpression, value);
}
// 执行查询
long count = service.count(queryWrapper); // 使用 count 而不是 list避免不必要的数据加载
if (count > 0) {
// 如果数据库中已经存在该值返回 true
System.out.println("Error: " + fieldName + " value already exists.");
return true;
}
}
}
return false; // 没有找到重复值
}
// 通过反射获取字段的 SFunction 表达式
private static <T> SFunction<T, ?> getFieldExpression(T entity, String fieldName) {
try {
// 根据字段名生成 SFunction 表达式
return (SFunction<T, ?>) entity.getClass().getDeclaredField(fieldName).get(null);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
return null;
}
}
}

View File

@ -0,0 +1,13 @@
package com.guwan.backend.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface RecoverIfDeleted {
String value() default "deleted"; // 逻辑删除字段
String businessKey() default "uniqueKey"; // 业务唯一键
}

View File

@ -2,7 +2,7 @@ package com.guwan.backend.aspect;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.guwan.backend.annotation.OperationLog;
import com.guwan.backend.entity.SysLog;
import com.guwan.backend.pojo.entity.SysLog;
import com.guwan.backend.mapper.SysLogMapper;
import com.guwan.backend.security.CustomUserDetails;
import jakarta.servlet.http.HttpServletRequest;
@ -49,23 +49,15 @@ public class OperationLogAspect {
SysLog sysLog = new SysLog();
try {
// 执行方法
Object result = point.proceed();
// 设置状态为成功
sysLog.setStatus(1);
return result;
} catch (Exception e) {
// 记录异常信息
sysLog.setStatus(0);
sysLog.setErrorMsg(e.getMessage());
throw e;
} finally {
try {
// 记录日志
saveLog(point, operationLog, beginTime, sysLog);
} catch (Exception e) {
log.error("记录操作日志失败", e);
}
saveLog(point, operationLog, beginTime, sysLog);
}
}
@ -84,6 +76,7 @@ public class OperationLogAspect {
// 获取当前用户信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//Java 16+ 模式匹配写法
if (authentication != null && authentication.getPrincipal() instanceof CustomUserDetails userDetails) {
sysLog.setUserId(userDetails.getUserId());
sysLog.setUsername(userDetails.getUsername());
@ -122,13 +115,13 @@ public class OperationLogAspect {
*/
private String getIpAddress(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// 多个代理的情况第一个IP为客户端真实IP

View File

@ -0,0 +1,73 @@
package com.guwan.backend.aspect;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.util.Arrays;
import java.util.Objects;
@Aspect
@Component
@Slf4j
@RequiredArgsConstructor
public class RequestLogAspect {
private final ObjectMapper objectMapper;
/**
* 定义切点匹配所有controller包下的公共方法
*/
@Pointcut("execution(public * com.guwan.backend.controller..*.*(..))")
public void controllerLog() {}
/**
* 前置通知在方法执行前打印请求日志
*/
@Before("controllerLog()")
public void doBefore(JoinPoint joinPoint) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
return; // 非HTTP请求上下文不处理
}
HttpServletRequest request = attributes.getRequest();
// 记录请求信息
log.info("===================== Request Start =====================");
log.info("URL : {}", request.getRequestURL().toString());
log.info("HTTP Method : {}", request.getMethod());
log.info("Class Method : {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
log.info("IP : {}", request.getRemoteAddr());
// 记录参数尝试序列化为JSON
Object[] args = joinPoint.getArgs();
if (args != null && args.length > 0) {
try {
// 过滤掉HttpServletRequest和HttpServletResponse类型的参数
Object[] loggableArgs = Arrays.stream(args)
.filter(arg -> !(arg instanceof HttpServletRequest) && !(arg instanceof jakarta.servlet.http.HttpServletResponse))
.toArray();
if (loggableArgs.length > 0) {
log.info("Request Args : {}", objectMapper.writeValueAsString(loggableArgs));
} else {
log.info("Request Args : No Loggable Arguments");
}
} catch (JsonProcessingException e) {
log.warn("Failed to serialize request arguments to JSON: {}", e.getMessage());
log.info("Request Args : {}", Arrays.toString(args)); // 序列化失败则打印原始toString
}
} else {
log.info("Request Args : No Arguments");
}
log.info("===================== Request End =======================");
}
}

View File

@ -1,84 +0,0 @@
package com.guwan.backend.client;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
@Slf4j
@Component
@RequiredArgsConstructor
public class Go2RTCClient {
private final RestTemplate restTemplate;
@Value("${go2rtc.api.url}")
private String apiUrl;
/**
* 创建流
*/
public void createStream(String streamId, String sourceUrl) {
String url = apiUrl + "/api/streams/" + streamId;
StreamConfig config = new StreamConfig(sourceUrl);
try {
restTemplate.postForEntity(url, config, String.class);
} catch (Exception e) {
log.error("创建流失败: {}", e.getMessage());
throw new RuntimeException("创建流失败", e);
}
}
/**
* 删除流
*/
public void deleteStream(String streamId) {
String url = apiUrl + "/api/streams/" + streamId;
try {
restTemplate.delete(url);
} catch (Exception e) {
log.error("删除流失败: {}", e.getMessage());
throw new RuntimeException("删除流失败", e);
}
}
/**
* 获取WebRTC Offer
*/
public String getOffer(String streamId, String sdp) {
String url = apiUrl + "/api/stream/" + streamId + "/webrtc";
WebRTCRequest request = new WebRTCRequest(sdp);
try {
ResponseEntity<WebRTCResponse> response =
restTemplate.postForEntity(url, request, WebRTCResponse.class);
return response.getBody().getSdp();
} catch (Exception e) {
log.error("获取WebRTC Offer失败: {}", e.getMessage());
throw new RuntimeException("获取WebRTC Offer失败", e);
}
}
@Data
@AllArgsConstructor
static class StreamConfig {
private String input;
}
@Data
@AllArgsConstructor
static class WebRTCRequest {
private String sdp;
}
@Data
static class WebRTCResponse {
private String sdp;
}
}

View File

@ -0,0 +1,70 @@
package com.guwan.backend.client;
import cn.hutool.json.JSONObject;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.multipart.MultipartFile;
import java.io.BufferedInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
@FeignClient(name = "ttsService", url = "http://127.0.0.1:8534")
public interface SimpleTTSClient {
@PostMapping(value = "/v1/audio/speech")
byte[] saveAudio(String jsonInputString);
}
//public class SimpleAudioSaver {
//
// public static void main(String[] args) {
// String urlString = "http://localhost:8534/v1/audio/speech"; // API URL
// String jsonInputString = "{\"input\": \"想听个啥123\", \"voice\": \"zh-CN-XiaoxiaoNeural\", \"style\": \"\", \"rate\": 0, \"pitch\": 0}";
//
// saveAudio(urlString, jsonInputString);
// }
//
// public static void saveAudio(String urlString, String jsonInputString) {
// try {
// // 创建 URL 对象
// URL url = new URL(urlString);
// // 打开连接
// HttpURLConnection connection = (HttpURLConnection) url.openConnection();
// // 设置请求方式为 POST
// connection.setRequestMethod("POST");
// connection.setDoOutput(true);
// connection.setRequestProperty("Content-Type", "application/json");
// connection.setRequestProperty("Accept", "audio/mpeg");
//
// // 发送请求体
// connection.getOutputStream().write(jsonInputString.getBytes("UTF-8"));
//
// // 处理响应
// if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
// // 获取输入流
// InputStream inputStream = new BufferedInputStream(connection.getInputStream());
// // 保存文件
// try (FileOutputStream outputStream = new FileOutputStream("output.mp3")) {
// byte[] buffer = new byte[1024];
// int bytesRead;
// while ((bytesRead = inputStream.read(buffer)) != -1) {
// outputStream.write(buffer, 0, bytesRead);
// }
// }
// System.out.println("Audio saved as output.mp3");
// } else {
// System.err.println("Failed to get audio. Response code: " + connection.getResponseCode());
// }
// } catch (IOException e) {
// e.printStackTrace();
// }
// }
//}

View File

@ -1,83 +0,0 @@
//package com.guwan.backend.client;
//
//import lombok.Data;
//import lombok.RequiredArgsConstructor;
//import lombok.extern.slf4j.Slf4j;
//import org.springframework.beans.factory.annotation.Value;
//import org.springframework.http.ResponseEntity;
//import org.springframework.stereotype.Component;
//import org.springframework.web.client.RestTemplate;
//
//@Slf4j
//@Component
//@RequiredArgsConstructor
//public class SrsClient {
//
// private final RestTemplate restTemplate;
//
// @Value("${srs.server.url}")
// private String srsServerUrl;
//
// /**
// * 开始录制
// */
// public void startRecord(String streamKey, RecordConfig config) {
// String url = String.format("%s/api/v1/streams/%s/recording/start", srsServerUrl, streamKey);
// try {
// ResponseEntity<String> response = restTemplate.postForEntity(url, config, String.class);
// if (!response.getStatusCode().is2xxSuccessful()) {
// throw new RuntimeException("开始录制失败: " + response.getBody());
// }
// } catch (Exception e) {
// log.error("开始录制失败", e);
// throw new RuntimeException("开始录制失败", e);
// }
// }
//
// /**
// * 停止录制
// */
// public void stopRecord(String streamKey) {
// String url = String.format("%s/api/v1/streams/%s/recording/stop", srsServerUrl, streamKey);
// try {
// ResponseEntity<String> response = restTemplate.postForEntity(url, null, String.class);
// if (!response.getStatusCode().is2xxSuccessful()) {
// throw new RuntimeException("停止录制失败: " + response.getBody());
// }
// } catch (Exception e) {
// log.error("停止录制失败", e);
// throw new RuntimeException("停止录制失败", e);
// }
// }
//
// /**
// * 获取流信息
// */
// public StreamInfo getStreamInfo(String streamKey) {
// String url = String.format("%s/api/v1/streams/%s", srsServerUrl, streamKey);
// try {
// ResponseEntity<StreamInfo> response = restTemplate.getForEntity(url, StreamInfo.class);
// return response.getBody();
// } catch (Exception e) {
// log.error("获取流信息失败", e);
// throw new RuntimeException("获取流信息失败", e);
// }
// }
//
// @Data
// public static class RecordConfig {
// private String format = "flv"; // 录制格式
// private String filePath; // 文件路径
// }
//
// @Data
// public static class StreamInfo {
// private String streamId; // 流ID
// private String clientId; // 客户端ID
// private String ip; // 客户端IP
// private Long startTime; // 开始时间
// private String status; // 状态
// private Long bytesIn; // 输入字节数
// private Long bytesOut; // 输出字节数
// }
//}

View File

@ -0,0 +1,19 @@
package com.guwan.backend.client;
import cn.hutool.json.JSONObject;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.multipart.MultipartFile;
/**
* 语音转文本
*/
@FeignClient(name = "voiceService", url = "http://127.0.0.1:8532")
public interface VoiceServiceClient {
@PostMapping(value = "/v1/audio/transcriptions", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
JSONObject voiceToText(@RequestPart("file") MultipartFile file,
@RequestPart("model") String model);
}

View File

@ -0,0 +1,25 @@
package com.guwan.backend.common;
public class BusinessException extends RuntimeException {
@java.io.Serial private static final long serialVersionUID = -2119302295305964305L;
public BusinessException() {}
public BusinessException(String message) {
super(message);
}
public BusinessException(String message, Throwable cause) {
super(message, cause);
}
public BusinessException(Throwable cause) {
super(cause);
}
public BusinessException(
String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}

View File

@ -0,0 +1,98 @@
package com.guwan.backend.common;
import lombok.Data;
import java.text.SimpleDateFormat;
import java.util.Date;
@Data
public class SearchResult<T> {
private Integer code;
private String message;
private T data;
private Long total;
private String time;
public SearchResult() {
this.time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
}
public static <T> SearchResult<T> success() {
SearchResult<T> result = new SearchResult<>();
result.setCode(200);
result.setMessage("操作成功");
return result;
}
public static <T> SearchResult<T> success(T data) {
SearchResult<T> result = new SearchResult<>();
result.setCode(200);
result.setMessage("操作成功");
result.setData(data);
return result;
}
public static <T> SearchResult<T> success(T data, Long total) {
SearchResult<T> result = new SearchResult<>();
result.setCode(200);
result.setMessage("操作成功");
result.setData(data);
result.setTotal(total);
return result;
}
public static <T> SearchResult<T> success(String message, T data) {
SearchResult<T> result = new SearchResult<>();
result.setCode(200);
result.setMessage(message);
result.setData(data);
return result;
}
public static <T> SearchResult<T> error(String message) {
SearchResult<T> result = new SearchResult<>();
result.setCode(500);
result.setMessage(message);
return result;
}
public static <T> SearchResult<T> error(Integer code, String message) {
SearchResult<T> result = new SearchResult<>();
result.setCode(code);
result.setMessage(message);
return result;
}
public static <T> SearchResult<T> error(Integer code, String message, T data) {
SearchResult<T> result = new SearchResult<>();
result.setCode(code);
result.setMessage(message);
result.setData(data);
return result;
}
// 常用状态码
public static final int SUCCESS = 200;
public static final int ERROR = 500;
public static final int UNAUTHORIZED = 401;
public static final int FORBIDDEN = 403;
public static final int NOT_FOUND = 404;
public static final int VALIDATE_FAILED = 400;
// 业务状态码
public static <T> SearchResult<T> validateFailed(String message) {
return error(VALIDATE_FAILED, message);
}
public static <T> SearchResult<T> unauthorized(String message) {
return error(UNAUTHORIZED, message);
}
public static <T> SearchResult<T> forbidden(String message) {
return error(FORBIDDEN, message);
}
public static <T> SearchResult<T> notFound(String message) {
return error(NOT_FOUND, message);
}
}

View File

@ -0,0 +1,48 @@
package com.guwan.backend.config;
import com.guwan.backend.store.PersistentChatMemoryStore;
import dev.langchain4j.memory.ChatMemory;
import dev.langchain4j.memory.chat.ChatMemoryProvider;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.chat.StreamingChatLanguageModel;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.TokenStream;
import dev.langchain4j.service.UserMessage;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class Aiconfig {
public interface Assistant {
String chat(@MemoryId String memoryId, @UserMessage String message);
// 流式响应
TokenStream stream(@MemoryId String memoryId, @UserMessage String message);
}
@Bean
public Assistant assistant(ChatLanguageModel qwenchatModel,
StreamingChatLanguageModel qwenstreamingchatModel) {
PersistentChatMemoryStore store = new PersistentChatMemoryStore();
ChatMemoryProvider chatMemoryProvider = memoryId -> MessageWindowChatMemory.builder()
.id(memoryId)
.maxMessages(10)
.chatMemoryStore(store)
.build();
return AiServices.builder(Assistant.class)
.chatLanguageModel(qwenchatModel)
.streamingChatLanguageModel(qwenstreamingchatModel)
.chatMemoryProvider(memoryId ->
MessageWindowChatMemory.builder().maxMessages(10)
.id(memoryId).build()
)
.chatMemoryProvider(chatMemoryProvider)
.build();
}
}

View File

@ -0,0 +1,38 @@
package com.guwan.backend.config;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.guwan.backend.controller.monitor.CacheMonitor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import java.util.concurrent.TimeUnit;
import static com.guwan.backend.constant.CacheConstants.CACHE_LIST;
@Slf4j
@EnableCaching
@Configuration
public class CaffeineConfig {
@Bean
@Primary
public CacheManager caffeineCacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.recordStats()
.expireAfterAccess(60, TimeUnit.MINUTES)
.initialCapacity(100)
.maximumSize(1000));
cacheManager.setCacheNames(CACHE_LIST);
return cacheManager;
}
@Bean
public CacheMonitor cacheMonitor(CacheManager cacheManager) {
return new CacheMonitor(cacheManager);
}
}

View File

@ -9,9 +9,7 @@ import org.springframework.web.filter.CorsFilter;
@Configuration
public class CorsConfig {
//get请求变成了options 后端预检过不了怎么改
//get请求变成options 后端预检无法通过怎么办 详见 securityFilterChain
@Bean
public CorsFilter corsFilter() {

View File

@ -24,6 +24,7 @@ public class DatabaseInitConfig implements ApplicationRunner {
ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
populator.addScript(new ClassPathResource("db/schema.sql"));
populator.addScript(new ClassPathResource("db/data.sql"));
//populator.addScript(new ClassPathResource("db/daaixianzun.sql"));
populator.setContinueOnError(true);
populator.execute(dataSource);
log.info("数据库初始化完成");

View File

@ -0,0 +1,37 @@
package com.guwan.backend.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.core.DockerClientBuilder;
import com.github.dockerjava.core.DefaultDockerClientConfig;
import com.github.dockerjava.transport.DockerHttpClient;
import com.github.dockerjava.httpclient5.ApacheDockerHttpClient;
import java.time.Duration;
@Configuration
public class DockerConfig {
@Bean
public DockerClient dockerClient() {
// 配置 Docker 主机
DefaultDockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder()
.withDockerHost("tcp://localhost:2375") // 设置 Docker 主机地址
.build();
// 创建 ApacheHttpClient5 实现
DockerHttpClient httpClient = new ApacheDockerHttpClient
.Builder()
.dockerHost(config.getDockerHost())
.maxConnections(100)
.connectionTimeout(Duration.ofSeconds(30))
.responseTimeout(Duration.ofSeconds(45))
.build();
// 创建 Docker 客户端并返回
return DockerClientBuilder.getInstance(config)
.withDockerHttpClient(httpClient)
.build();
}
}

View File

@ -8,10 +8,9 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EsMapperScan("com.guwan.backend.es.mapper")
@EsMapperScan("com.guwan.backend.elasticsearch.mapper")
@EnableConfigurationProperties(EasyEsConfigProperties.class)
public class EasyEsConfig {
@Bean
public IndexStrategyFactory indexStrategyFactory() {
return new IndexStrategyFactory();

View File

@ -0,0 +1,53 @@
package com.guwan.backend.config;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.kafka.common.serialization.StringSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.core.*;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class KafkaConfig {
@Bean
public ProducerFactory<String, String> producerFactory() {
Map<String, Object> config = new HashMap<>();
config.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
config.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
config.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
return new DefaultKafkaProducerFactory<>(config);
}
@Bean
public ConsumerFactory<String, String> consumerFactory() {
Map<String, Object> config = new HashMap<>();
config.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
config.put(ConsumerConfig.GROUP_ID_CONFIG, "book-recommendation-group");
config.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
config.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
return new DefaultKafkaConsumerFactory<>(config);
}
@Bean
public KafkaTemplate<String, String> kafkaTemplate() {
return new KafkaTemplate<>(producerFactory());
}
@Bean
public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, String> factory =
new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
return factory;
}
}

View File

@ -0,0 +1,34 @@
package com.guwan.backend.config;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* @author roc
*/
@Configuration
public class LocalDateTimeConfiguration {
@Value("${spring.jackson.date-format:yyyy-MM-dd HH:mm:ss}")
private String pattern;
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
return builder -> {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
//返回时间数据序列化
builder.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(formatter));
//接收时间数据反序列化
builder.deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(formatter));
};
}
}

View File

@ -1,6 +1,9 @@
package com.guwan.backend.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.guwan.backend.Handler.MyMetaObjectHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -20,4 +23,15 @@ public class MybatisPlusConfig {
return new MyMetaObjectHandler();
}
//mybatisplus分页流程为全量查询 => 本地分页
// 而如果不对查询结果进行拦截mybatis将不能执行分页操作自然也拿不到页数和总数的数据了
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
//创建拦截器对执行的sql进行拦截
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//对Mysql拦截
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}

View File

@ -0,0 +1,31 @@
package com.guwan.backend.config;
import com.guwan.backend.Handler.CourseWebSocketHandler;
import com.guwan.backend.Handler.CoursesWebSocketHandler;
import com.guwan.backend.websocket.ChatWebSocketHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private CourseWebSocketHandler courseWebSocketHandler;
@Autowired
private CoursesWebSocketHandler coursesWebSocketHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new ChatWebSocketHandler(), "/ws/chat")
.addHandler(coursesWebSocketHandler, "/ws/courses") // 首页
.addHandler(courseWebSocketHandler, "/ws/course/{courseId}")
.setAllowedOrigins("*"); // 允许所有来源
}
}

View File

@ -0,0 +1,45 @@
package com.guwan.backend.config;
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Configuration
@ConditionalOnProperty(prefix = "xxl.job", name = "enabled", havingValue = "true")
public class XxlJobConfig {
@Value("${xxl.job.admin.addresses}")
private String adminAddresses;
@Value("${xxl.job.accessToken}")
private String accessToken;
@Value("${xxl.job.executor.appname}")
private String appname;
@Value("${xxl.job.executor.address}")
private String address;
@Value("${xxl.job.executor.ip}")
private String ip;
@Value("${xxl.job.executor.port}")
private int port;
@Value("${xxl.job.executor.logpath}")
private String logPath;
@Value("${xxl.job.executor.logretentiondays}")
private int logRetentionDays;
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
log.info(">>>>>>>>>>> xxl-job config init.");
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
xxlJobSpringExecutor.setAppname(appname);
xxlJobSpringExecutor.setAddress(address);
xxlJobSpringExecutor.setIp(ip);
xxlJobSpringExecutor.setPort(port);
xxlJobSpringExecutor.setAccessToken(accessToken);
xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
return xxlJobSpringExecutor;
}
}

View File

@ -0,0 +1,21 @@
package com.guwan.backend.constant;
import java.util.List;
/**
* 本地缓存相关常量配置
*/
public class CacheConstants {
/**
*
* 缓存内容
*/
public static final List<String> CACHE_LIST = List.of(
"userCache",
"paperCache"
);
}

View File

@ -6,35 +6,49 @@ import java.util.List;
* 安全相关常量配置
*/
public class SecurityConstants {
/**
* API接口白名单
* 这些路径可以直接访问不需要认证
*/
public static final List<String> WHITE_LIST = List.of(
"/common/**", //公共接口
"/demo/**", // 测试接口
"/challenge",
"/ws/**",
"/faceTest", "/compareFaces",
"/minio/**",
"/bs/user/getEmailCode",
"/exam/api/paper/**",
"/bs/user/login",
"/bs/user/register",
"/bs/courses/**",
"/api/common/**", //公共接口
"/demo/**", // 测试接口
"/api/products",
"/api/user/register", // 用户注册
"/api/user/login", // 用户登录
"/api/user/getEmailCode", // 获取邮箱验证码
"/api/user/getPhoneCode", // 获取手机验证码
"/chat.html",
"/v3/api-docs/**", // Swagger API文档
"/swagger-ui/**", // Swagger UI
"/swagger-ui.html", // Swagger UI HTML
"/swagger-resources/**", // Swagger 资源
"/webjars/**" // Swagger UI 相关资源
"/api/user/register", // 用户注册
"/api/user/login", // 用户登录
"/api/user/getEmailCode", // 获取邮箱验证码
"/api/user/getPhoneCode", // 获取手机验证码
"/chat.html",
"/daxz.html/**",
"/polling-chat.html",
"/log-viewer.html",
"/ws/chat/**",
"/api/polling-chat/**",
"/v3/api-docs/**", // Swagger API文档
"/swagger-ui/**", // Swagger UI
"/swagger-ui.html", // Swagger UI HTML
"/swagger-resources/**", // Swagger 资源
"/webjars/**" // Swagger UI 相关资源
);
/**
* 静态资源白名单
* 这些路径用于访问静态资源不需要认证
*/
public static final List<String> STATIC_RESOURCES = List.of(
"/static/**", // 静态资源目录
"/public/**", // 公共资源目录
"/error" // 错误页面
"/static/**", // 静态资源目录
"/public/**", // 公共资源目录
"/error" // 错误页面
);
}

View File

@ -0,0 +1,11 @@
package com.guwan.backend.constant;
import java.util.Set;
public class SensitiveWordConstants {
public static final Set<String> SENSITIVE_WORD = Set.of(
"傻瓜",
"笨蛋",
"白痴"
);
}

View File

@ -0,0 +1,191 @@
package com.guwan.backend.controller;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.guwan.backend.common.Result;
import com.guwan.backend.mapper.UserMapper;
import com.guwan.backend.pojo.dto.user.*;
import com.guwan.backend.pojo.entity.User;
import com.guwan.backend.service.EmailService;
import com.guwan.backend.service.UserService;
import com.guwan.backend.util.MinioUtil;
import com.guwan.backend.util.RedisUtils;
import com.guwan.backend.util.SecurityUtil;
import com.guwan.backend.util.SmsUtils;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.thymeleaf.context.Context;
@Slf4j
@RestController
@RequestMapping("/bs/user")
@RequiredArgsConstructor
@Validated
public class BSUserController {
private final UserService userService;
private final EmailService emailService;
private final RedisUtils redisUtils;
private final MinioUtil minioUtil;
private final SecurityUtil securityUtil;
private final UserMapper userMapper;
@PostMapping("/register")
public Result<UserDTO> register(@RequestBody @Valid BSRegisterDTO request) {
try {
log.info("用户注册: {}", request);
return Result.success("注册成功", userService.register(request));
} catch (IllegalArgumentException e) {
return Result.validateFailed(e.getMessage());
} catch (Exception e) {
log.error("注册失败", e);
return Result.error("系统错误");
}
}
@PostMapping("/login")
public Result<String> login(@RequestBody @Valid LoginDto request) {
try {
log.info("用户登录: {}", request.getUsername());
return Result.success("登录成功", userService.login(request).getToken());
} catch (IllegalArgumentException e) {
return Result.unauthorized(e.getMessage());
} catch (Exception e) {
log.error("登录失败", e);
return Result.error("系统错误");
}
}
@GetMapping("/current")
public Result<UserDTO> getCurrentUser() {
UserDTO user = userService.getCurrentUser();
if (user == null) {
return Result.unauthorized("用户未登录");
}
return Result.success(user);
}
@GetMapping("/{id}")
public Result<UserDTO> getUserById(@PathVariable Long id) {
UserDTO user = userService.getUserById(id);
if (user == null) {
return Result.notFound("用户不存在");
}
return Result.success(user);
}
@PostMapping("/token/refresh")
public Result<String> refreshToken(@RequestHeader(value = "Authorization", required = false) String token) {
if (token == null || !token.startsWith("Bearer ")) {
return Result.error("无效的token");
}
try {
String newToken = userService.refreshToken(token.substring(7));
return Result.success(newToken);
} catch (Exception e) {
log.error("刷新token失败", e);
return Result.error(e.getMessage());
}
}
@PostMapping("/getEmailCode")
public Result getEmailCode(@RequestBody @Valid EmailDto emailDto) {
String email = emailDto.getEmail();
log.info("邮箱注册: {}", email);
if (!email.endsWith("@stumail.xsyu.edu.cn")){
return Result.error("对不起!您不属于西安石油大学学生!!!");
}
Context context = new Context();
context.setVariable("nowDate", DateUtil.now());
String code = RandomUtil.randomNumbers(6);
redisUtils.set(email, code, 10);
context.setVariable("code", code.toCharArray());
emailService.sendHtmlMessage(email,
"C/C++在线学习平台邮箱验证码", "email_template.html", context);
return Result.success("邮件验证码发送成功");
}
@PostMapping("/getPhoneCode")
public Result registerByPhone(@RequestBody @Valid PhoneDto phoneDto) throws Exception {
String phone = phoneDto.getPhone();
log.info("手机号注册: {}", phone);
String random = RandomUtil.randomNumbers(6);
SmsUtils.sendMessage(phone, random);
redisUtils.set(phone, random, 10);
return Result.success("手机验证码发送成功");
}
@PostMapping("/updateAvatar")
public Result updateAvatar(@RequestParam("file") MultipartFile file) throws Exception {
String url = minioUtil.getUrl(minioUtil.getFileUrl
("photo", minioUtil.uploadFile("photo", file,"avatar")));
Long currentUserId = securityUtil.getCurrentUserId();
userMapper.update(new LambdaUpdateWrapper<User>().eq(User::getId, currentUserId).set(User::getAvatar, url));
return Result.success(url);
}
@PostMapping("/password/reset")
public Result<Void> resetPassword(@RequestBody ChangePasswordDTO changePasswordDTO) {
log.debug("更改方式: {}, 内容: {}",
changePasswordDTO.getChangeWay(),
changePasswordDTO.getInfo());
try {
userService.resetPassword(changePasswordDTO);
return Result.success();
} catch (Exception e) {
log.error("重置密码失败", e);
return Result.error(e.getMessage());
}
}
@PutMapping("/info")
public Result<UserDTO> updateUserInfo(@RequestBody @Valid UserDTO userDTO) {
try {
return Result.success(userService.updateUserInfo(userDTO));
} catch (Exception e) {
log.error("更新用户信息失败", e);
return Result.error(e.getMessage());
}
}
}

View File

@ -0,0 +1,91 @@
package com.guwan.backend.controller;
import com.guwan.backend.annotation.OperationLog;
import com.guwan.backend.common.Result;
import com.guwan.backend.pojo.entity.BaseExam;
import com.guwan.backend.service.BaseExamService;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
/**
* (BaseExam)表控制层
*
* @author Guwan
* @since 2025-04-19 22:54:02
*/
@RestController
@RequestMapping("/baseExam")
public class BaseExamController {
/**
* 服务对象
*/
@Autowired
private BaseExamService baseExamService;
//@PreAuthorize("hasAuthority('ROLE_ADMIN')")
@OperationLog
@GetMapping
public Result queryByPage(@RequestParam(name = "page") Integer page,
@RequestParam(name = "page") Integer pageSize,
@RequestParam(name = "query") String query) {
return Result.success(baseExamService.queryByPage(page, pageSize, query));
// return Result.success();
}
/**
* 通过主键查询单条数据
*
* @param id 主键
* @return 单条数据
*/
@GetMapping("{id}")
public Result<BaseExam> queryById(@PathVariable("id") String id) {
return Result.success(this.baseExamService.getById(id));
}
/**
* 新增数据
*
* @param baseExam 实体
* @return 新增结果
*/
@PostMapping
public Result<BaseExam> add(BaseExam baseExam) {
this.baseExamService.save(baseExam);
return Result.success();
}
/**
* 编辑数据
*
* @param baseExam 实体
* @return 编辑结果
*/
@PutMapping
public Result<BaseExam> edit(BaseExam baseExam) {
//return Result.success(this.baseExamService.update(baseExam));
return Result.success();
}
/**
* 删除数据
*
* @param id 主键
* @return 删除是否成功
*/
@DeleteMapping
public Result<Boolean> deleteById(String id) {
return Result.success(this.baseExamService.removeById(id));
}
}

View File

@ -0,0 +1,47 @@
package com.guwan.backend.controller;
import com.guwan.backend.common.Result;
import com.guwan.backend.pojo.bo.CodeRunResult;
import com.guwan.backend.pojo.bo.TestCaseBO;
import com.guwan.backend.pojo.dto.SubmitCodeDTO;
import com.guwan.backend.pojo.response.ChallengesVO;
import com.guwan.backend.service.ChallengesService;
import com.guwan.backend.service.TestCaseAssemblyService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Slf4j
@RestController
@RequestMapping("/challenge")
@RequiredArgsConstructor
@Validated
public class ChallengeController {
private final ChallengesService challengesService;
private final TestCaseAssemblyService testCaseAssemblyService;
@GetMapping
public Result getChallenge() {
List<ChallengesVO> challenge = challengesService.getChallenge();
return Result.success(challenge);
}
@PostMapping
public Result submitCode(@RequestBody SubmitCodeDTO submitCodeDTO) {
CodeRunResult codeRunResult = challengesService.submitCode(submitCodeDTO);
return Result.success(codeRunResult);
}
}

View File

@ -1,27 +1,571 @@
package com.guwan.backend.controller;
import cn.hutool.json.JSONObject;
import com.guwan.backend.annotation.OperationLog;
import com.guwan.backend.client.SimpleTTSClient;
import com.guwan.backend.client.VoiceServiceClient;
import com.guwan.backend.common.Result;
import com.guwan.backend.mongodb.*;
import com.guwan.backend.pojo.entity.BookContent;
import com.guwan.backend.util.MinioUtil;
import dev.langchain4j.community.model.dashscope.QwenChatModel;
import dev.langchain4j.community.model.dashscope.QwenStreamingChatModel;
import dev.langchain4j.model.chat.response.ChatResponse;
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
import io.minio.MinioClient;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.apache.poi.xslf.usermodel.XMLSlideShow;
import org.apache.poi.xslf.usermodel.XSLFSlide;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.AutoDetectParser;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.parser.Parser;
import org.apache.tika.sax.BodyContentHandler;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import reactor.core.publisher.Flux;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Slf4j
@RestController
@RequestMapping("/common")
@RequestMapping("/api/common")
@RequiredArgsConstructor
public class CommonController {
private final MinioUtil minioUtil;
private final VoiceServiceClient voiceServiceClient;
private final SimpleTTSClient simpleTTSClient;
private final MinioClient minioClient;
//private final BookContentService bookContentService;
private final MongodbUserService mongodbUserService;
private final EveryReadDetailOfMongodbService everyReadDetailOfMongodbService;
private final QwenChatModel qwenChatModel;
private final QwenStreamingChatModel qwenStreamingChatModel;
private final TestDateRepository testDateRepository;
private final TestDateDao testDateDao;
@PostMapping("/uploadFile")
public Result<String> uploadFile(String bucketName, MultipartFile file){
return Result.success(minioUtil.getUrl(minioUtil.getFileUrl
(bucketName, minioUtil.uploadFile(bucketName, file))));
}
// @PostMapping("/addBookComment")
// public Result<String> addBookComment(String url) {
// log.debug(url);
// // "http://localhost:9000/txt/8357cf6b-9637-4354-9ee6-2717141f665a.txt";
// OkHttpClient client = new OkHttpClient();
//
// // 创建一个请求对象
// Request request = new Request.Builder()
// .url(url)
// .build();
//
// // 发起同步请求
// try {
// String content = getTextUsingOkHttp(client, request);
// ArrayList<BookContent> bookContents = processContent(content);
// bookContentService.saveBatch(bookContents);
//
// } catch (IOException e) {
// e.printStackTrace();
// }
//
// return Result.success("ok");
// }
// 通过 OkHttpClient 发起同步请求获取文件内容
public static String getTextUsingOkHttp(OkHttpClient client, Request request) throws IOException {
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful()) {
return response.body().string(); // 返回文件内容
} else {
throw new IOException("Unexpected code " + response);
}
}
}
// 处理文件内容提取卷和节
public static ArrayList<BookContent> processContent(String content) {
// 正则表达式提取卷和节
String volumePattern = "第([一二三四五六七八九十]+)卷"; // 提取卷
String sectionPattern = "第([一二三四五六七八九十零百]+)节:(.*)"; // 提取节及其节名
// 提取卷
Pattern volumeRegex = Pattern.compile(volumePattern);
Matcher volumeMatcher = volumeRegex.matcher(content);
// 提取节
Pattern sectionRegex = Pattern.compile(sectionPattern);
Matcher sectionMatcher = sectionRegex.matcher(content);
// 列表来存储所有卷和节的内容
List<String> volumes = new ArrayList<>(); // 存储所有卷的标题
List<String> sections = new ArrayList<>(); // 存储所有节的标题
List<String> sectionContents = new ArrayList<>(); // 存储每节的正文内容
// 收集卷的信息
while (volumeMatcher.find()) {
String volume = "" + volumeMatcher.group(1) + "";
volumes.add(volume);
}
// 收集节的信息
while (sectionMatcher.find()) {
String sectionTitle = "" + sectionMatcher.group(1) + "节:" + sectionMatcher.group(2).trim(); // 这里去掉节名前后空格
sections.add(sectionTitle);
// 获取节的正文内容
int start = sectionMatcher.end(); // 获取节标题之后的位置
int end = content.length(); // 默认到文件末尾
// 查找下一个节的位置即本节内容的结束位置
Matcher nextSectionMatcher = sectionRegex.matcher(content);
if (nextSectionMatcher.find(start)) {
end = nextSectionMatcher.start();
}
// 获取当前节的正文内容
String sectionContent = content.substring(start, end).trim();
sectionContents.add(sectionContent);
}
// 标记是否是第一次匹配到第一节
boolean isFirstSection = true;
ArrayList<BookContent> bookContents = new ArrayList<>();
int sectionId = 1;
// 输出卷和节信息
for (int i = 0; i < volumes.size(); i++) {
// 输出卷的标题
// 输出该卷的每一节标题和正文内容
for (int j = 0; j < sections.size(); j++) {
// System.out.print(volumes.get(i));
String section = sections.get(j);
String sectionContent = sectionContents.get(j);
// 输出节标题
// System.out.println(" " + section);
// 输出节的正文内容
// System.out.println(" 正文: " + sectionContent);
// 如果是第一节并且不是第一次出现递增卷的索引
if (section.contains("第一节") && !isFirstSection) {
i++; // 不是第一次才递增
}
// 第一次匹配到第一节标记为false
isFirstSection = false;
BookContent bookContent = new BookContent();
bookContent.setBookName("大爱仙尊");
bookContent.setVolume(volumes.get(i));
bookContent.setSection(section);
bookContent.setSectionContent(sectionContent);
bookContent.setSectionId(sectionId++);
System.out.println("bookContent = " + bookContent);
bookContents.add(bookContent);
}
}
return bookContents;
}
// @GetMapping("/getBookComment")
// public Result<String> getBookComment(Long id) {
// BookContent bookContent = bookContentService.getById(id);
// return Result.success(bookContent.getSectionContent());
// }
// @GetMapping("/getBookContent")
// public Result<String> getBookContent(String bookName, Long id) {
// BookContent bookContent = bookContentService.getBookContent(bookName, id);
// return Result.success(bookContent.getSectionContent());
// }
// @GetMapping("/getBookCommentByPath")
// public ResponseEntity<Map<String, Object>> getBookCommentByPath(@RequestParam("id") Long id) {
// // 从数据库中获取评论内容
// //String comments = bookContentService.getById(id).getSectionContent();
//
// BookContent byId = bookContentService.lambdaQuery()
// .eq(BookContent::getBookName, "大爱仙尊")
// .eq(BookContent::getSectionId, id).one();
//
// // 构造返回数据
// Map<String, Object> response = new HashMap<>();
// response.put("data", byId);
//
// return ResponseEntity.ok(response);
//
// }
@GetMapping("/testMongodb")
public Result testMongodb(){
User user = new User();
user.setId("1");
user.setName("1");
user.setAge(1L);
user.setEmail("1");
mongodbUserService.save(user);
return Result.success();
}
@PostMapping("/everyRead")
public Result everyRead(@RequestBody EveryReadDetailOfMongodb everyReadDetailOfMongodb){
everyReadDetailOfMongodbService.save(everyReadDetailOfMongodb);
return Result.success();
}
@GetMapping("/getUserOneTotalTime")
@OperationLog(description = "统计用户某本书的读书时间")
public Result getUserOneTotalTime(@RequestParam Long userId){
//return Result.success();
// everyReadDetailOfMongodbService.getUserOneTotalTime(userId);
return Result.success(everyReadDetailOfMongodbService.getUserOneTotalTime(userId));
}
/**
* model 默认值 /model/faster-whisper-small/
*/
@PostMapping("/convertVoiceToText")
public Result convertVoiceToText(MultipartFile file, String model) {
if (model == null){
model = "/model/faster-whisper-small/";
}
return Result.success(voiceServiceClient.voiceToText(file, model));
}
@PostMapping("/simpleTTS")
public Result simpleTTS(String input, String voiceModel) {
if (voiceModel == null) {
voiceModel = "zh-CN-XiaoxiaoNeural";
}
// 使用 JSONObject 构造JSON
JSONObject jsonObject = new JSONObject();
jsonObject.putOpt("input", input);
jsonObject.putOpt("voice", voiceModel);
String jsonInputString = jsonObject.toString();
System.out.println(jsonInputString);
// jsonInputString = "{\"input\": \"想听个啥123\", \"voice\": \"zh-CN-XiaoxiaoNeural\", \"style\": \"\", \"rate\": 0, \"pitch\": 0}";
String url = minioUtil.getUrl(minioUtil.getFileUrl("temp",
minioUtil.uploadByteArray(
simpleTTSClient.saveAudio(jsonInputString), "mp3")));
return Result.success(url);
}
@PostMapping("/testPart")
public void testPart(@RequestPart(value = "file", required = false) MultipartFile file,
@RequestPart(value = "str1", required = false) String str1,
@RequestPart(value = "f1", required = false) String f1) {
String s = null;
if (file != null) {
s = minioUtil.uploadFile("temp", file);
}
System.out.println("s = " + s);
System.out.println("str1 = " + str1);
System.out.println("f1 = " + f1);
}
@PostMapping("/testPartOfFloat")
public void testPartOfFloat(@RequestPart(value = "file", required = false) MultipartFile file,
@RequestPart(value = "str1", required = false) String str1,
@RequestPart(value = "f1", required = false) Float f1) {
String s = null;
if (file != null) {
s = minioUtil.uploadFile("temp", file);
}
System.out.println("s = " + s);
System.out.println("str1 = " + str1);
System.out.println("f1 = " + f1);
}
@PostMapping("/testParam")
public void testParm(@RequestPart(value = "file", required = false) MultipartFile file,
@RequestParam(value = "str1", required = true) String str1,
@RequestParam(value = "f1", required = false) Float f1) {
String s = null;
if (file != null) {
s = minioUtil.uploadFile("temp", file);
}
System.out.println("s = " + s);
System.out.println("str1 = " + str1);
System.out.println("f1 = " + f1);
}
@GetMapping("/testQwen")
public Result testQwen(@RequestParam(value = "str1", required = false) String message) {
var response = qwenChatModel.chat(message);
System.out.println("response = " + response);
return Result.success(response);
}
@GetMapping(value = "/testQwenStreaming", produces = "text/steam;charset=UTF-8")
public Flux<String> testQwenStreaming(
@RequestParam(value = "message", required = false) String message) {
Flux<String> flux = Flux.create(fluxSink -> {
qwenStreamingChatModel.chat(message, new StreamingChatResponseHandler() {
//每一次流式响应的文本
@Override
public void onPartialResponse(String partialResponse) {
fluxSink.next(partialResponse);
}
//响应结束的文本
@Override
public void onCompleteResponse(ChatResponse chatResponse) {
fluxSink.complete();
}
@Override
public void onError(Throwable throwable) {
fluxSink.error(throwable);
}
});
});
return flux;
}
@GetMapping("/PPTToImageConverter")
public void PPTToImageConverter() throws IOException {
String pptFile = "D:\\00_桌面\\公司简介.pptx"; // PPT 文件路径
String outputDir = "output_images"; // 输出目录
FileInputStream inputStream = new FileInputStream(pptFile);
XMLSlideShow ppt = new XMLSlideShow(inputStream);
inputStream.close();
// 创建输出目录
File dir = new File(outputDir);
if (!dir.exists()) {
dir.mkdirs();
}
// 获取幻灯片
int slideNumber = 1;
for (XSLFSlide slide : ppt.getSlides()) {
Dimension pgsize = ppt.getPageSize();
BufferedImage img = new BufferedImage(pgsize.width, pgsize.height, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = img.createGraphics();
// 设置背景为白色
graphics.setPaint(Color.WHITE);
graphics.fill(new Rectangle2D.Float(0, 0, pgsize.width, pgsize.height));
// 渲染幻灯片
slide.draw(graphics);
// 输出为图片
File outputFile = new File(outputDir + "/slide_" + slideNumber + ".png");
ImageIO.write(img, "png", outputFile);
System.out.println("Saved slide: " + outputFile.getAbsolutePath());
slideNumber++;
}
ppt.close();
}
@GetMapping("/videoDuration")
public void VideoDuration() throws IOException {
String preSignedUrl = "http://localhost:9000/videos/ffffad37-9804-4765-ae18-3f8dcda9bea8.mp4"; // Minio 预签名 URL
// minioClient.getObject()
InputStream stream = minioUtil.getFileInputStream("videos", "ffffad37-9804-4765-ae18-3f8dcda9bea8.mp4");
System.out.println("stream = " + stream);
try {
Metadata metadata = new Metadata();
Parser parser = new AutoDetectParser();
parser.parse(stream, new BodyContentHandler(), metadata, new ParseContext());
String duration = metadata.get("duration");
System.out.println("视频时长(毫秒): " + duration);
} catch (Exception e) {
e.printStackTrace();
}
}
@PostMapping("/testPostUseParam")
//测试公司写法
public void testPostUseParam(@RequestParam("ids") List<String> ids,
@RequestParam("str") String str){
System.out.println("ids = " + ids);
System.out.println("str = " + str);
}
@GetMapping("/testGetParam")
public void testGetParam(@RequestParam("ids") List<String> ids,
@RequestParam("str") String str){
System.out.println("ids = " + ids);
System.out.println("str = " + str);
}
@GetMapping("/testDate")
public void testDate(){
TestDate testDate = new TestDate();
testDate.setId(UUID.randomUUID().toString());
Instant now = Instant.now();
//Instant now = Instant.now();
// 获取当前系统默认时区
ZoneId zoneId = ZoneId.systemDefault();
// 获取今天的 00:00:00
Instant todayStart = LocalDate.now(zoneId)
.atStartOfDay(zoneId)
.toInstant();
// 假设 testDate 有一个 setDate(Instant instant) 方法
testDate.setDate(Date.from(todayStart));
// testDate.setDate(new Date());
testDateRepository.insert(testDate);
}
@GetMapping("/testDateFind")
public void testDateFind(){
TestDate testDate = new TestDate();
testDate.setId(UUID.randomUUID().toString());
Instant now = Instant.now();
//Instant now = Instant.now();
// 获取当前系统默认时区
ZoneId zoneId = ZoneId.systemDefault();
// 获取今天的 00:00:00
Instant todayStart = LocalDate.now(zoneId)
.atStartOfDay(zoneId)
.toInstant();
Date from = Date.from(todayStart);
List<TestDate> byDate = testDateDao.findByDate(from);
System.out.println("byDate = " + byDate);
}
@PostMapping("/sendCode")
public void sendCode(@RequestParam String code){
System.out.println("code = " + code);
}
}

View File

@ -0,0 +1,124 @@
package com.guwan.backend.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.guwan.backend.common.Result;
import com.guwan.backend.pojo.dto.BSCategory;
import com.guwan.backend.pojo.entity.Course;
import com.guwan.backend.pojo.response.courseDetail.CourseDetailVO;
import com.guwan.backend.service.BSCategoryService;
import com.guwan.backend.service.CourseService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.stream.Collectors;
/**
* 课程表(Courses)表控制层
*
* @author Guwan
* @since 2025-03-13 22:47:31
*/
@Slf4j
@RestController
@RequestMapping("/bs/courses")
@RequiredArgsConstructor
public class CourseController {
/**
* 服务对象
*/
private final CourseService courseService;
private final BSCategoryService categoryService;
/**
* 分页查询
*
* @param courses 筛选条件
* @param pageRequest 分页对象
* @return 查询结果
*/
@GetMapping("/queryByPage")
public Result queryByPage(@RequestParam("page") Integer pageNumber,
@RequestParam("size") Integer size) {
Page<Course> page = new Page<>(pageNumber, size);
LambdaQueryWrapper<Course> lambdaQueryWrapper = new LambdaQueryWrapper<>();
Page<Course> resultPage = this.courseService.page(page, lambdaQueryWrapper);
return Result.success(resultPage.getRecords().stream()
.peek(course -> {
course.setCategoryName(categoryService.list()
.stream()
.collect(Collectors.toMap(BSCategory::getId, BSCategory::getName))
.get(course.getCategoryId())); // 赋值类别名称
})
.toList());
}
@GetMapping("/getCourseDetail/{courseId}")
public Result getCourseDetail(@PathVariable("courseId") String courseId) {
CourseDetailVO courseDetail = courseService.getCourseDetail(courseId);
return Result.success(courseDetail);
}
/**
* 通过主键查询单条数据
*
* @param id 主键
* @return 单条数据
*/
@GetMapping("{id}")
public Result<Course> queryById(@PathVariable("id") Integer id) {
return Result.success(courseService.getById(id));
}
/**
* 新增数据
*
* @param course 实体
* @return 新增结果
*/
@PostMapping
public Result<Course> add(Course course) {
this.courseService.save(course);
return Result.success();
}
/**
* 编辑数据
*
* @param course 实体
* @return 编辑结果
*/
@PutMapping
public Result<Course> edit(Course course) {
//return Result.success(this.coursesService.update(courses));
return Result.success();
}
/**
* 删除数据
*
* @param id 主键
* @return 删除是否成功
*/
@DeleteMapping
public Result<Boolean> deleteById(Integer id) {
return Result.success(this.courseService.removeById(id));
}
}

View File

@ -1,38 +1,26 @@
package com.guwan.backend.controller;
import cn.easyes.core.conditions.LambdaEsQueryWrapper;
import cn.hutool.core.collection.CollectionUtil;
import com.arcsoft.face.ActiveDeviceInfo;
import com.arcsoft.face.ActiveFileInfo;
import com.arcsoft.face.FaceEngine;
import com.arcsoft.face.FaceInfo;
import com.arcsoft.face.enums.ExtractType;
import com.arcsoft.face.toolkit.ImageFactory;
import com.arcsoft.face.toolkit.ImageInfo;
import com.guwan.backend.common.Result;
import com.guwan.backend.es.document.ProductDocument;
import com.guwan.backend.es.mapper.ProductEsMapper;
import com.guwan.backend.face.dto.FaceRecognitionResDTO;
import com.guwan.backend.face.service.FaceEngineService;
import com.guwan.backend.elasticsearch.document.ProductDocument;
import com.guwan.backend.elasticsearch.mapper.ProductEsMapper;
import com.guwan.backend.mongodb.CategoryService;
import com.guwan.backend.mongodb.FileStorageService;
import com.guwan.backend.service.SwaggerAutoFillService;
import com.guwan.backend.util.MinioUtil;
import io.minio.GetObjectArgs;
import com.mongodb.client.gridfs.model.GridFSFile;
import io.minio.MinioClient;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.search.SearchHit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.Arrays;
import java.io.IOException;
import java.util.List;
import java.util.regex.Pattern;
@Slf4j
@RestController
@ -41,8 +29,6 @@ import java.util.regex.Pattern;
@Validated
public class DemoController {
@Autowired
private FaceEngineService faceEngineService;
@Autowired
private MinioClient minioClient;
@ -54,152 +40,19 @@ public class DemoController {
@Autowired
private ProductEsMapper productEsMapper;
private final FileStorageService fileStorageService;
private final CategoryService categoryService;
private final RestTemplate restTemplate;
private final SwaggerAutoFillService swaggerAutoFillService;
@PostMapping("/uploadFile")
public Result<String> uploadFile(String bucketName, MultipartFile file){
return Result.success(minioUtil.getUrl(minioUtil.getFileUrl
(bucketName, minioUtil.uploadFile(bucketName, file))));
(bucketName, minioUtil.uploadFile(bucketName, file, "avatar"))));
}
@GetMapping("demo111")
public int saveTenPerson() {
try {
try {
GetObjectArgs args = GetObjectArgs.builder()
.bucket("videos")
.object("James Gosling.jpg")
.build();
//判断人脸照片是否合格
//1.保存到本地
InputStream tempInputStream = minioClient.getObject(args);
//----------------算法检测----------------------------------------------
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length;
while ((length = tempInputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, length);
}
byte[] bytes = outputStream.toByteArray();
outputStream.close();
tempInputStream.close();
ImageInfo rgbData = ImageFactory.getRGBData(bytes);
List<FaceInfo> faceInfoList = faceEngineService.detectFaces(rgbData);
if (CollectionUtil.isNotEmpty(faceInfoList)) {
FaceInfo faceInfo = faceInfoList.get(0);
FaceRecognitionResDTO faceRecognitionResDTO = new FaceRecognitionResDTO();
faceRecognitionResDTO.setRect(faceInfo.getRect());
byte[] featureBytes = faceEngineService.extractFaceFeature(rgbData, faceInfo, ExtractType.REGISTER);
if (featureBytes != null) {
System.out.println("featureBytes = " + Arrays.toString(featureBytes));
}else{
log.error("图片不合格,未检测到人脸");
return 2;
}
}else{
log.error("图片不合格,未检测到人脸");
return 2;
}
} catch (Exception e) {
e.printStackTrace();
return 10;
}
} catch (Exception e) {
e.printStackTrace();
}
return 1;
}
@GetMapping("/offline")
public Result activeOffline() {
FaceEngine faceEngine;
String USER_HOME = System.getProperty("user.home");
System.out.println(USER_HOME);
String jvmName = System.getProperty("java.vm.name", "").toLowerCase();
String osName = System.getProperty("os.name", "").toLowerCase();
String osArch = System.getProperty("os.arch", "").toLowerCase();
String abiType = System.getProperty("sun.arch.abi", "").toLowerCase();
String libPath = System.getProperty("sun.boot.library.path", "").toLowerCase();
USER_HOME = System.getProperty("user.home");
if (jvmName.startsWith("dalvik") && osName.startsWith("linux")) {
osName = "android";
} else if (jvmName.startsWith("robovm") && osName.startsWith("darwin")) {
osName = "ios";
osArch = "arm";
} else if (osName.startsWith("mac os x") || osName.startsWith("darwin")) {
osName = "macosx";
} else {
int spaceIndex = osName.indexOf(' ');
if (spaceIndex > 0) {
osName = osName.substring(0, spaceIndex);
}
}
if (osArch.equals("i386") || osArch.equals("i486") || osArch.equals("i586") || osArch.equals("i686")) {
osArch = "x86";
} else if (osArch.equals("amd64") || osArch.equals("x86-64") || osArch.equals("x64")) {
osArch = "x86_64";
} else if (osArch.startsWith("aarch64") || osArch.startsWith("armv8") || osArch.startsWith("arm64")) {
osArch = "arm64";
} else if ((osArch.startsWith("arm")) && ((abiType.equals("gnueabihf")) || (libPath.contains("openjdk-armhf")))) {
osArch = "armhf";
} else if (osArch.startsWith("arm")) {
osArch = "arm";
}
String PLATFORM = osName + "-" + osArch;
System.out.println("PLATFORM = " + PLATFORM);
String CACHE_LIB_FOLDER = USER_HOME + "/.arcface/cache/" + "4.1" + "/" + PLATFORM + "/";
System.out.println("CACHE_LIB_FOLDER = " + CACHE_LIB_FOLDER);
if (Pattern.matches("windows.*", osName)) {
System.out.println("osName = " + osName);
faceEngine = new FaceEngine(CACHE_LIB_FOLDER);
}else{
// faceEngine = new FaceEngine("D:\\00_桌面\\ArcSoft_ArcFacePro_windows_x64_java_V4.1\\libs\\WIN64");
faceEngine = new FaceEngine(CACHE_LIB_FOLDER);
}
ActiveDeviceInfo activeDeviceInfo = new ActiveDeviceInfo();
int code = faceEngine.getActiveDeviceInfo(activeDeviceInfo);
// System.out.println("\n设备信息" + activeDeviceInfo.toString() + "\n"); // 这个com.arcsoft.face.model.ActiveDeviceInfo@4ad9b7b并不是设备信息
System.out.println("设备信息:" + activeDeviceInfo.getDeviceInfo() + "\n"); // 这个的结果才是设备信息
ActiveFileInfo activeFileInfo = new ActiveFileInfo();
faceEngine.getActiveFileInfo(activeFileInfo);
System.out.println("激活文件信息:" + activeFileInfo.toString());
System.out.println("activeFileInfo = " + activeFileInfo);
return Result.success(activeDeviceInfo.getDeviceInfo());
}
@ -254,4 +107,41 @@ public class DemoController {
return Result.success("demo222");
}
@PostMapping("/mongoFile")
public void mongoFile(@RequestPart("file") MultipartFile file) throws IOException {
fileStorageService.storeFile(file);
}
@GetMapping("/mongoFile")
public void getMongoFile(String fileId) throws IOException {
GridFSFile file = fileStorageService.getFile(fileId);
System.out.println("file = " + file);
}
@GetMapping("/saveCategories")
public void saveCategories(String fileId) throws IOException {
categoryService.saveCategories();
}
@GetMapping("/getAllCategories")
public void getAllCategories() {
System.out.println("categoryService.getAllCategories() = " + categoryService.getAllCategories());
}
@GetMapping("/testSwagger")
public void testSwagger(){
Object forObject = restTemplate.getForObject("http://localhost:8084/v3/api-docs", Object.class);
System.out.println("forObject = " + forObject);
}
@GetMapping("/parseSwagger")
public void parseSwagger(){
swaggerAutoFillService.parseSwagger();
}
}

View File

@ -0,0 +1,310 @@
package com.guwan.backend.controller;
import com.github.dockerjava.api.command.CreateContainerCmd;
import com.github.dockerjava.api.command.CreateContainerResponse;
import com.github.dockerjava.api.command.ExecCreateCmdResponse;
import com.github.dockerjava.api.model.*;
import com.github.dockerjava.core.DockerClientBuilder;
import com.github.dockerjava.core.command.ExecStartResultCallback;
import com.github.dockerjava.httpclient5.ApacheDockerHttpClient;
import com.guwan.backend.pojo.bo.TestCaseBO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.github.dockerjava.api.DockerClient;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.TimeUnit;
@RestController
@RequestMapping("/api/common")
public class DockerController {
private final DockerClient dockerClient;
@Autowired
public DockerController(DockerClient dockerClient) {
this.dockerClient = dockerClient;
}
@GetMapping("/docker-info")
public String getDockerInfo() {
Info info = dockerClient.infoCmd().exec(); // 获取 Docker 守护进程的信息
System.out.println("info = " + info);
return info.toString(); // Docker 信息返回到 HTTP 响应
}
/**
* 获取镜像列表
*
* @param
* @return
*/
@GetMapping("/imageList")
public List<Image> imageList() {
List<Image> imageList = dockerClient.listImagesCmd().withShowAll(true).exec();
System.out.println("imageList = " + imageList);
return imageList;
}
@GetMapping("/listContainers")
public List<Container> listContainers() {
List<Container> exec = dockerClient.listContainersCmd().exec();
exec.forEach(container ->
System.out.println("name = " + Arrays.toString(container.getNames())));
return exec;
}
@GetMapping("/create")
public void create() throws IOException {
String javaFilePath = "D:\\00_桌面\\demo.java"; // 这里填入 Java 文件路径
File javaFile = new File(javaFilePath);
Path localDir = Files.createTempDirectory("docker-java");
File localJavaFile = new File(localDir.toFile(), "Main.java");
Files.copy(javaFile.toPath(), localJavaFile.toPath());
String imageName = "openjdk:8"; // 使用官方 OpenJDK 11 镜像
HostConfig hostConfig = HostConfig.newHostConfig()
.withBinds(new Bind(localDir.toString(), new com.github.dockerjava.api.model.Volume("/java"))); // 绑定挂载
// 启动容器
String containerId = dockerClient.createContainerCmd(imageName)
.withCmd("bash", "-c", "javac /java/Main.java && java -cp /java Main && tail -f /dev/null")
.withHostConfig(hostConfig)
.withName("java-app-container")
.exec().getId();
dockerClient.startContainerCmd(containerId).exec();
System.out.println("Container started. Check the output in Docker.");
}
private static final String IMAGE_NAME = "gcc:latest";
private static final int TIMEOUT_SECONDS = 10;
/* @GetMapping("/testCpp")
public void testCpp() throws IOException {
System.out.println("dockerClient = " + dockerClient);
try {
// 创建容器
CreateContainerResponse container = dockerClient.createContainerCmd(IMAGE_NAME)
.withTty(true)
.withWorkingDir("/app")
.exec();
String containerId = container.getId();
dockerClient.startContainerCmd(containerId).exec();
// 写入代码使用cat和here-doc避免转义问题
String cppCode = "#include <iostream>\n"
+ "int main() { std::cout << \"Hello Docker!\\n\"; return 0; }";
execCommand(dockerClient, containerId,
"sh -c 'cat > /app/main.cpp <<EOF\n" + cppCode + "\nEOF'");
// 编译代码使用相对路径
execCommand(dockerClient, containerId, "g++ main.cpp -o main");
// 执行程序使用相对路径
String output = execCommand(dockerClient, containerId, "./main");
System.out.println("程序输出: " + output);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// 清理容器
*//*dockerClient.listContainersCmd().withShowAll(true).exec().forEach(c ->
dockerClient.removeContainerCmd(c.getId()).withForce(true).exec());*//*
//dockerClient.close();
}
}
private static String execCommand(DockerClient dockerClient, String containerId, String command)
throws Exception {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ExecCreateCmdResponse exec = dockerClient.execCreateCmd(containerId)
.withCmd("sh", "-c", command)
.withAttachStdout(true)
.withAttachStderr(true)
.exec();
dockerClient.execStartCmd(exec.getId())
.exec(new ExecStartResultCallback(outputStream, System.err))
.awaitCompletion(TIMEOUT_SECONDS, TimeUnit.SECONDS);
return outputStream.toString().trim();
}*/
@GetMapping("/judge")
public ResponseEntity<Map<String, Object>> judgeCode() {
Map<String, Object> result = new HashMap<>();
List<TestCaseBO> testCases = new ArrayList<>();
// 真实用户提交的C++代码示例A+B问题
String cppCode =
"#include <iostream>\n" +
"using namespace std;\n" +
"int main() {\n" +
" int a, b;\n" +
" cin >> a >> b;\n" +
" cout << a + b << endl;\n" +
" return 0;\n" +
"}";
// 定义测试用例
testCases.add(new TestCaseBO("1 2", "3")); // 基础测试
testCases.add(new TestCaseBO("-5 8", "3")); // 负数测试
testCases.add(new TestCaseBO("1000000 2000000", "3000000")); // 大数测试
testCases.add(new TestCaseBO("", "")); // 错误输入测试
testCases.add(new TestCaseBO("3", "")); // 不完整输入测试
String containerId = null;
try {
// 创建容器使用官方GCC镜像
CreateContainerResponse container = dockerClient.createContainerCmd("gcc:latest")
.withTty(true)
.withWorkingDir("/app")
.withHostConfig(HostConfig.newHostConfig()
.withMemory(100 * 1024 * 1024L) // 限制内存100MB
.withCpuCount(1L)) // 限制1核CPU
.exec();
containerId = container.getId();
dockerClient.startContainerCmd(containerId).exec();
// 1. 写入代码文件
execCommand(containerId, "mkdir -p /app", 10);
execCommand(containerId,
"bash -c 'cat > /app/main.cpp <<EOF\n" +
cppCode.replace("'", "'\"'\"'") + // 处理单引号
"\nEOF'", 10);
// 2. 编译代码带编译错误检查
String compileOutput = execCommand(containerId, "g++ main.cpp -o main -O2 -Wall", 10);
if (!compileOutput.isEmpty()) {
result.put("status", "compile_error");
result.put("message", compileOutput);
return ResponseEntity.ok(result);
}
// 3. 执行测试用例
List<Map<String, Object>> caseResults = new ArrayList<>();
for (int i = 0; i < testCases.size(); i++) {
TestCaseBO testCase = testCases.get(i);
Map<String, Object> caseResult = new HashMap<>();
caseResult.put("case_id", i + 1);
caseResult.put("input", testCase.input);
caseResult.put("expected", testCase.expectedOutput);
try {
// 写入输入文件
execCommand(containerId,
"bash -c 'cat > /app/input.txt <<EOF\n" +
testCase.input.replace("'", "'\"'\"'") +
"\nEOF'", 10);
// 执行程序带超时控制
String actualOutput = execCommand(containerId,
"timeout 2s ./main < /app/input.txt 2>&1", // 捕获标准错误
3 // 超时3秒
);
// 标准化输出处理
actualOutput = actualOutput.trim()
.replaceAll("\r\n", "\n")
.replaceAll("[ \\t]+", " ");
// 结果对比
boolean isPassed = actualOutput.equals(testCase.expectedOutput);
caseResult.put("status", isPassed ? "passed" : "failed");
caseResult.put("actual", actualOutput);
// 特殊错误类型检测
if (actualOutput.contains("Timeout")) {
caseResult.put("status", "time_limit_exceeded");
} else if (actualOutput.contains("runtime error")) {
caseResult.put("status", "runtime_error");
} else if (actualOutput.isEmpty()) {
caseResult.put("status", "no_output");
}
} catch (Exception e) {
caseResult.put("status", "system_error");
caseResult.put("message", e.getMessage());
}
caseResults.add(caseResult);
}
result.put("status", "completed");
result.put("cases", caseResults);
result.put("pass_count", caseResults.stream()
.filter(c -> "passed".equals(c.get("status")))
.count());
} catch (Exception e) {
result.put("status", "system_error");
result.put("message", e.getMessage());
} finally {
if (containerId != null) {
dockerClient.removeContainerCmd(containerId)
.withForce(true)
.exec();
}
}
return ResponseEntity.ok(result);
}
// 辅助方法执行容器命令
private String execCommand(String containerId, String command, int timeoutSeconds)
throws Exception {
ByteArrayOutputStream output = new ByteArrayOutputStream();
ExecCreateCmdResponse exec = dockerClient.execCreateCmd(containerId)
.withCmd("bash", "-c", command)
.withAttachStdout(true)
.withAttachStderr(true)
.exec();
dockerClient.execStartCmd(exec.getId())
.exec(new ExecStartResultCallback(output, output))
.awaitCompletion(timeoutSeconds, TimeUnit.SECONDS);
return output.toString().trim();
}
// 测试用例类
}

View File

@ -0,0 +1,81 @@
package com.guwan.backend.controller;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.CreateContainerResponse;
import com.github.dockerjava.api.command.ExecCreateCmdResponse;
import com.github.dockerjava.core.DockerClientBuilder;
import com.github.dockerjava.core.command.ExecStartResultCallback;
import com.github.dockerjava.httpclient5.ApacheDockerHttpClient;
import lombok.RequiredArgsConstructor;
import java.io.ByteArrayOutputStream;
import java.net.URI;
import java.util.concurrent.TimeUnit;
@RequiredArgsConstructor
public class DockerCppRunner {
private static final String IMAGE_NAME = "cpp-runner";
private static final int TIMEOUT_SECONDS = 10;
private final DockerClient dockerClient;
public static void main(String[] args) throws Exception {
DockerClient dockerClient = DockerClientBuilder.getInstance()
.withDockerHttpClient(
new ApacheDockerHttpClient.Builder()
.dockerHost(URI.create("tcp://localhost:2375"))
.build()
).build();
try {
// 创建容器
CreateContainerResponse container = dockerClient.createContainerCmd(IMAGE_NAME)
.withTty(true)
.exec();
String containerId = container.getId();
// 启动容器
dockerClient.startContainerCmd(containerId).exec();
// 写入 C++ 代码到容器内
String cppCode = "#include <iostream>\n"
+ "int main() { std::cout << \"Hello Docker!\\n\"; return 0; }";
// 将代码写入容器中的文件
dockerClient.execCreateCmd(containerId)
.withCmd("sh", "-c", "echo '" + cppCode + "' > /app/main.cpp")
.exec();
// 编译 C++ 代码
execCommand(dockerClient, containerId, "g++ /app/main.cpp -o /app/main");
// 运行编译后的程序
String output = execCommand(dockerClient, containerId, "/app/main");
System.out.println("程序输出: " + output);
} finally {
// 清理容器
dockerClient.listContainersCmd().withShowAll(true).exec().forEach(c ->
dockerClient.removeContainerCmd(c.getId()).withForce(true).exec());
dockerClient.close();
}
}
private static String execCommand(DockerClient dockerClient, String containerId, String command)
throws Exception {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ExecCreateCmdResponse exec = dockerClient.execCreateCmd(containerId)
.withCmd("sh", "-c", command)
.withAttachStdout(true)
.withAttachStderr(true)
.exec();
dockerClient.execStartCmd(exec.getId())
.exec(new ExecStartResultCallback(outputStream, System.err))
.awaitCompletion(TIMEOUT_SECONDS, TimeUnit.SECONDS);
return outputStream.toString().trim();
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,77 @@
package com.guwan.backend.controller;
import com.guwan.backend.common.Result;
import com.guwan.backend.config.Aiconfig;
import com.guwan.backend.security.CustomUserDetails;
import dev.langchain4j.community.model.dashscope.QwenChatModel;
import dev.langchain4j.community.model.dashscope.QwenStreamingChatModel;
import dev.langchain4j.model.chat.response.ChatResponse;
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import reactor.core.publisher.Flux;
/**
* ai 聊天控制层
*
* 1.聊天
* 2.会话管理
*
*/
@RequiredArgsConstructor
public class GPTController {
private final QwenChatModel qwenChatModel;
private final QwenStreamingChatModel qwenStreamingChatModel;
private final Aiconfig.Assistant assistant;
@GetMapping("/chatWithQwen")
public Result chatWithQwen(@RequestParam(value = "message", required = false) String message) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//Java 16+ 模式匹配写法
if (authentication != null && authentication.getPrincipal() instanceof CustomUserDetails userDetails) {
// sysLog.setUserId(userDetails.getUserId());
// sysLog.setUsername(userDetails.getUsername());
}
var response = assistant.chat("userId+chatId", message);
System.out.println("response = " + response);
return Result.success(response);
}
@GetMapping(value = "/testQwenStreaming", produces = "text/steam;charset=UTF-8")
public Flux<String> testQwenStreaming(@RequestParam(value = "message", required = false) String message) {
Flux<String> flux = Flux.create(fluxSink -> {
qwenStreamingChatModel.chat(message, new StreamingChatResponseHandler() {
//每一次流式响应的文本
@Override
public void onPartialResponse(String partialResponse) {
fluxSink.next(partialResponse);
}
//响应结束的文本
@Override
public void onCompleteResponse(ChatResponse chatResponse) {
fluxSink.complete();
}
@Override
public void onError(Throwable throwable) {
fluxSink.error(throwable);
}
});
});
return flux;
}
}

View File

@ -1,82 +0,0 @@
//package com.guwan.backend.controller;
//
//import com.guwan.backend.entity.LiveRoom;
//import com.guwan.backend.entity.LiveRoomDTO;
//import com.guwan.backend.service.LiveService;
//import com.guwan.backend.util.Result;
//import com.guwan.backend.util.SecurityUtil;
//import lombok.RequiredArgsConstructor;
//import lombok.extern.slf4j.Slf4j;
//import org.springframework.web.bind.annotation.*;
//import io.swagger.v3.oas.annotations.Operation;
//import io.swagger.v3.oas.annotations.media.Schema;
//import io.swagger.v3.oas.annotations.security.SecurityRequirement;
//import io.swagger.v3.oas.annotations.tags.Tag;
//
//@Slf4j
//@Tag(name = "直播管理", description = "直播相关接口")
//@RestController
//@RequestMapping("/api/live")
//@RequiredArgsConstructor
//public class LiveController {
//
// private final LiveService liveService;
// private final SecurityUtil securityUtil;
//
// @Operation(summary = "创建直播间")
// @SecurityRequirement(name = "bearer-jwt")
// @PostMapping("/room")
// public Result<LiveRoom> createLiveRoom(@RequestBody LiveRoomDTO dto) {
// try {
// return Result.success(liveService.createLiveRoom(dto));
// } catch (Exception e) {
// log.error("创建直播间失败", e);
// return Result.error(e.getMessage());
// }
// }
//
// @Operation(summary = "开始直播")
// @SecurityRequirement(name = "bearer-jwt")
// @PostMapping("/room/{id}/start")
// public Result<Void> startLive(@PathVariable Long id) {
// try {
// // 检查权限
// checkPermission(id);
// liveService.startLive(id);
// return Result.success();
// } catch (Exception e) {
// log.error("开始直播失败", e);
// return Result.error(e.getMessage());
// }
// }
//
// @Operation(summary = "结束直播")
// @SecurityRequirement(name = "bearer-jwt")
// @PostMapping("/room/{id}/end")
// public Result<Void> endLive(@PathVariable Long id) {
// try {
// // 检查权限
// checkPermission(id);
// liveService.endLive(id);
// return Result.success();
// } catch (Exception e) {
// log.error("结束直播失败", e);
// return Result.error(e.getMessage());
// }
// }
//
// /**
// * 检查权限
// */
// private void checkPermission(Long roomId) {
// LiveRoom room = liveService.getLiveRoom(roomId);
// if (room == null) {
// throw new IllegalArgumentException("直播间不存在");
// }
//
// Long currentUserId = securityUtil.getCurrentUserId();
// if (!room.getUserId().equals(currentUserId)) {
// throw new IllegalStateException("无权操作此直播间");
// }
// }
//}

View File

@ -1,80 +0,0 @@
package com.guwan.backend.controller;
import com.guwan.backend.common.Result;
import com.guwan.backend.dto.live.CreateRoomRequest;
import com.guwan.backend.dto.live.StartLiveRequest;
import com.guwan.backend.dto.live.WebRTCRequest;
import com.guwan.backend.dto.live.WebRTCResponse;
import com.guwan.backend.entity.LiveRoom;
import com.guwan.backend.service.LiveStreamService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
@Slf4j
@Tag(name = "直播管理", description = "直播相关接口")
@RestController
@RequestMapping("/api/live")
@RequiredArgsConstructor
public class LiveStreamController {
private final LiveStreamService liveStreamService;
@Operation(summary = "创建直播间")
@PostMapping("/room")
public Result<LiveRoom> createLiveRoom(@RequestBody CreateRoomRequest request) {
try {
LiveRoom room = liveStreamService.createLiveRoom(
request.getTitle(),
request.getDescription(),
request.getUserId()
);
return Result.success(room);
} catch (Exception e) {
log.error("创建直播间失败", e);
return Result.error(e.getMessage());
}
}
@Operation(summary = "开始直播")
@PostMapping("/room/{id}/start")
public Result<Void> startLive(
@PathVariable Long id,
@RequestBody StartLiveRequest request) {
try {
liveStreamService.startLive(id, request.getSourceUrl());
return Result.success();
} catch (Exception e) {
log.error("开始直播失败", e);
return Result.error(e.getMessage());
}
}
@Operation(summary = "结束直播")
@PostMapping("/room/{id}/end")
public Result<Void> endLive(@PathVariable Long id) {
try {
liveStreamService.endLive(id);
return Result.success();
} catch (Exception e) {
log.error("结束直播失败", e);
return Result.error(e.getMessage());
}
}
@Operation(summary = "获取WebRTC Offer")
@PostMapping("/room/{id}/webrtc")
public Result<WebRTCResponse> getWebRTCOffer(
@PathVariable Long id,
@RequestBody WebRTCRequest request) {
try {
String sdp = liveStreamService.getWebRTCOffer(id, request.getSdp());
return Result.success(new WebRTCResponse(sdp));
} catch (Exception e) {
log.error("获取WebRTC Offer失败", e);
return Result.error(e.getMessage());
}
}
}

View File

@ -0,0 +1,47 @@
package com.guwan.backend.controller;
import cn.easyes.core.biz.EsPageInfo;
import com.guwan.backend.elasticsearch.document.LogstashLog;
import com.guwan.backend.service.LogstashLogService;
import lombok.RequiredArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
import java.util.List;
@RestController
@RequestMapping("/api/common/logs")
@RequiredArgsConstructor
public class LogController {
private final LogstashLogService logstashLogService;
@GetMapping("/search")
public List<LogstashLog> searchLogs(
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date startTime,
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date endTime,
@RequestParam(required = false) String level,
@RequestParam(required = false) String keyword) {
if (keyword != null) {
return logstashLogService.searchLogsByKeyword(keyword, startTime, endTime);
} else {
return logstashLogService.queryLogsByTimeRangeAndLevel(startTime, endTime, level);
}
}
@GetMapping("/errors")
public EsPageInfo<LogstashLog> getRecentErrors(
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "10") int pageSize) {
return logstashLogService.getRecentErrorLogs(pageNum, pageSize);
}
@GetMapping
public EsPageInfo<LogstashLog> getLogs(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "20") int size) {
return logstashLogService.getLogsList(page, size);
}
}

View File

@ -0,0 +1,34 @@
package com.guwan.backend.controller;
import com.guwan.backend.common.Result;
import com.guwan.backend.util.MinioUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@RestController
@RequestMapping("/minio")
@RequiredArgsConstructor
public class MinioController {
private final MinioUtil minioUtil;
@PostMapping("/uploadBase64Image")
public Result uploadBase64Image(@RequestParam String bucketName,
@RequestParam String base64Image,
@RequestParam String folder){
String fileName = minioUtil.uploadBase64Image(bucketName, base64Image, folder);
return Result.success(fileName);
}
@PostMapping("/uploadFile")
public Result uploadFile(@RequestParam String bucketName,
@RequestPart("file") MultipartFile file,
@RequestParam String folder) {
String fileName = minioUtil.uploadFile(bucketName, file, folder);
String fileUrl = minioUtil.getFileUrl(bucketName, fileName);
String url = minioUtil.getUrl(fileUrl);
return Result.success(url);
}
}

View File

@ -0,0 +1,114 @@
package com.guwan.backend.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.guwan.backend.annotation.OperationLog;
import com.guwan.backend.common.Result;
import com.guwan.backend.common.SearchResult;
import com.guwan.backend.pojo.entity.Papers;
import com.guwan.backend.service.PapersService;
import com.guwan.backend.util.UUIDUtil;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 试卷控制层
*
* @author Guwan
* @since 2025-01-11 12:06:49
*/
@RestController
@RequestMapping("/api/common/papers")
@RequiredArgsConstructor
public class PapersController {
/**
* 服务对象
*/
private final PapersService papersService;
/**
* 分页查询
*
* @param papers 筛选条件
* @param pageRequest 分页对象
* @return 查询结果
*/
@GetMapping
public SearchResult<List<Papers>> queryByPage(@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "1") int size) {
Page<Papers> papersPage = new Page<>(page, size);
// 使用分页查询注意 lambdaQuery().page() 方法
IPage<Papers> resultPage = papersService.lambdaQuery()
.page(papersPage);
return SearchResult.success(resultPage.getRecords(), resultPage.getTotal());
}
/**
* 通过主键查询单条数据
*
* @param id 主键
* @return 单条数据
*/
@GetMapping("{id}")
@Operation(summary = "获取单张试卷信息")
@OperationLog(description = "获取单张试卷信息")
public Result<Papers> queryById(@PathVariable("id") Integer id) {
return Result.success(this.papersService.getById(id));
}
/**
* 新增数据
*
* @param papers 实体
* @return 新增结果
*/
@PostMapping
@Operation(summary = "创建试卷")
@OperationLog(description = "创建试卷")
public Result<Papers> add(@RequestBody Papers papers) {
papers.setPaperUuid(UUIDUtil.uuid());
papersService.save(papers);
return Result.success();
}
/**
* 编辑数据
*
* @param papers 实体
* @return 编辑结果
*/
@PutMapping
public Result<Papers> edit(Papers papers) {
//return Result.success(this.papersService.update(papers));
return Result.success();
}
/**
* 删除数据
*
* @param id 主键
* @return 删除是否成功
*/
@DeleteMapping
public Result<Boolean> deleteById(Integer id) {
return Result.success(this.papersService.removeById(id));
}
}

View File

@ -1,63 +0,0 @@
package com.guwan.backend.controller;
import com.guwan.backend.common.Result;
import com.guwan.backend.dto.product.ProductDTO;
import com.guwan.backend.service.ProductSearchService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Slf4j
@Tag(name = "商品搜索", description = "商品搜索相关接口")
@RestController
@RequestMapping("/api/products")
@RequiredArgsConstructor
public class ProductController {
@Autowired(required = false)
private ProductSearchService productSearchService;
@Operation(summary = "保存商品")
@PostMapping
public Result<ProductDTO> save(@RequestBody ProductDTO product) {
try {
if (productSearchService == null) {
return Result.error("搜索服务未启用");
}
productSearchService.saveOrUpdate(product);
return Result.success(product);
} catch (Exception e) {
log.error("保存商品失败", e);
return Result.error(e.getMessage());
}
}
@Operation(summary = "搜索商品")
@GetMapping("/search")
public Result<List<ProductDTO>> search(
@Parameter(description = "搜索关键词") @RequestParam String keyword) {
try {
return Result.success(productSearchService.search(keyword));
} catch (Exception e) {
log.error("搜索商品失败", e);
return Result.error(e.getMessage());
}
}
@Operation(summary = "按分类查询商品")
@GetMapping("/category/{category}")
public Result<List<ProductDTO>> getByCategory(
@Parameter(description = "商品分类") @PathVariable String category) {
try {
return Result.success(productSearchService.getByCategory(category));
} catch (Exception e) {
log.error("查询商品分类失败", e);
return Result.error(e.getMessage());
}
}
}

View File

@ -0,0 +1,111 @@
package com.guwan.backend.controller;
import com.guwan.backend.common.Result;
import com.guwan.backend.pojo.entity.Questions;
import com.guwan.backend.service.QuestionsService;
import com.guwan.backend.util.MultipleChoiceCompare;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.web.bind.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
/**
* (Questions)表控制层
*
* @author Guwan
* @since 2025-01-11 18:02:34
*/
@RestController
@RequestMapping("/api/common/questions")
public class QuestionsController {
/**
* 服务对象
*/
@Autowired
private QuestionsService questionsService;
/**
* 分页查询
*
* @param questions 筛选条件
* @param pageRequest 分页对象
* @return 查询结果
*/
@GetMapping
public Result<Page<Questions>> queryByPage(Questions questions, PageRequest pageRequest) {
// return Result.success(this.questionsService.queryByPage(questions, pageRequest));
return Result.success();
}
/**
* 通过主键查询单条数据
*
* @param id 主键
* @return 单条数据
*/
@GetMapping("{id}")
public Result<Questions> queryById(@PathVariable("id") Integer id) {
return Result.success(this.questionsService.getById(id));
}
/**
* 新增数据
*
* @param questions 实体
* @return 新增结果
*/
@PostMapping
public Result<Questions> add(Questions questions) {
this.questionsService.save(questions);
return Result.success();
}
/**
* 编辑数据
*
* @param questions 实体
* @return 编辑结果
*/
@PutMapping
public Result<Questions> edit(Questions questions) {
//return Result.success(this.questionsService.update(questions));
return Result.success();
}
/**
* 删除数据
*
* @param id 主键
* @return 删除是否成功
*/
@DeleteMapping
public Result<Boolean> deleteById(Integer id) {
return Result.success(this.questionsService.removeById(id));
}
@GetMapping("/compare")
public Result compare(Integer id) {
Questions question = questionsService.getById(id);
System.out.println(MultipleChoiceCompare.compare(question.getPoint(), question.getAnswer(),
"A"));
System.out.println(MultipleChoiceCompare.compare(question.getPoint(), question.getAnswer(),
"A, B"));
System.out.println(MultipleChoiceCompare.compare(question.getPoint(), question.getAnswer(),
"A, C"));
return Result.success();
}
}

View File

@ -0,0 +1,26 @@
package com.guwan.backend.controller;
import com.guwan.backend.common.Result;
import com.guwan.backend.pojo.dto.SubmitReviewDTO;
import com.guwan.backend.service.ReviewService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/review")
@RequiredArgsConstructor
public class ReviewController {
private final ReviewService reviewService;
@PostMapping("/submitReview")
public Result submitReview(@RequestBody SubmitReviewDTO submitReviewDTO){
System.out.println("submitReviewDTO = " + submitReviewDTO);
reviewService.submitReview(submitReviewDTO);
return Result.success();
}
}

View File

@ -0,0 +1,16 @@
package com.guwan.backend.controller;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/setAuthentication")
@RequiredArgsConstructor
public class SetController {
@GetMapping
public void setAuthentication(){
}
}

View File

@ -0,0 +1,13 @@
package com.guwan.backend.controller;
// 测试用例类
class TestCase {
String input; // 输入数据
String expectedOutput; // 预期输出
int timeoutSeconds = 2;// 超时时间
public TestCase(String input, String expectedOutput) {
this.input = input;
this.expectedOutput = expectedOutput;
}
}

View File

@ -1,15 +1,15 @@
/*
package com.guwan.backend.controller;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.RandomUtil;
import com.guwan.backend.common.Result;
import com.guwan.backend.dto.user.*;
import com.guwan.backend.pojo.dto.user.*;
import com.guwan.backend.service.EmailService;
import com.guwan.backend.service.UserService;
import com.guwan.backend.util.RedisUtils;
import com.guwan.backend.util.SmsUtils;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Email;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
@ -98,9 +98,13 @@ public class UserController {
log.info("邮箱注册: {}", email);
Context context = new Context();
context.setVariable("nowDate", DateUtil.now());
String code = RandomUtil.randomNumbers(6);
redisUtils.set(email, code, 10);
context.setVariable("code", code.toCharArray());
emailService.sendHtmlMessage(email,
@ -112,7 +116,9 @@ public class UserController {
@PostMapping("/getPhoneCode")
public Result registerByPhone(@RequestBody @Valid PhoneDto phoneDto) throws Exception {
String phone = phoneDto.getPhone();
log.info("手机号注册: {}", phone);
String random = RandomUtil.randomNumbers(6);
SmsUtils.sendMessage(phone, random);
@ -127,9 +133,14 @@ public class UserController {
@PostMapping("/password/reset")
public Result<Void> resetPassword(@RequestParam @Email String email) {
public Result<Void> resetPassword(@RequestBody ChangePasswordDTO changePasswordDTO) {
log.debug("更改方式: {}, 内容: {}",
changePasswordDTO.getChangeWay(),
changePasswordDTO.getInfo());
try {
userService.resetPassword(email);
userService.resetPassword(changePasswordDTO);
return Result.success();
} catch (Exception e) {
log.error("重置密码失败", e);
@ -148,4 +159,4 @@ public class UserController {
return Result.error(e.getMessage());
}
}
}
} */

View File

@ -1,130 +0,0 @@
package com.guwan.backend.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.guwan.backend.common.Result;
import com.guwan.backend.dto.video.VideoDTO;
import com.guwan.backend.dto.video.VideoUploadDTO;
import com.guwan.backend.service.VideoService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@Slf4j
@Tag(name = "视频管理", description = "视频相关接口")
@RestController
@RequestMapping("/api/videos")
@RequiredArgsConstructor
public class VideoController {
private final VideoService videoService;
@Operation(summary = "上传视频", description = "上传视频文件并返回视频信息")
// @SecurityRequirement(name = "bearer-jwt")
@PostMapping("/upload")
public Result<VideoDTO> uploadVideo(@RequestBody VideoUploadDTO videoUploadDTO) {
try {
VideoDTO video = videoService.uploadVideo(videoUploadDTO.getFileUrl(),
videoUploadDTO.getTitle(), videoUploadDTO.getDescription(), videoUploadDTO.getTags());
return Result.success(video);
} catch (Exception e) {
log.error("上传视频失败", e);
return Result.error(e.getMessage());
}
}
@Operation(summary = "更新视频信息")
@SecurityRequirement(name = "bearer-jwt")
@PutMapping("/{id}")
public Result<VideoDTO> updateVideo(
@Parameter(description = "视频ID") @PathVariable Long id,
@RequestBody VideoDTO videoDTO) {
try {
videoDTO.setId(id);
return Result.success(videoService.updateVideo(videoDTO));
} catch (Exception e) {
log.error("更新视频失败", e);
return Result.error(e.getMessage());
}
}
@Operation(summary = "删除视频")
@SecurityRequirement(name = "bearer-jwt")
@DeleteMapping("/{id}")
public Result<Void> deleteVideo(
@Parameter(description = "视频ID") @PathVariable Long id) {
try {
videoService.deleteVideo(id);
return Result.success();
} catch (Exception e) {
log.error("删除视频失败", e);
return Result.error(e.getMessage());
}
}
@Operation(summary = "获取视频详情")
@GetMapping("/{id}")
public Result<VideoDTO> getVideo(
@Parameter(description = "视频ID") @PathVariable Long id) {
try {
VideoDTO video = videoService.getVideoById(id);
if (video == null) {
return Result.notFound("视频不存在");
}
return Result.success(video);
} catch (Exception e) {
log.error("获取视频失败", e);
return Result.error(e.getMessage());
}
}
@Operation(summary = "获取视频列表", description = "支持分页和关键词搜索")
@GetMapping
public Result<IPage<VideoDTO>> getVideoList(
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer pageNum,
@Parameter(description = "每页条数") @RequestParam(defaultValue = "10") Integer pageSize,
@Parameter(description = "搜索关键词") @RequestParam(required = false) String keyword) {
try {
return Result.success(videoService.getVideoList(pageNum, pageSize, keyword));
} catch (Exception e) {
log.error("获取视频列表失败", e);
return Result.error(e.getMessage());
}
}
@Operation(summary = "增加视频观看次数")
@PostMapping("/{id}/view")
public Result<Void> incrementViewCount(
@Parameter(description = "视频ID") @PathVariable Long id) {
try {
videoService.incrementViewCount(id);
return Result.success();
} catch (Exception e) {
log.error("增加观看次数失败", e);
return Result.error(e.getMessage());
}
}
@Operation(summary = "视频点赞/取消点赞")
@SecurityRequirement(name = "bearer-jwt")
@PostMapping("/{id}/like")
public Result<Void> toggleLike(
@Parameter(description = "视频ID") @PathVariable Long id) {
try {
videoService.toggleLike(id);
return Result.success();
} catch (Exception e) {
log.error("点赞失败", e);
return Result.error(e.getMessage());
}
}
}

View File

@ -0,0 +1,53 @@
package com.guwan.backend.controller.monitor;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.stats.CacheStats;
import com.guwan.backend.annotation.OperationLog;
import com.guwan.backend.common.BusinessException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/api/cache")
public class CacheMonitor {
private final CacheManager cacheManager;
public CacheMonitor(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
@GetMapping("/stats")
@OperationLog(description = "根据名称获取本地缓存使用情况")
public Map<String, Object> getStats(String cacheName) {
CaffeineCacheManager caffeineCacheManager = (CaffeineCacheManager) cacheManager;
Cache<Object, Object> nativeCache = null;
try {
nativeCache = (Cache<Object, Object>)
caffeineCacheManager.getCache(cacheName).getNativeCache();
} catch (Exception e) {
// throw new RuntimeException(e);
throw new BusinessException("没有此本地缓存");
}
CacheStats stats = nativeCache.stats();
Map<String, Object> result = new HashMap<>();
result.put("hitCount 命中次数(成功从缓存中获取数据的次数)", stats.hitCount());
result.put("missCount 未命中次数(缓存中没有找到数据的次数)", stats.missCount());
result.put("loadCount 加载次数(需要从数据源重新加载数据的次数)", stats.loadCount());
result.put("evictionCount 驱逐次数(由于空间限制而被清除的缓存项数量)", stats.evictionCount());
result.put("hitRate 命中率", String.format("%.2f%%", stats.hitRate() * 100));
return result;
}
}

View File

@ -0,0 +1,38 @@
/*
package com.guwan.backend.converter;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
import org.springframework.stereotype.Component;
import java.lang.reflect.Type;
@Component
public class MultipartJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter {
*/
/**
* converter for support http request with header content-type: multipart/form-data
*//*
public MultipartJackson2HttpMessageConverter(ObjectMapper objectMapper) {
super(objectMapper, MediaType.APPLICATION_OCTET_STREAM);
}
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return false;
}
@Override
public boolean canWrite(Type type, Class<?> clazz, MediaType mediaType) {
return false;
}
@Override
protected boolean canWrite(MediaType mediaType) {
return false;
}
}*/

View File

@ -0,0 +1,59 @@
package com.guwan.backend.core;
import org.dozer.DozerBeanMapper;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 简单封装Dozer, 实现深度转换Bean<->Bean的Mapper.实现:
*
* 1. 持有Mapper的单例.
* 2. 返回值类型转换.
* 3. 批量转换Collection中的所有对象.
* 4. 区分创建新的B对象与将对象A值复制到已存在的B对象两种函数.
*
*/
public class BeanMapper {
/**
* 持有Dozer单例, 避免重复创建DozerMapper消耗资源.
*/
private static DozerBeanMapper dozerBeanMapper = new DozerBeanMapper();
/**
* 基于Dozer转换对象的类型.
*/
public static <T> T map(Object source, Class<T> destinationClass) {
return dozerBeanMapper.map(source, destinationClass);
}
/**
* 基于Dozer转换Collection中对象的类型.
*/
public static <T> List<T> mapList(Iterable<?> sourceList, Class<T> destinationClass) {
List<T> destinationList = new ArrayList();
for (Object sourceObject : sourceList) {
T destinationObject = dozerBeanMapper.map(sourceObject, destinationClass);
destinationList.add(destinationObject);
}
return destinationList;
}
/**
* 基于Dozer将对象A的值拷贝到对象B中.
*/
public static void copy(Object source, Object destinationObject) {
if(source!=null) {
dozerBeanMapper.map(source, destinationObject);
}
}
public static <T, S> List<T> mapList(Collection<S> source, Function<? super S, ? extends T> mapper) {
return source.stream().map(mapper).collect(Collectors.toList());
}
}

View File

@ -0,0 +1,21 @@
package com.guwan.backend.core.annon;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 数据字典注解
* @author bool
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Dict {
String dicCode();
String dicText() default "";
String dictTable() default "";
}

Some files were not shown because too many files have changed in this diff Show More