diff --git a/gym-manage-uniapp/api/groupCourse.js b/gym-manage-uniapp/api/groupCourse.js
new file mode 100644
index 0000000..8fb0d6e
--- /dev/null
+++ b/gym-manage-uniapp/api/groupCourse.js
@@ -0,0 +1,75 @@
+import request from "@/utils/request.js"
+
+export function getGroupCourseList(params = {}, options = {}) {
+ return request.get('/groupCourse/list', params, options)
+}
+
+export function getGroupCoursePage(params = {}, options = { cache: true, cacheTime: 5 * 60 * 1000 }) {
+ const { page = 0, size = 10, sort = 'id', order = 'asc', keyword } = params
+ return request.post('/groupCourse/page', { page, size, sort, order, keyword }, options)
+}
+
+export function getGroupCourseById(id, options = { cache: true, cacheTime: 15 * 60 * 1000 }) {
+ return request.get(`/groupCourse/${id}`, {}, options)
+}
+
+export function getGroupCourseDetail(id, options = { cache: true, cacheTime: 15 * 60 * 1000 }) {
+ return request.get(`/groupCourse/${id}/detail`, {}, options)
+}
+
+export function createGroupCourse(params) {
+ return request.post('/groupCourse', params)
+}
+
+export function updateGroupCourse(id, params) {
+ return request.put(`/groupCourse/${id}`, params)
+}
+
+export function cancelGroupCourse(id) {
+ return request.post(`/groupCourse/${id}/cancel`)
+}
+
+export function deleteGroupCourse(id) {
+ return request.delete(`/groupCourse/${id}`)
+}
+
+export function getGroupCourseTypes(params = {}, options = { cache: true, cacheTime: 10 * 60 * 1000 }) {
+ return request.get('/groupCourse/types', params, options)
+}
+
+export function getGroupCourseTypeById(id, options = { cache: true, cacheTime: 10 * 60 * 1000 }) {
+ return request.get(`/groupCourse/types/${id}`, {}, options)
+}
+
+export function getTypeLabels(typeId, options = { cache: true, cacheTime: 5 * 60 * 1000 }) {
+ return request.get(`/groupCourse/types/${typeId}/labels`, {}, options)
+}
+
+export function bookGroupCourse(params) {
+ return request.post('/groupCourse/book', params)
+}
+
+export function cancelBooking(bookingId, params) {
+ return request.post(`/groupCourse/booking/${bookingId}/cancel`, params)
+}
+
+export function getMemberBookings(memberId, options = {}) {
+ return request.get(`/groupCourse/bookings/member/${memberId}`, {}, options)
+}
+
+export default {
+ getGroupCourseList,
+ getGroupCoursePage,
+ getGroupCourseById,
+ getGroupCourseDetail,
+ createGroupCourse,
+ updateGroupCourse,
+ cancelGroupCourse,
+ deleteGroupCourse,
+ getGroupCourseTypes,
+ getGroupCourseTypeById,
+ getTypeLabels,
+ bookGroupCourse,
+ cancelBooking,
+ getMemberBookings
+}
\ No newline at end of file
diff --git a/gym-manage-uniapp/components/TabBar.vue b/gym-manage-uniapp/components/TabBar.vue
index 1ad9eb8..9b8d90b 100644
--- a/gym-manage-uniapp/components/TabBar.vue
+++ b/gym-manage-uniapp/components/TabBar.vue
@@ -91,11 +91,6 @@ function checkShouldShow() {
}
const shouldHide = HIDE_TABBAR_PAGES.includes(routePath)
shouldShowTabBar.value = !shouldHide
- console.log('=== TabBar 显示控制 ===')
- console.log('原始路径:', getCurrentRoutePath())
- console.log('标准化路径:', routePath)
- console.log('是否隐藏:', shouldHide)
- console.log('是否显示 TabBar:', shouldShowTabBar.value)
}
let routeWatcher = null
diff --git a/gym-manage-uniapp/components/groupCourse/CourseCard.vue b/gym-manage-uniapp/components/groupCourse/CourseCard.vue
index 2ff731e..4a2588a 100644
--- a/gym-manage-uniapp/components/groupCourse/CourseCard.vue
+++ b/gym-manage-uniapp/components/groupCourse/CourseCard.vue
@@ -47,9 +47,19 @@
-
-
- {{ formatDuration(course.startTime, course.endTime) }}
+
+
+
+ {{ formatDuration(course.startTime, course.endTime) }}
+
+
+ {{ label.labelName }}
+
@@ -109,6 +119,10 @@ const props = defineProps({
course: {
type: Object,
required: true
+ },
+ courseTypeLabels: {
+ type: Array,
+ default: () => []
}
})
@@ -150,9 +164,29 @@ const formatTime = (dateStr) => {
const formatDuration = (startStr, endStr) => {
if (!startStr || !endStr) return ''
+
const start = new Date(startStr)
const end = new Date(endStr)
- const minutes = Math.floor((end.getTime() - start.getTime()) / 60000)
+
+ if (isNaN(start.getTime()) || isNaN(end.getTime())) {
+ console.warn(`[CourseCard] 无效的时间格式: startTime=${startStr}, endTime=${endStr}`)
+ return ''
+ }
+
+ const diffMs = end.getTime() - start.getTime()
+ if (diffMs <= 0) {
+ console.warn(`[CourseCard] 结束时间小于或等于开始时间: startTime=${startStr}, endTime=${endStr}`)
+ return ''
+ }
+
+ const maxDurationMinutes = 24 * 60
+ const minutes = Math.floor(diffMs / 60000)
+
+ if (minutes > maxDurationMinutes) {
+ console.warn(`[CourseCard] 课程时长超过24小时,已修正: ${minutes}分钟 -> ${maxDurationMinutes}分钟`)
+ return `${maxDurationMinutes}分钟以上`
+ }
+
return `${minutes}分钟`
}
@@ -340,19 +374,35 @@ const handleBooking = () => {
flex: 1;
}
-/* 课程时长 */
-.course-duration {
+/* 课程标签区域 */
+.course-tags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12rpx;
+}
+
+/* 标签项 */
+.tag-item {
display: inline-flex;
align-items: center;
- gap: 8rpx;
+ gap: 6rpx;
padding: 10rpx 20rpx;
- background: linear-gradient(135deg, #EFF6FF 0%, #DBEAFE 100%);
border-radius: 20rpx;
font-size: 24rpx;
- color: #3B82F6;
font-weight: 500;
}
+/* 时长标签 */
+.duration-tag {
+ background: linear-gradient(135deg, #EFF6FF 0%, #DBEAFE 100%);
+ color: #3B82F6;
+}
+
+/* 类型标签 */
+.type-tag {
+ font-weight: 600;
+}
+
/* 卡片底部区域 */
.card-footer {
padding: 24rpx 32rpx 28rpx;
diff --git a/gym-manage-uniapp/components/index/CourseCard.vue b/gym-manage-uniapp/components/index/CourseCard.vue
new file mode 100644
index 0000000..c125221
--- /dev/null
+++ b/gym-manage-uniapp/components/index/CourseCard.vue
@@ -0,0 +1,273 @@
+
+
+
+
+
+
+
+
+
+
+ {{ course.tag }}
+
+
+
+ {{ course.name }}
+
+
+
+
+
+
+
+ {{ course.duration }}
+
+
+
+
+
+
+ {{ course.level }}
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gym-manage-uniapp/components/index/PageHeader.vue b/gym-manage-uniapp/components/index/PageHeader.vue
new file mode 100644
index 0000000..8fe980f
--- /dev/null
+++ b/gym-manage-uniapp/components/index/PageHeader.vue
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gym-manage-uniapp/components/index/RecommendCourses.vue b/gym-manage-uniapp/components/index/RecommendCourses.vue
index b02fc15..64d9d3f 100644
--- a/gym-manage-uniapp/components/index/RecommendCourses.vue
+++ b/gym-manage-uniapp/components/index/RecommendCourses.vue
@@ -6,7 +6,7 @@
推荐课程
-
+
查看更多
@@ -19,57 +19,12 @@
-
-
-
-
-
-
-
-
- {{ course.tag }}
-
-
-
- {{ course.name }}
-
-
-
-
-
-
-
- {{ course.duration }}
-
-
-
-
-
-
- {{ course.level }}
-
-
-
-
-
-
-
+ :course="course"
+ @join="handleJoinCourse"
+ />
@@ -78,6 +33,7 @@
\ No newline at end of file
diff --git a/gym-manage-uniapp/composables/useGroupCourseList.js b/gym-manage-uniapp/composables/useGroupCourseList.js
index d923875..8d70904 100644
--- a/gym-manage-uniapp/composables/useGroupCourseList.js
+++ b/gym-manage-uniapp/composables/useGroupCourseList.js
@@ -1,32 +1,27 @@
import { ref, computed } from 'vue'
-import { groupCourseService } from '@/request_api/groupCourse.mock.js'
+import { getGroupCoursePage, getTypeLabels } from '@/api/groupCourse.js'
export function useGroupCourseList() {
- // 分页相关
- const pageNum = ref(1)
+ const pageNum = ref(0)
const pageSize = ref(10)
const total = ref(0)
const totalPages = ref(0)
const loading = ref(false)
const hasMore = ref(true)
- // 团课列表数据
const courseList = ref([])
- // 搜索相关
const searchKeyword = ref('')
const hotKeywords = ref(['燃脂', '瑜伽', '单车', '普拉提', '高强度'])
- // 排序相关
const sortOptions = ref([
- { label: '默认排序', value: 'default' },
- { label: '价格从低到高', value: 'priceAsc' },
- { label: '价格从高到低', value: 'priceDesc' },
- { label: '剩余名额最多', value: 'spotsDesc' }
+ { label: '默认排序', value: 'id', order: 'asc' },
+ { label: '价格从低到高', value: 'storedValueAmount', order: 'asc' },
+ { label: '价格从高到低', value: 'storedValueAmount', order: 'desc' },
+ { label: '剩余名额最多', value: 'currentMembers', order: 'asc' }
])
const sortIndex = ref(0)
- // 时间段选择相关
const timePeriodOptions = ref([
{ label: '全部', value: 'all' },
{ label: '早上 (6-12 点)', value: 'morning', startHour: 6, endHour: 12 },
@@ -35,24 +30,20 @@ export function useGroupCourseList() {
])
const timePeriodIndex = ref(0)
- // 时间筛选相关
const showTimePicker = ref(false)
const startDate = ref('')
const endDate = ref('')
const timeRangeText = ref('')
- // 筛选后的课程列表
const filteredCourseList = computed(() => {
let result = [...courseList.value]
- // 关键词搜索
if (searchKeyword.value) {
result = result.filter(course =>
course.courseName.includes(searchKeyword.value)
)
}
- // 时间范围筛选
if (startDate.value || endDate.value) {
result = result.filter(course => {
const courseDate = course.startTime.split('T')[0]
@@ -62,7 +53,6 @@ export function useGroupCourseList() {
})
}
- // 时间段筛选
const timePeriod = timePeriodOptions.value[timePeriodIndex.value]
if (timePeriod.value !== 'all') {
result = result.filter(course => {
@@ -71,20 +61,18 @@ export function useGroupCourseList() {
})
}
- // 排序
- const sortType = sortOptions.value[sortIndex.value].value
- if (sortType === 'priceAsc') {
- result.sort((a, b) => (a.storedValueAmount || a.pointCardAmount) - (b.storedValueAmount || b.pointCardAmount))
- } else if (sortType === 'priceDesc') {
- result.sort((a, b) => (b.storedValueAmount || b.pointCardAmount) - (a.storedValueAmount || a.pointCardAmount))
- } else if (sortType === 'spotsDesc') {
- result.sort((a, b) => (b.maxMembers - b.currentMembers) - (a.maxMembers - a.currentMembers))
+ const sortType = sortOptions.value[sortIndex.value]
+ if (sortType.value !== 'id' || sortType.order !== 'asc') {
+ result.sort((a, b) => {
+ const valA = a[sortType.value] || 0
+ const valB = b[sortType.value] || 0
+ return sortType.order === 'asc' ? valA - valB : valB - valA
+ })
}
return result
})
- // 获取所有搜索参数
const getAllSearchParams = (searchBarRef, filterSectionRef, timePeriodRef, timeRangePickerRef) => {
const searchParams = searchBarRef?.getSearchParams?.() || { keyword: searchKeyword.value }
const filterParams = filterSectionRef?.getFilterParams?.() || { sortType: sortOptions.value[sortIndex.value].value }
@@ -102,27 +90,24 @@ export function useGroupCourseList() {
return allParams
}
- // 搜索处理
const handleSearch = (params) => {
console.log('[useGroupCourseList] 搜索触发:', params)
- uni.showToast({
- title: params.keyword ? `搜索:${params.keyword}` : '请输入关键词',
- icon: 'none'
- })
+ searchKeyword.value = params.keyword || ''
+ pageNum.value = 0
+ fetchCourseList()
}
- // 时间段变化处理
const onTimePeriodChange = (option) => {
console.log('[useGroupCourseList] 时间段选择:', option)
}
- // 时间范围确认处理
const onTimeRangeConfirm = (params) => {
console.log('[useGroupCourseList] 时间范围确认:', params)
+ startDate.value = params.startDate || ''
+ endDate.value = params.endDate || ''
timeRangeText.value = params.timeRangeText
}
- // 预约处理
const handleBooking = (course) => {
console.log('[useGroupCourseList] 预约课程:', course)
uni.showToast({
@@ -131,7 +116,6 @@ export function useGroupCourseList() {
})
}
- // 跳转详情
const goDetail = (courseId) => {
console.log('[useGroupCourseList] 跳转到课程详情:', courseId)
uni.navigateTo({
@@ -139,20 +123,33 @@ export function useGroupCourseList() {
})
}
- // 获取团课列表
const fetchCourseList = async (isLoadMore = false) => {
if (loading.value) return
loading.value = true
try {
- const result = await groupCourseService.getList({
- pageNum: pageNum.value,
- pageSize: pageSize.value
+ const sortOption = sortOptions.value[sortIndex.value]
+ console.log('[useGroupCourseList] 请求参数:', {
+ page: isLoadMore ? pageNum.value + 1 : pageNum.value,
+ size: pageSize.value,
+ sort: sortOption.value,
+ order: sortOption.order,
+ keyword: searchKeyword.value
+ })
+
+ const result = await getGroupCoursePage({
+ page: isLoadMore ? pageNum.value + 1 : pageNum.value,
+ size: pageSize.value,
+ sort: sortOption.value,
+ order: sortOption.order,
+ keyword: searchKeyword.value
})
- if (result.code === 0 && result.data) {
- const { list, total: totalCount, pageNum: currentPage, totalPages: pages } = result.data
+ console.log('[useGroupCourseList] 响应结果:', JSON.stringify(result, null, 2))
+
+ if (result && result.content) {
+ const { content: list, totalElements: totalCount, currentPage, totalPages: pages } = result
if (isLoadMore) {
courseList.value = [...courseList.value, ...list]
@@ -163,7 +160,7 @@ export function useGroupCourseList() {
total.value = totalCount
pageNum.value = currentPage
totalPages.value = pages
- hasMore.value = pageNum.value < totalPages.value
+ hasMore.value = currentPage < pages - 1
console.log('[useGroupCourseList] 团课列表获取成功:', {
total: total.value,
@@ -172,29 +169,41 @@ export function useGroupCourseList() {
hasMore: hasMore.value
})
} else {
- console.error('[useGroupCourseList] 获取团课列表失败:', result.message)
+ console.error('[useGroupCourseList] 获取团课列表失败:', {
+ result: result,
+ message: result?.message || '未知错误',
+ code: result?.code,
+ success: result?.success
+ })
}
} catch (error) {
- console.error('[useGroupCourseList] 获取团课列表异常:', error)
+ console.error('[useGroupCourseList] 获取团课列表异常 - 错误详情:', {
+ error: error,
+ message: error?.message || '无错误信息',
+ code: error?.code,
+ statusCode: error?.statusCode,
+ response: error?.response,
+ stack: error?.stack
+ })
+ uni.showToast({
+ title: `获取课程列表失败: ${error?.message || '网络错误'}`,
+ icon: 'none'
+ })
} finally {
loading.value = false
}
}
- // 加载更多
const loadMore = () => {
if (!hasMore.value || loading.value) return
- pageNum.value++
fetchCourseList(true)
}
- // 滚动到底部触发
const onScrollToLower = () => {
loadMore()
}
return {
- // 状态
pageNum,
pageSize,
total,
@@ -214,7 +223,6 @@ export function useGroupCourseList() {
timeRangeText,
filteredCourseList,
- // 方法
getAllSearchParams,
handleSearch,
onTimePeriodChange,
@@ -225,4 +233,4 @@ export function useGroupCourseList() {
loadMore,
onScrollToLower
}
-}
+}
\ No newline at end of file
diff --git a/gym-manage-uniapp/groupcourse-api.md b/gym-manage-uniapp/groupcourse-api.md
new file mode 100644
index 0000000..cad1497
--- /dev/null
+++ b/gym-manage-uniapp/groupcourse-api.md
@@ -0,0 +1,1518 @@
+# 团课管理模块 API 文档
+
+> **文档版本**: v1.0
+> **创建日期**: 2026-06-02
+> **作者**: 张翔
+> **状态**: 正式发布
+
+---
+
+## 📋 目录
+
+1. [概述](#概述)
+2. [基础路径](#基础路径)
+3. [团课管理接口](#团课管理接口)
+ - [获取所有团课](#获取所有团课)
+ - [分页获取团课](#分页获取团课)
+ - [根据ID获取团课详情](#根据ID获取团课详情)
+ - [创建团课](#创建团课)
+ - [更新团课](#更新团课)
+ - [取消团课](#取消团课)
+ - [团课签到](#团课签到)
+ - [删除团课](#删除团课)
+4. [团课预约接口](#团课预约接口)
+ - [预约团课](#预约团课)
+ - [取消预约](#取消预约)
+ - [查询会员预约记录](#查询会员预约记录)
+ - [查询预约详情](#查询预约详情)
+ - [查询课程预约记录](#查询课程预约记录)
+5. [团课类型管理接口](#团课类型管理接口)
+ - [获取所有团课类型](#获取所有团课类型)
+ - [根据ID获取团课类型](#根据ID获取团课类型)
+ - [搜索团课类型](#搜索团课类型)
+ - [根据分类获取团课类型](#根据分类获取团课类型)
+ - [获取所有分类](#获取所有分类)
+ - [创建团课类型](#创建团课类型)
+ - [更新团课类型](#更新团课类型)
+ - [删除团课类型](#删除团课类型)
+6. [团课标签管理接口](#团课标签管理接口)
+ - [获取所有标签](#获取所有标签)
+ - [根据ID获取标签](#根据ID获取标签)
+ - [搜索标签](#搜索标签)
+ - [获取类型的标签](#获取类型的标签)
+ - [创建标签](#创建标签)
+ - [更新标签](#更新标签)
+ - [删除标签](#删除标签)
+ - [为类型添加标签](#为类型添加标签)
+ - [从类型移除标签](#从类型移除标签)
+ - [清空类型标签](#清空类型标签)
+7. [数据模型](#数据模型)
+ - [GroupCourse(团课)](#GroupCourse团课)
+ - [GroupCourseBooking(团课预约)](#GroupCourseBooking团课预约)
+ - [GroupCourseType(团课类型)](#GroupCourseType团课类型)
+7. [状态码说明](#状态码说明)
+8. [业务规则](#业务规则)
+
+---
+
+## 概述
+
+团课管理模块提供团课的创建、编辑、查询、取消和签到功能,以及团课预约相关操作。采用 Spring WebFlux 响应式编程,支持高并发场景。
+
+## 基础路径
+
+所有接口的基础路径为: `http://{host}:{port}/api/groupCourse`
+
+---
+
+## 团课管理接口
+
+### 获取所有团课
+
+| 属性 | 值 |
+|------|-----|
+| **HTTP方法** | GET |
+| **接口路径** | `/api/groupCourse/list` |
+| **所属文件** | `GroupCourseHandler.java` |
+
+**请求参数**:
+
+| 参数名 | 类型 | 必填 | 默认值 | 说明 |
+|--------|------|------|--------|------|
+| includeDeleted | boolean | 否 | false | 是否包含已删除的团课 |
+
+**成功响应** (200 OK):
+
+```json
+[
+ {
+ "id": 1,
+ "courseName": "瑜伽入门",
+ "coachId": 1,
+ "courseType": 1,
+ "startTime": "2026-06-02T09:00:00",
+ "endTime": "2026-06-02T10:00:00",
+ "maxMembers": 20,
+ "currentMembers": 15,
+ "status": 0,
+ "location": "健身房A区",
+ "coverImage": "https://example.com/yoga.jpg",
+ "description": "适合初学者的瑜伽课程",
+ "createdAt": "2026-06-01T10:00:00",
+ "updatedAt": "2026-06-01T10:00:00"
+ }
+]
+```
+
+---
+
+### 分页获取团课
+
+| 属性 | 值 |
+|------|-----|
+| **HTTP方法** | POST |
+| **接口路径** | `/api/groupCourse/page` |
+| **所属文件** | `GroupCourseHandler.java` |
+
+**请求体**:
+
+```json
+{
+ "page": 0,
+ "size": 10,
+ "sort": "id",
+ "order": "asc",
+ "keyword": "瑜伽"
+}
+```
+
+| 参数名 | 类型 | 必填 | 默认值 | 说明 |
+|--------|------|------|--------|------|
+| page | int | 否 | 0 | 页码,从0开始 |
+| size | int | 否 | 10 | 每页数量,最大100 |
+| sort | string | 否 | id | 排序字段 |
+| order | string | 否 | asc | 排序方式(asc/desc) |
+| keyword | string | 否 | - | 搜索关键词 |
+
+**请求参数**:
+
+| 参数名 | 类型 | 必填 | 默认值 | 说明 |
+|--------|------|------|--------|------|
+| includeDeleted | boolean | 否 | false | 是否包含已删除的团课 |
+
+**成功响应** (200 OK):
+
+```json
+{
+ "data": [...],
+ "totalPages": 5,
+ "totalElements": 45,
+ "page": 0,
+ "size": 10
+}
+```
+
+---
+
+### 根据ID获取团课详情
+
+| 属性 | 值 |
+|------|-----|
+| **HTTP方法** | GET |
+| **接口路径** | `/api/groupCourse/{id}` |
+| **所属文件** | `GroupCourseHandler.java` |
+
+**路径参数**:
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| id | Long | 是 | 团课ID |
+
+**成功响应** (200 OK):
+
+```json
+{
+ "id": 1,
+ "courseName": "瑜伽入门",
+ "coachId": 1,
+ "courseType": 1,
+ "startTime": "2026-06-02T09:00:00",
+ "endTime": "2026-06-02T10:00:00",
+ "maxMembers": 20,
+ "currentMembers": 15,
+ "status": 0,
+ "location": "健身房A区",
+ "coverImage": "https://example.com/yoga.jpg",
+ "description": "适合初学者的瑜伽课程",
+ "pointCardAmount": 1,
+ "storedValueAmount": 50.00,
+ "createdAt": "2026-06-01T10:00:00",
+ "updatedAt": "2026-06-01T10:00:00"
+}
+```
+
+**失败响应** (404 Not Found):
+
+```json
+{}
+```
+
+---
+
+### 根据ID获取团课完整信息
+
+| 属性 | 值 |
+|------|-----|
+| **HTTP方法** | GET |
+| **接口路径** | `/api/groupCourse/{id}/detail` |
+| **所属文件** | `GroupCourseHandler.java` |
+
+**路径参数**:
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| id | Long | 是 | 团课ID |
+
+**成功响应** (200 OK):
+
+```json
+{
+ "id": 1,
+ "courseName": "瑜伽入门",
+ "coachId": 1,
+ "courseType": 1,
+ "startTime": "2026-06-02T09:00:00",
+ "endTime": "2026-06-02T10:00:00",
+ "maxMembers": 20,
+ "currentMembers": 15,
+ "status": 0,
+ "location": "健身房A区",
+ "coverImage": "https://example.com/yoga.jpg",
+ "description": "适合初学者的瑜伽课程",
+ "pointCardAmount": 1,
+ "storedValueAmount": 50.00,
+ "typeName": "瑜伽入门",
+ "typeCategory": "柔韧与平衡类",
+ "baseDifficulty": 2,
+ "difficultyLevel": "初级",
+ "calculatedDifficulty": 2,
+ "typeInfo": {
+ "id": 1,
+ "typeName": "瑜伽入门",
+ "baseDifficulty": 2,
+ "calculatedDifficulty": 2,
+ "difficultyLevel": "初级",
+ "description": "适合初学者的瑜伽课程,注重基础体式",
+ "category": "柔韧与平衡类",
+ "labels": [
+ {"id": 1, "labelName": "适合新手", "color": "#52c41a"},
+ {"id": 3, "labelName": "减压放松", "color": "#1890ff"}
+ ]
+ },
+ "labels": [
+ {"id": 1, "labelName": "适合新手", "color": "#52c41a"},
+ {"id": 3, "labelName": "减压放松", "color": "#1890ff"}
+ ],
+ "createdAt": "2026-06-01T10:00:00",
+ "updatedAt": "2026-06-01T10:00:00"
+}
+```
+
+**失败响应** (404 Not Found):
+
+```json
+{}
+```
+
+**说明**: 此接口返回团课的完整信息,包括:
+- 团课基础信息
+- 团课对应的类型信息(包含基础难度、综合难度、难度等级等)
+- 该类型的所有标签信息
+
+---
+
+### 创建团课
+
+| 属性 | 值 |
+|------|-----|
+| **HTTP方法** | POST |
+| **接口路径** | `/api/groupCourse` |
+| **所属文件** | `GroupCourseHandler.java` |
+
+**请求体**:
+
+```json
+{
+ "courseName": "动感单车",
+ "coachId": 2,
+ "courseType": 2,
+ "startTime": "2026-06-05T18:00:00",
+ "endTime": "2026-06-05T19:00:00",
+ "maxMembers": 25,
+ "location": "健身房B区",
+ "coverImage": "https://example.com/spinning.jpg",
+ "description": "高强度有氧运动课程",
+ "pointCardAmount": 1,
+ "storedValueAmount": 50.00
+}
+```
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| courseName | String | **是** | 课程名称 |
+| coachId | Long | 否 | 教练ID |
+| courseType | Long | 否 | 课程类型 |
+| startTime | LocalDateTime | 否 | 开始时间 |
+| endTime | LocalDateTime | 否 | 结束时间 |
+| maxMembers | Integer | 否 | 最大参与人数,默认20 |
+| location | String | 否 | 上课地点 |
+| coverImage | String | 否 | 封面图URL |
+| description | String | 否 | 课程描述 |
+| pointCardAmount | Integer | 否 | 点卡额度(消耗次数),默认1 |
+| storedValueAmount | BigDecimal | 否 | 储值卡额度(消耗金额),默认0 |
+
+**成功响应** (200 OK):
+
+```json
+{
+ "success": true,
+ "message": "团课创建成功",
+ "data": {
+ "id": 2,
+ "courseName": "动感单车",
+ "coachId": 2,
+ "courseType": 2,
+ "startTime": "2026-06-05T18:00:00",
+ "endTime": "2026-06-05T19:00:00",
+ "maxMembers": 25,
+ "currentMembers": 0,
+ "status": 0,
+ "location": "健身房B区",
+ "coverImage": "https://example.com/spinning.jpg",
+ "description": "高强度有氧运动课程",
+ "pointCardAmount": 1,
+ "storedValueAmount": 50.00
+ }
+}
+```
+
+**失败响应** (400 Bad Request):
+
+```json
+{
+ "success": false,
+ "message": "课程名称不能为空"
+}
+```
+
+---
+
+### 更新团课
+
+| 属性 | 值 |
+|------|-----|
+| **HTTP方法** | PUT |
+| **接口路径** | `/api/groupCourse/{id}` |
+| **所属文件** | `GroupCourseHandler.java` |
+
+**路径参数**:
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| id | Long | 是 | 团课ID |
+
+**请求体**:
+
+```json
+{
+ "courseName": "动感单车升级版",
+ "coachId": 2,
+ "maxMembers": 30,
+ "description": "升级版高强度有氧运动课程",
+ "pointCardAmount": 2,
+ "storedValueAmount": 80.00
+}
+```
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| courseName | String | 否 | 课程名称 |
+| coachId | Long | 否 | 教练ID |
+| courseType | Long | 否 | 课程类型 |
+| startTime | LocalDateTime | 否 | 开始时间 |
+| endTime | LocalDateTime | 否 | 结束时间 |
+| maxMembers | Integer | 否 | 最大参与人数 |
+| location | String | 否 | 上课地点 |
+| coverImage | String | 否 | 封面图URL |
+| description | String | 否 | 课程描述 |
+| pointCardAmount | Integer | 否 | 点卡额度(消耗次数) |
+| storedValueAmount | BigDecimal | 否 | 储值卡额度(消耗金额) |
+
+**成功响应** (200 OK):
+
+```json
+{
+ "success": true,
+ "message": "团课更新成功",
+ "data": {
+ "id": 2,
+ "courseName": "动感单车升级版",
+ "coachId": 2,
+ "maxMembers": 30,
+ "description": "升级版高强度有氧运动课程",
+ "pointCardAmount": 2,
+ "storedValueAmount": 80.00
+ }
+}
+```
+
+**失败响应** (400 Bad Request):
+
+```json
+{
+ "success": false,
+ "message": "团课不存在"
+}
+```
+
+---
+
+### 取消团课
+
+| 属性 | 值 |
+|------|-----|
+| **HTTP方法** | POST |
+| **接口路径** | `/api/groupCourse/{id}/cancel` |
+| **所属文件** | `GroupCourseHandler.java` |
+
+**路径参数**:
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| id | Long | 是 | 团课ID |
+
+**成功响应** (200 OK):
+
+```json
+{
+ "success": true,
+ "message": "团课取消成功",
+ "data": {
+ "id": 2,
+ "status": 1
+ }
+}
+```
+
+**失败响应** (400 Bad Request):
+
+```json
+{
+ "success": false,
+ "message": "课程取消需提前24小时"
+}
+```
+
+---
+
+### 团课签到
+
+| 属性 | 值 |
+|------|-----|
+| **HTTP方法** | POST |
+| **接口路径** | `/api/groupCourse/{courseId}/signin` |
+| **所属文件** | `GroupCourseHandler.java` |
+
+**路径参数**:
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| courseId | Long | 是 | 团课ID |
+
+**请求体**:
+
+```json
+{
+ "memberId": 1001
+}
+```
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| memberId | Long | **是** | 会员ID |
+
+**成功响应** (200 OK):
+
+```json
+{
+ "success": true,
+ "message": "签到成功",
+ "data": {
+ "id": 2,
+ "currentMembers": 16
+ }
+}
+```
+
+**失败响应** (400 Bad Request):
+
+```json
+{
+ "success": false,
+ "message": "课程已满员"
+}
+```
+
+---
+
+### 删除团课
+
+| 属性 | 值 |
+|------|-----|
+| **HTTP方法** | DELETE |
+| **接口路径** | `/api/groupCourse/{id}` |
+| **所属文件** | `GroupCourseHandler.java` |
+
+**路径参数**:
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| id | Long | 是 | 团课ID |
+
+**成功响应** (200 OK):
+
+```json
+{
+ "success": true,
+ "message": "团课删除成功"
+}
+```
+
+---
+
+## 团课预约接口
+
+### 预约团课
+
+| 属性 | 值 |
+|------|-----|
+| **HTTP方法** | POST |
+| **接口路径** | `/api/groupCourse/book` |
+| **所属文件** | `GroupCourseBookingHandler.java` |
+
+**请求体**:
+
+```json
+{
+ "courseId": 1,
+ "memberId": 1001,
+ "memberCardRecordId": 5001
+}
+```
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| courseId | Long | **是** | 团课ID |
+| memberId | Long | **是** | 会员ID |
+| memberCardRecordId | Long | **是** | 会员卡记录ID |
+
+**成功响应** (200 OK):
+
+```json
+{
+ "success": true,
+ "message": "预约成功",
+ "data": {
+ "id": 100,
+ "courseId": 1,
+ "memberId": 1001,
+ "memberCardRecordId": 5001,
+ "bookingTime": "2026-06-02T08:00:00",
+ "status": "0"
+ }
+}
+```
+
+---
+
+### 取消预约
+
+| 属性 | 值 |
+|------|-----|
+| **HTTP方法** | POST |
+| **接口路径** | `/api/groupCourse/booking/{bookingId}/cancel` |
+| **所属文件** | `GroupCourseBookingHandler.java` |
+
+**路径参数**:
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| bookingId | Long | 是 | 预约ID |
+
+**请求体**:
+
+```json
+{
+ "memberId": 1001
+}
+```
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| memberId | Long | **是** | 会员ID |
+
+**成功响应** (200 OK):
+
+```json
+{
+ "success": true,
+ "message": "取消成功",
+ "data": {
+ "id": 100,
+ "status": "1",
+ "cancelTime": "2026-06-02T09:00:00"
+ }
+}
+```
+
+---
+
+### 查询会员预约记录
+
+| 属性 | 值 |
+|------|-----|
+| **HTTP方法** | GET |
+| **接口路径** | `/api/groupCourse/bookings/member/{memberId}` |
+| **所属文件** | `GroupCourseBookingHandler.java` |
+
+**路径参数**:
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| memberId | Long | 是 | 会员ID |
+
+**成功响应** (200 OK):
+
+```json
+[
+ {
+ "id": 100,
+ "courseId": 1,
+ "courseName": "瑜伽入门",
+ "memberId": 1001,
+ "memberCardRecordId": 5001,
+ "bookingTime": "2026-06-02T08:00:00",
+ "status": "0",
+ "courseStartTime": "2026-06-02T09:00:00",
+ "location": "健身房A区"
+ }
+]
+```
+
+---
+
+### 查询预约详情
+
+| 属性 | 值 |
+|------|-----|
+| **HTTP方法** | GET |
+| **接口路径** | `/api/groupCourse/bookings/{bookingId}` |
+| **所属文件** | `GroupCourseBookingHandler.java` |
+
+**路径参数**:
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| bookingId | Long | 是 | 预约ID |
+
+**成功响应** (200 OK):
+
+```json
+{
+ "id": 100,
+ "courseId": 1,
+ "courseName": "瑜伽入门",
+ "memberId": 1001,
+ "memberCardRecordId": 5001,
+ "bookingTime": "2026-06-02T08:00:00",
+ "status": "0",
+ "courseStartTime": "2026-06-02T09:00:00",
+ "courseEndTime": "2026-06-02T10:00:00",
+ "location": "健身房A区"
+}
+```
+
+**失败响应** (404 Not Found):
+
+```json
+{}
+```
+
+---
+
+### 查询课程预约记录
+
+| 属性 | 值 |
+|------|-----|
+| **HTTP方法** | GET |
+| **接口路径** | `/api/groupCourse/bookings/course/{courseId}` |
+| **所属文件** | `GroupCourseBookingHandler.java` |
+
+**路径参数**:
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| courseId | Long | 是 | 团课ID |
+
+**成功响应** (200 OK):
+
+```json
+[
+ {
+ "id": 100,
+ "courseId": 1,
+ "courseName": "瑜伽入门",
+ "memberId": 1001,
+ "bookingTime": "2026-06-02T08:00:00",
+ "status": "0"
+ }
+]
+```
+
+---
+
+## 团课类型管理接口
+
+### 获取所有团课类型
+
+| 属性 | 值 |
+|------|-----|
+| **HTTP方法** | GET |
+| **接口路径** | `/api/groupCourse/types` |
+| **所属文件** | `GroupCourseTypeHandler.java` |
+
+**请求参数**:
+
+| 参数名 | 类型 | 必填 | 默认值 | 说明 |
+|--------|------|------|--------|------|
+| includeDeleted | boolean | 否 | false | 是否包含已删除的类型 |
+
+**成功响应** (200 OK):
+
+```json
+[
+ {
+ "id": 1,
+ "typeName": "瑜伽入门",
+ "baseDifficulty": 2,
+ "calculatedDifficulty": 2,
+ "difficultyLevel": "初级",
+ "description": "适合初学者的瑜伽课程,注重基础体式",
+ "category": "柔韧与平衡类",
+ "createdAt": "2026-06-01T10:00:00",
+ "updatedAt": "2026-06-01T10:00:00"
+ }
+]
+```
+
+---
+
+### 根据ID获取团课类型
+
+| 属性 | 值 |
+|------|-----|
+| **HTTP方法** | GET |
+| **接口路径** | `/api/groupCourse/types/{id}` |
+| **所属文件** | `GroupCourseTypeHandler.java` |
+
+**路径参数**:
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| id | Long | 是 | 团课类型ID |
+
+**成功响应** (200 OK):
+
+```json
+{
+ "id": 1,
+ "typeName": "瑜伽入门",
+ "baseDifficulty": 2,
+ "calculatedDifficulty": 2,
+ "difficultyLevel": "初级",
+ "description": "适合初学者的瑜伽课程,注重基础体式",
+ "category": "柔韧与平衡类",
+ "createdAt": "2026-06-01T10:00:00",
+ "updatedAt": "2026-06-01T10:00:00"
+}
+```
+
+**失败响应** (404 Not Found):
+
+```json
+{}
+```
+
+---
+
+### 搜索团课类型
+
+| 属性 | 值 |
+|------|-----|
+| **HTTP方法** | GET |
+| **接口路径** | `/api/groupCourse/types/search` |
+| **所属文件** | `GroupCourseTypeHandler.java` |
+
+**请求参数**:
+
+| 参数名 | 类型 | 必填 | 默认值 | 说明 |
+|--------|------|------|--------|------|
+| keyword | string | 否 | - | 搜索关键词(匹配类型名称) |
+
+**成功响应** (200 OK):
+
+```json
+[
+ {
+ "id": 1,
+ "typeName": "瑜伽入门",
+ "baseDifficulty": 2,
+ "difficultyLevel": "初级",
+ "category": "柔韧与平衡类"
+ }
+]
+```
+
+---
+
+### 根据分类获取团课类型
+
+| 属性 | 值 |
+|------|-----|
+| **HTTP方法** | GET |
+| **接口路径** | `/api/groupCourse/types/category/{category}` |
+| **所属文件** | `GroupCourseTypeHandler.java` |
+
+**路径参数**:
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| category | String | 是 | 分类名称 |
+
+**请求参数**:
+
+| 参数名 | 类型 | 必填 | 默认值 | 说明 |
+|--------|------|------|--------|------|
+| keyword | string | 否 | - | 搜索关键词 |
+
+**成功响应** (200 OK):
+
+```json
+[
+ {
+ "id": 1,
+ "typeName": "瑜伽入门",
+ "baseDifficulty": 2,
+ "difficultyLevel": "初级",
+ "category": "柔韧与平衡类"
+ }
+]
+```
+
+---
+
+### 获取所有分类
+
+| 属性 | 值 |
+|------|-----|
+| **HTTP方法** | GET |
+| **接口路径** | `/api/groupCourse/types/categories` |
+| **所属文件** | `GroupCourseTypeHandler.java` |
+
+**成功响应** (200 OK):
+
+```json
+["基础有氧与热身", "固定器械训练", "自重基础动作", "自由重量杠铃/哑铃", "高强度与爆发力", "柔韧与平衡类"]
+```
+
+---
+
+### 创建团课类型
+
+| 属性 | 值 |
+|------|-----|
+| **HTTP方法** | POST |
+| **接口路径** | `/api/groupCourse/types` |
+| **所属文件** | `GroupCourseTypeHandler.java` |
+
+**请求体**:
+
+```json
+{
+ "typeName": "核心力量训练",
+ "baseDifficulty": 4,
+ "description": "针对核心肌群的专项训练课程",
+ "category": "自重基础动作"
+}
+```
+
+| 参数名 | 类型 | 必填 | 默认值 | 说明 |
+|--------|------|------|--------|------|
+| typeName | String | **是** | - | 类型名称 |
+| baseDifficulty | Integer | 否 | 1 | 基础难度(1-10) |
+| description | String | 否 | - | 类型描述 |
+| category | String | 否 | - | 分类 |
+
+**成功响应** (200 OK):
+
+```json
+{
+ "success": true,
+ "message": "团课类型创建成功",
+ "data": {
+ "id": 40,
+ "typeName": "核心力量训练",
+ "baseDifficulty": 4,
+ "calculatedDifficulty": 4,
+ "difficultyLevel": "中级",
+ "description": "针对核心肌群的专项训练课程",
+ "category": "自重基础动作"
+ }
+}
+```
+
+**失败响应** (400 Bad Request):
+
+```json
+{
+ "success": false,
+ "message": "类型名称不能为空"
+}
+```
+
+---
+
+### 更新团课类型
+
+| 属性 | 值 |
+|------|-----|
+| **HTTP方法** | PUT |
+| **接口路径** | `/api/groupCourse/types/{id}` |
+| **所属文件** | `GroupCourseTypeHandler.java` |
+
+**路径参数**:
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| id | Long | 是 | 团课类型ID |
+
+**请求体**:
+
+```json
+{
+ "typeName": "核心力量训练进阶",
+ "baseDifficulty": 6,
+ "description": "进阶核心训练课程"
+}
+```
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| typeName | String | 否 | 类型名称 |
+| baseDifficulty | Integer | 否 | 基础难度(1-10) |
+| description | String | 否 | 类型描述 |
+| category | String | 否 | 分类 |
+
+**成功响应** (200 OK):
+
+```json
+{
+ "success": true,
+ "message": "团课类型更新成功",
+ "data": {
+ "id": 40,
+ "typeName": "核心力量训练进阶",
+ "baseDifficulty": 6,
+ "calculatedDifficulty": 6,
+ "difficultyLevel": "中高级",
+ "description": "进阶核心训练课程",
+ "category": "自重基础动作"
+ }
+}
+```
+
+---
+
+### 删除团课类型
+
+| 属性 | 值 |
+|------|-----|
+| **HTTP方法** | DELETE |
+| **接口路径** | `/api/groupCourse/types/{id}` |
+| **所属文件** | `GroupCourseTypeHandler.java` |
+
+**路径参数**:
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| id | Long | 是 | 团课类型ID |
+
+**成功响应** (200 OK):
+
+```json
+{
+ "success": true,
+ "message": "团课类型删除成功"
+}
+```
+
+---
+
+## 团课标签管理接口
+
+### 获取所有标签
+
+| 属性 | 值 |
+|------|-----|
+| **HTTP方法** | GET |
+| **接口路径** | `/api/groupCourse/labels` |
+| **所属文件** | `CourseLabelHandler.java` |
+
+**成功响应** (200 OK):
+
+```json
+[
+ {
+ "id": 1,
+ "labelName": "适合新手",
+ "color": "#52c41a",
+ "createdAt": "2026-06-01T10:00:00",
+ "updatedAt": "2026-06-01T10:00:00"
+ },
+ {
+ "id": 2,
+ "labelName": "中级过渡",
+ "color": "#faad14",
+ "createdAt": "2026-06-01T10:00:00",
+ "updatedAt": "2026-06-01T10:00:00"
+ }
+]
+```
+
+---
+
+### 根据ID获取标签
+
+| 属性 | 值 |
+|------|-----|
+| **HTTP方法** | GET |
+| **接口路径** | `/api/groupCourse/labels/{id}` |
+| **所属文件** | `CourseLabelHandler.java` |
+
+**路径参数**:
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| id | Long | 是 | 标签ID |
+
+**成功响应** (200 OK):
+
+```json
+{
+ "id": 1,
+ "labelName": "适合新手",
+ "color": "#52c41a",
+ "createdAt": "2026-06-01T10:00:00",
+ "updatedAt": "2026-06-01T10:00:00"
+}
+```
+
+**失败响应** (404 Not Found):
+
+```json
+{}
+```
+
+---
+
+### 搜索标签
+
+| 属性 | 值 |
+|------|-----|
+| **HTTP方法** | GET |
+| **接口路径** | `/api/groupCourse/labels/search` |
+| **所属文件** | `CourseLabelHandler.java` |
+
+**请求参数**:
+
+| 参数名 | 类型 | 必填 | 默认值 | 说明 |
+|--------|------|------|--------|------|
+| keyword | string | 否 | - | 搜索关键词(匹配标签名称) |
+
+**成功响应** (200 OK):
+
+```json
+[
+ {
+ "id": 1,
+ "labelName": "适合新手",
+ "color": "#52c41a"
+ }
+]
+```
+
+---
+
+### 获取类型的标签
+
+| 属性 | 值 |
+|------|-----|
+| **HTTP方法** | GET |
+| **接口路径** | `/api/groupCourse/types/{typeId}/labels` |
+| **所属文件** | `CourseLabelHandler.java` |
+
+**路径参数**:
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| typeId | Long | 是 | 团课类型ID |
+
+**成功响应** (200 OK):
+
+```json
+[
+ {
+ "id": 1,
+ "labelName": "适合新手",
+ "color": "#52c41a"
+ },
+ {
+ "id": 3,
+ "labelName": "减压放松",
+ "color": "#1890ff"
+ }
+]
+```
+
+---
+
+### 创建标签
+
+| 属性 | 值 |
+|------|-----|
+| **HTTP方法** | POST |
+| **接口路径** | `/api/groupCourse/labels` |
+| **所属文件** | `CourseLabelHandler.java` |
+
+**请求体**:
+
+```json
+{
+ "labelName": "燃脂塑形",
+ "color": "#f5222d"
+}
+```
+
+| 参数名 | 类型 | 必填 | 默认值 | 说明 |
+|--------|------|------|--------|------|
+| labelName | String | **是** | - | 标签名称(最大50字符) |
+| color | String | 否 | #1890ff | 标签颜色(十六进制颜色值) |
+
+**成功响应** (200 OK):
+
+```json
+{
+ "success": true,
+ "message": "标签创建成功",
+ "data": {
+ "id": 15,
+ "labelName": "燃脂塑形",
+ "color": "#f5222d"
+ }
+}
+```
+
+**失败响应** (400 Bad Request):
+
+```json
+{
+ "success": false,
+ "message": "标签名称已存在"
+}
+```
+
+---
+
+### 更新标签
+
+| 属性 | 值 |
+|------|-----|
+| **HTTP方法** | PUT |
+| **接口路径** | `/api/groupCourse/labels/{id}` |
+| **所属文件** | `CourseLabelHandler.java` |
+
+**路径参数**:
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| id | Long | 是 | 标签ID |
+
+**请求体**:
+
+```json
+{
+ "labelName": "燃脂塑形进阶",
+ "color": "#fa541c"
+}
+```
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| labelName | String | 否 | 标签名称(最大50字符) |
+| color | String | 否 | 标签颜色(十六进制颜色值) |
+
+**成功响应** (200 OK):
+
+```json
+{
+ "success": true,
+ "message": "标签更新成功",
+ "data": {
+ "id": 15,
+ "labelName": "燃脂塑形进阶",
+ "color": "#fa541c"
+ }
+}
+```
+
+---
+
+### 删除标签
+
+| 属性 | 值 |
+|------|-----|
+| **HTTP方法** | DELETE |
+| **接口路径** | `/api/groupCourse/labels/{id}` |
+| **所属文件** | `CourseLabelHandler.java` |
+
+**路径参数**:
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| id | Long | 是 | 标签ID |
+
+**成功响应** (200 OK):
+
+```json
+{
+ "success": true,
+ "message": "标签删除成功"
+}
+```
+
+---
+
+### 为类型添加标签
+
+| 属性 | 值 |
+|------|-----|
+| **HTTP方法** | POST |
+| **接口路径** | `/api/groupCourse/types/{typeId}/labels` |
+| **所属文件** | `CourseLabelHandler.java` |
+
+**路径参数**:
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| typeId | Long | 是 | 团课类型ID |
+
+**请求体**:
+
+```json
+{
+ "labelIds": [1, 3, 5]
+}
+```
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| labelIds | List\ | **是** | 要添加的标签ID列表 |
+
+**成功响应** (200 OK):
+
+```json
+{
+ "success": true,
+ "message": "标签添加成功"
+}
+```
+
+---
+
+### 从类型移除标签
+
+| 属性 | 值 |
+|------|-----|
+| **HTTP方法** | DELETE |
+| **接口路径** | `/api/groupCourse/types/{typeId}/labels/{labelId}` |
+| **所属文件** | `CourseLabelHandler.java` |
+
+**路径参数**:
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| typeId | Long | 是 | 团课类型ID |
+| labelId | Long | 是 | 标签ID |
+
+**成功响应** (200 OK):
+
+```json
+{
+ "success": true,
+ "message": "标签移除成功"
+}
+```
+
+---
+
+### 清空类型标签
+
+| 属性 | 值 |
+|------|-----|
+| **HTTP方法** | DELETE |
+| **接口路径** | `/api/groupCourse/types/{typeId}/labels` |
+| **所属文件** | `CourseLabelHandler.java` |
+
+**路径参数**:
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| typeId | Long | 是 | 团课类型ID |
+
+**成功响应** (200 OK):
+
+```json
+{
+ "success": true,
+ "message": "标签清空成功"
+}
+```
+
+---
+
+## 数据模型
+
+### GroupCourse(团课)
+
+| 字段名 | 类型 | 说明 |
+|--------|------|------|
+| id | Long | 主键ID |
+| courseName | String | 课程名称 |
+| coachId | Long | 教练ID |
+| courseType | Long | 课程类型 |
+| startTime | LocalDateTime | 开始时间 |
+| endTime | LocalDateTime | 结束时间 |
+| maxMembers | Integer | 最大参与人数 |
+| currentMembers | Integer | 当前参与人数 |
+| status | Long | 状态(0-正常,1-已取消,2-已结束) |
+| location | String | 上课地点 |
+| coverImage | String | 封面图URL |
+| description | String | 课程描述 |
+| pointCardAmount | Integer | 点卡额度(消耗次数),默认1 |
+| storedValueAmount | BigDecimal | 储值卡额度(消耗金额),默认0 |
+| createdBy | String | 创建人 |
+| updatedBy | String | 更新人 |
+| createdAt | LocalDateTime | 创建时间 |
+| updatedAt | LocalDateTime | 更新时间 |
+| deletedAt | LocalDateTime | 删除时间(软删除) |
+
+### GroupCourseBooking(团课预约)
+
+| 字段名 | 类型 | 说明 |
+|--------|------|------|
+| id | Long | 主键ID |
+| courseId | Long | 团课ID |
+| courseName | String | 团课名称 |
+| memberId | Long | 会员ID |
+| memberCardRecordId | Long | 会员卡记录ID |
+| bookingTime | LocalDateTime | 预约时间 |
+| status | String | 状态(0-已预约,1-已取消,2-已出席,3-缺席) |
+| cancelTime | LocalDateTime | 取消时间 |
+| courseStartTime | LocalDateTime | 课程开始时间 |
+| courseEndTime | LocalDateTime | 课程结束时间 |
+| location | String | 上课地点 |
+| createdBy | String | 创建人 |
+| updatedBy | String | 更新人 |
+| createdAt | LocalDateTime | 创建时间 |
+| updatedAt | LocalDateTime | 更新时间 |
+| deletedAt | LocalDateTime | 删除时间(软删除) |
+
+### GroupCourseType(团课类型)
+
+| 字段名 | 类型 | 说明 |
+|--------|------|------|
+| id | Long | 主键ID |
+| typeName | String | 类型名称 |
+| baseDifficulty | Integer | 基础难度(1-10) |
+| calculatedDifficulty | Integer | 综合难度系数(预留扩展字段) |
+| difficultyLevel | String | 难度等级描述(初级/中级/中高级/高级/专家级) |
+| description | String | 类型描述 |
+| category | String | 分类(如:有氧、力量、柔韧等) |
+| labels | List\ | 标签列表 |
+| createdBy | String | 创建人 |
+| updatedBy | String | 更新人 |
+| createdAt | LocalDateTime | 创建时间 |
+| updatedAt | LocalDateTime | 更新时间 |
+| deletedAt | LocalDateTime | 删除时间(软删除) |
+
+### CourseLabel(团课标签)
+
+| 字段名 | 类型 | 说明 |
+|--------|------|------|
+| id | Long | 主键ID |
+| labelName | String | 标签名称(最大50字符) |
+| color | String | 标签颜色(十六进制颜色值,默认#1890ff) |
+| createdBy | String | 创建人 |
+| updatedBy | String | 更新人 |
+| createdAt | LocalDateTime | 创建时间 |
+| updatedAt | LocalDateTime | 更新时间 |
+| deletedAt | LocalDateTime | 删除时间(软删除) |
+
+### GroupCourseDetail(团课完整信息)
+
+| 字段名 | 类型 | 说明 |
+|--------|------|------|
+| id | Long | 主键ID |
+| courseName | String | 课程名称 |
+| coachId | Long | 教练ID |
+| courseType | Long | 课程类型ID |
+| startTime | LocalDateTime | 开始时间 |
+| endTime | LocalDateTime | 结束时间 |
+| maxMembers | Integer | 最大参与人数 |
+| currentMembers | Integer | 当前参与人数 |
+| status | Long | 状态(0-正常,1-已取消,2-已结束) |
+| location | String | 上课地点 |
+| coverImage | String | 封面图URL |
+| description | String | 课程描述 |
+| pointCardAmount | Integer | 点卡额度(消耗次数) |
+| storedValueAmount | BigDecimal | 储值卡额度(消耗金额) |
+| typeName | String | 类型名称(快捷访问) |
+| typeCategory | String | 类型分类(快捷访问) |
+| baseDifficulty | Integer | 基础难度(快捷访问) |
+| difficultyLevel | String | 难度等级描述(快捷访问) |
+| calculatedDifficulty | Integer | 综合难度系数(快捷访问) |
+| typeInfo | GroupCourseType | 类型信息 |
+| labels | List\ | 标签列表 |
+| createdAt | LocalDateTime | 创建时间 |
+| updatedAt | LocalDateTime | 更新时间 |
+
+**难度等级对应关系**:
+
+| 基础难度 | 难度等级 |
+|----------|----------|
+| 1-2 | 初级 |
+| 3-4 | 中级 |
+| 5-6 | 中高级 |
+| 7-8 | 高级 |
+| 9-10 | 专家级 |
+
+**难度扩展说明**:
+
+`calculatedDifficulty` 字段为预留扩展字段,当前实现仅返回 `baseDifficulty`。未来可扩展的影响因素包括:
+
+1. **课程时长系数**:时长越长难度越高
+2. **教练难度调整系数**:教练可根据实际情况微调
+3. **会员等级适配系数**:根据会员等级动态调整显示难度
+4. **课程强度系数**:高强度课程难度加成
+
+---
+
+## 状态码说明
+
+### 团课状态
+
+| 状态码 | 含义 |
+|--------|------|
+| 0 | 正常 |
+| 1 | 已取消 |
+| 2 | 已结束 |
+
+### 预约状态
+
+| 状态码 | 含义 |
+|--------|------|
+| 0 | 已预约 |
+| 1 | 已取消 |
+| 2 | 已出席 |
+| 3 | 缺席 |
+
+---
+
+## 业务规则
+
+### 团课管理
+1. **创建团课**:课程名称为必填项
+2. **取消团课**:需提前24小时通知,否则拒绝操作
+3. **团课签到**:验证课程状态必须为正常,且未达最大人数限制
+4. **删除团课**:采用软删除机制,数据保留可恢复
+
+### 团课预约
+1. **预约团课**:需验证会员卡有效性和课程名额
+2. **取消预约**:需在课程开始前至少2小时取消
+
+---
+
+## 附录:错误响应格式
+
+所有接口的错误响应统一格式:
+
+```json
+{
+ "success": false,
+ "message": "错误描述信息"
+}
+```
+
+---
+
+*文档结束*
\ No newline at end of file
diff --git a/gym-manage-uniapp/pages.json b/gym-manage-uniapp/pages.json
index 378467d..152613b 100644
--- a/gym-manage-uniapp/pages.json
+++ b/gym-manage-uniapp/pages.json
@@ -14,6 +14,7 @@
{
"path": "pages/course/index",
"style": {
+ "navigationStyle": "custom",
"navigationBarTitleText": "课程",
"app-plus": {
"animationType": "fade-in",
@@ -244,6 +245,7 @@
{
"path": "pages/groupCourse/list",
"style": {
+ "navigationStyle": "custom",
"navigationBarTitleText": "团课列表"
}
},
@@ -266,6 +268,13 @@
"navigationBarTitleText": "搜索课程"
}
},
+ {
+ "path": "pages/recommendCourses/index",
+ "style": {
+ "navigationBarTitleText": "推荐课程",
+ "navigationStyle": "custom"
+ }
+ },
{
"path": "components/global/GlobalLoading",
"style": {
diff --git a/gym-manage-uniapp/pages/course/index.vue b/gym-manage-uniapp/pages/course/index.vue
index 8ae02bd..297f288 100644
--- a/gym-manage-uniapp/pages/course/index.vue
+++ b/gym-manage-uniapp/pages/course/index.vue
@@ -3,10 +3,7 @@
-
+
@@ -44,6 +41,7 @@
import { ref } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import RecommendCourses from '@/components/index/RecommendCourses.vue'
+import PageHeader from '@/components/index/PageHeader.vue'
import TabBar from '@/components/TabBar.vue'
import { PAGE, navigateToPage } from '@/common/constants/routes.js'
@@ -136,25 +134,10 @@ function goMyCourses() {
padding-bottom: 160rpx;
position: relative;
z-index: 2;
+ background: #F5F7FA;
}
-.tab-page__header {
- padding: 48rpx 32rpx 16rpx;
-}
-.tab-page__title {
- display: block;
- font-size: 40rpx;
- font-weight: $font-weight-bold;
- color: $text-dark;
-}
-
-.tab-page__subtitle {
- display: block;
- margin-top: 8rpx;
- font-size: 24rpx;
- color: $text-muted;
-}
.tab-page__actions {
display: flex;
diff --git a/gym-manage-uniapp/pages/groupCourse/detail.vue b/gym-manage-uniapp/pages/groupCourse/detail.vue
index 38c5df9..846ad1d 100644
--- a/gym-manage-uniapp/pages/groupCourse/detail.vue
+++ b/gym-manage-uniapp/pages/groupCourse/detail.vue
@@ -1,7 +1,7 @@
-
-
+
+
@@ -102,6 +102,27 @@
{{ course.location }}
+
+
+
+
+
+ 课程标签
+
+
+
+ {{ label.labelName }}
+
+
+ 暂无标签
+
+
+
@@ -138,39 +159,6 @@
ID: {{ course.coachId }}
-
-
-
- 支付方式
-
-
-
- 储值卡
- ¥{{ course.storedValueAmount }}
-
-
-
-
-
- 次卡
- {{ course.pointCardAmount }}次
-
-
-
-
-
- 免费
-
-
-
@@ -201,16 +189,40 @@
-
-
- ¥{{ selectedPrice.amount }}
-
-
-
- {{ selectedPrice.amount }}次
-
- 免费
+
+
+
+
+
+
+ 储值卡
+
+
+ 次卡
+
+
+
+
+
+ ¥{{ course.storedValueAmount }}
+ ⚡{{ course.pointCardAmount }}次
+
+
+
+
+
+ 免费
+
+ ¥{{ course.storedValueAmount }}
+
+
+ ⚡
+ {{ course.pointCardAmount }}次
+
+
+
+
{{ canBook ? '立即预约' : (course.currentMembers >= course.maxMembers ? '已满员' : statusText) }}
@@ -222,7 +234,7 @@
+
+
\ No newline at end of file
diff --git a/gym-manage-uniapp/request_api/groupCourse.mock.js b/gym-manage-uniapp/request_api/groupCourse.mock.js
index 173f70f..9f08681 100644
--- a/gym-manage-uniapp/request_api/groupCourse.mock.js
+++ b/gym-manage-uniapp/request_api/groupCourse.mock.js
@@ -301,6 +301,48 @@ const mockCourseList = [
}
]
+// 模拟团课类型标签数据
+const mockCourseTypeLabels = [
+ {
+ "id": "1",
+ "createBy": null,
+ "updateBy": null,
+ "createdAt": "2026-06-11T11:29:07.859339",
+ "updatedAt": "2026-06-11T11:29:07.859339",
+ "deletedAt": null,
+ "labelName": "高级进阶",
+ "color": "#f5222d",
+ "description": "适合高级学员"
+ },
+ {
+ "id": "2",
+ "createBy": null,
+ "updateBy": null,
+ "createdAt": "2026-06-11T11:29:07.859339",
+ "updatedAt": "2026-06-11T11:29:07.859339",
+ "deletedAt": null,
+ "labelName": "增肌强化",
+ "color": "#13c2c2",
+ "description": "有助于增肌强化"
+ }
+]
+
+// 团课类型与标签的关联关系
+const courseTypeLabelMap = {
+ '1': [
+ { id: '1', labelName: '高级进阶', color: '#f5222d' },
+ { id: '2', labelName: '增肌强化', color: '#13c2c2' }
+ ],
+ '2': [
+ { id: '3', labelName: '燃脂挑战', color: '#fa8c16' },
+ { id: '4', labelName: 'HIIT高强度', color: '#722ed1' }
+ ],
+ '3': [
+ { id: '5', labelName: '塑形美体', color: '#eb2f96' },
+ { id: '6', labelName: '核心训练', color: '#52c41a' }
+ ]
+}
+
/**
* 模拟团课API(测试环境)
* 接口签名与真实API保持一致
@@ -401,6 +443,25 @@ export const groupCourseMockApi = {
})
}, 300)
})
+ },
+
+ /**
+ * 基于类型ID获取标签列表
+ * @param {string} courseType - 团课类型ID
+ * @returns {Promise} - 标签列表数据
+ */
+ getLabelsByCourseType: (courseType) => {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ console.log('[groupCourse.mock.js] 模拟获取团课类型标签:', courseType)
+ const labels = courseTypeLabelMap[courseType] || []
+ resolve({
+ code: 0,
+ message: 'success',
+ data: labels
+ })
+ }, 200)
+ })
}
}
diff --git a/gym-manage-uniapp/utils/request.js b/gym-manage-uniapp/utils/request.js
index ccffb07..1316ef0 100644
--- a/gym-manage-uniapp/utils/request.js
+++ b/gym-manage-uniapp/utils/request.js
@@ -169,33 +169,42 @@ export const request = (options) => {
}
}
+ console.log(`[API] 请求开始: ${method} ${BASE_URL + url}`)
+ console.log(`[API] 请求参数:`, data)
+ console.log(`[API] 请求头:`, requestHeader)
+
uni.request({
url: BASE_URL + url,
method: method,
data: data,
header: requestHeader,
success: (res) => {
+ console.log(`[API] 响应成功: ${method} ${BASE_URL + url}`)
+ console.log(`[API] 响应状态码:`, res.statusCode)
+ console.log(`[API] 响应头:`, res.header)
+ console.log(`[API] 响应数据:`, JSON.stringify(res.data, null, 2))
+
if (res.statusCode === 200) {
- // 如果启用缓存,保存响应数据
if (cache && cacheKey && res.data) {
setCache(cacheKey, res.data, cacheTime)
}
resolve(res.data)
} else if (res.statusCode === 401) {
- // token过期,清除token并提示重新登录
clearToken()
uni.showToast({
title: '登录已过期,请重新登录',
icon: 'none'
})
- reject({ code: 401, message: '登录已过期' })
+ reject({ code: 401, message: '登录已过期', statusCode: res.statusCode, response: res })
} else {
- reject({ code: res.statusCode, message: res.data?.message || '请求失败' })
+ console.error(`[API] 请求失败: ${res.statusCode} - ${res.data?.message || '未知错误'}`)
+ reject({ code: res.statusCode, message: res.data?.message || '请求失败', statusCode: res.statusCode, response: res })
}
},
fail: (err) => {
- console.error(`[API] 请求失败: ${url}`, err)
- reject({ code: -1, message: '网络请求失败', error: err })
+ console.error(`[API] 请求失败: ${method} ${BASE_URL + url}`)
+ console.error(`[API] 错误详情:`, JSON.stringify(err, null, 2))
+ reject({ code: -1, message: '网络请求失败', error: err, url: BASE_URL + url, method: method })
}
})
})
diff --git a/gym-manage-uniapp/vite.config.js b/gym-manage-uniapp/vite.config.js
index 400a2ca..cf4c3c8 100644
--- a/gym-manage-uniapp/vite.config.js
+++ b/gym-manage-uniapp/vite.config.js
@@ -12,11 +12,10 @@ export default defineConfig({
},
server: {
proxy: {
- // 匹配所有 /api/ 开头的请求(排除静态文件)
'/api': {
- target: 'http://192.168.5.15:8084', // 你的后端SpringBoot地址
- changeOrigin: true, // 开启跨域伪装
- // 只代理真正的后端API请求,排除 .js .vue 等静态文件
+ target: 'https://www.gymmanage.xyz',
+ changeOrigin: true,
+ secure: true,
bypass: function(req, res, proxyOptions) {
if (req.url.indexOf('.js') !== -1 || req.url.indexOf('.vue') !== -1 ||
req.url.indexOf('.css') !== -1 || req.url.indexOf('.json') !== -1) {