在线课程评价系统是一款基于Spring Boot + Vue3的全栈应用,面向高校师生提供课程评价、教学反馈、数据可视化分析等功能。系统包含Web管理端和用户门户,日均承载10万+课程数据,支持高并发访问和实时数据更新。
项目核心价值:
用户层 -> 网关层 -> 业务层 -> 数据层
↑ ↑ ↑
Nginx Spring MySQL
JWT Cloud Redis
Gateway Elasticsearch
// 基于Spring Security的权限控制
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public 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();
}
}
核心功能流程图:
<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>
// 使用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);
// 定时任务持久化到MySQL
if (redisTemplate.opsForZSet().size(key) % 100 == 0) {
asyncTaskExecutor.execute(() -> persistRating(courseId));
}
}
// 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);
}
多维度评价体系:
实时数据更新:
可视化分析:
安全机制:
# Docker Compose部署示例
version: '3'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root
ports:
- "3306:3306"
redis:
image: redis:6-alpine
ports:
- "6379:6379"
elasticsearch:
image: elasticsearch:7.17.0
environment:
- discovery.type=single-node
ports:
- "9200:9200"
backend:
build: ./backend
ports:
- "8080:8080"
depends_on:
- mysql
- redis
- elasticsearch
frontend:
build: ./frontend
ports:
- "80:80"
项目成果:
未来规划:
通过本项目实践,完整走过了需求分析、技术选型、架构设计、开发测试到最终部署的全流程。系统在性能优化、安全防护、用户体验等方面都进行了深入探索,为后续教育类项目的开发积累了宝贵经验。
// 评价实体类设计
@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;
@JsonIgnore
private Long userId; // 匿名时不返回
@CreationTimestamp
private 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缓存 + 异步持久化)
*/
@Transactional
public 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() {
// 获取所有待处理课程ID
Set<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;
@PostMapping
public 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组件关键实现:
课程标签:
关键技术实现说明:
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}
该实现方案具有以下优势:
后续优化方向:
// 安全配置类(Spring Security + JWT)
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
@Bean
public 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();
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
@Bean
public 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();
@PostConstruct
public 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 {
@Bean
public 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();
@Override
public 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);
}
}
}
安全增强实现说明:
JWT认证体系:
// 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防御体系:
// 自定义HttpServletRequestWrapper
public class XssRequestWrapper extends HttpServletRequestWrapper {
private final HtmlSanitizer sanitizer;
public XssRequestWrapper(HttpServletRequest request, HtmlSanitizer sanitizer) {
super(request);
this.sanitizer = sanitizer;
}
@Override
public String getParameter(String name) {
return sanitizer.sanitize(super.getParameter(name));
}
@Override
public String[] getParameterValues(String name) {
String[] values = super.getParameterValues(name);
if (values == null) return null;
return Arrays.stream(values)
.map(sanitizer::sanitize)
.toArray(String[]::new);
}
}
可视化安全控制:
// 数据权限校验注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("@visualizationSecurity.checkCourseAccess(#courseId)")
public @interface CheckCourseAccess {}
// 安全校验服务
@Service
public class VisualizationSecurity {
public boolean checkCourseAccess(Long courseId) {
// 实现课程访问权限校验逻辑
return true;
}
}
监控与审计增强:
// 审计日志切面
@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();
}
该实现方案的特点:
纵深防御体系:
可视化安全:
性能优化:
可维护性:
典型应用场景:
详细评价: