Compare commits
63 Commits
Author | SHA1 | Date |
---|---|---|
|
d25063164b | |
|
1b4f330af6 | |
|
46adeb5622 | |
|
676cf30e44 | |
|
6d80269b2c | |
|
bebac77236 | |
|
37a7af487b | |
|
a3decaedb4 | |
|
e01fe45e8b | |
|
8a81b6bae3 | |
|
52f7f2c1c7 | |
|
36b70e9450 | |
|
bc26f13802 | |
|
80275af210 | |
|
0ce8b27eea | |
|
b2be9c9058 | |
|
1917337775 | |
|
6b3a6217b8 | |
|
470d3caf21 | |
|
53b536f2fc | |
|
8b8e5fa497 | |
|
e1a782fc44 | |
|
7fe4cda97b | |
|
1c6b2f75b6 | |
|
0c6404829d | |
|
756dea6abf | |
|
2863e90f07 | |
|
627b12f099 | |
|
45b2eea3d2 | |
|
9afa1cb1c4 | |
|
38a72d5ea2 | |
|
3738110395 | |
|
629678241f | |
|
deed1bb13a | |
|
ce9823c369 | |
|
f6651db2b8 | |
|
17c571c61d | |
|
2423e27b34 | |
|
a9212be666 | |
|
d82bf584bb | |
|
a6a12a3b04 | |
|
89f8550dff | |
|
f51134ea29 | |
|
556802f406 | |
|
9693cfc524 | |
|
7f94911665 | |
|
91b482d0bc | |
|
fc7524589c | |
|
736b824d7f | |
|
4b5a912784 | |
|
f0e417f1d7 | |
|
4deba9f86c | |
|
102c9c4a38 | |
|
45512694b6 | |
|
1551cc1263 | |
|
9551f322f0 | |
|
1ffa7c4a75 | |
|
8154807f90 | |
|
256743d3b9 | |
|
bc378ee9ec | |
|
0350786d00 | |
|
378c4cbdf8 | |
|
85b2729a04 |
|
@ -1 +1 @@
|
|||
EWEPEPEOGMGTELIZJUGECKIUJDBCJTCNISGPBNHLJTJUBHEWGNAKGEGAIOHJDQAJGNCFDRFZJEDMJTGFGBEAAWGLBZAUCNHCCPBZCIIKBJATGYARHRAZHXFRBIEBCHIXAFDLFQBZFVJQGTCEHDIMIVGQEJAJIYHXHTISIVETBACMCCGFDPEKDQHYGGAPFXIOCJAAGOHYIFHNHZHWIGCZIGHHDMDXHQGTFLJOHGAYIVBGIHIQHHHJDJFWHMCRHJAMHZESGWGGAUJRGDHAGHIQITIJIUAXAEIZGBGZCCHJAHASGNCVIIJAIYFOEQGFELEIEDECJTCXBCAPIKHTFTHBAGJTERHQGSAFEUDIHLDDITDPIMACAOGTIIHFEMHLHIHYJTFDFDGWAKHJEIEIJFGABQJCIHIADYCXAHIMJTHOASFLGFFIBJJEHDHLHOCMDIGGDOJDHNBQCJENFUBAGOAZITIRJSFBBUCUHABLHRFVIUBWCADJCMDXDPATBPJSJRJLBRABGVFTDNFOAQDOARDEBRHJAQGYHIGMBDCJCSJKFBBLGECAEFEYCVCHAEAZJRIOFEHLCJILEHJVGYIVCWGHCMGJGLBTFMHFCAEAAUJQJLAEARDHDFHDBJJGALEHFNGSAIHOJUBOEAJDDFFYFTINITHTBNIJFDHLEAFGBFHFFQGHGFGREVFHFDCZGYEVBWAZDSCAGLDMIAAEFOAXIXFECSFQDWHFFHCFASFSGAHVJSDBBZJQAZBXARILBAJFJEHCANAIABBMECBJJFIOGYGHBXCUCVBDJOCYBZDZAJEXAXEPFRFOGVHQAOJLCYBOHFEKJFIJBDHDDCEAAUJVDRIGGGGCJOFVECAHAQFSBSGYJVGKCQDDHPGUCIARFAIEJGGDITAUDIIVBBJUEFCIDTGJGYJODRDEJPBMFNCXAKCPAIGOJQGHBZHQJUCOBKCKDPJSGGCVCAIWFVHIIEAJJMEFGRHZDEDACFBQJODGDVJBAUBXGKGCFZERAHIOALGKGAILDNHQGGAZDEIGATBTCWHMDKGSIWFMHAIZHREBJBEFENDFBRBLGLCMERJAEOBXCNDBHVCSJBDMEHCLJLCFFOGVGWATBOJBFJEQETHGESEXFDIIFDAGJPDNHEDSFNBRIVFMFPGOEEIHEFCOCKJGJAIZJIFTIGAWITGWDXGBEFDTJHFXBF
|
||||
EWEPEPEOGMGTELIZJUGECKIUJDBCJTCNISGPBNHLJTJUBHEWGNAKGEGAIOHJDQAJGNCFDRFZJEDMJTGKGSHREQHMATEHBXBBELGFHLHTHOEGBAEOIHCLEOHMBGAAGYCLCYAXCDJTCCEAIOJNEBAXFCBLJQCMJPFBHWAADIFPHXAFDQDXEHCODEIXBWDBCOEZIAFNBYGJFLJEFIHFIDETGZDMJAAYBVBWDKGOFEJDDXITHJCOJTHIHOIAETIBFLAWIZCBDNJSCBGXGQFLCOAEGXIHEKGXFPCAACCFGOGJIZISFMACEFJJAYDTIWCYHQFTDDGVANDNBICDBRJIBBIHHWFZHRHWJCBACHHSFPCKINHXHPEVDMAHAJBHHNBTFLEQEPDQIWCXBEGVEPCYECIFEHAMBQJIGUELIAAWHABFDTGYIPGDGOHVCJDFDDGUBTBECFFLIGJJBJJOGNJDHKIUBVHFIRAVHQGKJLBQIUAVFTICHLHFEAAGGAJODVIRINBWHXEDAWCMGOIHDZCNAFIEEEGXJMHXBAFXCJGNDSCOFPGLFNBQDRATGQEMAMDHHCEDJRDAITICJNJSJADSBAAVHNERBZJIAOIUAVIHFODFJDBQJBIPEXJOCXDSEAJUBXECHVCQGJGGFRIPIVGVHPBRAUIADJEWBPHAHXBICDBKELCIGEIBFLHHGFHNDDDMIRBYIIDODBDYBAIPBQAWHFAVFMJRGHHCDYJGDQJACYHGAXGSIPAGIWFJDRBRGXHJESHZHPAGDKGWFUIKJOEPITFCEXELAVITCPBFGXHNEDANGLDLCMDRHBEAEHEYIKAJDSDFCMGQIJAWHAGOJNFYIHDWACBTGBIHENHJFNITEWCIBPCEIYDVFCJQGZHDILIUGYFJBAGTICJVDYJLGIBMJAAUFIJNIEERJOIMDCFAANCEEXHBBBGNESJKCMDYDBGOBCBVEDAFCBIQAKHVHUIUEOHVBPCJIRDGIFCQBWAFACELFSGYEMENEUITANIAIMBLBAAQGCHOGFDSENIFEWFVEYHZIVEUGXAXGCHSCXEOALHQGPCYDIBRHYHCEFAFHNAMCVHBBUFPJMHPDXIHBXGXJPFRCHBPGPFWABCKIEHOIZFLJECTERFOHAHZGXGLIJAAFBCRDCGOFPBCIGJPBXJMEYGQDVAXHMJGBIIOCLDYJQHNJMECBK
|
|
@ -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?
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
|
@ -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).
|
|
@ -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>
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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 |
|
@ -0,0 +1,10 @@
|
|||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 可以移除或保留样式 */
|
||||
</style>
|
|
@ -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'
|
||||
})
|
||||
}
|
|
@ -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'
|
||||
})
|
||||
}
|
|
@ -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 |
|
@ -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>
|
|
@ -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>
|
|
@ -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')
|
|
@ -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 }
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -0,0 +1,10 @@
|
|||
<template>
|
||||
<div>
|
||||
<h1>仪表盘</h1>
|
||||
<p>欢迎来到管理后台!</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
// 未来可以在这里加载统计数据等
|
||||
</script>
|
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
11
|
||||
</template>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
|
@ -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"]
|
||||
}
|
|
@ -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/, '')
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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] ------------------------------------------------------------------------
|
|
@ -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
|
|
@ -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; // 当前用户是否已点赞
|
||||
|
||||
}
|
|
@ -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.
|
@ -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.
|
83
docs/rtc.md
83
docs/rtc.md
|
@ -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
|
|
@ -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
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
## 角色
|
||||
管理员<br>
|
||||
用户<br>
|
||||
教师<br>
|
||||
只要有多角色 就要设计 <br>
|
||||
RBAC——基于角色权限的模型
|
||||
|
||||
角色表 用户表 用户角色表 权限表 角色权限表 <br>
|
||||
|
||||
要做全控制
|
||||
表单 按钮 视图
|
||||
|
||||
merge和rebase
|
||||
|
||||
|
||||
|
250
pom.xml
250
pom.xml
|
@ -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>
|
||||
<!– 排除新依赖中的 logback-classic –>
|
||||
<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>
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
毕设后端
|
||||
<br>
|
||||
如果运行 暂时屏蔽face包
|
||||
|
||||
|
||||
mvn dependency:tree > deps.txt
|
||||
<br>
|
||||
分析依赖
|
||||
这次主要是日志的
|
||||
slf4j-reload4
|
||||
log4j-api
|
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"; // 错误提示消息
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"; // 业务唯一键
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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 =======================");
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
// }
|
||||
// }
|
||||
//}
|
|
@ -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; // 输出字节数
|
||||
// }
|
||||
//}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -9,9 +9,7 @@ import org.springframework.web.filter.CorsFilter;
|
|||
@Configuration
|
||||
public class CorsConfig {
|
||||
|
||||
//get请求变成了options 后端预检过不了怎么改
|
||||
|
||||
|
||||
//get请求变成options 后端预检无法通过怎么办 详见 securityFilterChain
|
||||
|
||||
@Bean
|
||||
public CorsFilter corsFilter() {
|
||||
|
|
|
@ -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("数据库初始化完成");
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
};
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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("*"); // 允许所有来源
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
);
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -12,19 +12,33 @@ public class SecurityConstants {
|
|||
* 这些路径可以直接访问,不需要认证
|
||||
*/
|
||||
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 相关资源
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -32,9 +46,9 @@ public class SecurityConstants {
|
|||
* 这些路径用于访问静态资源,不需要认证
|
||||
*/
|
||||
public static final List<String> STATIC_RESOURCES = List.of(
|
||||
"/static/**", // 静态资源目录
|
||||
"/public/**", // 公共资源目录
|
||||
"/error" // 错误页面
|
||||
"/static/**", // 静态资源目录
|
||||
"/public/**", // 公共资源目录
|
||||
"/error" // 错误页面
|
||||
|
||||
);
|
||||
}
|
|
@ -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(
|
||||
"傻瓜",
|
||||
"笨蛋",
|
||||
"白痴"
|
||||
);
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
// 测试用例类
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -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
|
@ -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;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -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("无权操作此直播间");
|
||||
// }
|
||||
// }
|
||||
//}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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(){
|
||||
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
} */
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}*/
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue