基于 echart+ redis 的刷题日历项目设计与实现
文章目录
- 基于 Vue + Java 的刷题日历项目设计方案
- 一、项目概述
- 二、前端设计与实现
- (一)技术选型与环境搭建
- (二)页面布局与组件设计
- (三)与后端交互
- 三、后端设计与实现
- (一)技术选型与架构设计
- (二)数据库设计
- (三)Redis 缓存策略
- (四)接口设计与实现
基于 Vue + Java 的刷题日历项目设计方案
在当今数字化学习的浪潮中,刷题成为许多人提升知识技能的重要方式。为了更好地帮助用户记录和分析刷题情况,我们设计了一个基于 Vue + Java 的刷题日历项目,前端采用 Vue3 + Echarts 进行构建,后端则运用 Redis + MySQL + Spring Boot 技术栈,实现了丰富的功能,如点击刷题小日历图展示当天刷题记录、热力值显示以及提交次数统计等。
一、项目概述
刷题日历项目旨在为用户提供一个直观、便捷的刷题数据可视化平台。通过日历的形式展示用户的刷题历程,让用户清晰地了解自己在不同时间的学习状态和成果,同时借助数据分析功能,帮助用户发现学习规律,优化学习计划。
二、前端设计与实现
(一)技术选型与环境搭建
- 前端采用 Vue3 作为核心框架,它具有高效的响应式系统、简洁的语法和良好的组件化开发能力,能够快速构建用户界面。使用 Vue CLI 初始化项目,配置相关依赖,如 Echarts 用于数据可视化展示。
- 安装 Echarts 及其 Vue 组件库,以便在项目中方便地使用 Echarts 绘制各种图表,如日历图、柱状图等用于展示刷题数据。
(二)页面布局与组件设计
- 日历组件:设计一个刷题日历组件,该组件以日历的形式呈现每个月的日期。使用 Vue 的组件化思想,将日历的头部(显示月份、年份、切换按钮等)、日期格子等分别封装成子组件,便于维护和扩展。在日期格子上,根据当天是否有刷题记录,显示不同的样式,例如有刷题记录的日期显示为彩色背景,无记录则为灰色背景。
<template>
<div class="calendar">
<div class="calendar-header">
<!-- 月份和年份显示以及切换按钮 -->
</div>
<div class="calendar-body">
<div
v-for="(week, weekIndex) in calendarWeeks"
:key="weekIndex"
class="calendar-week"
>
<div
v-for="(day, dayIndex) in week"
:key="dayIndex"
class="calendar-day"
:class="{ 'has-record': day.hasRecord }"
@click="handleDayClick(day)"
>
{{ day.date }}
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
// 存储日历数据,包括每个日期的信息
calendarWeeks: [],
};
},
methods: {
handleDayClick(day) {
// 点击日期时触发,用于获取当天刷题数据并展示
this.$emit('day-click', day);
},
},
};
</script>
<style scoped>
.calendar {
width: 300px;
margin: 0 auto;
}
.calendar-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
background-color: #f5f5f5;
}
.calendar-week {
display: flex;
}
.calendar-day {
flex: 1;
text-align: center;
padding: 10px;
border: 1px solid #ddd;
}
.has-record {
background-color: #007bff;
color: #fff;
}
</style>
- 数据展示组件:当用户点击日历中的某个日期时,弹出数据展示组件。该组件包括刷题记录列表,展示当天所做的题目详情,如题目名称、所属知识点等;热力值显示区域,通过颜色渐变或热力图的形式直观地呈现当天刷题的强度或难度分布;提交次数统计图表,使用柱状图或折线图展示当天不同类型题目(如选择题、填空题、简答题等)的提交次数变化。
<template>
<div class="data-popup">
<div class="record-list">
<div v-for="(record, index) in records" :key="index">
<p>题目名称: {{ record.questionName }}</p>
<p>知识点: {{ record.knowledgePoint }}</p>
<p>提交结果: {{ record.submitResult }}</p>
</div>
</div>
<div class="heat-value">
<p>热力值: {{ heatValue }}</p>
<!-- 这里可以使用 Echarts 绘制热力图或颜色渐变表示热力值 -->
</div>
<div class="submit-chart">
<!-- 使用 Echarts 绘制提交次数统计图表 -->
<echarts :option="submitChartOption" />
</div>
</div>
</template>
<script>
import echarts from 'echarts';
export default {
props: {
records: {
type: Array,
required: true,
},
heatValue: {
type: Number,
required: true,
},
submitChartOption: {
type: Object,
required: true,
},
},
};
</script>
<style scoped>
.data-popup {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: #fff;
padding: 20px;
border: 1px solid #ddd;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
width: 400px;
height: 400px;
overflow-y: auto;
}
.record-list {
margin-bottom: 20px;
}
.heat-value {
margin-bottom: 20px;
}
.submit-chart {
height: 200px;
}
</style>
(三)与后端交互
- 使用 Axios 库与后端进行数据交互。在页面加载时,发送请求获取用户的刷题日历数据,包括每个日期的刷题状态、提交次数等信息,后端返回 JSON 数据,前端根据数据渲染日历组件。当用户点击日期时,再次发送请求获取当天的详细刷题记录数据,用于数据展示组件的渲染。
import axios from 'axios';
export default {
data() {
return {
calendarData: [],
};
},
mounted() {
// 页面加载时获取日历数据
this.getCalendarData();
},
methods: {
getCalendarData() {
axios
.get('/api/calendarData')
.then((response) => {
this.calendarData = response.data;
// 根据数据渲染日历组件
this.renderCalendar();
})
.catch((error) => {
console.error('获取日历数据失败', error);
});
},
handleDayClick(day) {
// 点击日期时获取当天刷题数据
axios
.get(`/api/dayData/${day.date}`)
.then((response) => {
// 展示数据展示组件并传入数据
this.showDataPopup(response.data.records, response.data.heatValue, response.data.submitChartOption);
})
.catch((error) => {
console.error('获取当天数据失败', error);
});
},
},
};
三、后端设计与实现
(一)技术选型与架构设计
- 后端采用 Spring Boot 框架,它能够快速搭建基于 Java 的 Web 应用程序,提供了丰富的插件和自动配置功能,简化了开发流程。结合 MySQL 数据库存储用户信息、题目信息、刷题记录等持久化数据,Redis 用于缓存高频访问的数据,如最近的刷题记录、热门题目信息等,以提高系统性能。
- 采用分层架构设计,包括表现层(Controller)、业务逻辑层(Service)、数据访问层(DAO)和数据持久层(数据库)。表现层接收前端请求,调用业务逻辑层处理业务,业务逻辑层进行数据处理和业务规则验证,数据访问层与数据库或 Redis 进行交互,实现数据的读写操作。
(二)数据库设计
- 用户表(user):
- id:用户 ID,主键,自增长。
- username:用户名,唯一约束。
- password:密码。
- email:邮箱。
CREATE TABLE user (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
email VARCHAR(100)
);
- 题目表(question):
- id:题目 ID,主键,自增长。
- question_content:题目内容。
- answer:答案。
- knowledge_point:知识点。
- difficulty_level:难度等级(1 - 简单,2 - 中等,3 - 困难)。
CREATE TABLE question (
id INT AUTO_INCREMENT PRIMARY KEY,
question_content VARCHAR(255) NOT NULL,
answer VARCHAR(255) NOT NULL,
knowledge_point VARCHAR(100) NOT NULL,
difficulty_level INT NOT NULL
);
- 刷题记录表(practice_record):
- id:刷题记录 ID,主键,自增长。
- user_id:用户 ID,外键关联 user 表。
- question_id:题目 ID,外键关联 question 表。
- practice_time:刷题时间。
- submit_result:提交结果(正确/错误)。
CREATE TABLE practice_record (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
question_id INT NOT NULL,
practice_time TIMESTAMP NOT NULL,
submit_result VARCHAR(10) NOT NULL,
FOREIGN KEY (user_id) REFERENCES user(id),
FOREIGN KEY (question_id) REFERENCES question(id)
);
(三)Redis 缓存策略
- 将用户最近一周的刷题记录缓存到 Redis 中,以提高查询速度。设置合适的缓存过期时间,例如一周后自动过期,确保数据的及时性。当有新的刷题记录产生时,先更新数据库,然后同步更新 Redis 缓存。对于热门题目信息,也进行缓存,例如按照题目被刷题的频率进行排序,缓存排名前 N 的题目,方便前端快速获取并展示。
import redis.clients.jedis.Jedis;
public class RedisCacheService {
private static final String REDIS_KEY_PREFIX = "practice:";
private Jedis jedis;
public RedisCacheService() {
// 初始化 Jedis 连接
this.jedis = new Jedis("localhost", 6379);
}
public void cacheRecentPracticeRecords(int userId, List<PracticeRecord> records) {
// 缓存最近刷题记录,键为 user:userId:recent_records,值为 JSON 序列化的记录列表
String key = REDIS_KEY_PREFIX + userId + ":recent_records";
// 将记录列表转换为 JSON 字符串(这里假设使用了 JSON 序列化工具)
String value = JSONSerializer.serialize(records);
// 设置缓存过期时间为一周(以秒为单位)
jedis.setex(key, 7 * 24 * 60 * 60, value);
}
public List<PracticeRecord> getRecentPracticeRecords(int userId) {
// 获取缓存的最近刷题记录
String key = REDIS_KEY_PREFIX + userId + ":recent_records";
String value = jedis.get(key);
if (value!= null) {
// 将 JSON 字符串反序列化为记录列表(这里假设使用了 JSON 反序列化工具)
return JSONDeserializer.deserialize(value, new TypeToken<List<PracticeRecord>>() {}.getType());
}
return null;
}
public void cachePopularQuestions(List<Question> questions) {
// 缓存热门题目,键为 practice:popular_questions,值为 JSON 序列化的题目列表
String key = REDIS_KEY_PREFIX + "popular_questions";
String value = JSONSerializer.serialize(questions);
// 设置缓存过期时间为一天(以秒为单位)
jedis.setex(key, 24 * 60 * 60, value);
}
public List<Question> getPopularQuestions() {
// 获取缓存的热门题目
String key = REDIS_KEY_PREFIX + "popular_questions";
String value = jedis.get(key);
if (value!= null) {
// 将 JSON 字符串反序列化为题目列表(这里假设使用了 JSON 反序列化工具)
return JSONDeserializer.deserialize(value, new TypeToken<List<Question>>() {}.getType());
}
return null;
}
public void close() {
// 关闭 Jedis 连接
jedis.close();
}
}
(四)接口设计与实现
- 日历数据接口:接收前端请求,根据用户 ID 查询数据库中该用户在指定月份的刷题记录,统计每个日期的刷题状态(是否有刷题)、提交次数等信息,封装成 JSON 格式返回给前端。
import java.util.ArrayList;
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class CalendarController {
private final PracticeRecordService practiceRecordService;
public CalendarController(PracticeRecordService practiceRecordService) {
this.practiceRecordService = practiceRecordService;
}
@GetMapping("/api/calendarData/{userId}/{month}")
public List<CalendarDayData> getCalendarData(@PathVariable int userId, @PathVariable int month) {
// 获取指定月份的刷题记录数据
List<PracticeRecord> records = practiceRecordService.getPracticeRecordsByMonth(userId, month);
// 统计每个日期的刷题信息
List<CalendarDayData> calendarData = new ArrayList<>();
// 这里假设 CalendarDayData 是一个包含日期、刷题状态、提交次数等信息的类
// 遍历月份中的每一天,初始化 CalendarDayData 对象并设置默认值
for (int i = 1; i <= 31; i++) {
CalendarDayData dayData = new CalendarDayData(i, false, 0);
calendarData.add(dayData);
}
// 根据刷题记录更新 CalendarDayData 对象的信息
for (PracticeRecord record : records) {
int day = record.getPracticeTime().getDayOfMonth();
CalendarDayData dayData = calendarData.get(day - 1);
dayData.setHasRecord(true);
dayData.setSubmitCount(dayData.getSubmitCount() + 1);
}
return calendarData;
}
}
- 当天刷题详情接口:当用户点击日历日期时,后端根据日期和用户 ID 查询刷题记录表,获取当天的详细刷题记录,包括题目详情、提交结果等数据,同时统计当天的热力值(如根据题目难度加权计算)和不同类型题目的提交次数,将这些数据返回给前端用于展示。
import java.util.ArrayList;
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class DayDataController {
private final PracticeRecordService practiceRecordService;
private final QuestionService questionService;
public DayDataController(PracticeRecordService practiceRecordService, QuestionService questionService) {
this.practiceRecordService = practiceRecordService;
this.questionService = questionService;
}
@GetMapping("/api/dayData/{userId}/{date}")
public DayData getDayData(@PathVariable int userId, @PathVariable String date) {
// 获取当天刷题记录
List<PracticeRecord> records = practiceRecordService.getPracticeRecordsByDate(userId, date);
List<RecordDetail> recordDetails = new ArrayList<>();
// 构建刷题记录详情列表
for (PracticeRecord record : records) {
Question question = questionService.getQuestionById(record.getQuestionId());
RecordDetail detail = new RecordDetail(question.getQuestionContent(), question.getKnowledgePoint(), record.getSubmitResult());
recordDetails.add(detail);
}
// 计算热力值
int heatValue = calculateHeatValue(records);
// 统计提交次数
SubmissionCount submissionCount = countSubmissionTypes(records);
// 构建提交次数统计图表选项(这里假设使用 Echarts,构建对应的 JSON 格式选项)
EchartsOption submitChartOption = buildSubmitChartOption(submissionCount);
return new DayData(recordDetails, heatValue, submitChartOption);
}
private int calculateHeatValue(List<PracticeRecord> records) {
int heatValue = 0;
for (PracticeRecord record : records) {
Question question = questionService.getQuestionById(record.getQuestionId());
heatValue += question.getDifficultyLevel();
}
return heatValue;
}
private SubmissionCount countSubmissionTypes(List<PracticeRecord> records) {
SubmissionCount count = new SubmissionCount();
for (PracticeRecord record : records) {
if (record.getSubmitResult().equals("正确")) {
count.increaseCorrectCount();
} else {
count.increaseIncorrectCount();
}
// 根据题目类型进一步细分统计
Question question = questionService.getQuestionById(record.getQuestionId());