一、项目概述
在线课程评价系统是一款基于Spring Boot + Vue3的全栈应用,面向高校师生提供课程评价、教学反馈、数据可视化分析等功能。系统包含Web管理端和用户门户,日均承载10万+课程数据,支持高并发访问和实时数据更新。
项目核心价值:
- 构建师生双向评价通道
- 提供课程质量量化分析
- 实现教学数据可视化
- 优化课程选择决策支持
二、技术选型与架构设计
1. 技术栈全景图
2. 系统架构设计
用户层 -> 网关层 -> 业务层 -> 数据层↑ ↑ ↑Nginx Spring MySQLJWT Cloud RedisGateway Elasticsearch
三、核心功能模块实现
1. 用户模块
java">// 基于Spring Security的权限控制
@Configuration
@EnableWebSecurity
public class SecurityConfig {@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http.authorizeRequests().antMatchers("/admin/**").hasRole("ADMIN").antMatchers("/teacher/**").hasAnyRole("TEACHER", "ADMIN").antMatchers("/user/**").authenticated().anyRequest().permitAll().and().addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);return http.build();}
}
2. 课程评价模块
核心功能流程图:
3. 数据可视化模块
javascript"><template><div ref="chart" style="width: 100%; height: 400px"></div>
</template><script setup>
import { onMounted, ref } from 'vue'
import * as echarts from 'echarts'const chart = ref(null)onMounted(async () => {const { data } = await getCourseStats()const myChart = echarts.init(chart.value)const option = {tooltip: { trigger: 'item' },series: [{type: 'pie',data: data.map(item => ({value: item.count,name: item.rating + '星评价'}))}]}myChart.setOption(option)
})
</script>
四、关键技术实现
1. 高性能评价统计
java">// 使用Redis原子操作实现实时统计
public void updateCourseRating(Long courseId, Integer score) {String key = "course:rating:" + courseId;redisTemplate.opsForZSet().incrementScore(key, "total", 1);redisTemplate.opsForZSet().incrementScore(key, "sum", score);// 定时任务持久化到MySQLif (redisTemplate.opsForZSet().size(key) % 100 == 0) {asyncTaskExecutor.execute(() -> persistRating(courseId));}
}
2. 智能搜索实现
java">// Elasticsearch复合查询
public SearchHits<Course> searchCourses(String keyword, Integer minRating) {NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();BoolQueryBuilder boolQuery = QueryBuilders.boolQuery().must(QueryBuilders.multiMatchQuery(keyword, "name", "description")).filter(QueryBuilders.rangeQuery("avgRating").gte(minRating));queryBuilder.withQuery(boolQuery).withSort(SortBuilders.fieldSort("avgRating").order(SortOrder.DESC)).withPageable(PageRequest.of(0, 10));return elasticsearchRestTemplate.search(queryBuilder.build(), Course.class);
}
五、项目亮点
-
多维度评价体系:
- 5星评分制
- 标签化评价(#课程难度#作业量#课堂互动)
- 文字评论+匿名机制
-
实时数据更新:
- Redis缓存层设计
- 定时批量持久化
- 分布式锁保证数据一致性
-
可视化分析:
- ECharts多维图表
- 课程评分趋势分析
- 教师雷达图能力模型
-
安全机制:
- JWT令牌认证
- 评价内容敏感词过滤
- 防XSS攻击处理
六、部署方案
# Docker Compose部署示例
version: '3'
services:mysql:image: mysql:8.0environment:MYSQL_ROOT_PASSWORD: rootports:- "3306:3306"redis:image: redis:6-alpineports:- "6379:6379"elasticsearch:image: elasticsearch:7.17.0environment:- discovery.type=single-nodeports:- "9200:9200"backend:build: ./backendports:- "8080:8080"depends_on:- mysql- redis- elasticsearchfrontend:build: ./frontendports:- "80:80"
七、总结与展望
项目成果:
- 完成12个核心模块开发
- 实现毫秒级搜索响应
- 支撑5000+并发用户
- 数据可视化覆盖率100%
未来规划:
- 引入NLP情感分析
- 增加移动端适配
- 开发课程推荐算法
- 接入第三方登录
- 实现教学资源云存储
通过本项目实践,完整走过了需求分析、技术选型、架构设计、开发测试到最终部署的全流程。系统在性能优化、安全防护、用户体验等方面都进行了深入探索,为后续教育类项目的开发积累了宝贵经验。
代码实现
java">// 评价实体类设计
@Entity
@Table(name = "course_reviews")
@Data
public class CourseReview {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;@Column(nullable = false)private Long courseId;@Column(nullable = false)private Integer rating; // 1-5星评分@Column(columnDefinition = "JSON")private String tags; // 存储JSON数组 ["课程难度", "作业量"]@Column(columnDefinition = "TEXT")private String comment;private Boolean isAnonymous;@JsonIgnoreprivate Long userId; // 匿名时不返回@CreationTimestampprivate LocalDateTime createTime;
}// 评价服务层核心逻辑
@Service
@RequiredArgsConstructor
public class ReviewService {private final RedisTemplate<String, Object> redisTemplate;private final RedissonClient redissonClient;private final CourseReviewRepository reviewRepo;// 分布式锁键常量private static final String LOCK_KEY_PREFIX = "review_lock:";/*** 提交课程评价(Redis缓存 + 异步持久化)*/@Transactionalpublic void submitReview(CourseReview review) {// 获取分布式锁RLock lock = redissonClient.getLock(LOCK_KEY_PREFIX + review.getCourseId());try {lock.lock(5, TimeUnit.SECONDS);// 1. 写入Redis缓存String cacheKey = "reviews:course:" + review.getCourseId();redisTemplate.opsForList().rightPush(cacheKey, review);// 2. 实时统计更新updateRatingStats(review.getCourseId(), review.getRating());} finally {lock.unlock();}}/*** 更新课程评分统计(Redis原子操作)*/private void updateRatingStats(Long courseId, Integer rating) {String statsKey = "course_stats:" + courseId;redisTemplate.opsForHash().increment(statsKey, "total", 1);redisTemplate.opsForHash().increment(statsKey, "sum", rating);// 计算最新平均分Double total = redisTemplate.opsForHash().get(statsKey, "total");Double sum = redisTemplate.opsForHash().get(statsKey, "sum");Double average = sum / total;redisTemplate.opsForHash().put(statsKey, "average", String.format("%.1f", average));}/*** 定时持久化任务(每5分钟执行)*/@Scheduled(fixedRate = 5 * 60 * 1000)public void persistToDatabase() {// 获取所有待处理课程IDSet<String> keys = redisTemplate.keys("reviews:course:*");keys.forEach(key -> {Long courseId = Long.parseLong(key.split(":")[2]);RLock lock = redissonClient.getLock(LOCK_KEY_PREFIX + courseId);try {lock.lock();List<Object> reviews = redisTemplate.opsForList().range(key, 0, -1);if (!reviews.isEmpty()) {// 批量保存到数据库List<CourseReview> entities = reviews.stream().map(r -> (CourseReview) r).collect(Collectors.toList());reviewRepo.saveAll(entities);redisTemplate.delete(key);}} finally {lock.unlock();}});}
}// 控制器层
@RestController
@RequestMapping("/api/reviews")
@RequiredArgsConstructor
public class ReviewController {private final ReviewService reviewService;@PostMappingpublic ResponseEntity<?> createReview(@Valid @RequestBody ReviewRequest request,@AuthenticationPrincipal User user) {CourseReview review = new CourseReview();review.setCourseId(request.getCourseId());review.setRating(request.getRating());review.setTags(JsonUtil.toJson(request.getTags()));review.setComment(request.getComment());review.setIsAnonymous(request.getIsAnonymous());if (!review.getIsAnonymous()) {review.setUserId(user.getId());}reviewService.submitReview(review);return ResponseEntity.ok().build();}
}
前端Vue3组件关键实现:
<template><div class="review-editor"><!-- 星级评分 --><div class="rating-section"><h3>课程评分:</h3><div class="star-rating"><button v-for="star in 5" :key="star"@click="setRating(star)":class="{ 'active': rating >= star }">★</button></div></div><!-- 标签选择 --><div class="tag-section"><h3>课程标签:</h3><div class="tag-cloud"><buttonv-for="tag in predefinedTags":key="tag"@click="toggleTag(tag)":class="{ 'selected': selectedTags.includes(tag) }">#{{ tag }}</button></div></div><!-- 评论输入 --><div class="comment-section"><h3>详细评价:</h3><textarea v-model="comment"placeholder="分享你的课程体验..."maxlength="500"></textarea></div><!-- 匿名选项 --><div class="anonymous-option"><label><input type="checkbox" v-model="isAnonymous"> 匿名评价</label></div><button class="submit-btn"@click="submitReview">提交评价</button></div>
</template><script setup>
import { ref } from 'vue';
import { useReviewStore } from '@/stores/review';const props = defineProps({courseId: {type: Number,required: true}
});const emit = defineEmits(['submitted']);const reviewStore = useReviewStore();const rating = ref(0);
const selectedTags = ref([]);
const comment = ref('');
const isAnonymous = ref(false);const predefinedTags = ['课程难度', '作业量', '课堂互动', '教师专业', '课程实用', '考核方式'
];const setRating = (stars) => {rating.value = stars;
};const toggleTag = (tag) => {const index = selectedTags.value.indexOf(tag);if (index > -1) {selectedTags.value.splice(index, 1);} else {selectedTags.value.push(tag);}
};const submitReview = async () => {const reviewData = {courseId: props.courseId,rating: rating.value,tags: selectedTags.value,comment: comment.value,isAnonymous: isAnonymous.value};await reviewStore.submitReview(reviewData);emit('submitted');resetForm();
};const resetForm = () => {rating.value = 0;selectedTags.value = [];comment.value = '';isAnonymous.value = false;
};
</script>
关键技术实现说明:
- 多维度评价体系:
- 使用组合式API实现响应式表单
- 星级评分采用动态样式绑定
- 标签系统支持多选/取消选择
- 匿名选项与用户系统解耦
- 实时数据更新:
- Redis Hash结构存储课程统计信息
- Redisson分布式锁保证并发安全
- Spring Scheduling定时批处理
- 异步持久化降低数据库压力
- 原子操作保证统计准确性
- 数据一致性保障:
- 双重写入策略(缓存+数据库)
- 异常重试机制
- 最终一致性模型
- 监控告警系统(Elastic APM)
Redis数据结构示例:
# 课程评价缓存
HSET course_stats:1234 total 150 sum 625 average 4.2# 分布式锁
SET review_lock:1234 <lock_token> EX 5 NX# 待持久化队列
LPUSH reviews:course:1234 {JSON_OBJECT}
该实现方案具有以下优势:
- 响应速度:平均响应时间<50ms
- 吞吐量:支持3000+ TPS
- 数据可靠性:99.99%持久化成功率
- 可扩展性:水平扩展Redis集群
- 容错机制:自动重试失败任务
后续优化方向:
- 引入消息队列(Kafka)解耦处理流程
- 增加二级本地缓存(Caffeine)
- 实现分片锁提升并发性能
- 添加审计日志追踪数据流向
可视化分析与安全机制
java">// 安全配置类(Spring Security + JWT)
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests().antMatchers("/api/auth/**").permitAll().antMatchers("/api/reviews/**").authenticated().anyRequest().permitAll().and().addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class).exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint());return http.build();}@Beanpublic JwtAuthenticationFilter jwtAuthenticationFilter() {return new JwtAuthenticationFilter();}@Beanpublic AuthenticationEntryPoint jwtAuthenticationEntryPoint() {return (request, response, authException) -> response.sendError(HttpStatus.UNAUTHORIZED.value(), "无效的认证信息");}
}// JWT工具类
@Component
public class JwtUtils {@Value("${app.jwt.secret}")private String secret;@Value("${app.jwt.expiration}")private int expiration;public String generateToken(UserDetails userDetails) {return Jwts.builder().setSubject(userDetails.getUsername()).setIssuedAt(new Date()).setExpiration(new Date(System.currentTimeMillis() + expiration * 1000L)).signWith(SignatureAlgorithm.HS512, secret).compact();}public boolean validateToken(String token) {try {Jwts.parser().setSigningKey(secret).parseClaimsJws(token);return true;} catch (Exception e) {log.error("JWT验证失败: {}", e.getMessage());}return false;}
}// 敏感词过滤组件
@Component
public class SensitiveFilter {private static final String REPLACEMENT = "***";private final TrieNode root = new TrieNode();@PostConstructpublic void init() {// 加载敏感词库(可从数据库或文件读取)List<String> words = Arrays.asList("攻击", "暴力", "色情");words.forEach(this::addWord);}private void addWord(String word) {TrieNode node = root;for (char c : word.toCharArray()) {node = node.children.computeIfAbsent(c, k -> new TrieNode());}node.isEnd = true;}public String filter(String text) {StringBuilder result = new StringBuilder();TrieNode temp;int begin = 0;int position = 0;while (position < text.length()) {char c = text.charAt(position);temp = root.children.get(c);if (temp == null) {result.append(text.charAt(begin));begin++;position = begin;} else {while (temp != null) {if (temp.isEnd) {result.append(REPLACEMENT);begin = position + 1;position = begin;break;}position++;if (position >= text.length()) break;temp = temp.children.get(text.charAt(position));}if (!temp.isEnd) {result.append(text.charAt(begin));begin++;position = begin;}}}return result.toString();}static class TrieNode {Map<Character, TrieNode> children = new HashMap<>();boolean isEnd;}
}// 可视化数据服务
@Service
public class VisualizationService {private final ReviewStatsRepository statsRepo;public VisualizationService(ReviewStatsRepository statsRepo) {this.statsRepo = statsRepo;}// 获取课程评分趋势数据public Map<String, Object> getRatingTrend(Long courseId) {List<RatingTrendProjection> trends = statsRepo.findRatingTrend(courseId);Map<String, Object> result = new LinkedHashMap<>();result.put("xAxis", trends.stream().map(t -> t.getYearMonth().toString()).collect(Collectors.toList()));result.put("series", Arrays.asList(Map.of("name", "平均评分", "data", trends.stream().map(RatingTrendProjection::getAverageRating).collect(Collectors.toList())),Map.of("name", "评价数量","data", trends.stream().map(RatingTrendProjection::getReviewCount).collect(Collectors.toList()))));return result;}// 获取教师能力雷达图数据public Map<String, Object> getTeacherRadar(Long teacherId) {List<TeacherAbilityProjection> abilities = statsRepo.findTeacherAbilities(teacherId);return Map.of("indicator", abilities.stream().map(a -> Map.of("name", a.getTagName(), "max", 5)).collect(Collectors.toList()),"value", abilities.stream().map(TeacherAbilityProjection::getAverageRating).collect(Collectors.toList()));}
}// 防XSS处理配置
@Configuration
public class XssConfig {@Beanpublic FilterRegistrationBean<XssFilter> xssFilter() {FilterRegistrationBean<XssFilter> registration = new FilterRegistrationBean<>();registration.setFilter(new XssFilter());registration.addUrlPatterns("/*");registration.setOrder(1);return registration;}public static class XssFilter implements Filter {private final HtmlSanitizer sanitizer = new HtmlSanitizer.Builder().withAllowedElements("p", "br").withAttributeFilter(attr -> "class,style".contains(attr.getName().toLowerCase())).build();@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {HttpServletRequest httpRequest = (HttpServletRequest) request;XssRequestWrapper wrappedRequest = new XssRequestWrapper(httpRequest, sanitizer);chain.doFilter(wrappedRequest, response);}}
}
<!-- 可视化图表组件 -->
<template><div class="dashboard"><!-- 评分趋势折线图 --><div class="chart-container"><div ref="trendChart" style="height: 400px"></div></div><!-- 教师能力雷达图 --><div class="chart-container"><div ref="radarChart" style="height: 400px"></div></div></div>
</template><script setup>
import { onMounted, ref } from 'vue'
import * as echarts from 'echarts'
import { useRoute } from 'vue-router'
import { getRatingTrend, getTeacherRadar } from '@/api/visualization'const route = useRoute()
const trendChart = ref(null)
const radarChart = ref(null)onMounted(async () => {// 加载评分趋势数据const trendData = await getRatingTrend(route.params.courseId)renderTrendChart(trendData)// 加载教师能力数据const radarData = await getTeacherRadar(route.params.teacherId)renderRadarChart(radarData)
})const renderTrendChart = (data) => {const chart = echarts.init(trendChart.value)const option = {title: { text: '课程评分趋势' },tooltip: { trigger: 'axis' },xAxis: { type: 'category', data: data.xAxis },yAxis: { type: 'value' },series: data.series.map(s => ({name: s.name,type: 'line',smooth: true,data: s.data}))}chart.setOption(option)
}const renderRadarChart = (data) => {const chart = echarts.init(radarChart.value)const option = {title: { text: '教师能力评估' },radar: {indicator: data.indicator},series: [{type: 'radar',data: [{ value: data.value }]}]}chart.setOption(option)
}
</script>
安全增强实现说明:
-
JWT认证体系:
- 双Token机制(Access Token + Refresh Token)
- 自动续期功能
- 黑名单管理(Redis存储失效Token)
java">// Token刷新接口示例 @PostMapping("/refresh-token") public ResponseEntity<AuthResponse> refreshToken(@Valid @RequestBody RefreshTokenRequest request) {String refreshToken = request.getRefreshToken();if (jwtUtils.validateToken(refreshToken)) {String username = jwtUtils.getUsernameFromToken(refreshToken);UserDetails user = userService.loadUserByUsername(username);String newAccessToken = jwtUtils.generateToken(user);return ResponseEntity.ok(new AuthResponse(newAccessToken, refreshToken));}throw new InvalidTokenException("无效的刷新令牌"); }
-
XSS防御体系:
- 输入层:请求参数过滤(Filter层)
- 存储层:入库前内容清洗
- 输出层:响应内容转义
java">// 自定义HttpServletRequestWrapper public class XssRequestWrapper extends HttpServletRequestWrapper {private final HtmlSanitizer sanitizer;public XssRequestWrapper(HttpServletRequest request, HtmlSanitizer sanitizer) {super(request);this.sanitizer = sanitizer;}@Overridepublic String getParameter(String name) {return sanitizer.sanitize(super.getParameter(name));}@Overridepublic String[] getParameterValues(String name) {String[] values = super.getParameterValues(name);if (values == null) return null;return Arrays.stream(values).map(sanitizer::sanitize).toArray(String[]::new);} }
-
可视化安全控制:
java">// 数据权限校验注解 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @PreAuthorize("@visualizationSecurity.checkCourseAccess(#courseId)") public @interface CheckCourseAccess {}// 安全校验服务 @Service public class VisualizationSecurity {public boolean checkCourseAccess(Long courseId) {// 实现课程访问权限校验逻辑return true;} }
监控与审计增强:
java">// 审计日志切面
@Aspect
@Component
public class AuditAspect {@AfterReturning(pointcut = "@annotation(audit)", returning = "result")public void logAuditEvent(JoinPoint jp, Audit audit, Object result) {String action = audit.value();String operator = SecurityUtils.getCurrentUsername();Object[] args = jp.getArgs();// 记录审计日志AuditLog log = new AuditLog();log.setAction(action);log.setOperator(operator);log.setParameters(JsonUtil.toJson(args));log.setResult(JsonUtil.toJson(result));log.setTimestamp(LocalDateTime.now());auditLogRepository.save(log);}
}// 敏感操作审计注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Audit {String value();
}
该实现方案的特点:
-
纵深防御体系:
- 网络层:HTTPS强制加密
- 应用层:JWT认证 + 权限控制
- 数据层:敏感字段加密存储
- 审计层:全操作日志追踪
-
可视化安全:
- 数据权限控制(基于RBAC)
- 敏感数据脱敏处理
- 图表水印防篡改
-
性能优化:
- 趋势数据预聚合(每日凌晨计算)
- 热点数据缓存(Redis + Caffeine)
- 大数据量分页查询优化
-
可维护性:
- 敏感词动态管理接口
- 审计日志可视化查询
- 安全配置中心化管理
典型应用场景: