作为一名长期从事体育信息化系统开发的工程师,我最近完成了一个足球青训俱乐部管理系统的全栈项目。这个系统采用SpringBoot+Vue的主流技术栈,解决了传统青训机构在学员管理、课程安排、财务统计等方面的痛点。记得去年帮本地一家青训俱乐部做咨询时,他们的教练还在用Excel表格记录学员信息,经常出现课程冲突、缴费遗漏等问题,这正是促使我开发这套系统的初衷。
系统核心价值在于将俱乐部的日常运营全面数字化:
采用SpringBoot 2.7 + MyBatis Plus的组合,这是我经过多个项目验证的稳定方案。数据库选用MySQL 8.0,主要考虑因素:
关键包结构设计:
code复制com
├── config # 安全配置/异常处理
├── controller # 对外接口
├── service # 业务逻辑
│ ├── impl # 实现类
├── mapper # 数据访问层
├── entity # 数据实体
└── util # 工具类
特别说明权限控制方案:采用RBAC模型,但针对青训场景做了简化:
Vue3 + Element Plus的组合提供了良好的开发体验。项目中有几个值得分享的优化点:
javascript复制// 在permission.js中动态添加路由
router.addRoute({
path: '/training',
component: Layout,
children: [{
path: 'record',
component: () => import('@/views/training/record'),
meta: { title: '训练记录', roles: ['coach'] }
}]
})
css复制/* 使用rem适配不同屏幕 */
@media screen and (max-width: 768px) {
:root {
font-size: 12px;
}
.el-dialog {
width: 90% !important;
}
}
从试训到毕业的全流程设计:
java复制@NotNull(message = "监护人联系方式不能为空")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式错误")
private String guardianPhone;
java复制public class GroupingService {
public String autoGroup(Trainee trainee) {
int age = calculateAge(trainee.getBirthDate());
String level = trainee.getSkillLevel();
if (age < 8) return "U8-" + level;
else if (age < 10) return "U10-" + level;
// 更多年龄分段...
}
}
sql复制CREATE TABLE `training_record` (
`record_id` varchar(20) NOT NULL,
`trainee_id` varchar(20) NOT NULL,
`coach_id` varchar(20) NOT NULL,
`training_date` date NOT NULL,
`endurance_score` tinyint DEFAULT NULL,
`skill_score` tinyint DEFAULT NULL,
`video_url` varchar(255) DEFAULT NULL,
PRIMARY KEY (`record_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
排课是青训机构最头疼的问题之一,我们的解决方案:
java复制public boolean checkConflict(Schedule newSchedule) {
return scheduleMapper.exists(
new QueryWrapper<Schedule>()
.eq("location", newSchedule.getLocation())
.eq("coach_id", newSchedule.getCoachId())
.le("start_time", newSchedule.getEndTime())
.ge("end_time", newSchedule.getStartTime())
);
}
javascript复制// 前端批量创建接口
const handleBatchCreate = () => {
const weeks = [1, 3, 5] // 周一、三、五
weeks.forEach(weekDay => {
axios.post('/api/schedules', {
...formData,
startTime: calculateWeekTime(weekDay),
endTime: calculateWeekTime(weekDay, 2) // 2小时课程
})
})
}
vue复制<template>
<FullCalendar :options="calendarOptions" />
</template>
<script setup>
const calendarOptions = {
initialView: 'timeGridWeek',
events: '/api/schedules',
eventClick: handleEventClick
}
</script>
当多个家长同时缴费时可能出现超卖,我们的解决方案:
java复制@Version
private Integer version;
public boolean pay(Long courseId) {
Course course = courseMapper.selectById(courseId);
if (course.getRemainSeats() > 0) {
course.setRemainSeats(course.getRemainSeats() - 1);
return courseMapper.updateById(course) > 0;
}
return false;
}
java复制public boolean payWithLock(Long courseId) {
String lockKey = "course:" + courseId;
try {
// 尝试获取锁
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (locked != null && locked) {
// 执行业务逻辑
return pay(courseId);
}
} finally {
redisTemplate.delete(lockKey);
}
return false;
}
当学员数量超过5000时,列表查询变慢的解决方案:
INDEX idx_age_level (age, training_level)java复制@Cacheable(value = "trainee", key = "#id")
public Trainee getById(String id) {
return traineeMapper.selectById(id);
}
推荐使用Docker Compose编排服务:
yaml复制version: '3'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
volumes:
- ./mysql-data:/var/lib/mysql
backend:
build: ./backend
ports:
- "8080:8080"
depends_on:
- mysql
frontend:
build: ./frontend
ports:
- "80:80"
关键配置建议:
yaml复制management:
endpoints:
web:
exposure:
include: health,metrics,info
endpoint:
health:
show-details: always
java复制@Slf4j
@RestController
public class TrainingController {
@GetMapping("/training/{id}")
public ResponseEntity<?> getTraining(@PathVariable String id) {
log.info("查询训练记录,ID:{}", id);
// ...
}
}
javascript复制import * as Sentry from "@sentry/vue";
Sentry.init({
dsn: "your-dsn",
integrations: [new BrowserTracing()],
tracesSampleRate: 0.2
});
这个系统在实际使用中还在不断迭代,近期正在开发的功能包括:
在开发过程中我深刻体会到,体育培训行业的信息化不是简单地把线下流程搬到线上,而是要结合教学场景重新设计数字化的业务流程。比如我们最初设计的签到功能需要手动点击,后来改为教练端一键批量签到,节省了课前的准备时间。