团队作业4——项目冲刺
三剑客Alpha冲刺完整报告
一、每日冲刺博客(第1-7天)
第1天:冲刺计划与团队准备
博客标题:智教协作队Alpha冲刺第1天 | 冲刺计划与团队准备
1. 会议(照片)

2. 工作进展
| 成员 | 昨天已完成的工作 | 今天计划完成的工作 | 工作中遇到的困难 |
|---|---|---|---|
| 王梓涵 | 完成登录页面原型设计 | 搭建登录页面静态框架,定义表单数据结构 | 暂无,原型参考明确 |
| 林嘉俊 | 完成用户表数据库设计 | 搭建后端项目架构,定义用户认证接口规范 | Spring Security依赖版本需与Spring Boot匹配 |
| 廖鸿基 | 整理核心功能测试点清单 | 完成登录与课件上传功能的测试用例初稿 | 部分接口参数未明确,需同步后端接口定义 |
3. 项目燃尽图

“冲刺第1天,剩余工时50小时”
4. 代码/文档登入记录

林嘉俊(后端项目初始化)
对应Issue:[#1 后端项目架构搭建](https://gitee.com/zhijia-team/teaching-system/issues/1)
// 项目核心配置类(标注关键依赖)
@SpringBootApplication
@ComponentScan(basePackages = "com.zhijia") // 扫描自定义组件
@MapperScan("com.zhijia.mapper") // 扫描MyBatis mapper接口
public class TeachingSystemApplication {
public static void main(String[] args) {
SpringApplication.run(TeachingSystemApplication.class, args);
System.out.println("教学系统后端服务启动成功");
}
}
// pom.xml核心依赖(节选)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
5. 项目程序/模块的最新(运行)截图
登录:

6. 每日每人总结
-
王梓涵:“今天把登录页面的HTML结构搭好了,明天重点做CSS样式和Element组件适配,确保和原型一致。”
-
林嘉俊:“项目架构搭完了,Security依赖版本问题解决了,明天开始写登录接口的Controller层逻辑。”
-
廖鸿基:“测试用例框架搭好了,等后端接口定义完就补充参数校验部分,确保用例覆盖所有异常场景。”
第2天:登录页面与认证接口初版完成
博客标题:智教协作队Alpha冲刺第2天 | 登录页面与认证接口初版完成
1. 工作进展
| 成员 | 昨天已完成的工作 | 今天计划完成的工作 | 工作中遇到的困难 |
|---|---|---|---|
| 王梓涵 | 完成登录页面静态框架搭建 | 实现表单校验逻辑,完成页面样式优化 | 表单正则规则与后端要求不一致,需重新对齐手机号/账号格式 |
| 林嘉俊 | 完成后端项目架构搭建 | 完成登录接口Controller与Service层开发,实现密码加密存储 | BCrypt加密后密码比对失败,需调整加密逻辑 |
| 廖鸿基 | 完成登录功能测试用例初稿 | 完善测试用例,搭建Postman测试集合,配置接口请求头 | 暂无,接口文档已同步 |
2. 项目燃尽图

3. 代码/文档登入记录

王梓涵(前端登录页面)
对应Issue:[#6 前端:用户登录页面开发](https://gitee.com/zhijia-team/teaching-system/issues/6)
<template>
<div class="login-container">
<el-card shadow="hover" class="login-card">
<h2 class="login-title">教学课件管理系统</h2>
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" label-width="80px">
<el-form-item label="账号" prop="account">
<el-input v-model="loginForm.account" placeholder="请输入工号/学号" prefix-icon="User"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="loginForm.password" type="password" placeholder="请输入密码" prefix-icon="Lock"></el-input>
</el-form-item>
<el-form-item label="角色" prop="role">
<el-select v-model="loginForm.role" placeholder="请选择角色">
<el-option label="教师" value="teacher"></el-option>
<el-option label="学生" value="student"></el-option>
<el-option label="管理员" value="admin"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleLogin" class="login-btn">登录</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script setup>
import { reactive, ref } from 'vue';
import { ElMessage } from 'element-plus';
import { useRouter } from 'vue-router';
const router = useRouter();
const loginForm = reactive({
account: '',
password: '',
role: ''
});
// 与后端对齐的表单校验规则
const loginRules = {
account: [
{ required: true, message: '请输入账号', trigger: 'blur' },
{ min: 5, max: 12, message: '账号长度为5-12位', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ pattern: /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,16}$/, message: '密码含大小写字母和数字,长度8-16位', trigger: 'blur' }
],
role: [{ required: true, message: '请选择角色', trigger: 'change' }]
};
const loginFormRef = ref(null);
// 登录按钮点击事件(待对接接口)
const handleLogin = async () => {
const valid = await loginFormRef.value.validate();
if (valid) {
ElMessage.success('表单校验通过,准备请求接口');
// 后续对接后端登录接口
}
};
</script>
林嘉俊(用户认证接口)
对应Issue:[#2 后端:用户认证接口开发](https://gitee.com/zhijia-team/teaching-system/issues/2)
// 登录请求参数DTO
@Data
public class LoginDTO {
@NotBlank(message = "账号不能为空")
@Size(min = 5, max = 12, message = "账号长度为5-12位")
private String account;
@NotBlank(message = "密码不能为空")
private String password;
@NotBlank(message = "角色不能为空")
private String role; // teacher/student/admin
}
// 用户认证Controller
@RestController
@RequestMapping("/api/auth")
@CrossOrigin // 解决前端跨域问题
public class AuthController {
@Autowired
private AuthService authService;
/**
* 用户登录接口
* @param loginDTO 登录请求参数
* @return 登录结果(token+用户信息)
*/
@PostMapping("/login")
public ResultVO<LoginVO> login(@Valid @RequestBody LoginDTO loginDTO) {
return authService.login(loginDTO);
}
}
// 密码加密与比对工具类
@Component
public class PasswordUtil {
// BCrypt加密逻辑,解决之前比对失败问题
public String encryptPassword(String rawPassword) {
return BCrypt.hashpw(rawPassword, BCrypt.gensalt(10));
}
public boolean matchPassword(String rawPassword, String encryptedPassword) {
return BCrypt.checkpw(rawPassword, encryptedPassword);
}
}
4. 项目程序/模块的最新(运行)截图
首页:热门课程

5. 每日每人总结
-
王梓涵:“表单校验和样式都搞定了,和后端确认了参数规则,明天就能对接接口实现登录功能。”
-
林嘉俊:“登录接口通了,密码加密问题解决了,明天写课件上传相关的实体类和接口。”
-
廖鸿基:“Postman测试集合建好了,登录接口的正向和反向用例都测过了,接口返回符合预期。”
第3天:课件列表页面与基础接口开发
博客标题:智教协作队Alpha冲刺第3天 | 课件列表页面与基础接口开发
1. 工作进展
| 成员 | 昨天已完成的工作 | 今天计划完成的工作 | 工作中遇到的困难 |
|---|---|---|---|
| 王梓涵 | 完成登录页面表单校验与样式 | 实现登录接口联调,开发课件列表页面布局与数据渲染 | 登录后token存储与请求头携带逻辑不清晰,需用Vuex管理状态 |
| 林嘉俊 | 完成登录接口开发与测试 | 开发课件实体类、Mapper及列表查询接口,配置MinIO文件存储 | MinIO客户端连接超时,需检查配置参数与服务状态 |
| 廖鸿基 | 完成登录接口测试用例执行 | 编写课件列表接口测试用例,设计课件数据构造脚本 | 暂无,接口文档已提前获取 |
2. 项目燃尽图

3. 代码/文档登入记录

王梓涵(Vuex状态管理与课件列表)
对应Issue:[#7 前端:课件列表页面开发](https://gitee.com/zhijia-team/teaching-system/issues/7)
<!-- 课件列表页面核心代码 -->
<template>
<div class="courseware-container">
<el-page-header content="课件列表"></el-page-header>
<el-card shadow="hover" class="search-card">
<el-input v-model="searchKey" placeholder="请输入课件名称搜索" suffix-icon="Search" @keyup.enter="getCoursewareList"></el-input>
</el-card>
<el-table :data="coursewareList" border stripe style="width: 100%; margin-top: 20px;">
<el-table-column prop="name" label="课件名称" width="200"></el-table-column>
<el-table-column prop="teacherName" label="上传教师" width="120"></el-table-column>
<el-table-column prop="subject" label="学科" width="100"></el-table-column>
<el-table-column prop="grade" label="年级" width="100"></el-table-column>
<el-table-column prop="uploadTime" label="上传时间" width="180"></el-table-column>
<el-table-column prop="fileSize" label="文件大小" width="100"></el-table-column>
<el-table-column label="操作" width="180">
<template #default="scope">
<el-button type="primary" size="small" @click="handlePreview(scope.row)">预览</el-button>
<el-button type="success" size="small" @click="handleDownload(scope.row)">下载</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[10, 20, 50]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
style="margin-top: 20px; text-align: right;"
></el-pagination>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { useStore } from 'vuex';
import axios from 'axios';
const store = useStore();
const coursewareList = ref([]);
const currentPage = ref(1);
const pageSize = ref(10);
const total = ref(0);
const searchKey = ref('');
// 初始化时获取课件列表
onMounted(() => {
getCoursewareList();
});
// 获取课件列表(携带token)
const getCoursewareList = async () => {
const token = store.state.user.token;
const response = await axios.get('/api/courseware/list', {
params: {
pageNum: currentPage.value,
pageSize: pageSize.value,
keyword: searchKey.value
},
headers: {
'Authorization': `Bearer ${token}` // 携带JWT令牌
}
});
coursewareList.value = response.data.data.list;
total.value = response.data.data.total;
};
// 分页相关方法
const handleSizeChange = (val) => {
pageSize.value = val;
getCoursewareList();
};
const handleCurrentChange = (val) => {
currentPage.value = val;
getCoursewareList();
};
// 预览和下载方法(待实现)
const handlePreview = (row) => {
console.log('预览课件', row);
};
const handleDownload = (row) => {
console.log('下载课件', row);
};
</script>
林嘉俊(课件接口与MinIO配置)
对应Issue:[#3 后端:课件上传/版本接口开发](https://gitee.com/zhijia-team/teaching-system/issues/3)
// 课件实体类
@Data
@TableName("courseware")
public class Courseware {
@TableId(type = IdType.AUTO)
private Long id;
private String name; // 课件名称
private Long teacherId; // 上传教师ID
private String teacherName; // 教师姓名(冗余)
private String subject; // 学科(如数学、语文)
private String grade; // 年级(如高一、高二)
private String fileUrl; // MinIO文件存储路径
private String fileSize; // 文件大小(格式化显示)
private String fileType; // 文件类型(PPT/PDF/MP4等)
private Integer version; // 版本号
private Date uploadTime; // 上传时间
private Integer viewCount; // 浏览次数
private Integer downloadCount; // 下载次数
}
// MinIO配置类(解决连接超时问题)
@Configuration
public class MinIOConfig {
@Value("${minio.endpoint}")
private String endpoint;
@Value("${minio.access-key}")
private String accessKey;
@Value("${minio.secret-key}")
private String secretKey;
@Value("${minio.bucket-name}")
private String bucketName;
@Bean
public MinioClient minioClient() {
// 配置客户端,增加连接超时设置
return MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.httpClient(HttpClient.builder()
.connectTimeout(Duration.ofSeconds(5)) // 连接超时5秒
.writeTimeout(Duration.ofSeconds(30)) // 写入超时30秒
.readTimeout(Duration.ofSeconds(30)) // 读取超时30秒
.build())
.build();
}
// 初始化时创建存储桶
@PostConstruct
public void initBucket() throws Exception {
MinioClient client = minioClient();
boolean exists = client.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
if (!exists) {
client.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
System.out.println("MinIO存储桶创建成功:" + bucketName);
}
}
}
// 课件列表查询接口
@RestController
@RequestMapping("/api/courseware")
@CrossOrigin
public class CoursewareController {
@Autowired
private CoursewareService coursewareService;
/**
* 分页查询课件列表
* @param pageNum 页码
* @param pageSize 每页条数
* @param keyword 搜索关键词
* @return 分页结果
*/
@GetMapping("/list")
public ResultVO<PageVO<Courseware>> getCoursewareList(
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String keyword) {
PageVO<Courseware> pageVO = coursewareService.getCoursewareList(pageNum, pageSize, keyword);
return ResultVO.success(pageVO);
}
}
4. 项目程序/模块的最新(运行)截图
个人中心:



5. 每日每人总结
-
王梓涵:“Vuex状态管理搞定了,token能正常携带,课件列表能渲染数据了,明天做上传页面和预览功能。”
-
林嘉俊:“MinIO连接问题解决了,课件列表接口通了,明天开发文件上传和版本管理接口。”
-
廖鸿基:“课件列表接口的测试用例写完了,构造了10条测试数据,分页和搜索功能都测过了没问题。”
第4天:课件上传功能开发与联调
博客标题:智教协作队Alpha冲刺第4天 | 课件上传功能开发与联调
1. 工作进展
| 成员 | 昨天已完成的工作 | 今天计划完成的工作 | 工作中遇到的困难 |
|---|---|---|---|
| 王梓涵 | 完成课件列表页面开发与数据渲染 | 开发课件上传页面,实现大文件分片上传功能 | 大文件上传进度条计算不准确,分片上传逻辑复杂 |
| 林嘉俊 | 完成课件列表接口与MinIO配置 | 开发课件上传、分片合并接口,实现版本管理逻辑 | 分片合并时文件顺序错乱,需通过索引控制合并顺序 |
| 廖鸿基 | 完成课件列表接口测试 | 测试课件上传功能,重点测试大文件与多版本上传场景 | 大文件测试时网络波动导致上传失败,需增加重试机制测试 |
2. 项目燃尽图

3. 代码/文档登入记录

王梓涵(大文件分片上传组件)
对应Issue:[#8 前端:课件上传页面开发](https://gitee.com/zhijia-team/teaching-system/issues/8)
<template>
<div class="upload-container">
<el-page-header content="课件上传"></el-page-header>
<el-card shadow="hover" class="upload-card">
<el-form ref="uploadForm" :model="uploadForm" :rules="uploadRules">
<el-form-item label="课件名称" prop="name">
<el-input v-model="uploadForm.name" placeholder="请输入课件名称"></el-input>
</el-form-item>
<el-form-item label="所属学科" prop="subject">
<el-select v-model="uploadForm.subject" placeholder="请选择学科">
<el-option label="数学" value="数学"></el-option>
<el-option label="语文" value="语文"></el-option>
<el-option label="英语" value="英语"></el-option>
</el-select>
</el-form-item>
<el-form-item label="适用年级" prop="grade">
<el-select v-model="uploadForm.grade" placeholder="请选择年级">
<el-option label="高一" value="高一"></el-option>
<el-option label="高二" value="高二"></el-option>
<el-option label="高三" value="高三"></el-option>
</el-select>
</el-form-item>
<el-form-item label="上传文件" prop="file">
<el-upload
class="upload-demo"
:auto-upload="false"
:on-change="handleFileChange"
:file-list="fileList"
accept=".ppt,.pptx,.pdf,.mp4,.mp3"
<el-button type="primary">点击选择文件</el-button>
<div class="upload-tip">支持PPT/PDF/MP4/MP3格式,最大支持100MB</div>
</el-upload>
<el-progress :percentage="uploadProgress" v-if="uploadProgress > 0" style="margin-top:10px;"></el-progress>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleUpload">开始上传</el-button>
<el-button @click="resetForm" style="margin-left:10px;">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script setup>
import { reactive, ref } from 'vue';
import { ElMessage, ElProgress } from 'element-plus';
import { useStore } from 'vuex';
import axios from 'axios';
const store = useStore();
const uploadForm = reactive({
name: '',
subject: '',
grade: '',
file: null
});
const fileList = ref([]);
const uploadProgress = ref(0);
const uploadRules = {
name: [{ required: true, message: '请输入课件名称', trigger: 'blur' }],
subject: [{ required: true, message: '请选择学科', trigger: 'change' }],
grade: [{ required: true, message: '请选择年级', trigger: 'change' }],
file: [{ required: true, message: '请选择文件', trigger: 'change' }]
};
const uploadFormRef = ref(null);
// 选择文件回调
const handleFileChange = (uploadFile) => {
uploadForm.file = uploadFile.raw;
fileList.value = [uploadFile];
};
// 分片上传核心逻辑(解决进度条不准问题)
const handleUpload = async () => {
const valid = await uploadFormRef.value.validate();
if (!valid) return;
const file = uploadForm.file;
const chunkSize = 5 * 1024 * 1024; // 5MB分片
const totalChunks = Math.ceil(file.size / chunkSize);
const fileMd5 = await getFileMd5(file); // 计算文件唯一标识
const token = store.state.user.token;
// 1. 预上传请求(告知后端文件信息)
const preUploadRes = await axios.post('/api/courseware/pre-upload', {
fileName: file.name,
fileSize: file.size,
fileType: file.type,
fileMd5: fileMd5,
totalChunks: totalChunks,
...uploadForm
}, { headers: { 'Authorization': `Bearer ${token}` } });
const { uploadId, existingChunks } = preUploadRes.data.data;
let uploadedChunks = existingChunks.length;
uploadProgress.value = Math.round((uploadedChunks / totalChunks) * 100);
// 2. 上传未完成分片
for (let i = 0; i < totalChunks; i++) {
if (existingChunks.includes(i)) continue;
const start = i * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('chunk', chunk);
formData.append('chunkIndex', i);
formData.append('uploadId', uploadId);
formData.append('fileMd5', fileMd5);
await axios.post('/api/courseware/upload-chunk', formData, {
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'multipart/form-data' },
onUploadProgress: (e) => {
// 计算当前分片上传进度,叠加到总进度
const chunkProgress = Math.round((e.loaded / e.total) * 100);
const currentTotalProgress = Math.round(((uploadedChunks * 100) + chunkProgress) / totalChunks);
uploadProgress.value = currentTotalProgress > uploadProgress.value ? currentTotalProgress : uploadProgress.value;
}
});
uploadedChunks++;
}
// 3. 合并分片
await axios.post('/api/courseware/merge-chunk', {
uploadId: uploadId,
fileMd5: fileMd5,
fileName: file.name
}, { headers: { 'Authorization': `Bearer ${token}` } });
uploadProgress.value = 100;
ElMessage.success("课件上传成功!");
resetForm();
};
// 计算文件MD5(简化版)
const getFileMd5 = (file) => {
return new Promise((resolve) => {
const reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = (e) => {
const spark = new SparkMD5.ArrayBuffer();
spark.append(e.target.result);
resolve(spark.end());
};
});
};
// 重置表单
const resetForm = () => {
uploadFormRef.value.resetFields();
fileList.value = [];
uploadProgress.value = 0;
uploadForm.file = null;
};
</script>
林嘉俊(课件上传与版本管理接口)
对应Issue:[#3 后端:课件上传/版本接口开发](https://gitee.com/zhijia-team/teaching-system/issues/3)
// 预上传请求DTO
@Data
public class PreUploadDTO {
@NotBlank(message = "课件名称不能为空")
private String name;
@NotBlank(message = "学科不能为空")
private String subject;
@NotBlank(message = "年级不能为空")
private String grade;
@NotBlank(message = "文件MD5不能为空")
private String fileMd5;
private String fileName;
private Long fileSize;
private String fileType;
private Integer totalChunks;
}
// 课件上传Service层(核心逻辑)
@Service
public class CoursewareServiceImpl implements CoursewareService {
@Autowired
private MinioClient minioClient;
@Autowired
private CoursewareMapper coursewareMapper;
@Autowired
private UploadRecordMapper uploadRecordMapper;
@Value("${minio.bucket-name}")
private String bucketName;
// 预上传处理:检查文件是否已上传,生成uploadId
@Override
@Transactional
public PreUploadVO preUpload(PreUploadDTO dto, Long teacherId, String teacherName) {
// 1. 检查是否存在相同MD5的文件(去重)
UploadRecord record = uploadRecordMapper.selectOne(new QueryWrapper<UploadRecord>()
.eq("file_md5", dto.getFileMd5()));
PreUploadVO vo = new PreUploadVO();
vo.setUploadId(UUID.randomUUID().toString().replace("-", ""));
// 2. 若文件已存在,返回已上传分片
if (record != null) {
vo.setExistingChunks(JSON.parseArray(record.getUploadedChunks(), Integer.class));
// 检查是否为同一课件的新版本
Courseware oldCourseware = coursewareMapper.selectOne(new QueryWrapper<Courseware>()
.eq("teacher_id", teacherId)
.eq("name", dto.getName()));
if (oldCourseware != null) {
vo.setOldVersion(oldCourseware.getVersion());
}
return vo;
}
// 3. 新增上传记录
record = new UploadRecord();
record.setFileMd5(dto.getFileMd5());
record.setFileName(dto.getFileName());
record.setUploadId(vo.getUploadId());
record.setTotalChunks(dto.getTotalChunks());
record.setUploadedChunks("[]"); // 初始无已上传分片
record.setStatus(0); // 上传中
uploadRecordMapper.insert(record);
return vo;
}
// 分片上传:接收分片并存储到MinIO
@Override
public void uploadChunk(MultipartFile chunk, Integer chunkIndex, String uploadId, String fileMd5) {
// 1. 获取上传记录
UploadRecord record = uploadRecordMapper.selectOne(new QueryWrapper<UploadRecord>()
.eq("upload_id", uploadId)
.eq("file_md5", fileMd5));
if (record == null) {
throw new BusinessException("上传记录不存在");
}
// 2. 上传分片到MinIO(以uploadId作为临时目录)
String chunkPath = "chunks/" + uploadId + "/" + chunkIndex;
try {
minioClient.putObject(PutObjectArgs.builder()
.bucket(bucketName)
.object(chunkPath)
.stream(chunk.getInputStream(), chunk.getSize(), -1)
.contentType(chunk.getContentType())
.build());
} catch (Exception e) {
throw new BusinessException("分片上传失败:" + e.getMessage());
}
// 3. 更新已上传分片列表(解决顺序错乱问题)
List<Integer> existingChunks = JSON.parseArray(record.getUploadedChunks(), Integer.class);
if (!existingChunks.contains(chunkIndex)) {
existingChunks.add(chunkIndex);
// 排序确保分片顺序正确
Collections.sort(existingChunks);
record.setUploadedChunks(JSON.toJSONString(existingChunks));
uploadRecordMapper.updateById(record);
}
}
// 合并分片并创建课件记录(含版本管理)
@Override
@Transactional
public void mergeChunk(String uploadId, String fileMd5, String fileName, Long teacherId, String teacherName) {
// 1. 合并MinIO中的分片
UploadRecord record = uploadRecordMapper.selectOne(new QueryWrapper<UploadRecord>()
.eq("upload_id", uploadId)
.eq("file_md5", fileMd5));
String targetPath = "courseware/" + teacherId + "/" + System.currentTimeMillis() + "-" + fileName;
// 2. 调用MinIO合并接口
try {
minioClient.composeObject(ComposeObjectArgs.builder()
.bucket(bucketName)
.object(targetPath)
.sources(IntStream.range(0, record.getTotalChunks())
.mapToObj(i -> ComposeSource.builder()
.bucket(bucketName)
.object("chunks/" + uploadId + "/" + i)
.build())
.collect(Collectors.toList()))
.build());
} catch (Exception e) {
throw new BusinessException("分片合并失败:" + e.getMessage());
}
// 3. 版本管理:查询同一课件的最新版本
Courseware oldCourseware = coursewareMapper.selectOne(new QueryWrapper<Courseware>()
.eq("teacher_id", teacherId)
.eq("name", record.getFileName().split("\\.")[0])
.orderByDesc("version")
.last("limit 1"));
Integer version = oldCourseware == null ? 1 : oldCourseware.getVersion() + 1;
// 4. 新增课件记录
Courseware courseware = new Courseware();
courseware.setName(record.getFileName().split("\\.")[0]);
courseware.setTeacherId(teacherId);
courseware.setTeacherName(teacherName);
courseware.setSubject(record.getSubject());
courseware.setGrade(record.getGrade());
courseware.setFileUrl(targetPath);
courseware.setFileType(fileName.substring(fileName.lastIndexOf(".")));
courseware.setFileSize(formatFileSize(record.getFileSize()));
courseware.setVersion(version);
courseware.setUploadTime(new Date());
courseware.setViewCount(0);
courseware.setDownloadCount(0);
coursewareMapper.insert(courseware);
}
// 文件大小格式化(B→KB/MB)
private String formatFileSize(Long fileSize) {
if (fileSize < 1024 * 1024) {
return String.format("%.2f KB", fileSize / 1024.0);
} else {
return String.format("%.2f MB", fileSize / (1024.0 * 1024));
}
}
}
4. 项目程序/模块的最新(运行)截图
①学校管理

学院的增删改查
②班级管理


班级的增删改查
5. 每日每人总结
-
王梓涵:“分片上传功能搞定了,进度条计算精准了,明天开发课件预览和学生评分组件,重点做星标评分的交互。”
-
林嘉俊:“上传和版本管理接口通了,解决了分片顺序问题,明天开发评分接口和权限控制逻辑,确保学生只能评自己的课程。”
-
廖鸿基:“测试了10MB和50MB的文件上传,多版本上传也测了,接口稳定性没问题,明天准备评分功能的测试用例。”
第5天:学生评分功能开发与权限控制
博客标题:智教协作队Alpha冲刺第5天 | 评分功能落地与权限管控
1. 工作进展
| 成员 | 昨天已完成的工作 | 今天计划完成的工作 | 工作中遇到的困难 |
| 王梓涵 | 完成课件上传页面开发 | 开发课程评分组件、学生课程列表页面,实现星标评分交互 | 星标评分与评语提交的联动逻辑,需防止重复提交 |
| 林嘉俊 | 完成课件上传与版本接口开发 | 开发评分接口、权限拦截器,实现课程数据权限过滤 | 权限拦截器与JWT令牌结合时,用户角色信息获取延迟 |
| 廖鸿基 | 完成课件上传功能测试 | 编写评分功能测试用例,测试权限控制场景(越权访问拦截) | 构造不同角色的JWT令牌测试复杂,需后端提供测试令牌接口 |
2. 项目燃尽图

3. 代码/文档登入记录


王梓涵(星标评分组件)
对应Issue:[#9 前端:学生评分功能开发](https://gitee.com/zhijia-team/teaching-system/issues/9)
<template>
<div class="score-container">
<el-page-header content="我的课程评分"></el-page-header>
<el-card shadow="hover" v-for="course in courseList" :key="course.id" class="course-card">
<div class="course-info">
<h3>{{ course.name }}</h3>
<p>授课教师:{{ course.teacherName }} | 学科:{{ course.subject }} | 年级:{{ course.grade }}</p>
</div>
<div class="score-section">
<el-rate v-model="scoreForm[course.id]" :max="5" @change="handleScoreChange(course.id)"></el-rate>
<el-input
v-model="commentForm[course.id]"
placeholder="请输入评语(10-50字)"
type="textarea"
rows="2"
style="margin-top:10px;"
></el-input>
<el-button
type="primary"
style="margin-top:10px;"
@click="handleSubmit(course.id)"
:disabled="submitLoading[course.id]"
>
<el-icon v-if="submitLoading[course.id]"><Loading></Loading></el-icon>
{{ course.scored ? "已评分" : "提交评分" }}
</el-button>
</div>
</el-card>
</div>
</template>
<script setup>
import { reactive, ref, onMounted } from 'vue';
import { ElMessage, ElRate, ElLoading } from 'element-plus';
import { useStore } from 'vuex';
import axios from 'axios';
import { Loading } from '@element-plus/icons-vue';
const store = useStore();
const courseList = ref([]);
const scoreForm = reactive({}); // 存储各课程评分:{courseId: score}
const commentForm = reactive({}); // 存储各课程评语:{courseId: comment}
const submitLoading = reactive({}); // 防止重复提交:{courseId: boolean}
// 初始化获取学生所选课程及已有评分
onMounted(() => {
getStudentCourseList();
});
// 获取学生课程列表
const getStudentCourseList = async () => {
const token = store.state.user.token;
const res = await axios.get('/api/student/course', {
headers: { 'Authorization': `Bearer ${token}` }
});
courseList.value = res.data.data;
// 初始化表单(回填已有评分)
courseList.value.forEach(course => {
if (course.score) {
scoreForm[course.id] = course.score;
commentForm[course.id] = course.comment;
} else {
scoreForm[course.id] = 0;
commentForm[course.id] = '';
}
submitLoading[course.id] = false;
});
};
// 提交评分
const handleSubmit = async (courseId) => {
const score = scoreForm[courseId];
const comment = commentForm[courseId];
if (score < 1) {
ElMessage.warning("请选择评分");
return;
}
if (comment.length < 10 || comment.length > 50) {
ElMessage.warning("评语需10-50字");
return;
}
submitLoading[courseId] = true;
const token = store.state.user.token;
try {
await axios.post('/api/score/submit', {
courseId: courseId,
score: score,
comment: comment
}, { headers: { 'Authorization': `Bearer ${token}` } });
ElMessage.success("评分提交成功!");
// 标记为已评分
const course = courseList.value.find(item => item.id === courseId);
course.scored = true;
} catch (e) {
ElMessage.error("提交失败:" + e.response.data.message);
} finally {
submitLoading[courseId] = false;
}
};
// 评分变化回调(可选)
const handleScoreChange = (courseId) => {
console.log("课程" + courseId + "评分:" + scoreForm[courseId]);
};
</script>
林嘉俊(权限拦截器与评分接口)
对应Issue:[#10 后端:权限控制与评分接口开发](https://gitee.com/zhijia-team/teaching-system/issues/10)
// 自定义权限注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireRole {
String[] value(); // 允许的角色,如{"teacher", "admin"}
}
// JWT权限拦截器(解决角色信息延迟问题)
@Component
public class JwtAuthInterceptor implements HandlerInterceptor {
@Autowired
private JwtUtil jwtUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 获取Token
String token = request.getHeader("Authorization");
if (token == null || !token.startsWith("Bearer ")) {
throw new BusinessException("请先登录");
}
token = token.substring(7);
// 2. 解析Token获取用户信息(缓存用户信息到ThreadLocal)
Claims claims = jwtUtil.parseToken(token);
Long userId = Long.valueOf(claims.get("userId").toString());
String role = claims.get("role").toString();
UserContext.setUserId(userId);
UserContext.setRole(role);
// 3. 角色权限校验
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
RequireRole requireRole = handlerMethod.getMethodAnnotation(RequireRole.class);
if (requireRole != null) {
String[] allowRoles = requireRole.value();
if (!Arrays.asList(allowRoles).contains(role)) {
throw new BusinessException("无权限访问");
}
}
}
return true;
}
// 清除ThreadLocal,防止内存泄漏
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserContext.clear();
}
}
// 评分接口(带权限控制)
@RestController
@RequestMapping("/api/score")
@CrossOrigin
public class ScoreController {
@Autowired
private ScoreService scoreService;
// 学生提交评分(仅学生可访问)
@PostMapping("/submit")
@RequireRole("student")
public ResultVO submitScore(@Valid @RequestBody ScoreSubmitDTO dto) {
Long studentId = UserContext.getUserId();
scoreService.submitScore(dto, studentId);
return ResultVO.success("评分提交成功");
}
// 教师查看课程评分(仅教师可访问)
@GetMapping("/course/{courseId}")
@RequireRole("teacher")
public ResultVO<List<ScoreVO>> getCourseScore(@PathVariable Long courseId) {
Long teacherId = UserContext.getUserId();
// 权限过滤:仅能查看自己授课的课程评分
List<ScoreVO> scoreList = scoreService.getCourseScore(courseId, teacherId);
return ResultVO.success(scoreList);
}
// 测试用令牌生成接口(供测试人员使用)
@GetMapping("/test/token")
@RequireRole("admin")
public ResultVO<String> generateTestToken(@RequestParam String role, @RequestParam Long userId) {
String token = jwtUtil.generateToken(userId, role);
return ResultVO.success(token);
}
}
4. 项目程序/模块的最新(运行)截图
③用户管理



④授予管理

管理“学生/教师/课程管理员”对某课程是否有查看的权限
5. 每日每人总结
-
王梓涵:“评分组件的防重复提交做好了,学生和教师的评分页面都开发完了,明天重点做成绩统计的前端页面,特别是图表展示部分。”
-
林嘉俊:“权限拦截器和评分接口通了,用ThreadLocal解决了角色信息延迟问题,明天开发成绩统计和Excel导出接口,确保统计逻辑准确。”
-
廖鸿基:“权限场景都测过了,包括越权访问、重复评分等异常场景,后端给了测试令牌接口后效率高多了,明天准备成绩统计功能的测试用例。”
第6天:成绩统计功能开发与Excel导出
博客标题:智教协作队Alpha冲刺第6天 | 成绩统计与数据导出落地
1. 工作进展
| 成员 | 昨天已完成的工作 | 今天计划完成的工作 | 工作中遇到的困难 |
| 王梓涵 | 完成学生评分组件开发 | 开发成绩统计页面,实现分数段图表展示,对接Excel导出接口 | ECharts图表数据联动延迟,跨班级对比图表样式需优化 |
| 林嘉俊 | 完成权限拦截器与评分接口开发 | 开发成绩统计接口、Excel导出工具类,实现跨班级数据对比 | 大数据量Excel导出时内存溢出,需用SXSSFWorkbook优化 |
| 廖鸿基 | 完成评分功能与权限测试 | 测试成绩统计功能,重点测试Excel导出与跨班级对比场景 | 大数据量导出测试时,需构造1000条以上成绩数据,效率较低 |
2. 项目燃尽图

3. 代码/文档登入记录

王梓涵(成绩统计图表页面)
对应Issue:[#11 前端:成绩统计页面开发](https://gitee.com/zhijia-team/teaching-system/issues/11)
<template>
<div class="stat-container">
<el-page-header content="成绩统计分析"></el-page-header>
<el-card shadow="hover" class="filter-card">
<el-form :model="filterForm" inline>
<el-form-item label="课程">
<el-select v-model="filterForm.courseId" placeholder="请选择课程" @change="getStatData">
<el-option v-for="course in courseList" :key="course.id" :label="course.name" :value="course.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="班级">
<el-select v-model="filterForm.classIds" placeholder="请选择班级(可多选)" multiple @change="getStatData">
<el-option v-for="cls in classList" :key="cls.id" :label="cls.name" :value="cls.id"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="exportExcel">导出Excel</el-button>
</el-form-item>
</el-form>
</el-card>
<div class="chart-container" style="margin-top:20px;">
<el-card shadow="hover" class="chart-card">
<h3 slot="header">分数段分布(人数)</h3>
<div style="height:400px;">
<echarts :option="scoreSectionOption" style="width:100%;height:100%;"></echarts>
</div>
</el-card>
<el-card shadow="hover" class="chart-card" style="margin-top:20px;">
<h3 slot="header">班级平均分对比</h3>
<div style="height:400px;">
<echarts :option="classAvgOption" style="width:100%;height:100%;"></echarts>
</div>
</el-card>
</div>
<el-card shadow="hover" style="margin-top:20px;">
<h3 slot="header">统计详情</h3>
<el-table :data="statDetail" border stripe>
<el-table-column label="统计项" prop="item" width="150"></el-table-column>
<el-table-column label="数值" prop="value"></el-table-column>
</el-table>
</el-card>
</div>
</template>
<script setup>
import { reactive, ref, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { useStore } from 'vuex';
import axios from 'axios';
import * as echarts from 'echarts';
const store = useStore();
const filterForm = reactive({
courseId: '',
classIds: []
});
const courseList = ref([]);
const classList = ref([]);
const statDetail = ref([]);
// ECharts配置
const scoreSectionOption = ref({});
const classAvgOption = ref({});
// 初始化加载课程和班级列表
onMounted(() => {
getCourseAndClassList();
});
// 获取课程和班级数据
const getCourseAndClassList = async () => {
const token = store.state.user.token;
const res = await axios.get('/api/teacher/course-class', {
headers: { 'Authorization': `Bearer ${token}` }
});
courseList.value = res.data.data.courses;
classList.value = res.data.data.classes;
};
// 获取统计数据
const getStatData = async () => {
if (!filterForm.courseId || filterForm.classIds.length === 0) {
ElMessage.warning("请选择课程和班级");
return;
}
const token = store.state.user.token;
const res = await axios.get('/api/grade/stat', {
params: {
courseId: filterForm.courseId,
classIds: filterForm.classIds.join(',')
},
headers: { 'Authorization': `Bearer ${token}` }
});
const data = res.data.data;
// 填充统计详情
statDetail.value = [
{ item: '参与人数', value: data.totalCount },
{ item: '平均分', value: data.avgScore.toFixed(2) },
{ item: '最高分', value: data.maxScore },
{ item: '最低分', value: data.minScore },
{ item: '及格率', value: (data.passRate * 100).toFixed(2) + '%' },
{ item: '优秀率(85分及以上)', value: (data.excellentRate * 100).toFixed(2) + '%' }
];
// 初始化分数段分布图表(解决联动延迟问题)
initScoreSectionChart(data.scoreSection);
// 初始化班级平均分对比图表
initClassAvgChart(data.classAvgList);
};
// 分数段分布图表初始化
const initScoreSectionChart = (scoreSectionData) => {
scoreSectionOption.value = {
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
legend: { data: ['人数'], top: 0 },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: {
type: 'category',
data: Object.keys(scoreSectionData),
axisLabel: { rotate: 0 }
},
yAxis: { type: 'value', name: '人数' },
series: [{
name: '人数',
type: 'bar',
data: Object.values(scoreSectionData),
itemStyle: { color: '#409EFF' },
emphasis: { itemStyle: { color: '#67C23A' } }
}]
};
};
// 班级平均分对比图表初始化
const initClassAvgChart = (classAvgList) => {
classAvgOption.value = {
tooltip: { trigger: 'item' },
legend: { data: ['平均分'], top: 0 },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: { type: 'category', data: classAvgList.map(item => item.className) },
yAxis: { type: 'value', name: '平均分', min: 0, max: 100 },
series: [{
name: '平均分',
type: 'line',
data: classAvgList.map(item => item.avgScore.toFixed(2)),
symbol: 'circle',
symbolSize: 8,
lineStyle: { width: 2 },
itemStyle: { color: '#E6A23C' }
}]
};
};
// 导出Excel
const exportExcel = async () => {
if (!filterForm.courseId || filterForm.classIds.length === 0) {
ElMessage.warning("请选择课程和班级");
return;
}
ElMessageBox.confirm(
"确定要导出当前统计数据吗?",
"导出确认",
{ confirmButtonText: "确定", cancelButtonText: "取消", type: "info" }
).then(async () => {
const token = store.state.user.token;
// 构造导出请求(响应为文件流)
const response = await axios.get('/api/grade/export', {
params: {
courseId: filterForm.courseId,
classIds: filterForm.classIds.join(',')
},
headers: { 'Authorization': `Bearer ${token}` },
responseType: 'blob' // 关键:指定响应类型为blob
});
// 处理文件流,触发下载
const blob = new Blob([response.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
const fileName = `成绩统计_${courseList.value.find(c => c.id === filterForm.courseId).name}_${new Date().toLocaleDateString()}.xlsx`;
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
ElMessage.success("导出成功!");
});
};
</script>
林嘉俊(成绩统计与Excel导出接口)
对应Issue:[#12 后端:成绩统计与导出接口开发](https://gitee.com/zhijia-team/teaching-system/issues/12)
// 成绩统计结果VO
@Data
public class GradeStatVO {
private Integer totalCount; // 参与人数
private Double avgScore; // 平均分
private Integer maxScore; // 最高分
private Integer minScore; // 最低分
private Double passRate; // 及格率(60分及以上)
private Double excellentRate; // 优秀率(85分及以上)
private Map<String, Integer> scoreSection; // 分数段分布:key=分数段,value=人数
private List<ClassAvgVO> classAvgList; // 班级平均分列表
}
// Excel导出工具类(解决内存溢出问题)
@Component
public class ExcelExportUtil {
/**
* 大数据量Excel导出(SXSSFWorkbook)
*/
public void exportGradeExcel(List<GradeDetailVO> dataList, OutputStream outputStream) {
// 1. 使用SXSSFWorkbook,设置内存中保留的行数(超过则写入磁盘)
SXSSFWorkbook workbook = new SXSSFWorkbook(100);
SXSSFSheet sheet = workbook.createSheet("成绩详情");
// 2. 定义表头
String[] headers = {"班级", "姓名", "学号", "课程", "成绩", "等级"};
SXSSFRow headerRow = sheet.createRow(0);
for (int i = 0; i < headers.length; i++) {
SXSSFCell cell = headerRow.createCell(i);
cell.setCellValue(headers[i]);
// 设置表头样式
CellStyle headerStyle = workbook.createCellStyle();
Font font = workbook.createFont();
font.setBold(true);
headerStyle.setFont(font);
headerStyle.setAlignment(HorizontalAlignment.CENTER);
cell.setCellStyle(headerStyle);
}
// 3. 填充数据
for (int i = 0; i < dataList.size(); i++) {
GradeDetailVO data = dataList.get(i);
SXSSFRow dataRow = sheet.createRow(i + 1);
dataRow.createCell(0).setCellValue(data.getClassName());
dataRow.createCell(1).setCellValue(data.getStudentName());
dataRow.createCell(2).setCellValue(data.getStudentAccount());
dataRow.createCell(3).setCellValue(data.getCourseName());
dataRow.createCell(4).setCellValue(data.getScore());
// 成绩等级判断
String level = data.getScore() >= 85 ? "优秀" : (data.getScore() >= 60 ? "及格" : "不及格");
dataRow.createCell(5).setCellValue(level);
}
// 4. 自适应列宽
for (int i = 0; i < headers.length; i++) {
sheet.autoSizeColumn(i);
// 调整列宽,避免内容被截断
sheet.setColumnWidth(i, sheet.getColumnWidth(i) + 2048);
}
// 5. 写入输出流
try {
workbook.write(outputStream);
} catch (IOException e) {
throw new BusinessException("Excel导出失败:" + e.getMessage());
} finally {
// 6. 清理临时文件,释放内存
workbook.dispose();
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
// 成绩统计与导出接口
@RestController
@RequestMapping("/api/grade")
@CrossOrigin
public class GradeController {
@Autowired
private GradeService gradeService;
@Autowired
private ExcelExportUtil excelExportUtil;
// 成绩统计接口(仅教师和管理员可访问)
@GetMapping("/stat")
@RequireRole({"teacher", "admin"})
public ResultVO<GradeStatVO> getGradeStat(
@RequestParam Long courseId,
@RequestParam String classIds) {
Long teacherId = UserContext.getUserId();
List<Long> classIdList = Arrays.stream(classIds.split(","))
.map(Long::valueOf)
.collect(Collectors.toList());
GradeStatVO statVO = gradeService.getGradeStat(courseId, classIdList, teacherId);
return ResultVO.success(statVO);
}
// 成绩导出接口
@GetMapping("/export")
@RequireRole({"teacher", "admin"})
public void exportGradeExcel(
@RequestParam Long courseId,
@RequestParam String classIds,
HttpServletResponse response) {
Long teacherId = UserContext.getUserId();
List<Long> classIdList = Arrays.stream(classIds.split(","))
.map(Long::valueOf)
.collect(Collectors.toList());
List<GradeDetailVO> dataList = gradeService.getGradeDetail(courseId, classIdList, teacherId);
// 设置响应头,触发浏览器下载
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("UTF-8");
String fileName = null;
try {
fileName = URLEncoder.encode("成绩统计.xlsx", "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
// 导出Excel
try {
excelExportUtil.exportGradeExcel(dataList, response.getOutputStream());
} catch (IOException e) {
throw new BusinessException("导出流写入失败:" + e.getMessage());
}
}
}
4. 项目程序/模块的最新(运行)截图
⑤学习对象管理

⑥课程科目管理

⑦角色权限管理

具体设置 特定角色 对特定接口的权限
5. 每日每人总结
-
王梓涵:“成绩统计的图表功能搞定了,优化了图表联动逻辑,导出接口也对接好了,明天做系统整体联调,把各个模块串起来。”
-
林嘉俊:“用SXSSFWorkbook解决了Excel导出内存溢出问题,统计接口的逻辑也没问题,明天做接口异常处理和日志优化,确保系统稳定。”
-
廖鸿基:“统计和导出功能都测过了,包括1000条数据的大数据场景,接口响应时间在2秒内,符合预期,明天做系统全流程测试。”
第7天:Alpha阶段收尾与全流程测试
博客标题:智教协作队Alpha冲刺第7天 | Alpha阶段圆满收尾
1. 工作进展
| 成员 | 昨天已完成的工作 | 今天计划完成的工作 | 工作中遇到的困难 |
| 王梓涵 | 完成成绩统计页面开发 | 系统全流程联调,修复前端页面兼容性问题,完善操作提示 | 平板端课件预览页面排版错乱,需适配不同屏幕尺寸 |
| 林嘉俊 | 完成成绩统计与导出接口开发 | 接口异常处理优化,添加操作日志,修复已知Bug | 日志打印过多导致性能轻微下降,需调整日志级别 |
| 廖鸿基 | 完成成绩统计功能测试 | 执行系统全流程测试,编写Alpha阶段测试报告,收集用户体验反馈 | 部分边界场景测试用例遗漏,需补充后重新执行 |
2. 项目燃尽图
【截图位置】插入Leangoo燃尽图第7天截图,标注“剩余工时0小时,Alpha阶段核心任务全部完成”,燃尽线与理想线基本重合
3. 代码/文档登入记录


王梓涵(平板端适配优化)
对应Issue:[#13 前端:多端兼容性优化](https://gitee.com/zhijia-team/teaching-system/issues/13)
// 课件预览页面的响应式样式优化(解决平板端排版问题)
<style scoped>
.preview-container {
padding: 15px;
box-sizing: border-box;
}
/* 桌面端样式 */
.courseware-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.preview-content {
width: 100%;
height: 800px;
border: 1px solid #e6e6e6;
}
/* 平板端适配(屏幕宽度768px-1200px) */
@media screen and (max-width: 1200px) and (min-width: 768px) {
.courseware-info {
flex-direction: column;
align-items: flex-start;
}
.courseware-info > div {
margin-bottom: 10px;
width: 100%;
}
.preview-content {
height: 500px; /* 适配平板屏幕高度 */
}
/* 按钮组样式调整 */
.btn-group {
display: flex;
justify-content: space-between;
width: 100%;
}
.btn-group > el-button {
flex: 1;
margin-right: 5px;
}
}
/* 小屏平板适配(屏幕宽度小于768px) */
@media screen and (max-width: 768px) {
.preview-content {
height: 350px;
}
.courseware-title {
font-size: 16px;
font-weight: bold;
}
}
// 全局操作提示优化(统一提示样式)
import { ElMessage } from 'element-plus';
export const showMessage = (message, type = 'info') => {
ElMessage({
message: message,
type: type,
duration: 2000,
center: true // 提示框居中显示,提升用户体验
});
};
// 在登录、上传等功能中调用
// showMessage("登录成功", "success");
// showMessage("请选择文件", "warning");
林嘉俊(接口优化与日志配置)
对应Issue:[#14 后端:接口优化与日志配置](https://gitee.com/zhijia-team/teaching-system/issues/14)
// 全局异常处理类(统一异常响应格式)
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
// 业务异常处理
@ExceptionHandler(BusinessException.class)
public ResultVO handleBusinessException(BusinessException e) {
logger.warn("业务异常:{}", e.getMessage());
return ResultVO.error(e.getMessage());
}
// 参数校验异常处理
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResultVO handleValidException(MethodArgumentNotValidException e) {
// 获取校验失败的字段和提示信息
String message = e.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining(","));
logger.warn("参数校验异常:{}", message);
return ResultVO.error(message);
}
// 系统异常处理(不暴露具体异常信息给前端)
@ExceptionHandler(Exception.class)
public ResultVO handleException(Exception e) {
logger.error("系统异常:", e); // 打印完整异常栈
return ResultVO.error("系统繁忙,请稍后再试");
}
}
// 日志配置优化(调整日志级别,减少冗余日志)
// application.yml日志配置
logging:
level:
root: INFO # 根日志级别为INFO
com.zhijia.mapper: DEBUG # MyBatis日志为DEBUG,便于调试SQL
com.zhijia.controller: INFO # 控制器日志为INFO
com.zhijia.service: INFO # 服务层日志为INFO
file:
name: logs/teaching-system.log # 日志文件路径
pattern:
console: '%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n' # 控制台日志格式
file: '%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n' # 文件日志格式
// 操作日志注解(记录关键业务操作)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OperLog {
String value() default ""; // 操作描述
}
// 操作日志切面
@Aspect
@Component
public class OperLogAspect {
private static final Logger operLogger = LoggerFactory.getLogger("operLog");
@Pointcut("@annotation(com.zhijia.annotation.OperLog)")
public void operLogPointcut() {}
@Around("operLogPointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 1. 记录操作前信息
OperLog operLog = ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(OperLog.class);
String operDesc = operLog.value();
Long userId = UserContext.getUserId();
String role = UserContext.getRole();
String requestUrl = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getRequestURI();
// 2. 执行目标方法
long startTime = System.currentTimeMillis();
Object result = joinPoint.proceed();
long costTime = System.currentTimeMillis() - startTime;
// 3. 记录操作日志
operLogger.info("操作人ID:{},角色:{},操作描述:{},请求地址:{},耗时:{}ms",
userId, role, operDesc, requestUrl, costTime);
return result;
}
}
// 在关键接口上添加操作日志注解
@PostMapping("/upload")
@RequireRole("teacher")
@OperLog("教师上传课件")
public ResultVO uploadCourseware(...) {
// 课件上传逻辑
}
4. 项目程序/模块的最新(运行)截图
课程界面:搜索、筛选课程
用户的角色role不同,在该界面的接口处理逻辑不同
学生,教师,课程管理员都会查询user_to_course表,仅获取有权限的课程
超级管理员会获取全部的课程

5. 每日每人总结与Alpha阶段感悟
-
王梓涵:“今天把平板端的兼容性问题解决了,全流程跑下来很顺畅。Alpha阶段最大的收获是学会了前后端联调的技巧,从原型到页面落地,看着自己写的代码能正常运行特别有成就感。后续可以优化课件预览的加载速度,提升用户体验。”
-
林嘉俊:“全局异常处理和日志优化做完后,系统稳定性提升了不少。这7天冲刺让我明白,后端开发不仅要实现功能,还要考虑异常场景和性能问题。接下来可以研究MinIO的文件预热,让大课件预览更快。”
-
廖鸿基:“全流程测试发现了3个边界Bug,都已经修复了,测试报告也写完了。通过这次测试,我对‘测试驱动开发’有了更深的理解,提前设计用例能帮开发少走很多弯路。后续可以增加自动化测试脚本,提高测试效率。”
二、Alpha阶段冲刺总结
1. 冲刺目标完成情况
Alpha阶段计划完成的核心功能全部落地,具体完成情况如下:
| 功能模块 | 计划功能点 | 完成情况 | 备注 |
| 用户登录与权限 | 多角色登录、权限控制、JWT认证 | 100%完成 | 新增测试令牌接口,便于测试 |
| 课件管理 | 多格式上传、版本管理、分片上传、预览 | 100%完成 | 支持PPT/PDF/MP4/MP3格式,最大100MB |
| 学生评分 | 星标评分、评语提交、匿名反馈 | 100%完成 | 添加防重复提交机制 |
| 成绩统计与导出 | 多维度统计、跨班对比、Excel导出 | 100%完成 | 优化大数据量导出性能 |
2. 团队协作亮点
-
每日站会高效同步:固定15分钟站会,明确当日目标,及时暴露问题,团队响应时间不超过1小时。
-
代码规范统一:提前制定Git分支规范(master/dev/feature)和代码注释规范,减少合并冲突。
-
前后端并行开发:通过接口文档提前对齐参数,前端开发页面的同时后端开发接口,提升开发效率。
-
测试同步跟进:测试人员提前编写用例,开发完成后立即执行测试,缩短Bug修复周期。
3. 存在的问题与改进方向
(1)当前问题
-
大课件(50MB以上)预览加载较慢,需等待3-5秒;
-
平板端部分按钮点击区域较小,操作不够便捷;
-
缺乏自动化测试脚本,回归测试效率较低。
(2)改进方向
-
技术优化:引入MinIO文件预签名URL和分片加载,提升大课件预览速度;
-
交互优化:调整平板端按钮尺寸和间距,增加点击区域;
-
测试优化:开发Junit单元测试和Postman自动化测试脚本,实现关键接口自动回归。
4. 下一阶段计划
-
Beta阶段重点:解决Alpha阶段遗留问题,新增课件收藏、成绩预警功能(低于60分自动提醒教师);
-
用户调研:邀请10位教师和50名学生进行Beta版本试用,收集真实使用反馈;
-
部署优化:将系统部署到云服务器,实现公网访问,便于大规模测试。
Alpha阶段的7天冲刺,是团队从“需求”到“产品”的一次完整实践。我们不仅完成了功能开发,更在协作中磨合了节奏、提升了能力。接下来,我们将带着Alpha阶段的经验和问题,全力投入Beta阶段的开发,打造更贴合教学需求的实用系统。

浙公网安备 33010602011771号