Compare commits

...

43 Commits

Author SHA1 Message Date
lbw
fcf381b8f1 fix(exam): 优化考试单词识别与文件上传功能
- 修改application.yml默认激活环境为pro,调整上传文件大小限制为30MB
- 更新pro配置中的模板文件版本和路径
- 修复考试单词ID乱序问题,添加生成试卷成功日志
- 优化考试识别逻辑,确保ansSheetPath变量正确处理,完善异常捕获与文件删除机制
- 增加上传文件格式白名单校验,强化文件存储路径和命名安全性
- 用NIO替换File操作,确保上传目录存在并合理创建
- 优化PngUtil图像二值化处理,增加自适应阈值和形态学操作减少噪声
- 修改未背熟单词识别阈值,调整检测区域坐标和日志输出
- 注释部分冗余图像预处理代码,完善日志和异常信息提示
- 统一文件上传与识别过程的错误处理和日志记录,提升系统稳定性和可维护性
2026-01-06 14:48:01 +08:00
lbw
fb29acc145 feat(ui): 提升界面响应式支持和移动端适配体验
- 新增移动端全屏对话框支持及标签宽度和位置动态调整,优化新增班级、年级和学生弹窗布局
- 所有对话框增加屏幕宽度监听,实现自动切换移动端和桌面端样式
- 表格组件增加移动端列表视图,隐藏侧边栏并改进分页和按钮自适应,提升小屏幕浏览体验
- Dialog及详情弹窗添加最大高度限制并启用滚动,防止移动端显示区域拥挤
- 登录页增加安全区域内边距,保证iOS等设备显示完整性
- 新增移动端菜单抽屉组件,支持手机端侧边栏交互显示
- 学生详情页调整词汇热力图列数,实现移动端更合理布局
- 表格和按钮统一增设触控友好大尺寸区域,提升移动端操作便利性
- 修正后端空词汇ID查询问题,避免空列表导致查询异常
- 统一隐藏小屏幕时的固定侧边栏,避免界面混乱和重复显示
- 搜索页和上传页表格添加移动端适配样式和展开收起逻辑,提升列表浏览灵活性
2026-01-05 18:47:50 +08:00
lbw
7182371c92 fix(enlish-service): 优化单词数据处理及模板配置更新
- 修改开发和生产环境配置中的数据库连接及模板文件路径
- ExamWordsController新增单词列表拆分为两部分返回
- ExamWordsServiceImpl增加单词数量不足时补充逻辑,确保单词数量满足要求
- LessonPlansServiceImpl优化教案数据组装,增加班级信息及单词列表拆分功能
- PngUtil调整图像二值化阈值,完善轮廓检测及未背熟单词识别逻辑,移除冗余代码
- SaTokenConfigure更新路由权限配置,添加对tts接口的不拦截支持
- 删除StudentExamWordsDOMapper中is_completed条件,调整查询方式
- UserController修正接口日志注释,准确描述修改用户信息功能
- VocabularyBankDOMapper新增根据年级与排除ID查询单词接口及SQL映射
- WordExportUtil更新导出配置,支持拆分单词列表绑定两个集合以适应新结构
2026-01-05 18:09:36 +08:00
lbw
09b326c07a feat(login): 优化登录注册页UI和交互体验
- 使用全屏背景图和渐变叠加提升视觉效果
- 增加登录注册切换标签及动画过渡效果
- 表单输入框增加图标和玻璃质感样式
- 调整表单布局,简化登录表单,丰富注册表单字段
- 替换原有按钮为渐变风格提升点击感
- 引入FontAwesome图标库支持图标显示
- 增加输入框聚焦和悬停状态样式
- 优化Loading状态文案展示逻辑
2026-01-05 12:56:32 +08:00
lbw
49963bb49c feat(user): 添加用户信息修改功能及对应验证码校验
- 在管理员页面新增修改用户信息表单,支持姓名、手机号、密码修改
- 实现验证码发送倒计时与发送状态管理
- 新增接口支持用户信息更新,包含密码和手机号校验
- 后端校验验证码有效性,编码密码后更新用户信息
- 修改用户信息后强制登出,确保安全性
- 优化登录状态判断,登出后跳转至登录页
- 取消部分日志打印,调整发送验证码缓存过期时间为5分钟
2026-01-05 12:07:15 +08:00
lbw
bf2a80917c style(ui): 优化页面容器及布局样式,调整头部组件图标和交互
- 为多个页面容器添加最小高度类,保证页面满屏显示
- 调整部分主内容区高度样式,增强布局一致性和视觉整洁
- 替换头部组件Logo为自定义SVG图标,提升品牌识别度
- 优化头部用户菜单按钮交互和样式,统一暗黑模式视觉效果
- 调整TTS页面词汇列表布局,支持移动端和桌面端不同显示方式
- 修改学生详情页面样式,提升各模块容器的统一性和分隔感
- 修正历史数据日期格式,将“T”替换为空格以增强可读性
2026-01-05 11:21:55 +08:00
lbw
deabd5f7f5 feat(exam): 优化考试判卷逻辑并完善相关接口
- 新增获取未完成判卷数量接口 getExamUnfinishedCount
- 修改定时任务 autoJudgeExamWords 使用未完成判卷数量判断
- Mapper 添加 selectUnfinishedCount 方法,SQL查询未完成记录数
- 优化文件上传判卷时日志输出,增加“上传文件成功”日志
- 修正判卷时未识别学生和考试的提示信息
- 移除学生查询页面中班级和年级ID显示,仅保留名称显示
- 测试类中添加判卷方法调用测试逻辑
- 配置文件增加 Sa-Token 相关日志调试配置
2026-01-04 18:52:50 +08:00
lbw
0802f6fe70 fix(lesson-plan): 优化学案生成文本及缓存时长
- 调整学案生成时异常提示文案为“学案正在生成,请耐心等待”
- 缩短Redis缓存过期时间由12分钟改为7分钟,提高资源释放效率
- 修正生成标题中去除学生ID显示,避免冗余信息
- 将词汇列表转换为Word对象,隐藏单词展示内容以模拟填空效果
- 调整多轮练习词汇列表展示样式,统一处理单词显示
- 混合练习中隐藏词汇释义文本,防止提前泄露答案
- 保持数据结构适配前端练习需求,提升用户体验
2026-01-04 15:01:42 +08:00
lbw
679241588f style(ui): 优化页面布局和样式细节
- admid页面将主区域拆分为两栏布局,调整新增用户按钮颜色统一为primary
- admid页面生成邀请码区域样式微调,增加标题字体大小并统一边距
- 全局主色从#409eff更改为#2563eb,更新顶部加载条颜色
- class页和learningPlan页整体布局调整,包含容器内间距、面板样式统一及按钮样式修改
- uploadpng页布局调整,容器添加内边距,使用panel-shell统一面板样式
- header组件logo图片添加width和height属性,调整导航栏最小高度及z-index提升层级
- 修改CSS变量定义,统一主色调、圆角和背景样式,实现暗黑模式和浅色模式背景渐变效果
- 移除一些多余的class名和调整padding,统一整体界面空间分布和层次感
2026-01-04 12:11:39 +08:00
lbw
fe7128dd4e feat(layout): 为多页面添加侧边栏布局组件
- 在admid页面和class页面包裹el-container,插入Sidebar侧边栏组件
- 在LearningPlan和PlanTTS页面同样新增侧边栏布局
- 重构Header组件样式,采用fluent风格透明卡片和按钮样式
- 增加main.css中panel-shell的样式定义以支持新布局视觉效果
- 优化部分按钮及菜单交互样式,提升整体一致性与视觉体验
2026-01-04 11:10:29 +08:00
lbw
1184ea7895 feat(student): 添加学习分析功能组件及相关交互
- 在班级页面列表操作栏增加“学情分析”按钮,点击弹出学情分析对话框
- 新增StudyAnalysis组件,封装学习分析生成与展示逻辑
- 学生详情页替换原有学习分析区域,统一使用StudyAnalysis组件
- 移除学生页原有学习分析相关状态管理和接口调用,简化代码
- 通过定时器模拟加载进度条,提升生成学习分析时的用户体验
2026-01-04 10:17:59 +08:00
lbw
e468be74b7 feat(exam): 添加按班级、年级、学生姓名筛选考试结果功能
- 在ExamWordsResultReqVO中新增classId、gradeId、studentName字段
- 修改ExamWordsJudgeService接口及实现,支持按筛选条件获取考试结果
- 在ExamWordsJudgeResultDOMapper中添加按学生ID列表分页查询方法
- 扩展StudentDOMapper,新增按班级、年级和姓名查询学生列表方法
- 修改ExamWordsController,支持从请求体接收筛选参数
- 修改前端exam.js,调用接口时传递筛选参数
- 在uploadpng.vue页面新增筛选表单,支持班级、年级、学生姓名输入
- 增加班级和年级选项数据的获取
- 实现筛选查询、重置功能及班级切换自动同步年级
- 在App.vue中配置Element Plus中文语言环境
- 调整配置文件,更新学习计划模板路径
- 修改接口权限配置,关闭plan/word/voice路径的鉴权
2025-12-31 19:21:19 +08:00
lbw
57166af2f4 Merge branch 'master' of https://git.yinlihupo.cn/LeiBoWen/en-edu 2025-12-31 17:24:03 +08:00
lbw
fcb8ac9c22 feat(plan): 添加周末计划生成日志记录
- 在生成周末计划开始处添加日志输出
- 有助于调试和监控计划生成流程
2025-12-31 17:22:55 +08:00
7f2fda16ec Merge pull request 'fix(lesson-plans): 修正补差词汇汇总及周末计划分割逻辑' (#1) from detached into master
Reviewed-on: #1
2025-12-31 16:55:54 +08:00
lbw
9cd43c0e74 fix(lesson-plans): 修正补差词汇汇总及周末计划分割逻辑
- 新增totalWords列表汇总同步词汇和补差词汇
- 修改周末计划分割逻辑,使用totalWords列表长度均分两部分
- 纠正checkList子列表截取范围,避免越界错误
- 优化学习计划数据构建过程,保证数据完整性
2025-12-31 16:53:28 +08:00
lbw
b86f37443c 保存数据库文件 2025-12-31 16:28:35 +08:00
lbw
36e5231c6c feat(plan): 添加学生学案查看及下载功能
- 在学生列表表格中新增“查看学案”按钮,支持查看对应学生的学案列表
- 新增StudentPlanListDialog组件,实现学案列表展示和学案文件下载
- 后端新增查询学生学案接口,支持按学生ID获取未完成学案列表
- 后端数据层和服务层添加按学生ID查询学案的方法
- 调整计划生成相关逻辑,优化学案数据字段命名
- Vue前端调用新增接口,实现学生学案列表动态加载与下载操作
- 完善学案状态显示和列表交互体验
2025-12-31 16:20:52 +08:00
lbw
868e0bb7bd feat(plan): 支持学案生成状态轮询与进度显示
- 新增接口检查学案是否正在生成,防止重复生成任务
- 使用 Redis 缓存标识学案生成状态,设置 12 分钟过期时间
- 生成学案时记录状态至 Redis,生成完成后自动清除
- Vue 学案列表新增学案生成进度条显示与已生成标签
- 新增组件事件监听生成成功,触发轮询检测学案状态
- 轮询间隔 10 秒,动态更新学案生成进度,最高至 95%
- 路由离开与组件卸载时停止所有轮询,防止内存泄漏
- 优化学案生成逻辑,新增小测试卷自动关联及数据入库
- 更新配置文件模板路径,提高文档管理一致性
2025-12-31 15:41:53 +08:00
lbw
504dd8d964 refactor(exam): 重构考试判卷任务与新增教案小测功能
- 将自动判卷任务类从job包迁移到task包并添加日志打印,增强可维护性
- 优化自动判卷逻辑,仅当有未完成试卷时触发判卷处理
- 修正判卷结果中学生姓名和考试标题的空值保护,防止空指针异常
- 扩展考试类型常量,新增教案小测类型常量EXAM_TYPE_TEST
- 修改exam_words表插入语句,新增type字段支持不同考试类型
- 优化判卷逻辑:仅摸底考试计算等级,其他考试添加答题数提示信息
- 判卷完成后调用学案服务,自动完成学生对应学案计划
- 在教案计划生成中增加教案小测试卷的生成与存储
- 新增PlanExamDO数据对象及对应Mapper,实现学案与考试的关联映射
- 修改MySQL及Redis配置文件,完善环境配置
- 修正文档生成模板路径及生成逻辑,优化导出功能
- 屏蔽部分测试用例中的直接调用,防止无效执行和输出
- 清理部分测试代码中的硬编码和无用注释,提升代码整洁度
- 删除无用OMR测试代码,注释多余测试实现,简化测试类
- 统一二维码链接域名,指向正式环境地址
- 移除enlish-service中对词汇库插入的测试注释,避免误执行
2025-12-31 14:16:43 +08:00
lbw
6277e3ab42 fix(permission): 修复权限路由跳转逻辑
- 调整判断条件,允许未登录用户访问 /plan/tts 路径
- 防止未登录时强制跳转到登录页,提升用户体验
- 修正 application-dev.yml 中 speech 配置缩进问题,保证配置正确解析
2025-12-30 11:59:47 +08:00
lbw
0f5169c1d7 refactor(ai): 重构AI客户端类并添加句子分析接口
- 将 DifyArticleClient 重命名为 DifyClient,调整相关调用引用
- 增加 sendSentenceAnalyze 方法,实现批量词汇句子分析功能
- 更新配置文件,新增 analyzeKey 和 sentenceKey 两个AI接口密钥配置
- 修改LessonPlansServiceImpl中生成连词成句功能,调用新句子分析接口
- 优化LessonPlanConstant常量,调整部分键名格式,去除空格
- 修改相关测试类适配 DifyClient 改动
- 相关VO类添加学生姓名和试题名称字段,丰富展示数据
- 更新前端table组件,替换显示学生姓名、班级名、年级名等信息字段
- 调整路由首页路径为“/”,修改Header组件对应链接
- 其他若干细节修改,如异常日志优化、时间格式展示等
2025-12-30 10:30:26 +08:00
lbw
5ebf40101d refactor(class): 优化班级页面布局与删除功能
- 调整学生查询区域样式,使其占用两行空间
- 移除年级和单元列表中的删除按钮及新增操作相关代码
- 新增删除学生与班级前的确认弹窗提示,防止误操作
- 捕获删除确认框的取消或关闭事件,避免错误提示
- 主动导入 Element Plus 组件库样式文件,保证样式完整
- 修正角色同步代码中 Redis 写入方式,避免二次序列化问题
2025-12-29 15:55:36 +08:00
lbw
5858bf2ecc feat(admin): 新增邀请码生成及注册校验功能
- 在管理员页面新增邀请码生成面板,支持限制使用次数和有效期
- 新增后端接口支持创建邀请码,邀请码存储在Redis并设置过期时间
- 用户注册接口新增邀请码参数,校验邀请码有效性和剩余使用次数
- 注册时成功使用邀请码后,Redis中对应邀请码的使用次数减1
- 登录接口及相关服务层逻辑新增邀请码字段支持
- 后端权限配置增加/admin路径的root角色校验
- 优化角色权限同步时Redis存储格式为列表类型
- 调整SaToken相关接口实现以支持角色ID转换逻辑
2025-12-29 15:44:05 +08:00
lbw
bddf6c0936 feat(student): 添加学习分析生成进度显示
- 在分析生成期间显示进度条和提示信息
- 引入 analyzeProgress 变量动态更新进度百分比
- 使用定时器模拟进度增长,达到 90% 后等待完成
- 分析完成后将进度设置为 100% 并清除定时器
- 调整模板逻辑,区分加载中和结果显示状态
2025-12-29 14:50:27 +08:00
lbw
340bc5b5e3 feat(auth): 实现登录注册功能和路由权限控制
- 新增 Login.vue 实现登录与注册界面,支持手机号、密码、验证码等验证
- 添加登录状态保持并在登录成功后设置 token
- 修改路由配置,新增 /login 路由,并调整默认班级页路由为 /class
- 移除 Header 组件中原有登录按钮,改为通过路由控制访问权限
- 实现路由前置守卫,根据 token 自动跳转登录页或班级页
- 添加验证码发送功能及倒计时禁用按钮逻辑
- 完善表单校验规则,区分登录和注册模式验证字段
2025-12-29 14:45:44 +08:00
lbw
2d76ed507e feat(tts): 集成OpenAI语音合成功能并支持计划单词语音生成
- 新增TTS工具类TTSUtil,实现文本到语音的转换并通过HTTP响应返回音频流
- 在LessonPlanController添加获取计划单词列表及单词语音生成接口
- 前端新增PlanTTS页面,实现计划单词TTS的加载、生成、播放及下载功能
- 路由新增PlanTTS路由,支持访问TTS生成功能页面
- 配置文件application-dev.yml新增OpenAI TTS相关配置
- WordExportUtil生成计划文档时嵌入对应页面二维码图片
- 引入spring-ai-openai相关依赖支持OpenAI模型调用
- 新增单词语音相关请求与响应VO类,方便接口数据传输
- 新增计划单词获取接口plan/word/voice对应前端api
- 新增计划单词语音合成接口plan/word/voice/tts对应前端api
- 添加二维码生成逻辑,用于生成计划文档中的二维码图片链接
- 添加单元测试模版VoiceTest,预留TTS工具类测试接口
2025-12-29 12:44:16 +08:00
lbw
494ab77486 feat(plan): 支持学案生成时指定单词数
- 在 AddLessonPlanReqVO 中新增 wordSize 字段
- 修改 LessonPlansService 接口及实现,支持 wordSize 参数
- 优化学案生成逻辑,按指定单词数切分词汇列表
- 更新前端 LessonPlanDialog,添加单词数输入框
- 修改生成学案接口及调用,传递 wordSize 参数
- 增加查询学生词汇掌握详情接口及实现
- 添加学生词汇统计展示组件及页面集成
- 调整词汇相关 Mapper,修正记忆强度条件范围
- 更新权限配置,允许访问学生单词详情接口
2025-12-27 17:21:25 +08:00
lbw
d3cfa80613 feat(student): 新增分阶段学习评语功能
- 添加StudentStageLearningRemarkDO数据对象类
- 新增StudentStageLearningRemarkDOMapper接口及对应XML映射文件
- 修改generatorConfig.xml,增加student_stage_learning_remark表生成配置
- 在StudentServiceImpl中注入StudentStageLearningRemarkDOMapper
- 学习分析结果保存时同时插入分阶段学习评语数据
- exam模块打印生成各类测试日志信息(摸底、期中、期末)
2025-12-25 19:08:27 +08:00
lbw
66eb02cb08 更新数据库v2 2025-12-25 18:37:18 +08:00
lbw
0b0311d2d9 refactor(exam): 优化考试单词生成逻辑并新增期中期末类型
- 调整考试类型选择,增加“期中”和“期末”选项
- 删除旧的gradeId和level参数,简化接口参数为studentId和type
- 新增考试类型常量:期中(2)、期末(3)
- 实现期中考试和期末考试生成逻辑,分别根据年级及单元名称筛选词汇
- 调整服务层方法签名及调用,支持新考试类型生成流程
- 扩展Mapper接口,支持按单元名称和单元ID查询词汇
- 优化导出逻辑,导出文件名和压缩包名称根据考试标题动态生成
- 调整测试代码,适配新的方法参数和实现细节
2025-12-25 18:05:43 +08:00
lbw
7b68184787 fix(judge): 修正分级升级与降级边界逻辑
- 升级时确保等级至少为8级
- 降级时确保等级不低于1级
- 修正多处比较操作,避免越界导致异常
- 优化提示信息中的等级显示,保证一致性
- 增强系统分级判定的鲁棒性
2025-12-25 17:07:12 +08:00
lbw
bc9334f5ab fix(examWords): 修复考试单词顺序混乱和标记阈值调整
- 限制单词释义显示长度,避免过长显示问题
- 修复获取单词后单词 ID 顺序混乱问题,增加更新考试记录单词 ID 顺序功能
- 增加 ExamWordsDOMapper 中更新单词 ID 顺序的方法及对应 XML 配置
- 在 ExamWordsService 中新增更新单词 ID 顺序方法及其实现
- 调整 PngUtil 中未背熟单词标记阈值由 800 降至 500,增强识别准确性
- 优化测试用例,增加对未掌握单词的输出日志
- 更新测试数据文件路径及格式对应关系,改进词汇插入逻辑,完善变量赋值
- 统一单词实体中音标和词性赋值,保证完整词汇信息展现
2025-12-25 17:01:21 +08:00
lbw
aff862d161 feat(student): 新增学生词汇掌握详情及热力图展示功能
- 新增FindStudentMasteryDetailReqVO和FindStudentMasteryDetailRspVO数据类
- 学生接口新增/ student/mastery/detail,用于查询学生词汇掌握详情
- StudentService及实现类添加查询词汇掌握详情的方法
- WordMasteryLogDOMapper新增selectAllByStudentId方法支持查询
- SaTokenConfigure增加对新接口的免认证配置
- 前端api新增getStudentWordMastery方法
- 学生页面新增WordMasteryHeatmap组件并展示词汇掌握热力图
- 创建WordMasteryHeatmap组件,支持动态请求数据及Echarts热力图渲染
- 热力图按记忆强度排序,提供丰富的鼠标悬停提示信息
2025-12-24 16:26:22 +08:00
lbw
15e909c318 refactor(role): 使用 RedisTemplate 替换 StringRedisTemplate 优化角色数据同步
- 将 RoleServiceImpl 中 stringRedisTemplate 替换为更通用的 redisTemplate
- 调整角色和用户角色关系写入 Redis 的代码,删除存储过期时间参数
- 添加日志记录,输出同步到 Redis 的用户角色关系数据
- 删除 LoginServiceImpl 中未实现的 initUserRole 方法
- 新增 RoleService 的单元测试 RoleTest,验证角色权限同步功能
- 移除 UserRoleRelDOMapper.xml 中 selectAll 查询的 is_deleted 过滤条件
2025-12-24 15:35:23 +08:00
lbw
260c2c79f1 feat(student): 实现学生学习分析功能
- 新增AnalyzeStudentStudyReqVO用于分析请求参数封装
- StudentService接口新增analyzeStudentStudy方法及其实现
- 实现分析逻辑,查询最近7天学生考试及单词掌握记录,构造分析数据
- 通过DifyArticleClient调用外部AI服务生成学习分析结果
- 使用Redis缓存分析结果,设置3天过期
- 新增ExamWordsJudgeResultDetail和WordMasteryDetail数据模型
- Mapper新增支持根据学生ID和时间范围查询考试结果和单词掌握日志
- DifyArticleClient新增sendStudentAnalyze方法调用分析接口
- 前端学生页面新增学习分析面板及调用接口,支持超时设置
- 修改路由权限配置,允许访问学习分析接口
- 添加markdown-it库支持分析结果富文本渲染
- 移除RoleServiceImpl中redis设置过期时间,改为永久保存
2025-12-24 15:22:18 +08:00
lbw
4135b72648 feat(admin): 实现用户管理列表及新增用户功能
- 新增用户列表页面,实现分页查询和条件筛选
- 增加新增用户弹窗表单,支持姓名、手机号及密码录入和校验
- 后端新增 AdminController 提供用户列表查询和创建接口
- 完善 UserService 和 RoleService,支持分页用户数据获取及用户角色映射
- 丰富数据库 Mapper 增加用户及用户角色相关查询插入操作
- 定时任务 UserRoleTask 调整调用角色服务更新权限缓存
- 前端接口封装新建用户相关请求便于调用
- 使用密码加密存储新建用户密码保障安全
2025-12-24 11:25:27 +08:00
lbw
5404f295e4 feat(auth): 实现用户菜单及登出功能
- 在Header组件添加用户下拉菜单,支持显示用户名和操作选项
- 新增点击文档隐藏菜单的事件监听与清理
- 实现登出功能,调用后端登出接口,清理登录状态并跳转主页
- 路由新增管理员页面/admid及其组件admid.vue
- 删除unused的首页index.vue页面文件
- 后端新增登出接口/logout,支持用户会话注销
- 修正登录服务实现,修复密码匹配逻辑错误
- 客户端api新增logout接口调用后端登出功能
2025-12-24 10:25:45 +08:00
lbw
948144e7b2 数据库文件 2025-12-22 19:04:24 +08:00
lbw
bc4c74f881 feat(user): 实现用户角色权限管理和登录态完善
- 新增异步任务支持,启用@EnableAsync注解
- 添加用户信息响应VO类FindUserInfoRspVO
- 修改MyBatis逆向生成配置,调整映射的表为user_role_rel
- 全局异常处理新增未登录异常处理方法
- Vue头部组件Header.vue完善登录状态显示,显示用户名或登录按钮
- 新增获取用户信息的前端API接口getUserInfo
- 新增UserController,提供获取当前用户信息接口
- UserDOMapper新增selectById方法及对应XML配置
- 设计角色与用户角色关系数据对象及MyBatis映射文件
- 新增RoleDO和UserRoleRelDO数据对象及对应Mapper接口和XML映射
- 实现UserService及其实现类UserServiceImpl,支持推送角色权限到Redis
- 新增定时任务UserRoleTask,定时同步权限数据到Redis
- 配置SaToken权限拦截器,设置登录校验及排除路径
- 实现StpInterface接口,自定义权限与角色列表获取逻辑
- 响应码枚举中添加未登录状态码NOT_LOGIN
2025-12-22 19:03:02 +08:00
lbw
f4498e5676 feat(auth): 实现基于阿里云短信验证码的登录注册功能
- 新增阿里云短信发送客户端配置及属性绑定类
- 集成阿里云短信服务实现验证码发送功能
- 基于 Sa-Token 完成登录状态管理和 token 生成
- 实现手机号验证码登录、密码登录及验证码注册支持
- 添加密码加密 Bean,使用 BCrypt 保障密码安全
- 新增 Redis 缓存验证码,实现验证码有效期和校验
- Vue 前端新增登录弹窗组件,支持三种登录模式切换
- 统一 Axios 请求添加 Token 请求头及响应错误提示
- 更新配置文件,加入 Sa-Token 相关配置项
- 调整后端数据库实体生成配置,新增用户表映射
- 添加前端依赖包 @vueuse/integrations 和 universal-cookie
- 新增前端 Cookie 操作逻辑,用于 Token 的存取管理
- 优化 Header 组件,增加 Login 按钮触发登录弹窗
2025-12-22 17:26:21 +08:00
lbw
515bd8fae2 feat(exam): 实现考试阶段单词判卷与学生水平智能诊断
- 新增ActionType枚举定义系统动作类型
- 新增DiagnosisResult和ZoneStats数据模型支持诊断结果及区域统计
- 优化ExamWordsJudgeServiceImpl判卷逻辑,支持识别图片、更新考试判卷结果
- 基于分区词汇掌握情况,实现学生当前水平年级的智能判定
- 实现基于多分区准确率的升级、降级、复习和触发重测等动作建议
- 更新学生实际年级actualGradeId并展示在学生详情页面
- 修正ExamWordsConstant年级常量及年级名称映射方法
- 优化前端生成试题对年级和难度的校验逻辑,简化参数传递
- 修改服务端端口及API代理配置,保持一致性
- 调整相关数据库Mapper,支持批量查询和更新实际年级字段
- 修改错误信息字段命名,统一为msg
- 增删改代码注释与日志,提升容错性和可读性
2025-12-22 14:11:11 +08:00
lbw
065da854ee feat(exam): 支持按单个学生和考试类型生成考试试题
- 修改生成试题按钮仅在选中特定一个学生时可用,避免多选时误操作
- 在考试生成对话框新增“类型”选择项,支持“摸底”和“期中|期末”类型
- 调整后台接口,使用单个学生ID和考试类型替代学生ID列表参数
- 优化考试生成服务,新增摸底考试生成逻辑,按年级分区随机抽词汇
- 考试相关数据对象新增类型字段,保持数据完整性和一致性
- 修改考试判卷服务,将错误信息字段统一为msg,避免字段混淆
- 调整数据库操作,支持单个学生考试与词汇随机获取
- 同步更新测试用例和词汇库数据插入逻辑,确保环境一致性
- 修复界面生成按钮状态和对话框提交按钮的校验逻辑,提升用户体验
2025-12-18 17:21:37 +08:00
185 changed files with 30714 additions and 883 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

7142
docx/enlish.sql Normal file

File diff suppressed because one or more lines are too long

5728
docx/enlish_v2.sql Normal file

File diff suppressed because one or more lines are too long

11395
docx/enlish_v3.sql Normal file

File diff suppressed because one or more lines are too long

View File

@@ -105,6 +105,74 @@
<artifactId>tess4j</artifactId> <artifactId>tess4j</artifactId>
</dependency> </dependency>
<!-- 阿里云短信发送 -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dysmsapi20170525</artifactId>
</dependency>
<!-- Sa-Token 权限认证在线文档https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-template</artifactId>
</dependency>
<!-- 密码加密 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@@ -3,11 +3,13 @@ package com.yinlihupo.enlish.service;
import org.mybatis.spring.annotation.MapperScan; import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication @SpringBootApplication
@MapperScan("com.yinlihupo.enlish.service.domain.mapper") @MapperScan("com.yinlihupo.enlish.service.domain.mapper")
@EnableScheduling @EnableScheduling
@EnableAsync
public class EnlishServiceApplication { public class EnlishServiceApplication {
public static void main(String[] args) { public static void main(String[] args) {

View File

@@ -0,0 +1,13 @@
package com.yinlihupo.enlish.service.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@ConfigurationProperties(prefix = "aliyun")
@Component
@Data
public class AliyunAccessKeyProperties {
private String accessKeyId;
private String accessKeySecret;
}

View File

@@ -0,0 +1,36 @@
package com.yinlihupo.enlish.service.config;
import com.aliyun.dysmsapi20170525.Client;
import com.aliyun.teaopenapi.models.Config;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@Slf4j
public class AliyunSmsClientConfig {
@Resource
private AliyunAccessKeyProperties aliyunAccessKeyProperties;
@Bean
public Client smsClient() {
try {
Config config = new Config()
// 必填
.setAccessKeyId(aliyunAccessKeyProperties.getAccessKeyId())
// 必填
.setAccessKeySecret(aliyunAccessKeyProperties.getAccessKeySecret());
// Endpoint 请参考 https://api.aliyun.com/product/Dysmsapi
config.endpoint = "dysmsapi.aliyuncs.com";
return new Client(config);
} catch (Exception e) {
log.error("初始化阿里云短信发送客户端错误: ", e);
return null;
}
}
}

View File

@@ -0,0 +1,19 @@
package com.yinlihupo.enlish.service.config;
import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
@Component
public class PasswordEncoderConfig {
@Bean
public PasswordEncoder passwordEncoder() {
// BCrypt 是一种安全且适合密码存储的哈希算法,它在进行哈希时会自动加入“盐”,增加密码的安全性。
return new BCryptPasswordEncoder();
}
}

View File

@@ -0,0 +1,41 @@
package com.yinlihupo.enlish.service.config;
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.router.SaHttpMethod;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@Slf4j
public class SaTokenConfigure implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SaInterceptor(handler -> {
log.info("Sa-Token 拦截器: {}", handler);
SaRouter.match(SaHttpMethod.OPTIONS)
.free(r -> System.out.println("--------OPTIONS预检请求不做处理"))
.back();
SaRouter.match("/**")
.notMatch("/login/**")
.notMatch("/plan/word/voice")
.notMatch("/plan/word/voice/tts")
.check(r -> StpUtil.checkLogin());
SaRouter.match("/admin/**")
.notMatch("/plan/word/voice")
.notMatch("/plan/word/voice/tts")
.check(r -> StpUtil.checkRole("root"));
}))
.addPathPatterns("/**")
.excludePathPatterns("/error");
}
}

View File

@@ -0,0 +1,50 @@
package com.yinlihupo.enlish.service.config;
import cn.dev33.satoken.stp.StpInterface;
import com.google.common.cache.Cache;
import com.yinlihupo.enlish.service.constant.RoleConstants;
import com.yinlihupo.framework.common.util.JsonUtils;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
@Slf4j
public class StpInterfaceImpl implements StpInterface {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
return List.of();
}
@Override
public List<String> getRoleList(Object loginId, String loginType) {
long l = 0L;
if (loginId instanceof String loginIdStr) {
l = Long.parseLong(loginIdStr);
}
return userToRole(l);
}
private List<String> userToRole(Long userId) {
String keys = stringRedisTemplate.opsForValue().get(RoleConstants.buildUserRoleKey(userId));
if (keys != null) {
try {
return JsonUtils.parseList(keys, String.class);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
return List.of();
}
}

View File

@@ -1,6 +1,153 @@
package com.yinlihupo.enlish.service.constant; package com.yinlihupo.enlish.service.constant;
public interface ExamWordsConstant { public class ExamWordsConstant {
int PGN_COL = 53; public static final int PGN_COL = 53;
public static final int GRADE_1 = 1;
public static final int GRADE_2 = 2;
public static final int GRADE_3 = 3;
public static final int GRADE_4 = 4;
public static final int GRADE_5 = 5;
public static final int GRADE_6 = 6;
public static final int GRADE_7 = 7;
public static final int GRADE_8 = 8;
public static final String GRADE_1_NAME = "一年级";
public static final String GRADE_2_NAME = "二年级";
public static final String GRADE_3_NAME = "三年级";
public static final String GRADE_4_NAME = "四年级";
public static final String GRADE_5_NAME = "五年级";
public static final String GRADE_6_NAME = "六年级";
public static final String GRADE_7_NAME = "初一";
public static final String GRADE_8_NAME = "初二";
public static final int ZONE_A_SIZE = 10;
public static final int ZONE_B_SIZE = 20;
public static final int ZONE_C_SIZE = 28;
public static final int ZONE_D_SIZE = 21;
public static final int ZONE_E_SIZE = 14;
public static final int ZONE_F_SIZE = 7;
// 摸底
public static final int EXAM_TYPE_BASELINE = 1;
// 中期
public static final int EXAM_TYPE_MIDTERM = 2;
// 期末
public static final int EXAM_TYPE_FINAL = 3;
// 小测
public static final int EXAM_TYPE_TEST = 4;
public static int getZoneA(int gradeId) {
return switch (gradeId) {
case GRADE_1 -> GRADE_2;
case GRADE_2 -> GRADE_3;
case GRADE_3 -> GRADE_4;
case GRADE_4 -> GRADE_5;
case GRADE_5 -> GRADE_6;
case GRADE_6 -> GRADE_7;
case GRADE_7 -> GRADE_8;
default -> 0;
};
}
public static int getZoneB(int gradeId) {
return switch (gradeId) {
case GRADE_1 -> GRADE_1;
case GRADE_2 -> GRADE_2;
case GRADE_3 -> GRADE_3;
case GRADE_4 -> GRADE_4;
case GRADE_5 -> GRADE_5;
case GRADE_6 -> GRADE_6;
case GRADE_7 -> GRADE_7;
case GRADE_8 -> GRADE_8;
default -> 0;
};
}
public static int getZoneC(int gradeId) {
return switch (gradeId) {
case GRADE_1 -> GRADE_1;
case GRADE_2 -> GRADE_1;
case GRADE_3 -> GRADE_2;
case GRADE_4 -> GRADE_3;
case GRADE_5 -> GRADE_4;
case GRADE_6 -> GRADE_5;
case GRADE_7 -> GRADE_6;
case GRADE_8 -> GRADE_7;
default -> 0;
};
}
public static int getZoneD(int gradeId) {
return switch (gradeId) {
case GRADE_1 -> GRADE_1;
case GRADE_2 -> GRADE_1;
case GRADE_3 -> GRADE_1;
case GRADE_4 -> GRADE_2;
case GRADE_5 -> GRADE_3;
case GRADE_6 -> GRADE_4;
case GRADE_7 -> GRADE_5;
case GRADE_8 -> GRADE_6;
default -> 0;
};
}
public static int getZoneE(int gradeId) {
return switch (gradeId) {
case GRADE_1 -> GRADE_1;
case GRADE_2 -> GRADE_1;
case GRADE_3 -> GRADE_1;
case GRADE_4 -> GRADE_1;
case GRADE_5 -> GRADE_2;
case GRADE_6 -> GRADE_3;
case GRADE_7 -> GRADE_4;
case GRADE_8 -> GRADE_5;
default -> 0;
};
}
public static int getZoneF(int gradeId) {
return switch (gradeId) {
case GRADE_1 -> GRADE_1;
case GRADE_2 -> GRADE_1;
case GRADE_3 -> GRADE_1;
case GRADE_4 -> GRADE_1;
case GRADE_5 -> GRADE_1;
case GRADE_6 -> GRADE_2;
case GRADE_7 -> GRADE_3;
case GRADE_8 -> GRADE_4;
default -> 0;
};
}
public static String getGradeName(int gradeId) {
return switch (gradeId) {
case GRADE_1 -> GRADE_1_NAME;
case GRADE_2 -> GRADE_2_NAME;
case GRADE_3 -> GRADE_3_NAME;
case GRADE_4 -> GRADE_4_NAME;
case GRADE_5 -> GRADE_5_NAME;
case GRADE_6 -> GRADE_6_NAME;
case GRADE_7 -> GRADE_7_NAME;
case GRADE_8 -> GRADE_8_NAME;
default -> "";
};
}
public static String day2Chinese(int day) {
return switch (day) {
case 1 -> "";
case 2 -> "";
case 3 -> "";
case 4 -> "";
case 5 -> "";
case 6 -> "";
case 7 -> "";
default -> "";
};
}
} }

View File

@@ -1,10 +1,17 @@
package com.yinlihupo.enlish.service.constant; package com.yinlihupo.enlish.service.constant;
public interface LessonPlanConstant { public class LessonPlanConstant {
String TITLE = "Title"; public static final String TITLE = "Title";
String PASSAGE = "The Passage"; public static final String PASSAGE = "ThePassage";
String QUIZ = "Quiz"; public static final String QUIZ = "Quiz";
String ANSWER_KEY_EXPLANATION = "Answer Key & Explanation"; public static final String ANSWER_KEY_EXPLANATION = "AnswerKey&Explanation";
String FULL_TRANSLATION = "Full Translation"; public static final String FULL_TRANSLATION = "FullTranslation";
// 正在生成学案标识
public static final String GENERATING_PLAN = "GeneratingPlan";
public static String buildGeneratePlanContent(Integer studentId) {
return GENERATING_PLAN + ":" + studentId;
}
} }

View File

@@ -0,0 +1,11 @@
package com.yinlihupo.enlish.service.constant;
public class RoleConstants {
public final static String USER_ROLE = "user:role";
public final static String ROLE = "role";
public static String buildUserRoleKey(Long userId) {
return USER_ROLE + ":" + userId;
}
}

View File

@@ -0,0 +1,10 @@
package com.yinlihupo.enlish.service.constant;
public class StudentConstant {
public static final String ANALYZE_STUDENT_STUDY = "analyzeStudentStudy";
public static String buildAnalyzeStudentStudyKey(Integer studentId) {
return ANALYZE_STUDENT_STUDY + ":" + studentId;
}
}

View File

@@ -0,0 +1,16 @@
package com.yinlihupo.enlish.service.constant;
public class UserRedisConstants {
public static final String USER_LOGIN_CODE = "user:login:code:";
public static final String USER_INVITATION_CODE = "user:invitation:code:";
public static String buildUserLoginCode(String phone) {
return USER_LOGIN_CODE + phone;
}
public static String buildUserInvitationCode(String code) {
return USER_INVITATION_CODE + code;
}
}

View File

@@ -0,0 +1,91 @@
package com.yinlihupo.enlish.service.controller;
import com.yinlihupo.enlish.service.constant.UserRedisConstants;
import com.yinlihupo.enlish.service.domain.dataobject.RoleDO;
import com.yinlihupo.enlish.service.domain.dataobject.UserDO;
import com.yinlihupo.enlish.service.model.vo.admin.CreateInvitationCodeReqVO;
import com.yinlihupo.enlish.service.model.vo.admin.CreateInvitationCodeRspVO;
import com.yinlihupo.enlish.service.model.vo.user.CreateUserReqVO;
import com.yinlihupo.enlish.service.model.vo.user.FindUserListRepVO;
import com.yinlihupo.enlish.service.model.vo.user.FindUserListRspVO;
import com.yinlihupo.enlish.service.service.RoleService;
import com.yinlihupo.enlish.service.service.UserService;
import com.yinlihupo.framework.biz.operationlog.aspect.ApiOperationLog;
import com.yinlihupo.framework.common.response.PageResponse;
import com.yinlihupo.framework.common.response.Response;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeUnit;
@RequestMapping("/admin/")
@RestController
public class AdminController {
@Resource
private UserService userService;
@Resource
private PasswordEncoder passwordEncoder;
@Resource
private RoleService roleService;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@PostMapping("user/list")
@ApiOperationLog(description = "查询用户列表")
public PageResponse<FindUserListRspVO> listUser(@RequestBody FindUserListRepVO findUserListRepVO) {
Integer userTotal = userService.findUserTotal();
if (userTotal == null || userTotal == 0) {
throw new RuntimeException("暂无用户");
}
Integer page = findUserListRepVO.getPage();
Integer pageSize = findUserListRepVO.getPageSize();
String name = findUserListRepVO.getName();
List<UserDO> usersList = userService.findUsersList(page, pageSize, name.isEmpty() ? null : name);
Map<Long, RoleDO> userId2RoleMap = roleService.findUserId2RoleMap(usersList.stream().map(UserDO::getId).toList());
return PageResponse.success(usersList.stream().map(userDO -> FindUserListRspVO.builder()
.id(userDO.getId())
.name(userDO.getName())
.phone(userDO.getPhone())
.roleName(userId2RoleMap.get(userDO.getId()).getRoleName())
.build()).toList(), page, userTotal, pageSize);
}
@PostMapping("user/create")
public Response<Void> createUser(@RequestBody CreateUserReqVO createUserReqVO) {
String name = createUserReqVO.getName();
String phone = createUserReqVO.getPhone();
String password = createUserReqVO.getPassword();
userService.createUser(UserDO.builder()
.name(name)
.phone(phone)
.password(passwordEncoder.encode(password))
.build());
return Response.success();
}
@PostMapping("user/create/invitation/code")
@ApiOperationLog(description = "创建邀请码")
public Response<CreateInvitationCodeRspVO> createInvitationCode(@RequestBody CreateInvitationCodeReqVO createInvitationCodeReqVO) {
String code = String.valueOf(new Random().nextInt(1000000));
Integer limit = createInvitationCodeReqVO.getLimit();
Integer expire = createInvitationCodeReqVO.getExpire();
redisTemplate.opsForValue().set(UserRedisConstants.buildUserInvitationCode(code), limit);
redisTemplate.expire(UserRedisConstants.buildUserInvitationCode(code), expire, TimeUnit.DAYS);
return Response.success(CreateInvitationCodeRspVO.builder().invitationCode(code).build());
}
}

View File

@@ -42,25 +42,28 @@ public class ExamWordsController {
@PostMapping("generate") @PostMapping("generate")
public void generateFeltExamWords(@RequestBody GenerateExamWordsReqVO generateExamWordsReqVO, HttpServletResponse response) { public void generateFeltExamWords(@RequestBody GenerateExamWordsReqVO generateExamWordsReqVO, HttpServletResponse response) {
Integer gradeId = generateExamWordsReqVO.getGradeId(); Integer type = generateExamWordsReqVO.getType();
Integer level = generateExamWordsReqVO.getLevel(); Integer studentId = generateExamWordsReqVO.getStudentId();
List<Integer> studentIds = generateExamWordsReqVO.getStudentIds(); if (studentId == null) {
if (studentIds == null || studentIds.isEmpty() || gradeId == null || level == null) {
throw new RuntimeException("参数错误"); throw new RuntimeException("参数错误");
} }
try { try {
ExamWordsDO examWordsDO = examWordsService.generateExamWords(gradeId, level, studentIds); ExamWordsDO examWordsDO = examWordsService.generateExamWords(studentId, type);
if (examWordsDO == null || examWordsDO.getWordIds().isEmpty()) { if (examWordsDO == null || examWordsDO.getWordIds().isEmpty()) {
throw new RuntimeException("没有单词"); throw new RuntimeException("没有单词");
} }
List<VocabularyBankDO> vocabularyBankDOS = vocabularyService.findVocabularyBankDOListById(examWordsDO.getWordIds()); List<VocabularyBankDO> vocabularyBankDOS = vocabularyService.findVocabularyBankDOListById(examWordsDO.getWordIds());
List<Word> assessmentWords = vocabularyBankDOS.stream().map(vocabularyBankDO -> Word.builder() List<Word> assessmentWords = vocabularyBankDOS.stream().map(vocabularyBankDO -> Word.builder()
.id(vocabularyBankDO.getId()) .id(vocabularyBankDO.getId())
.title(vocabularyBankDO.getWord()) .title(vocabularyBankDO.getWord())
.definition(vocabularyBankDO.getDefinition()) .definition(vocabularyBankDO.getDefinition().length() > 6 ? vocabularyBankDO.getDefinition().substring(0, 6) : vocabularyBankDO.getDefinition())
.build()).toList(); .build()).toList();
// bug: 获取单词后单词的id会乱序、 需要重新更新考试记录中的 id
List<StudentDetail> studentDetailList = studentService.getStudentDetailList(studentIds); examWordsDO.setWordIds(assessmentWords.stream().map(Word::getId).toList());
examWordsService.updateExamWordsWordIdsOrder(examWordsDO);
log.info("生成试卷成功 {}", examWordsDO);
List<StudentDetail> studentDetailList = studentService.getStudentDetailList(Collections.singletonList(studentId));
List<Map<String, Object>> maps = studentDetailList.stream().map(studentDetail -> { List<Map<String, Object>> maps = studentDetailList.stream().map(studentDetail -> {
Map<String, Object> data = new HashMap<>(); Map<String, Object> data = new HashMap<>();
data.put("examId", examWordsDO.getId()); data.put("examId", examWordsDO.getId());
@@ -69,10 +72,16 @@ public class ExamWordsController {
data.put("examStr", examWordsDO.getTitle()); data.put("examStr", examWordsDO.getTitle());
data.put("words", assessmentWords); data.put("words", assessmentWords);
data.put("answer", assessmentWords); data.put("answer", assessmentWords);
List<Word> words1 = assessmentWords.subList(0, assessmentWords.size() / 2);
List<Word> words2 = assessmentWords.subList(assessmentWords.size() / 2, assessmentWords.size());
data.put("words1", words1);
data.put("words2", words2);
return data; return data;
}).toList(); }).toList();
WordExportUtil.generateExamWords(maps, response, templateWordPath); WordExportUtil.generateExamWords(maps, examWordsDO, response, templateWordPath);
} catch (Exception e) { } catch (Exception e) {
@@ -104,18 +113,23 @@ public class ExamWordsController {
PageResponse<ExamWordsResultRspVO> getExamWordsResult(@RequestBody ExamWordsResultReqVO examWordsResultReqVO) { PageResponse<ExamWordsResultRspVO> getExamWordsResult(@RequestBody ExamWordsResultReqVO examWordsResultReqVO) {
Integer page = examWordsResultReqVO.getPage(); Integer page = examWordsResultReqVO.getPage();
Integer size = examWordsResultReqVO.getSize(); Integer size = examWordsResultReqVO.getSize();
Integer classId = examWordsResultReqVO.getClassId();
Integer gradeId = examWordsResultReqVO.getGradeId();
String studentName = examWordsResultReqVO.getStudentName();
Integer total = examWordsJudgeService.getExamWordsJudgeResultCount(); Integer total = examWordsJudgeService.getExamWordsJudgeResultCount();
List<ExamWordsJudgeResultDO> examWordsJudgeResult = examWordsJudgeService.getExamWordsJudgeResult(page, size); List<ExamWordsJudgeResultDO> examWordsJudgeResult = examWordsJudgeService.getExamWordsJudgeResult(page, size, classId, gradeId, studentName);
List<ExamWordsResultRspVO> list = examWordsJudgeResult.stream().map(examWordsJudgeResultDO -> ExamWordsResultRspVO List<ExamWordsResultRspVO> list = examWordsJudgeResult.stream().map(examWordsJudgeResultDO -> ExamWordsResultRspVO
.builder() .builder()
.id(examWordsJudgeResultDO.getId()) .id(examWordsJudgeResultDO.getId())
.studentId(examWordsJudgeResultDO.getStudentId()) .studentId(examWordsJudgeResultDO.getStudentId())
.studentName(examWordsJudgeResultDO.getStudentId() != null ? studentService.getStudentById(examWordsJudgeResultDO.getStudentId()).getName() : "")
.examWordsTitle(examWordsJudgeResultDO.getExamWordsId() != null ? examWordsService.getExamWordsDOById(examWordsJudgeResultDO.getExamWordsId()).getTitle() : "")
.examWordsId(examWordsJudgeResultDO.getExamWordsId()) .examWordsId(examWordsJudgeResultDO.getExamWordsId())
.startDate(examWordsJudgeResultDO.getStartDate()) .startDate(examWordsJudgeResultDO.getStartDate())
.correctWordCount(examWordsJudgeResultDO.getCorrectWordCount()) .correctWordCount(examWordsJudgeResultDO.getCorrectWordCount())
.wrongWordCount(examWordsJudgeResultDO.getWrongWordCount()) .wrongWordCount(examWordsJudgeResultDO.getWrongWordCount())
.isFinished(examWordsJudgeResultDO.getIsFinished()) .isFinished(examWordsJudgeResultDO.getIsFinished())
.errorMsg(examWordsJudgeResultDO.getErrorMsg()) .msg(examWordsJudgeResultDO.getMsg())
.build() .build()
).toList(); ).toList();
return PageResponse.success(list, page, total, size); return PageResponse.success(list, page, total, size);
@@ -134,7 +148,7 @@ public class ExamWordsController {
.correctWordCount(examWordsJudgeResultDO.getCorrectWordCount()) .correctWordCount(examWordsJudgeResultDO.getCorrectWordCount())
.wrongWordCount(examWordsJudgeResultDO.getWrongWordCount()) .wrongWordCount(examWordsJudgeResultDO.getWrongWordCount())
.isFinished(examWordsJudgeResultDO.getIsFinished()) .isFinished(examWordsJudgeResultDO.getIsFinished())
.errorMsg(examWordsJudgeResultDO.getErrorMsg()) .errorMsg(examWordsJudgeResultDO.getMsg())
.correctWordIds(examWordsJudgeResultDO.getCorrectWordIds()) .correctWordIds(examWordsJudgeResultDO.getCorrectWordIds())
.wrongWordIds(examWordsJudgeResultDO.getWrongWordIds()) .wrongWordIds(examWordsJudgeResultDO.getWrongWordIds())
.build(); .build();

View File

@@ -1,22 +1,27 @@
package com.yinlihupo.enlish.service.controller; package com.yinlihupo.enlish.service.controller;
import com.yinlihupo.enlish.service.constant.LessonPlanConstant;
import com.yinlihupo.enlish.service.domain.dataobject.LessonPlansDO; import com.yinlihupo.enlish.service.domain.dataobject.LessonPlansDO;
import com.yinlihupo.enlish.service.model.vo.plan.AddLessonPlanReqVO; import com.yinlihupo.enlish.service.domain.dataobject.VocabularyBankDO;
import com.yinlihupo.enlish.service.model.vo.plan.DownLoadLessonPlanReqVO; import com.yinlihupo.enlish.service.model.vo.plan.*;
import com.yinlihupo.enlish.service.service.LessonPlansService; import com.yinlihupo.enlish.service.service.LessonPlansService;
import com.yinlihupo.enlish.service.utils.TTSUtil;
import com.yinlihupo.enlish.service.utils.WordExportUtil; import com.yinlihupo.enlish.service.utils.WordExportUtil;
import com.yinlihupo.framework.biz.operationlog.aspect.ApiOperationLog; import com.yinlihupo.framework.biz.operationlog.aspect.ApiOperationLog;
import com.yinlihupo.framework.common.response.PageResponse;
import com.yinlihupo.framework.common.response.Response; import com.yinlihupo.framework.common.response.Response;
import com.yinlihupo.framework.common.util.JsonUtils; import com.yinlihupo.framework.common.util.JsonUtils;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
@@ -28,8 +33,13 @@ public class LessonPlanController {
@Resource @Resource
private LessonPlansService lessonPlanService; private LessonPlansService lessonPlanService;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource(name = "taskExecutor") @Resource(name = "taskExecutor")
private Executor taskExecutor; private Executor taskExecutor;
@Resource
private TTSUtil ttsUtil;
@Value("${templates.plan.weekday}") @Value("${templates.plan.weekday}")
private String planWeekday; private String planWeekday;
@@ -41,8 +51,12 @@ public class LessonPlanController {
public Response<String> generateLessonPlan(@RequestBody AddLessonPlanReqVO addLessonPlanReqVO) { public Response<String> generateLessonPlan(@RequestBody AddLessonPlanReqVO addLessonPlanReqVO) {
Integer studentId = addLessonPlanReqVO.getStudentId(); Integer studentId = addLessonPlanReqVO.getStudentId();
Integer unitId = addLessonPlanReqVO.getUnitId(); Integer unitId = addLessonPlanReqVO.getUnitId();
Integer wordSize = addLessonPlanReqVO.getWordSize();
try { try {
taskExecutor.execute(() -> lessonPlanService.generateLessonPlans(studentId, unitId)); if (redisTemplate.opsForValue().get(LessonPlanConstant.buildGeneratePlanContent(studentId)) != null) {
throw new RuntimeException("学案正在生成,请耐心等待");
}
taskExecutor.execute(() -> lessonPlanService.generateLessonPlans(studentId, unitId, wordSize));
return Response.success("生成学案成功,请等待 10 分钟"); return Response.success("生成学案成功,请等待 10 分钟");
} catch (Exception e) { } catch (Exception e) {
log.error(e.getMessage()); log.error(e.getMessage());
@@ -59,13 +73,63 @@ public class LessonPlanController {
try { try {
Map<String, Object> map = JsonUtils.parseMap(lessonPlanById.getContentDetails(), String.class, Object.class); Map<String, Object> map = JsonUtils.parseMap(lessonPlanById.getContentDetails(), String.class, Object.class);
if (!lessonPlanById.getTitle().contains("复习")) { if (!lessonPlanById.getTitle().contains("复习")) {
WordExportUtil.generateLessonPlanDocx(map, lessonPlanById.getTitle(), response, planWeekday, true); WordExportUtil.generateLessonPlanDocx(map, lessonPlanById, response, planWeekday, true);
} else { } else {
WordExportUtil.generateLessonPlanDocx(map, lessonPlanById.getTitle(), response, planWeekend, false); WordExportUtil.generateLessonPlanDocx(map, lessonPlanById, response, planWeekend, false);
} }
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
@PostMapping("word/voice")
@ApiOperationLog(description = "获取单词")
public Response<FindWordVoiceRspVO> findPlanWordVoice(@RequestBody FindWordVoiceReqVO findWordVoiceReqVO) {
Integer id = findWordVoiceReqVO.getPlanId();
LessonPlansDO lessonPlanById = lessonPlanService.findLessonPlanById(id);
try {
Map<String, Object> map = JsonUtils.parseMap(lessonPlanById.getContentDetails(), String.class, Object.class);
Object syncVocabList = map.get("syncVocabList");
List<VocabularyBankDO> list = JsonUtils.parseList(JsonUtils.toJsonString(syncVocabList), VocabularyBankDO.class);
List<String> words = list.stream().map(VocabularyBankDO::getWord).toList();
return Response.success(FindWordVoiceRspVO.builder().words(words).build());
} catch (Exception e) {
log.error(e.getMessage());
return Response.fail("获取单词失败");
}
}
@PostMapping("word/voice/tts")
public void findPlanWordVoiceTTS(@RequestBody FindWordTTSVoiceReqVO findWordVoiceReqVO, HttpServletResponse response) {
ttsUtil.generateWordVoice(findWordVoiceReqVO.getText(), response);
}
@PostMapping("check")
@ApiOperationLog(description = "检测学案是否在生成")
public Response<String> checkLessonPlan(@RequestBody FindIsGeneratePlanReqVO findIsGeneratePlanReqVO) {
Integer studentId = findIsGeneratePlanReqVO.getStudentId();
String key = LessonPlanConstant.buildGeneratePlanContent(studentId);
if (redisTemplate.opsForValue().get(key) != null) {
return Response.fail();
}
return Response.success("学案生成完成");
}
@PostMapping("student/list")
@ApiOperationLog(description = "查询学生学案")
public Response<FindPlanStudentListRspVO> findStudentPlans(@RequestBody FindPlanStudentReqVO findPlanStudentReqVO) {
List<LessonPlansDO> lessonPlansDOS = lessonPlanService.findLessonPlansByStudentId(findPlanStudentReqVO.getStudentId());
List<LessonPlanItem> list = lessonPlansDOS.stream().map(lessonPlansDO -> LessonPlanItem
.builder()
.id(lessonPlansDO.getId())
.isFinished(0)
.title(lessonPlansDO.getTitle())
.build())
.toList();
return Response.success(FindPlanStudentListRspVO.builder().lessonPlanItems(list).build());
}
} }

View File

@@ -0,0 +1,60 @@
package com.yinlihupo.enlish.service.controller;
import cn.dev33.satoken.stp.StpUtil;
import com.yinlihupo.enlish.service.model.vo.login.LoginReqVO;
import com.yinlihupo.enlish.service.model.vo.login.VerificationCodeReqVO;
import com.yinlihupo.enlish.service.service.LoginService;
import com.yinlihupo.framework.biz.operationlog.aspect.ApiOperationLog;
import com.yinlihupo.framework.common.response.Response;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/login/")
@RestController
@Slf4j
public class LoginController {
@Resource
private LoginService loginService;
@PostMapping("login")
@ApiOperationLog(description = "登录")
public Response<String> login(@RequestBody LoginReqVO loginReqVO) {
try {
loginService.login(loginReqVO.getPhone(), loginReqVO.getName(), loginReqVO.getPassword(), loginReqVO.getCode(), loginReqVO.getInvitationCode());
return Response.success(StpUtil.getTokenInfo().getTokenValue());
} catch (Exception e) {
log.error("注册或登录失败 {}", e.getMessage());
return Response.fail("注册或登录失败 " + e.getMessage());
}
}
@PostMapping("logout")
@ApiOperationLog(description = "登出")
public Response<Void> logout() {
try {
StpUtil.logout();
return Response.success();
} catch (Exception e) {
log.error("登出失败 {}", e.getMessage());
return Response.fail("登出失败 " + e.getMessage());
}
}
@PostMapping("sendVerificationCode")
@ApiOperationLog(description = "发送验证码")
public Response<Void> sendVerificationCode(@RequestBody VerificationCodeReqVO verificationCodeReqVO) {
try {
loginService.sendVerificationCode(verificationCodeReqVO.getPhone());
return Response.success();
} catch (Exception e) {
log.error("发送验证码失败 {}", e.getMessage());
return Response.fail("发送验证码失败 " + e.getMessage());
}
}
}

View File

@@ -4,6 +4,8 @@ package com.yinlihupo.enlish.service.controller;
import com.yinlihupo.enlish.service.domain.dataobject.ClassDO; import com.yinlihupo.enlish.service.domain.dataobject.ClassDO;
import com.yinlihupo.enlish.service.domain.dataobject.GradeDO; import com.yinlihupo.enlish.service.domain.dataobject.GradeDO;
import com.yinlihupo.enlish.service.domain.dataobject.StudentDO; import com.yinlihupo.enlish.service.domain.dataobject.StudentDO;
import com.yinlihupo.enlish.service.model.bo.StudentDetail;
import com.yinlihupo.enlish.service.model.bo.exam.WordMasteryDetail;
import com.yinlihupo.enlish.service.model.vo.student.*; import com.yinlihupo.enlish.service.model.vo.student.*;
import com.yinlihupo.enlish.service.service.ClassService; import com.yinlihupo.enlish.service.service.ClassService;
import com.yinlihupo.enlish.service.service.GradeService; import com.yinlihupo.enlish.service.service.GradeService;
@@ -46,6 +48,8 @@ public class StudentController {
.id(studentDO.getId()) .id(studentDO.getId())
.name(studentDO.getName()) .name(studentDO.getName())
.classId(studentDO.getClassId()) .classId(studentDO.getClassId())
.className(classService.findClassById(studentDO.getClassId()).getTitle())
.gradeName(gradeService.findByClassId(studentDO.getGradeId()).getTitle())
.gradeId(studentDO.getGradeId()) .gradeId(studentDO.getGradeId())
.build()).toList(); .build()).toList();
@@ -60,12 +64,13 @@ public class StudentController {
StudentDO studentById = studentService.getStudentById(studentId); StudentDO studentById = studentService.getStudentById(studentId);
ClassDO classById = classService.findClassById(studentById.getClassId()); ClassDO classById = classService.findClassById(studentById.getClassId());
GradeDO byClassId = gradeService.findByClassId(studentById.getGradeId()); GradeDO byClassId = gradeService.findByClassId(studentById.getGradeId());
GradeDO actualGradeById = gradeService.findByClassId(studentById.getActualGradeId());
FindStudentDetailRspVO findStudentDetailRspVO = FindStudentDetailRspVO.builder() FindStudentDetailRspVO findStudentDetailRspVO = FindStudentDetailRspVO.builder()
.id(studentById.getId()) .id(studentById.getId())
.name(studentById.getName()) .name(studentById.getName())
.className(classById.getTitle()) .className(classById.getTitle())
.gradeName(byClassId.getTitle()) .gradeName(byClassId.getTitle())
.actualGrade(actualGradeById != null ? actualGradeById.getTitle() : "")
.build(); .build();
return Response.success(findStudentDetailRspVO); return Response.success(findStudentDetailRspVO);
@@ -84,4 +89,28 @@ public class StudentController {
studentService.deleteStudent(deleteStudentReqVO.getStudentId()); studentService.deleteStudent(deleteStudentReqVO.getStudentId());
return Response.success(); return Response.success();
} }
@PostMapping("analyze")
@ApiOperationLog(description = "学生学习分析")
public Response<String> analyzeStudentStudy(@RequestBody AnalyzeStudentStudyReqVO analyzeStudentStudyReqVO) {
String analyzeStudentStudy = studentService.analyzeStudentStudy(analyzeStudentStudyReqVO.getStudentId());
return Response.success(analyzeStudentStudy);
}
@PostMapping("mastery/detail")
@ApiOperationLog(description = "查询学生单词掌握详情")
public Response<List<FindStudentMasteryDetailRspVO>> findStudentMasteryDetail(@RequestBody FindStudentMasteryDetailReqVO findStudentMasteryDetailReqVO) {
Integer studentId = findStudentMasteryDetailReqVO.getStudentId();
List<WordMasteryDetail> studentWordMasteryDetail = studentService.findStudentWordMasteryDetail(studentId);
List<FindStudentMasteryDetailRspVO> list = studentWordMasteryDetail.stream().map(wordMasteryDetail -> FindStudentMasteryDetailRspVO.builder()
.word(wordMasteryDetail.getWord())
.reviewCount(wordMasteryDetail.getReviewCount())
.memoryStrength(wordMasteryDetail.getMemoryStrength())
.updateTime(wordMasteryDetail.getUpdate_time())
.build()
).toList();
return Response.success(list);
}
} }

View File

@@ -0,0 +1,55 @@
package com.yinlihupo.enlish.service.controller;
import cn.dev33.satoken.stp.StpUtil;
import com.yinlihupo.enlish.service.domain.dataobject.UserDO;
import com.yinlihupo.enlish.service.model.vo.user.FindUserInfoRspVO;
import com.yinlihupo.enlish.service.model.vo.user.UpdateUserInfoReqVO;
import com.yinlihupo.enlish.service.service.UserService;
import com.yinlihupo.framework.biz.operationlog.aspect.ApiOperationLog;
import com.yinlihupo.framework.common.response.Response;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user/")
@Slf4j
public class UserController {
@Resource
private UserService userService;
@PostMapping("info")
public Response<FindUserInfoRspVO> info() {
UserDO user = userService.findUser();
FindUserInfoRspVO findUserInfoRspVO = FindUserInfoRspVO.builder()
.id(user.getId())
.name(user.getName())
.build();
return Response.success(findUserInfoRspVO);
}
@PostMapping("update-user-info")
@ApiOperationLog(description = "修改用户信息")
public Response<String> updatePassword(@RequestBody UpdateUserInfoReqVO updateUserInfoReqVO) {
try {
String code = updateUserInfoReqVO.getCode();
String newPassword = updateUserInfoReqVO.getNewPassword();
String phone = updateUserInfoReqVO.getPhone();
String name = updateUserInfoReqVO.getName();
userService.updateUserInfo(newPassword, code, phone, name);
StpUtil.logout();
return Response.success();
} catch (Exception e) {
log.error("修改密码失败 {}", e.getMessage());
return Response.fail(e.getMessage());
}
}
}

View File

@@ -1,6 +1,8 @@
package com.yinlihupo.enlish.service.controller; package com.yinlihupo.enlish.service.controller;
import com.yinlihupo.enlish.service.domain.dataobject.VocabularyBankDO; import com.yinlihupo.enlish.service.domain.dataobject.VocabularyBankDO;
import com.yinlihupo.enlish.service.model.vo.vocabulary.FindStudentWordDetailReqVO;
import com.yinlihupo.enlish.service.model.vo.vocabulary.FindStudentWordDetailRspVO;
import com.yinlihupo.enlish.service.model.vo.vocabulary.FindWordTitleReqVO; import com.yinlihupo.enlish.service.model.vo.vocabulary.FindWordTitleReqVO;
import com.yinlihupo.enlish.service.model.vo.vocabulary.FindWordTitleRspVO; import com.yinlihupo.enlish.service.model.vo.vocabulary.FindWordTitleRspVO;
import com.yinlihupo.enlish.service.service.VocabularyService; import com.yinlihupo.enlish.service.service.VocabularyService;
@@ -30,4 +32,11 @@ public class VocabularyController {
.build(); .build();
return Response.success(findWordTitleRspVO); return Response.success(findWordTitleRspVO);
} }
@PostMapping("student/detail")
@ApiOperationLog(description = "查询学生单词详情")
public Response<FindStudentWordDetailRspVO> findStudentWordDetail(@RequestBody FindStudentWordDetailReqVO vo) {
return Response.success(vocabularyService.findStudentWordDetail(vo.getId()));
}
} }

View File

@@ -19,6 +19,8 @@ public class ExamWordsDO {
private Integer level; private Integer level;
private Integer type;
private String title; private String title;
private LocalDateTime createdAt; private LocalDateTime createdAt;

View File

@@ -13,6 +13,7 @@ import java.util.List;
@Data @Data
@Builder @Builder
public class ExamWordsJudgeResultDO { public class ExamWordsJudgeResultDO {
private Integer id; private Integer id;
private String ansSheetPath; private String ansSheetPath;
@@ -33,6 +34,6 @@ public class ExamWordsJudgeResultDO {
private List<Integer> wrongWordIds; private List<Integer> wrongWordIds;
private String errorMsg; private String msg;
} }

View File

@@ -0,0 +1,21 @@
package com.yinlihupo.enlish.service.domain.dataobject;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class PlanExamDO {
private Integer id;
private Integer planId;
private Integer examId;
}

View File

@@ -0,0 +1,28 @@
package com.yinlihupo.enlish.service.domain.dataobject;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class RoleDO {
private Long id;
private String roleName;
private String roleKey;
private Integer status;
private Date createTime;
private Integer isDeleted;
}

View File

@@ -21,6 +21,8 @@ public class StudentDO {
private Integer gradeId; private Integer gradeId;
private Integer actualGradeId;
private Integer isDeleted; private Integer isDeleted;
private LocalDateTime startTime; private LocalDateTime startTime;

View File

@@ -0,0 +1,23 @@
package com.yinlihupo.enlish.service.domain.dataobject;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class StudentStageLearningRemarkDO {
private Integer id;
private Integer studentId;
private LocalDateTime createTime;
private String commentContent;
}

View File

@@ -0,0 +1,25 @@
package com.yinlihupo.enlish.service.domain.dataobject;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class UserDO {
private Long id;
private String password;
private String name;
private String openid;
private String phone;
private String email;
}

View File

@@ -0,0 +1,23 @@
package com.yinlihupo.enlish.service.domain.dataobject;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class UserRoleRelDO {
private Long id;
private Long userId;
private Long roleId;
private Date createTime;
}

View File

@@ -7,4 +7,6 @@ public interface ExamWordsDOMapper {
int insert(ExamWordsDO record); int insert(ExamWordsDO record);
ExamWordsDO selectById(Integer id); ExamWordsDO selectById(Integer id);
void updateWordIdsOrder(ExamWordsDO examWordsDO);
} }

View File

@@ -11,7 +11,7 @@ public interface ExamWordsJudgeResultDOMapper {
List<ExamWordsJudgeResultDO> selectUnfinishedExamWordsJudgeResultDOList(int count); List<ExamWordsJudgeResultDO> selectUnfinishedExamWordsJudgeResultDOList(int count);
int updateErrorMsg(@Param("id") Integer id, @Param("errorMsg") String errorMsg); int updateMsg(@Param("id") Integer id, @Param("msg") String msg);
int updateExamWordsJudgeResultDO(@Param("examWordsJudgeResultDO") ExamWordsJudgeResultDO examWordsJudgeResultDO); int updateExamWordsJudgeResultDO(@Param("examWordsJudgeResultDO") ExamWordsJudgeResultDO examWordsJudgeResultDO);
@@ -19,7 +19,13 @@ public interface ExamWordsJudgeResultDOMapper {
Integer selectCount(); Integer selectCount();
Integer selectUnfinishedCount();
ExamWordsJudgeResultDO selectDetailById(@Param("id") Integer id); ExamWordsJudgeResultDO selectDetailById(@Param("id") Integer id);
List<ExamWordsJudgeResultDO> selectByStudentId(@Param("studentId") Integer studentId); List<ExamWordsJudgeResultDO> selectByStudentId(@Param("studentId") Integer studentId);
List<ExamWordsJudgeResultDO> selectByStudentIdAndLimitTime(@Param("studentId") Integer studentId);
List<ExamWordsJudgeResultDO> selectByPageAndStudentIds(@Param("startIndex") Integer startIndex, @Param("pageSize") Integer pageSize, @Param("studentIds") List<Integer> studentIds);
} }

View File

@@ -11,6 +11,8 @@ public interface GradeUnitDOMapper {
GradeUnitDO selectByUnitId(@Param("unitId") Integer unitId); GradeUnitDO selectByUnitId(@Param("unitId") Integer unitId);
List<GradeUnitDO> selectByUnitIds(@Param("unitIds") List<Integer> unitIds);
int insert(GradeUnitDO record); int insert(GradeUnitDO record);
int deleteByUnitId(@Param("unitId") Integer unitId); int deleteByUnitId(@Param("unitId") Integer unitId);

View File

@@ -14,4 +14,6 @@ public interface LessonPlansDOMapper {
List<LessonPlansDO> findLessonPlansByStudentId(@Param("ids") List<Integer> ids); List<LessonPlansDO> findLessonPlansByStudentId(@Param("ids") List<Integer> ids);
LessonPlansDO selectByLessonId(@Param("lessonId") Integer lessonId); LessonPlansDO selectByLessonId(@Param("lessonId") Integer lessonId);
List<LessonPlansDO> selectByStudentId(@Param("studentId") Integer studentId);
} }

View File

@@ -0,0 +1,9 @@
package com.yinlihupo.enlish.service.domain.mapper;
import com.yinlihupo.enlish.service.domain.dataobject.PlanExamDO;
public interface PlanExamDOMapper {
void insert(PlanExamDO record);
PlanExamDO selectByExamId(Integer examId);
}

View File

@@ -0,0 +1,13 @@
package com.yinlihupo.enlish.service.domain.mapper;
import com.yinlihupo.enlish.service.domain.dataobject.RoleDO;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface RoleDOMapper {
List<RoleDO> selectAll();
List<RoleDO> selectByIds(@Param("ids") List<Long> ids);
}

View File

@@ -21,4 +21,12 @@ public interface StudentDOMapper {
void deleteById(Integer id); void deleteById(Integer id);
int selectStudentCountByClassId(@Param("classId") Integer classId); int selectStudentCountByClassId(@Param("classId") Integer classId);
int updateStudentActualGradeId(@Param("studentId") Integer studentId, @Param("gradeId") Integer gradeId);
List<StudentDO> selectStudentDOListByClassId(@Param("classId") Integer classId);
List<StudentDO> selectStudentDOListByGradeId(@Param("gradeId") Integer gradeId);
List<StudentDO> selectStudentDOListByName(@Param("name") String name);
} }

View File

@@ -8,7 +8,7 @@ import java.util.List;
public interface StudentExamWordsDOMapper { public interface StudentExamWordsDOMapper {
int insertStudentsExam(@Param("studentIds") List<Integer> studentIds, @Param("examWordsId") Integer examWordsId); int insertStudentsExam(@Param("studentId") Integer studentId, @Param("examWordsId") Integer examWordsId);
StudentExamWordsDO selectByStudentIdAndExamWordsId(@Param("studentId") Integer studentId, @Param("examWordsId") Integer examWordsId); StudentExamWordsDO selectByStudentIdAndExamWordsId(@Param("studentId") Integer studentId, @Param("examWordsId") Integer examWordsId);

View File

@@ -0,0 +1,8 @@
package com.yinlihupo.enlish.service.domain.mapper;
import com.yinlihupo.enlish.service.domain.dataobject.StudentStageLearningRemarkDO;
public interface StudentStageLearningRemarkDOMapper {
void insert(StudentStageLearningRemarkDO studentStageLearningRemarkDO);
}

View File

@@ -25,4 +25,6 @@ public interface UnitDOMapper {
List<UnitDO> selectUnitDOList(@Param("startIndex") Integer startIndex, @Param("pageSize") Integer pageSize); List<UnitDO> selectUnitDOList(@Param("startIndex") Integer startIndex, @Param("pageSize") Integer pageSize);
Integer selectUnitDOListCount(); Integer selectUnitDOListCount();
List<UnitDO> selectByUnitName(@Param("unitName") String unitName);
} }

View File

@@ -0,0 +1,25 @@
package com.yinlihupo.enlish.service.domain.mapper;
import com.yinlihupo.enlish.service.domain.dataobject.UserDO;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface UserDOMapper {
UserDO selectByPhone(String phone);
void insert(UserDO userDO);
void updatePassword(@Param("id") Long id, @Param("password") String password);
void updateUserInfo(@Param("id") Long id, @Param("name") String name, @Param("password") String password, @Param("phone") String phone);
UserDO selectById(Long id);
List<UserDO> selectUserDOList(@Param("name") String name, @Param("startIndex") Integer startIndex, @Param("pageSize") Integer pageSize);
int selectUserTotal();
void createUser(UserDO userDO);
}

View File

@@ -0,0 +1,13 @@
package com.yinlihupo.enlish.service.domain.mapper;
import com.yinlihupo.enlish.service.domain.dataobject.UserRoleRelDO;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface UserRoleRelDOMapper {
List<UserRoleRelDO> selectAll();
List<UserRoleRelDO> selectByUserIds(@Param("userIds") List<Long> userIds);
}

View File

@@ -21,5 +21,11 @@ public interface VocabularyBankDOMapper {
List<VocabularyBankDO> selectVocabularyBankListSelfCheck(@Param("gradeId") Integer gradeId, @Param("studentId") Integer studentId, @Param("ids") List<Integer> ids, @Param("wordCount") Integer wordCount); List<VocabularyBankDO> selectVocabularyBankListSelfCheck(@Param("gradeId") Integer gradeId, @Param("studentId") Integer studentId, @Param("ids") List<Integer> ids, @Param("wordCount") Integer wordCount);
List<VocabularyBankDO> selectVocabularyBankListByGradeIdRandom(@Param("gradeId") Integer gradeId, @Param("wordCount") Integer wordCount);
Integer selectWordTotal(); Integer selectWordTotal();
List<VocabularyBankDO> selectByUnitIds(@Param("unitIds") List<Integer> unitIds);
List<VocabularyBankDO> selectByGradeIdAndNotMatchIds(@Param("gradeId") Integer gradeId, @Param("ids") List<Integer> ids, @Param("wordCount") Integer wordCount);
} }

View File

@@ -16,4 +16,12 @@ public interface WordMasteryLogDOMapper {
int batchUpdateStudentMastery(@Param("wordMasteryLogDOs") List<WordMasteryLogDO> wordMasteryLogDOs); int batchUpdateStudentMastery(@Param("wordMasteryLogDOs") List<WordMasteryLogDO> wordMasteryLogDOs);
int selectStudentStrengthCount(@Param("studentId") Integer studentId); int selectStudentStrengthCount(@Param("studentId") Integer studentId);
List<WordMasteryLogDO> selectByStudentIdAndLimitTime(@Param("studentId") Integer studentId);
List<WordMasteryLogDO> selectAllByStudentId(@Param("studentId") Integer studentId);
Integer selectMasteryCount(@Param("studentId") Integer studentId);
Integer selectNotMasteryCount(@Param("studentId") Integer studentId);
} }

View File

@@ -13,7 +13,7 @@ public enum ResponseCodeEnum implements BaseExceptionInterface {
// ----------- 通用异常状态码 ----------- // ----------- 通用异常状态码 -----------
SYSTEM_ERROR("AUTH-10000", "出错啦,后台小哥正在努力修复中..."), SYSTEM_ERROR("AUTH-10000", "出错啦,后台小哥正在努力修复中..."),
PARAM_NOT_VALID("AUTH-10001", "参数错误"), PARAM_NOT_VALID("AUTH-10001", "参数错误"),
NOT_LOGIN("AUTH-10002", "请先登录")
// ----------- 业务异常状态码 ----------- // ----------- 业务异常状态码 -----------
; ;

View File

@@ -1,6 +1,7 @@
package com.yinlihupo.enlish.service.exception; package com.yinlihupo.enlish.service.exception;
import cn.dev33.satoken.exception.NotLoginException;
import com.yinlihupo.enlish.service.enums.ResponseCodeEnum; import com.yinlihupo.enlish.service.enums.ResponseCodeEnum;
import com.yinlihupo.framework.common.exception.BizException; import com.yinlihupo.framework.common.exception.BizException;
import com.yinlihupo.framework.common.response.Response; import com.yinlihupo.framework.common.response.Response;
@@ -96,4 +97,11 @@ public class GlobalExceptionHandler {
log.error("{} request error, ", request.getRequestURI(), e); log.error("{} request error, ", request.getRequestURI(), e);
return Response.fail(ResponseCodeEnum.SYSTEM_ERROR); return Response.fail(ResponseCodeEnum.SYSTEM_ERROR);
} }
@ExceptionHandler({ NotLoginException.class })
@ResponseBody
public Response<Object> handleNotLoginException(HttpServletRequest request, NotLoginException e) {
log.warn("{} request error, ", request.getRequestURI(), e);
return Response.fail(ResponseCodeEnum.NOT_LOGIN);
}
} }

View File

@@ -0,0 +1,27 @@
package com.yinlihupo.enlish.service.model.bo;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class Sentence {
@JsonProperty("target_word")
private String targetWord;
@JsonProperty("grade_level")
private String gradeLevel;
@JsonProperty("question")
private String question;
@JsonProperty("chinese_clue")
private String chineseClue;
@JsonProperty("correct_answer")
private String correctAnswer;
@JsonProperty("grammar_point")
private String grammarPoint;
}

View File

@@ -0,0 +1,9 @@
package com.yinlihupo.enlish.service.model.bo.exam;
public enum ActionType {
PASS, // 保持当前进度
UPGRADE, // 升级
DOWNGRADE, // 降级回填
STAY_AND_REVIEW, // 保持年级但进入复习模式
TRIGGER_RETEST // 触发熔断二测
}

View File

@@ -0,0 +1,17 @@
package com.yinlihupo.enlish.service.model.bo.exam;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class DiagnosisResult {
int determinedLevel; // 系统判定的真实等级
ActionType actionType; // 建议的系统动作
String message; // 展示给用户的文案
}

View File

@@ -0,0 +1,28 @@
package com.yinlihupo.enlish.service.model.bo.exam;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.List;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class ExamWordsJudgeResultDetail {
private Integer correctWordCount;
private Integer wrongWordCount;
private LocalDateTime startDate;
private List<String> correctWords;
private List<String> wrongWords;
private String msg;
}

View File

@@ -0,0 +1,23 @@
package com.yinlihupo.enlish.service.model.bo.exam;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class WordMasteryDetail {
private String word;
private Integer reviewCount;
private Double memoryStrength;
private LocalDateTime update_time;
}

View File

@@ -0,0 +1,19 @@
package com.yinlihupo.enlish.service.model.bo.exam;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class ZoneStats {
private int gradeId;
private int totalCount;
private int correctCount;
private double accuracy;
}

View File

@@ -0,0 +1,18 @@
package com.yinlihupo.enlish.service.model.vo.admin;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class CreateInvitationCodeReqVO {
// 限制人数
private Integer limit;
// 有效期
private Integer expire;
}

View File

@@ -0,0 +1,16 @@
package com.yinlihupo.enlish.service.model.vo.admin;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class CreateInvitationCodeRspVO {
private String invitationCode;
}

View File

@@ -13,4 +13,8 @@ public class ExamWordsResultReqVO {
private Integer page; private Integer page;
private Integer size; private Integer size;
private Integer classId;
private Integer gradeId;
private String studentName;
} }

View File

@@ -12,6 +12,7 @@ import java.time.LocalDateTime;
@Data @Data
@Builder @Builder
public class ExamWordsResultRspVO { public class ExamWordsResultRspVO {
private Integer id; private Integer id;
private String ansSheetPath; private String ansSheetPath;
@@ -20,6 +21,10 @@ public class ExamWordsResultRspVO {
private Integer examWordsId; private Integer examWordsId;
private String studentName;
private String examWordsTitle;
private Integer correctWordCount; private Integer correctWordCount;
private Integer wrongWordCount; private Integer wrongWordCount;
@@ -28,5 +33,5 @@ public class ExamWordsResultRspVO {
private LocalDateTime startDate; private LocalDateTime startDate;
private String errorMsg; private String msg;
} }

View File

@@ -14,7 +14,6 @@ import java.util.List;
@Builder @Builder
public class GenerateExamWordsReqVO { public class GenerateExamWordsReqVO {
private Integer gradeId; private Integer type;
private Integer level; private Integer studentId;
private List<Integer> studentIds;
} }

View File

@@ -0,0 +1,19 @@
package com.yinlihupo.enlish.service.model.vo.login;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class LoginReqVO {
private String phone;
private String name;
private String password;
private String code;
private String invitationCode;
}

View File

@@ -0,0 +1,15 @@
package com.yinlihupo.enlish.service.model.vo.login;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class VerificationCodeReqVO {
private String phone;
}

View File

@@ -13,4 +13,5 @@ public class AddLessonPlanReqVO {
private Integer studentId; private Integer studentId;
private Integer unitId; private Integer unitId;
private Integer wordSize;
} }

View File

@@ -0,0 +1,15 @@
package com.yinlihupo.enlish.service.model.vo.plan;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class FindIsGeneratePlanReqVO {
private Integer studentId;
}

View File

@@ -0,0 +1,17 @@
package com.yinlihupo.enlish.service.model.vo.plan;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class FindPlanStudentListRspVO {
List<LessonPlanItem> lessonPlanItems;
}

View File

@@ -0,0 +1,14 @@
package com.yinlihupo.enlish.service.model.vo.plan;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class FindPlanStudentReqVO {
private Integer studentId;
}

View File

@@ -0,0 +1,17 @@
package com.yinlihupo.enlish.service.model.vo.plan;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class FindWordTTSVoiceReqVO {
String text;
String voice;
String format;
}

View File

@@ -0,0 +1,15 @@
package com.yinlihupo.enlish.service.model.vo.plan;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class FindWordVoiceReqVO {
private Integer planId;
}

View File

@@ -0,0 +1,17 @@
package com.yinlihupo.enlish.service.model.vo.plan;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class FindWordVoiceRspVO {
private List<String> words;
}

View File

@@ -1,5 +1,6 @@
package com.yinlihupo.enlish.service.model.vo.student; package com.yinlihupo.enlish.service.model.vo.student;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
@@ -16,5 +17,6 @@ public class AddStudentReqVO {
private String name; private String name;
private Integer classId; private Integer classId;
private Integer gradeId; private Integer gradeId;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime createTime; private LocalDateTime createTime;
} }

View File

@@ -0,0 +1,15 @@
package com.yinlihupo.enlish.service.model.vo.student;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class AnalyzeStudentStudyReqVO {
private Integer studentId;
}

View File

@@ -15,4 +15,5 @@ public class FindStudentDetailRspVO {
private String name; private String name;
private String className; private String className;
private String gradeName; private String gradeName;
private String actualGrade;
} }

View File

@@ -0,0 +1,15 @@
package com.yinlihupo.enlish.service.model.vo.student;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class FindStudentMasteryDetailReqVO {
private Integer studentId;
}

View File

@@ -0,0 +1,22 @@
package com.yinlihupo.enlish.service.model.vo.student;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class FindStudentMasteryDetailRspVO {
private String word;
private Integer reviewCount;
private Double memoryStrength;
private LocalDateTime updateTime;
}

View File

@@ -15,4 +15,6 @@ public class StudentItemRspVO {
private String name; private String name;
private Integer classId; private Integer classId;
private Integer gradeId; private Integer gradeId;
private String className;
private String gradeName;
} }

View File

@@ -0,0 +1,17 @@
package com.yinlihupo.enlish.service.model.vo.user;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class CreateUserReqVO {
private String name;
private String phone;
private String password;
}

View File

@@ -0,0 +1,16 @@
package com.yinlihupo.enlish.service.model.vo.user;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class FindUserInfoRspVO {
private Long id;
private String name;
}

View File

@@ -0,0 +1,17 @@
package com.yinlihupo.enlish.service.model.vo.user;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class FindUserListRepVO {
private Integer page;
private Integer pageSize;
private String name;
}

View File

@@ -0,0 +1,17 @@
package com.yinlihupo.enlish.service.model.vo.user;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class FindUserListRspVO {
private Long id;
private String name;
private String phone;
private String roleName;
}

View File

@@ -0,0 +1,18 @@
package com.yinlihupo.enlish.service.model.vo.user;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Data
public class UpdateUserInfoReqVO {
private String newPassword;
private String name;
private String phone;
private String code;
}

View File

@@ -0,0 +1,15 @@
package com.yinlihupo.enlish.service.model.vo.vocabulary;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class FindStudentWordDetailReqVO {
private Integer id;
}

View File

@@ -0,0 +1,27 @@
package com.yinlihupo.enlish.service.model.vo.vocabulary;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Data
public class FindStudentWordDetailRspVO {
/**
* 已掌握单词数
*/
private Integer masteredWordCount;
/**
* 未掌握单词数
*/
private Integer unmasteredWordCount;
/**
* 待审查单词数(推荐使用,简洁通用)
*/
private Integer pendingReviewWordCount;
}

View File

@@ -9,10 +9,12 @@ public interface ExamWordsJudgeService {
void judgeExamWords(int count); void judgeExamWords(int count);
List<ExamWordsJudgeResultDO> getExamWordsJudgeResult(Integer page, Integer pageSize); List<ExamWordsJudgeResultDO> getExamWordsJudgeResult(Integer page, Integer pageSize, Integer classId, Integer gradeId, String studentName);
Integer getExamWordsJudgeResultCount(); Integer getExamWordsJudgeResultCount();
Integer getExamUnfinishedCount();
ExamWordsJudgeResultDO getExamWordsJudgeResultDOById(Integer id); ExamWordsJudgeResultDO getExamWordsJudgeResultDOById(Integer id);
List<ExamWordsJudgeResultDO> getStudentExamWordsResultList(Integer studentId); List<ExamWordsJudgeResultDO> getStudentExamWordsResultList(Integer studentId);

View File

@@ -8,7 +8,11 @@ import java.util.List;
public interface ExamWordsService { public interface ExamWordsService {
ExamWordsDO generateExamWords(Integer gradeId, Integer level, List<Integer> studentIds); ExamWordsDO generateExamWords(Integer studentId, Integer type);
int saveExamWordsPngToDbAndLocal(MultipartFile file); int saveExamWordsPngToDbAndLocal(MultipartFile file);
void updateExamWordsWordIdsOrder(ExamWordsDO examWordsDO);
ExamWordsDO getExamWordsDOById(Integer id);
} }

View File

@@ -5,9 +5,11 @@ import com.yinlihupo.enlish.service.domain.dataobject.LessonPlansDO;
import java.util.List; import java.util.List;
public interface LessonPlansService { public interface LessonPlansService {
void generateLessonPlans(Integer studentId, Integer unitId); void generateLessonPlans(Integer studentId, Integer unitId, Integer wordSize);
List<LessonPlansDO> findLessonPlans(List<Integer> ids); List<LessonPlansDO> findLessonPlans(List<Integer> ids);
LessonPlansDO findLessonPlanById(Integer id); LessonPlansDO findLessonPlanById(Integer id);
List<LessonPlansDO> findLessonPlansByStudentId(Integer studentId);
} }

View File

@@ -0,0 +1,8 @@
package com.yinlihupo.enlish.service.service;
public interface LoginService {
void login(String phone, String name, String reqPassword, String reqCode, String invitationCode);
void sendVerificationCode(String phone);
}

View File

@@ -0,0 +1,13 @@
package com.yinlihupo.enlish.service.service;
import com.yinlihupo.enlish.service.domain.dataobject.RoleDO;
import java.util.List;
import java.util.Map;
public interface RoleService {
void pushRolePermission2Redis();
Map<Long, RoleDO> findUserId2RoleMap(List<Long> userIds);
}

View File

@@ -3,6 +3,7 @@ package com.yinlihupo.enlish.service.service;
import com.yinlihupo.enlish.service.domain.dataobject.StudentDO; import com.yinlihupo.enlish.service.domain.dataobject.StudentDO;
import com.yinlihupo.enlish.service.model.bo.StudentDetail; import com.yinlihupo.enlish.service.model.bo.StudentDetail;
import com.yinlihupo.enlish.service.model.bo.exam.WordMasteryDetail;
import com.yinlihupo.enlish.service.model.vo.student.AddStudentReqVO; import com.yinlihupo.enlish.service.model.vo.student.AddStudentReqVO;
import java.util.List; import java.util.List;
@@ -20,4 +21,8 @@ public interface StudentService {
void addStudent(AddStudentReqVO addStudentReqVO); void addStudent(AddStudentReqVO addStudentReqVO);
void deleteStudent(Integer studentId); void deleteStudent(Integer studentId);
String analyzeStudentStudy(Integer studentId);
List<WordMasteryDetail> findStudentWordMasteryDetail(Integer studentId);
} }

View File

@@ -0,0 +1,18 @@
package com.yinlihupo.enlish.service.service;
import com.yinlihupo.enlish.service.domain.dataobject.UserDO;
import java.util.List;
public interface UserService {
UserDO findUser();
List<UserDO> findUsersList(int page, int limit, String name);
Integer findUserTotal();
void createUser(UserDO userDO);
void updateUserInfo(String password, String reqCode, String phone, String name);
}

View File

@@ -2,10 +2,13 @@ package com.yinlihupo.enlish.service.service;
import com.yinlihupo.enlish.service.domain.dataobject.VocabularyBankDO; import com.yinlihupo.enlish.service.domain.dataobject.VocabularyBankDO;
import com.yinlihupo.enlish.service.model.vo.vocabulary.FindStudentWordDetailRspVO;
import java.util.List; import java.util.List;
public interface VocabularyService { public interface VocabularyService {
List<VocabularyBankDO> findVocabularyBankDOListById(List<Integer> ids); List<VocabularyBankDO> findVocabularyBankDOListById(List<Integer> ids);
FindStudentWordDetailRspVO findStudentWordDetail(Integer studentId);
} }

View File

@@ -1,30 +1,31 @@
package com.yinlihupo.enlish.service.service.exam; package com.yinlihupo.enlish.service.service.exam;
import com.yinlihupo.enlish.service.domain.dataobject.ExamWordsDO; import com.yinlihupo.enlish.service.constant.ExamWordsConstant;
import com.yinlihupo.enlish.service.domain.dataobject.ExamWordsJudgeResultDO; import com.yinlihupo.enlish.service.domain.dataobject.*;
import com.yinlihupo.enlish.service.domain.dataobject.VocabularyBankDO;
import com.yinlihupo.enlish.service.domain.mapper.*; import com.yinlihupo.enlish.service.domain.mapper.*;
import com.yinlihupo.enlish.service.service.ExamWordsService; import com.yinlihupo.enlish.service.service.ExamWordsService;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.*;
import java.util.List;
import java.util.UUID;
@Service @Service
@Slf4j @Slf4j
public class ExamWordsServiceImpl implements ExamWordsService { public class ExamWordsServiceImpl implements ExamWordsService {
@Resource
private GradeUnitDOMapper gradeUnitDOMapper;
@Resource @Resource
private VocabularyBankDOMapper vocabularyBankDOMapper; private VocabularyBankDOMapper vocabularyBankDOMapper;
@Resource @Resource
@@ -33,6 +34,11 @@ public class ExamWordsServiceImpl implements ExamWordsService {
private StudentExamWordsDOMapper studentExamWordsDOMapper; private StudentExamWordsDOMapper studentExamWordsDOMapper;
@Resource @Resource
private ExamWordsJudgeResultDOMapper examWordsJudgeResultDOMapper; private ExamWordsJudgeResultDOMapper examWordsJudgeResultDOMapper;
@Resource
private StudentDOMapper studentDOMapper;
@Resource
private UnitDOMapper unitDOMapper;
@Value("${templates.count}") @Value("${templates.count}")
private Integer wordCount; private Integer wordCount;
@Value("${tmp.png}") @Value("${tmp.png}")
@@ -40,33 +46,121 @@ public class ExamWordsServiceImpl implements ExamWordsService {
@Override @Override
@Transactional(rollbackFor = RuntimeException.class) @Transactional(rollbackFor = RuntimeException.class)
public ExamWordsDO generateExamWords(Integer gradeId, Integer level, List<Integer> studentIds) { public ExamWordsDO generateExamWords(Integer studentId, Integer type) {
List<Integer> unitIds = gradeUnitDOMapper.selectUnitIdsByGradeId(gradeId); ExamWordsDO examWordsDO;
List<VocabularyBankDO> vocabularyBankDOS = new ArrayList<>();
int count = wordCount; if (type == ExamWordsConstant.EXAM_TYPE_BASELINE) {
for (Integer unitId : unitIds) { log.info("生成摸底测试");
List<VocabularyBankDO> words = vocabularyBankDOMapper.selectVocabularyBankDOListByUnitId(unitId, 20); examWordsDO = generateBaselineExamWords(studentId);
vocabularyBankDOS.addAll(words); } else if (type == ExamWordsConstant.EXAM_TYPE_MIDTERM) {
count -= 20; log.info("生成期中测试");
if (count <= 0) { examWordsDO = generateMidtermExamWords(studentId);
break; } else {
log.info("生成期末测试");
examWordsDO = generateFinalExamWords(studentId);
} }
List<Integer> wordIds = new ArrayList<>(examWordsDO.getWordIds());
if (wordIds.size() < wordCount) {
log.info("单词数量不足,补充单词");
StudentDO studentDO = studentDOMapper.selectStudentById(studentId);
List<VocabularyBankDO> vocabularyBankDOS = vocabularyBankDOMapper.selectByGradeIdAndNotMatchIds(studentDO.getGradeId(), wordIds, wordCount - wordIds.size());
List<Integer> list = new ArrayList<>(vocabularyBankDOS.stream().map(VocabularyBankDO::getId).toList());
wordIds.addAll(list);
examWordsDO.setWordIds(wordIds);
} }
return examWordsDO;
}
private ExamWordsDO generateBaselineExamWords(Integer studentId) {
StudentDO studentDO = studentDOMapper.selectStudentById(studentId);
Integer gradeId = studentDO.getActualGradeId();
int zoneA = ExamWordsConstant.getZoneA(gradeId);
int zoneASize = ExamWordsConstant.ZONE_A_SIZE;
List<VocabularyBankDO> vocabularyBankDOS = new ArrayList<>(vocabularyBankDOMapper.selectVocabularyBankListByGradeIdRandom(zoneA, zoneASize));
int zoneB = ExamWordsConstant.getZoneB(gradeId);
int zoneBSize = ExamWordsConstant.ZONE_B_SIZE;
vocabularyBankDOS.addAll(vocabularyBankDOMapper.selectVocabularyBankListByGradeIdRandom(zoneB, zoneBSize));
int zoneC = ExamWordsConstant.getZoneC(gradeId);
int zoneCSize = ExamWordsConstant.ZONE_C_SIZE;
vocabularyBankDOS.addAll(vocabularyBankDOMapper.selectVocabularyBankListByGradeIdRandom(zoneC, zoneCSize));
int zoneD = ExamWordsConstant.getZoneD(gradeId);
int zoneDSize = ExamWordsConstant.ZONE_D_SIZE;
vocabularyBankDOS.addAll(vocabularyBankDOMapper.selectVocabularyBankListByGradeIdRandom(zoneD, zoneDSize));
int zoneE = ExamWordsConstant.getZoneE(gradeId);
int zoneESize = ExamWordsConstant.ZONE_E_SIZE;
vocabularyBankDOS.addAll(vocabularyBankDOMapper.selectVocabularyBankListByGradeIdRandom(zoneE, zoneESize));
int zoneF = ExamWordsConstant.getZoneF(gradeId);
int zoneFSize = ExamWordsConstant.ZONE_F_SIZE;
vocabularyBankDOS.addAll(vocabularyBankDOMapper.selectVocabularyBankListByGradeIdRandom(zoneF, zoneFSize));
ExamWordsDO examWordsDO = ExamWordsDO.builder() ExamWordsDO examWordsDO = ExamWordsDO.builder()
.gradeId(gradeId) .gradeId(gradeId)
.level(level) .level(1)
.title(LocalDateTime.now() + "测试") .type(ExamWordsConstant.EXAM_TYPE_BASELINE)
.title("摸低测试" + studentDO.getName())
.createdAt(LocalDateTime.now()) .createdAt(LocalDateTime.now())
.wordIds(vocabularyBankDOS.stream().map(VocabularyBankDO::getId).toList()) .wordIds(vocabularyBankDOS.stream().map(VocabularyBankDO::getId).toList())
.build(); .build();
return getExamWordsDO(studentId, examWordsDO);
}
private ExamWordsDO generateMidtermExamWords(Integer studentId) {
StudentDO studentDO = studentDOMapper.selectStudentById(studentId);
Integer gradeId = studentDO.getGradeId();
List<UnitDO> unitDOS = unitDOMapper.selectByUnitName(ExamWordsConstant.getGradeName(gradeId) + "");
ExamWordsDO examWordsDO = getExamWordsDO(studentId, studentDO, gradeId, unitDOS, ExamWordsConstant.EXAM_TYPE_MIDTERM);
examWordsDO.setTitle("期中测试" + studentDO.getName());
return getExamWordsDO(studentId, examWordsDO);
}
private ExamWordsDO generateFinalExamWords(Integer studentId) {
StudentDO studentDO = studentDOMapper.selectStudentById(studentId);
Integer gradeId = studentDO.getGradeId();
List<UnitDO> unitDOS = unitDOMapper.selectByUnitName(ExamWordsConstant.getGradeName(gradeId));
ExamWordsDO examWordsDO = getExamWordsDO(studentId, studentDO, gradeId, unitDOS, ExamWordsConstant.EXAM_TYPE_FINAL);
examWordsDO.setTitle("期末测试" + studentDO.getName());
return getExamWordsDO(studentId, examWordsDO);
}
@NonNull
private ExamWordsDO getExamWordsDO(Integer studentId, StudentDO studentDO, Integer gradeId, List<UnitDO> unitDOS, Integer type) {
if (unitDOS.isEmpty()) {
throw new RuntimeException("没有找到对应的单元");
}
List<VocabularyBankDO> vocabularyBankDOS = vocabularyBankDOMapper.selectByUnitIds(unitDOS.stream().map(UnitDO::getId).toList());
ExamWordsDO examWordsDO = ExamWordsDO.builder()
.gradeId(gradeId)
.level(1)
.type(type)
.title(studentDO.getName())
.createdAt(LocalDateTime.now())
.wordIds(vocabularyBankDOS.stream().map(VocabularyBankDO::getId).toList())
.build();
return getExamWordsDO(studentId, examWordsDO);
}
@NonNull
private ExamWordsDO getExamWordsDO(Integer studentId, ExamWordsDO examWordsDO) {
int insert = examWordsDOMapper.insert(examWordsDO); int insert = examWordsDOMapper.insert(examWordsDO);
if (insert <= 0) { if (insert <= 0) {
throw new RuntimeException("插入考试失败"); throw new RuntimeException("插入考试失败");
} }
int insertStudentsExam = studentExamWordsDOMapper.insertStudentsExam(studentIds, examWordsDO.getId()); int insertStudentsExam = studentExamWordsDOMapper.insertStudentsExam(studentId, examWordsDO.getId());
if (insertStudentsExam <= 0) { if (insertStudentsExam <= 0) {
throw new RuntimeException("插入学生关联考试失败"); throw new RuntimeException("插入学生关联考试失败");
} }
@@ -77,29 +171,65 @@ public class ExamWordsServiceImpl implements ExamWordsService {
@Override @Override
@Transactional(rollbackFor = RuntimeException.class) @Transactional(rollbackFor = RuntimeException.class)
public int saveExamWordsPngToDbAndLocal(MultipartFile file) { public int saveExamWordsPngToDbAndLocal(MultipartFile file) {
// 1. 基础校验:判空
File dir = new File(tmpPng); if (file == null || file.isEmpty()) {
if (!dir.exists()) { throw new RuntimeException("上传文件不能为空");
dir.mkdirs();
} }
try { // 2. 安全校验:检查后缀名白名单
String originalFilename = file.getOriginalFilename(); String originalFilename = file.getOriginalFilename();
String suffix = ""; String extension = StringUtils.getFilenameExtension(originalFilename); // Spring工具类
if (originalFilename != null && originalFilename.contains(".")) { List<String> allowedExtensions = Arrays.asList("png", "jpg", "jpeg");
suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
if (extension == null || !allowedExtensions.contains(extension.toLowerCase())) {
throw new RuntimeException("不支持的文件格式,仅支持: " + allowedExtensions);
} }
String newFileName = UUID.randomUUID() + suffix;
String path = tmpPng + newFileName;
File dest = new File(path); // 3. 准备目录 (使用 NIO)
file.transferTo(dest); // 假设 tmpPng 是配置好的基础路径字符串
Path directoryPath = Paths.get(tmpPng);
try {
if (!Files.exists(directoryPath)) {
Files.createDirectories(directoryPath);
}
// 4. 生成文件名 (防止文件名冲突)
String newFileName = UUID.randomUUID().toString() + "." + extension;
// 5. 组合最终路径 (自动处理分隔符)
Path targetPath = directoryPath.resolve(newFileName);
// 6. 保存文件
file.transferTo(targetPath.toAbsolutePath().toFile());
String string = targetPath.toAbsolutePath().toFile().toString();
log.info("文件上传成功路径为 {}", string);
if (!targetPath.toFile().exists()) {
log.error("文件上传失败: {}", newFileName);
throw new RuntimeException("文件上传失败");
}
// 7. 入库
// 建议:存相对路径或文件名,不要存 targetPath.toString() 的绝对路径
// 这里为了演示,假设 insert 依然接受字符串,建议存 newFileName
int insert = examWordsJudgeResultDOMapper.insert(targetPath.toString());
log.info("上传文件成功: {}", newFileName);
return insert;
return examWordsJudgeResultDOMapper.insert(path);
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException("上传失败", e); log.error("文件上传失败: {}", originalFilename, e);
throw new RuntimeException("上传失败,请稍后重试", e);
}
} }
@Override
public void updateExamWordsWordIdsOrder(ExamWordsDO examWordsDO) {
examWordsDOMapper.updateWordIdsOrder(examWordsDO);
}
@Override
public ExamWordsDO getExamWordsDOById(Integer id) {
return examWordsDOMapper.selectById(id);
} }

View File

@@ -1,16 +1,15 @@
package com.yinlihupo.enlish.service.service.judge; package com.yinlihupo.enlish.service.service.judge;
import com.yinlihupo.enlish.service.domain.dataobject.ExamWordsDO; import com.yinlihupo.enlish.service.constant.ExamWordsConstant;
import com.yinlihupo.enlish.service.domain.dataobject.ExamWordsJudgeResultDO; import com.yinlihupo.enlish.service.domain.dataobject.*;
import com.yinlihupo.enlish.service.domain.dataobject.StudentExamWordsDO; import com.yinlihupo.enlish.service.domain.mapper.*;
import com.yinlihupo.enlish.service.domain.dataobject.WordMasteryLogDO;
import com.yinlihupo.enlish.service.domain.mapper.ExamWordsDOMapper;
import com.yinlihupo.enlish.service.domain.mapper.ExamWordsJudgeResultDOMapper;
import com.yinlihupo.enlish.service.domain.mapper.StudentExamWordsDOMapper;
import com.yinlihupo.enlish.service.domain.mapper.WordMasteryLogDOMapper;
import com.yinlihupo.enlish.service.model.bo.CoordinatesXY; import com.yinlihupo.enlish.service.model.bo.CoordinatesXY;
import com.yinlihupo.enlish.service.model.bo.StudentExamId; import com.yinlihupo.enlish.service.model.bo.StudentExamId;
import com.yinlihupo.enlish.service.model.bo.exam.ActionType;
import com.yinlihupo.enlish.service.model.bo.exam.DiagnosisResult;
import com.yinlihupo.enlish.service.model.bo.exam.ZoneStats;
import com.yinlihupo.enlish.service.service.ExamWordsJudgeService; import com.yinlihupo.enlish.service.service.ExamWordsJudgeService;
import com.yinlihupo.enlish.service.service.StudentLessonPlansService;
import com.yinlihupo.enlish.service.utils.PngUtil; import com.yinlihupo.enlish.service.utils.PngUtil;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -20,8 +19,8 @@ import org.springframework.transaction.annotation.Transactional;
import java.io.File; import java.io.File;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.*;
import java.util.List; import java.util.stream.Collectors;
@Service @Service
@Slf4j @Slf4j
@@ -36,22 +35,35 @@ public class ExamWordsJudgeServiceImpl implements ExamWordsJudgeService {
private ExamWordsDOMapper examWordsDOMapper; private ExamWordsDOMapper examWordsDOMapper;
@Resource @Resource
private WordMasteryLogDOMapper wordMasteryLogDOMapper; private WordMasteryLogDOMapper wordMasteryLogDOMapper;
@Resource
private VocabularyBankDOMapper vocabularyBankDOMapper;
@Resource
private GradeUnitDOMapper gradeUnitDOMapper;
@Resource
private StudentDOMapper studentDOMapper;
@Resource
private PlanExamDOMapper planExamDOMapper;
@Resource
private StudentLessonPlansService studentLessonPlansService;
@Value("${templates.data}") @Value("${templates.data}")
private String tessdataPath; private String tessdataPath;
@Override @Override
@Transactional(rollbackFor = Exception.class)
public void judgeExamWords(int count) { public void judgeExamWords(int count) {
List<ExamWordsJudgeResultDO> examWordsJudgeResultDOS = examWordsJudgeResultDOMapper.selectUnfinishedExamWordsJudgeResultDOList(count); List<ExamWordsJudgeResultDO> examWordsJudgeResultDOS = examWordsJudgeResultDOMapper.selectUnfinishedExamWordsJudgeResultDOList(count);
for (ExamWordsJudgeResultDO examWordsJudgeResultDO : examWordsJudgeResultDOS) { for (ExamWordsJudgeResultDO examWordsJudgeResultDO : examWordsJudgeResultDOS) {
String ansSheetPath = examWordsJudgeResultDO.getAnsSheetPath(); String ansSheetPath = null;
try {
ansSheetPath = examWordsJudgeResultDO.getAnsSheetPath();
List<CoordinatesXY> coordinatesXIES = PngUtil.analysisXY(ansSheetPath); List<CoordinatesXY> coordinatesXIES = PngUtil.analysisXY(ansSheetPath);
// 从图片中获取学生 id 和考试 id // 从图片中获取学生 id 和考试 id
StudentExamId studentExamId = PngUtil.analyzeExamWordsIdAndStudentId(ansSheetPath, tessdataPath, coordinatesXIES); StudentExamId studentExamId = PngUtil.analyzeExamWordsIdAndStudentId(ansSheetPath, tessdataPath, coordinatesXIES);
Integer examWordsJudgeResultDOId = examWordsJudgeResultDO.getId(); Integer examWordsJudgeResultDOId = examWordsJudgeResultDO.getId();
if (studentExamId == null) { if (studentExamId == null) {
examWordsJudgeResultDOMapper.updateErrorMsg(examWordsJudgeResultDOId, "识别学生 id 和考试 id"); log.info("找到学生 id 和考试 id");
examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDOId, "未识别学生和考试");
continue; continue;
} }
@@ -59,18 +71,18 @@ public class ExamWordsJudgeServiceImpl implements ExamWordsJudgeService {
Integer examWordsId = studentExamId.getExamId(); Integer examWordsId = studentExamId.getExamId();
StudentExamWordsDO studentExamWordsDO = studentExamWordsDOMapper.selectByStudentIdAndExamWordsId(studentId, examWordsId); StudentExamWordsDO studentExamWordsDO = studentExamWordsDOMapper.selectByStudentIdAndExamWordsId(studentId, examWordsId);
if (studentExamWordsDO == null) { if (studentExamWordsDO == null) {
examWordsJudgeResultDOMapper.updateErrorMsg(examWordsJudgeResultDOId, "未找到学生 id 和考试 id 对应的考试记录"); examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDOId, "未找到学生 id 和考试 id 对应的考试记录");
continue; continue;
} }
log.info("studentId:{},examWordsId:{}", studentId, examWordsId); log.info("studentId:{},examWordsId:{}", studentId, examWordsId);
if (studentExamWordsDO.getIsCompleted() == 1) { if (studentExamWordsDO.getIsCompleted() == 1) {
examWordsJudgeResultDOMapper.updateErrorMsg(examWordsJudgeResultDOId, "考试记录此前已识别"); examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDOId, "考试记录此前已识别");
continue; continue;
} }
ExamWordsDO examWordsDO = examWordsDOMapper.selectById(examWordsId); ExamWordsDO examWordsDO = examWordsDOMapper.selectById(examWordsId);
if(examWordsDO == null) { if (examWordsDO == null) {
examWordsJudgeResultDOMapper.updateErrorMsg(examWordsJudgeResultDOId, "未找到考试"); examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDOId, "未找到考试");
continue; continue;
} }
@@ -88,13 +100,26 @@ public class ExamWordsJudgeServiceImpl implements ExamWordsJudgeService {
.wrongWordCount(unmemorizedWordIds.size()) .wrongWordCount(unmemorizedWordIds.size())
.isFinished(1) .isFinished(1)
.build(); .build();
if (examWordsDO.getType().equals(ExamWordsConstant.EXAM_TYPE_BASELINE)) {
// 判断考试等级
judgeExamActualGrade(wordsJudgeResultDO, examWordsDO);
} else {
wordsJudgeResultDO.setMsg("此次考试" + examWordsDO.getTitle() + "答对单词数为" + memorizedWordIds.size());
}
int updated = examWordsJudgeResultDOMapper.updateExamWordsJudgeResultDO(wordsJudgeResultDO); int updated = examWordsJudgeResultDOMapper.updateExamWordsJudgeResultDO(wordsJudgeResultDO);
if (updated != 1) { if (updated != 1) {
examWordsJudgeResultDOMapper.updateErrorMsg(examWordsJudgeResultDOId, "更新考试记录失败"); examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDOId, "更新考试记录失败");
continue; continue;
} }
log.info("更新考试记录成功"); log.info("更新考试记录成功");
PlanExamDO planExamDO = planExamDOMapper.selectByExamId(examWordsId);
if (planExamDO != null) {
studentLessonPlansService.finishStudentLessonPlan(studentId, planExamDO.getPlanId());
log.info("完成学案成功, planId: {}", planExamDO.getPlanId());
}
List<WordMasteryLogDO> wordMasteryLogDOS = new ArrayList<>(unmemorizedWordIds.stream().map(wordId -> WordMasteryLogDO.builder() List<WordMasteryLogDO> wordMasteryLogDOS = new ArrayList<>(unmemorizedWordIds.stream().map(wordId -> WordMasteryLogDO.builder()
.wordId(wordId) .wordId(wordId)
.studentId(studentId) .studentId(studentId)
@@ -112,27 +137,219 @@ public class ExamWordsJudgeServiceImpl implements ExamWordsJudgeService {
.update_time(LocalDateTime.now()) .update_time(LocalDateTime.now())
.build() .build()
).toList()); ).toList());
int batched = wordMasteryLogDOMapper.batchUpdateStudentMastery(wordMasteryLogDOS); wordMasteryLogDOMapper.batchUpdateStudentMastery(wordMasteryLogDOS);
if (batched == 0) { log.info("更新单词掌握记录成功");
examWordsJudgeResultDOMapper.updateErrorMsg(examWordsJudgeResultDOId, "更新学生记忆力记录失败");
continue;
}
int updateStudentExamWordsFinished = studentExamWordsDOMapper.updateStudentExamWordsFinished(studentId, examWordsId); int updateStudentExamWordsFinished = studentExamWordsDOMapper.updateStudentExamWordsFinished(studentId, examWordsId);
if (updateStudentExamWordsFinished != 1) { if (updateStudentExamWordsFinished != 1) {
examWordsJudgeResultDOMapper.updateErrorMsg(examWordsJudgeResultDOId, "更新学生考试为结束时失败"); examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDOId, "更新学生考试为结束时失败");
} }
boolean delete = new File(ansSheetPath).delete(); boolean delete = new File(ansSheetPath).delete();
if (delete) { if (delete) {
log.info("删除文件成功:{}", ansSheetPath); log.info("删除文件成功:{}", ansSheetPath);
} else {
log.error("删除文件失败:{}", ansSheetPath);
}
} catch (Exception e) {
log.error("识别考试失败 {}", e.getMessage());
if (ansSheetPath != null) {
new File(ansSheetPath).delete();
}
examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDO.getId(), e.getMessage());
} }
} }
} }
private void judgeExamActualGrade(ExamWordsJudgeResultDO wordsJudgeResultDO, ExamWordsDO examWordsDO) {
List<VocabularyBankDO> vocabularyBankDOS = vocabularyBankDOMapper.selectVocabularyBankDOListByIds(examWordsDO.getWordIds());
Map<Integer, List<VocabularyBankDO>> unitId2Words = vocabularyBankDOS.stream().collect(Collectors.groupingBy(VocabularyBankDO::getUnitId));
List<GradeUnitDO> gradeUnitDOS = gradeUnitDOMapper.selectByUnitIds(unitId2Words.keySet().stream().toList());
// unitId -> gradeId
Map<Integer, Integer> unitId2GradeId = gradeUnitDOS.stream()
.collect(Collectors.toMap(
GradeUnitDO::getUnitId,
GradeUnitDO::getGradeId,
(existing, replacement) -> existing // 如果重复,保留第一个(或根据业务决定)
));
// gradeId -> List<VocabularyBankDO>
Map<Integer, List<VocabularyBankDO>> gradeId2Words = vocabularyBankDOS.stream()
.filter(vocab -> vocab.getUnitId() != null)
.filter(vocab -> unitId2GradeId.containsKey(vocab.getUnitId()))
.collect(Collectors.groupingBy(
vocab -> unitId2GradeId.get(vocab.getUnitId())
));
// 核心数据结构GradeId -> List<WordId> (试卷里包含的每个年级的单词有哪些)
Map<Integer, Set<Integer>> gradeId2WordIdsMap = new HashMap<>();
for (VocabularyBankDO vocab : vocabularyBankDOS) {
if (vocab.getUnitId() != null && unitId2GradeId.containsKey(vocab.getUnitId())) {
Integer gradeId = unitId2GradeId.get(vocab.getUnitId());
gradeId2WordIdsMap.computeIfAbsent(gradeId, k -> new HashSet<>()).add(vocab.getId());
}
}
// 统计各区域正确率 (Calculate Accuracy per Zone)
// 锚点年级 (G_test),即这张卷子是按哪个年级生成的
Integer anchorGrade = examWordsDO.getGradeId();
// 挑战区
int zoneA = ExamWordsConstant.getZoneA(anchorGrade);
ZoneStats zoneAStats = calculateZoneStats(gradeId2WordIdsMap, wordsJudgeResultDO, zoneA);
// 当前核心
int zoneB = ExamWordsConstant.getZoneB(anchorGrade);
ZoneStats zoneBStats = calculateZoneStats(gradeId2WordIdsMap, wordsJudgeResultDO, zoneB);
// 回溯 Level 1
int zoneC = ExamWordsConstant.getZoneC(anchorGrade);
ZoneStats zoneCStats = calculateZoneStats(gradeId2WordIdsMap, wordsJudgeResultDO, zoneC);
// 回溯 Level 2
int zoneD = ExamWordsConstant.getZoneD(anchorGrade);
ZoneStats zoneDStats = calculateZoneStats(gradeId2WordIdsMap, wordsJudgeResultDO, zoneD);
// 回溯 Level 3
int zoneE = ExamWordsConstant.getZoneE(anchorGrade);
ZoneStats zoneEStats = calculateZoneStats(gradeId2WordIdsMap, wordsJudgeResultDO, zoneE);
// 地基兜底
int zoneF = ExamWordsConstant.getZoneF(anchorGrade);
ZoneStats zoneFStats = calculateZoneStats(gradeId2WordIdsMap, wordsJudgeResultDO, zoneF);
// 地基区
ZoneStats zoneFoundationStats = calculateFoundationStats(gradeId2WordIdsMap, wordsJudgeResultDO, zoneD);
DiagnosisResult diagnosisResult = diagnoseStudentLevel(anchorGrade, zoneAStats, zoneBStats, zoneCStats, zoneDStats, zoneFoundationStats);
studentDOMapper.updateStudentActualGradeId(wordsJudgeResultDO.getStudentId(), diagnosisResult.getDeterminedLevel());
log.info("判断结果:{}", diagnosisResult);
wordsJudgeResultDO.setMsg(diagnosisResult.getMessage());
}
/**
* 计算特定年级的统计数据
*/
private ZoneStats calculateZoneStats(Map<Integer, Set<Integer>> map, ExamWordsJudgeResultDO result, Integer targetGrade) {
Set<Integer> totalWords = map.getOrDefault(targetGrade, Collections.emptySet());
if (totalWords.isEmpty()) return new ZoneStats(targetGrade, 0, 0, 0.0);
long correctCount = result.getCorrectWordIds().stream().filter(totalWords::contains).count();
double accuracy = (double) correctCount / totalWords.size();
return new ZoneStats(targetGrade, totalWords.size(), (int)correctCount, accuracy);
}
/**
* 计算地基区(所有低于某年级)的统计数据
*/
private ZoneStats calculateFoundationStats(Map<Integer, Set<Integer>> map, ExamWordsJudgeResultDO result, Integer maxGradeThreshold) {
Set<Integer> foundationWords = new HashSet<>();
map.forEach((grade, words) -> {
if (grade <= maxGradeThreshold) {
foundationWords.addAll(words);
}
});
if (foundationWords.isEmpty()) return new ZoneStats(-1, 0, 0, 0.0);
long correctCount = result.getCorrectWordIds().stream().filter(foundationWords::contains).count();
double accuracy = (double) correctCount / foundationWords.size();
return new ZoneStats(-1, foundationWords.size(), (int)correctCount, accuracy);
}
/**
* 核心诊断逻辑矩阵
*/
private DiagnosisResult diagnoseStudentLevel(Integer anchorGrade, ZoneStats zoneA, ZoneStats zoneB, ZoneStats zoneC, ZoneStats zoneD, ZoneStats zoneFoundation) {
// --- 场景 1: 熔断机制 (Meltdown) ---
// 针对 G8/G9 等高年级如果地基区G4/G5及以下错误率极高
// 阈值设为 20% (即 Zone F 正确率 < 20%)
if (zoneFoundation.getTotalCount() > 0 && zoneFoundation.getAccuracy() < 0.20 && anchorGrade >= 7) {
return new DiagnosisResult(
4, // 强制下沉到 G4 (根据文档逻辑,熔断通常锚定 G4)
ActionType.TRIGGER_RETEST,
"严重预警!检测到您的基础词汇(" + ExamWordsConstant.getGradeName(anchorGrade - 3) + "及以下)存在大面积坍塌。系统已为您启动【基础词汇专项排查】,请勿担心,这是为了更好地起跳。"
);
}
// --- 场景 2: 进阶/跳级 (Upgrade) ---
// 必须满足:当前年级(B) > 80% 且 挑战年级(A) > 60% (如果有题的话)
boolean canUpgrade = zoneB.getAccuracy() >= 0.8 && (zoneA.getTotalCount() == 0 || zoneA.getAccuracy() >= 0.6);
if (canUpgrade) {
return new DiagnosisResult(
Math.max(anchorGrade + 1, ExamWordsConstant.GRADE_8),
ActionType.UPGRADE,
"恭喜!您对" + ExamWordsConstant.getGradeName(anchorGrade) + " 的掌握非常扎实,且具备挑战" + ExamWordsConstant.getGradeName(Math.max(anchorGrade + 1, ExamWordsConstant.GRADE_8)) + " 的潜力。系统将为您解锁更高阶词库。"
);
}
// --- 场景 3: 正常回溯诊断 (Standard Diagnosis) ---
// 3.1 当前年级崩盘 (Zone B < 60%)
if (zoneB.getAccuracy() < 0.6) {
// 检查上一级 (Zone C)
if (zoneC.getAccuracy() >= 0.8) {
// "中考空心病" / "基础扎实但新课未动"
// 定级:保持在当前年级,但侧重复习
return new DiagnosisResult(
anchorGrade,
ActionType.STAY_AND_REVIEW,
"基础尚可(" + ExamWordsConstant.getGradeName(zoneC.getGradeId()) + "掌握较好),但" + ExamWordsConstant.getGradeName(anchorGrade) + " 新词汇存在较大缺口。建议:重点攻克本年级核心动词。"
);
} else if (zoneC.getAccuracy() >= 0.6) {
// Zone C 勉强及格Zone B 不及格 -> 降一级
return new DiagnosisResult(
Math.max(anchorGrade - 1, ExamWordsConstant.GRADE_1),
ActionType.DOWNGRADE,
"检测到" + ExamWordsConstant.getGradeName(anchorGrade) + " 学习吃力,且" + ExamWordsConstant.getGradeName(zoneC.getGradeId()) + " 存在模糊点。建议降级回溯,巩固 " + ExamWordsConstant.getGradeName(zoneC.getGradeId()) + ""
);
} else {
// Zone C 也崩了 (< 60%) -> 连降两级或更多
// 检查 Zone D
if (zoneD.getTotalCount() > 0 && zoneD.getAccuracy() >= 0.6) {
return new DiagnosisResult(
Math.max(anchorGrade - 2, ExamWordsConstant.GRADE_1),
ActionType.DOWNGRADE,
"您的" + ExamWordsConstant.getGradeName(zoneC.getGradeId()) + "" + ExamWordsConstant.getGradeName(anchorGrade) + " 均存在脱节。建议从 " + ExamWordsConstant.getGradeName(zoneD.getGradeId()) + " 开始系统补漏。"
);
} else {
// 彻底崩盘,可能需要熔断,或者定级到更低
return new DiagnosisResult(
Math.max(ExamWordsConstant.GRADE_1, anchorGrade - 3),
ActionType.DOWNGRADE,
"基础薄弱,建议暂停当前进度,从低年级核心高频词重新开始。"
);
}
}
}
// 3.2 正常达标 (Zone B 60% - 80%)
return new DiagnosisResult(
anchorGrade,
ActionType.PASS,
"当前年级达标。建议继续保持,并尝试在阅读中增加长难句练习。"
);
}
@Override @Override
public List<ExamWordsJudgeResultDO> getExamWordsJudgeResult(Integer page, Integer pageSize) { public List<ExamWordsJudgeResultDO> getExamWordsJudgeResult(Integer page, Integer pageSize, Integer classId, Integer gradeId, String studentName) {
List<Integer> studentIds = new ArrayList<>();
if (classId != null) {
studentIds.addAll(studentDOMapper.selectStudentDOListByClassId(classId).stream().map(StudentDO::getId).toList());
}
if (gradeId != null) {
studentIds.addAll(studentDOMapper.selectStudentDOListByGradeId(gradeId).stream().map(StudentDO::getId).toList());
}
if (!studentName.isEmpty()) {
studentIds.addAll(studentDOMapper.selectStudentDOListByName(studentName).stream().map(StudentDO::getId).toList());
}
if (studentIds.isEmpty()) {
return examWordsJudgeResultDOMapper.selectByPage((page - 1) * pageSize, pageSize); return examWordsJudgeResultDOMapper.selectByPage((page - 1) * pageSize, pageSize);
} else {
return examWordsJudgeResultDOMapper.selectByPageAndStudentIds((page - 1) * pageSize, page * pageSize, studentIds);
}
} }
@Override @Override
@@ -140,6 +357,11 @@ public class ExamWordsJudgeServiceImpl implements ExamWordsJudgeService {
return examWordsJudgeResultDOMapper.selectCount(); return examWordsJudgeResultDOMapper.selectCount();
} }
@Override
public Integer getExamUnfinishedCount() {
return examWordsJudgeResultDOMapper.selectUnfinishedCount();
}
@Override @Override
public ExamWordsJudgeResultDO getExamWordsJudgeResultDOById(Integer id) { public ExamWordsJudgeResultDO getExamWordsJudgeResultDOById(Integer id) {
return examWordsJudgeResultDOMapper.selectDetailById(id); return examWordsJudgeResultDOMapper.selectDetailById(id);

View File

@@ -0,0 +1,121 @@
package com.yinlihupo.enlish.service.service.login;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.RandomUtil;
import com.aliyun.dysmsapi20170525.Client;
import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
import com.aliyun.teautil.models.RuntimeOptions;
import com.yinlihupo.enlish.service.constant.UserRedisConstants;
import com.yinlihupo.enlish.service.domain.dataobject.UserDO;
import com.yinlihupo.enlish.service.domain.mapper.UserDOMapper;
import com.yinlihupo.enlish.service.service.LoginService;
import com.yinlihupo.framework.common.util.JsonUtils;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.Duration;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@Service
@Slf4j
public class LoginServiceImpl implements LoginService {
@Resource
private UserDOMapper userDOMapper;
@Resource
private PasswordEncoder passwordEncoder;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private Client client;
@Override
@Transactional(rollbackFor = Exception.class)
public void login(String phone, String name, String reqPassword, String reqCode, String invitationCode) {
UserDO userDO = userDOMapper.selectByPhone(phone);
log.info("userDO:{}", userDO);
String code = JsonUtils.toJsonString(redisTemplate.opsForValue().get(UserRedisConstants.buildUserLoginCode(phone)));
if (userDO == null) {
if (code == null || !code.equals(reqCode)) {
throw new RuntimeException("验证码错误");
}
Object invitationObj = redisTemplate.opsForValue().get(UserRedisConstants.buildUserInvitationCode(invitationCode));
if (invitationObj == null) {
throw new RuntimeException("邀请码错误");
}
int invitationLimit = Integer.parseInt(JsonUtils.toJsonString(invitationObj));
if (invitationLimit <= 0) {
throw new RuntimeException("邀请码已使用完毕");
}
redisTemplate.opsForValue().set(UserRedisConstants.buildUserInvitationCode(invitationCode), invitationLimit - 1);
userDO = UserDO.builder()
.phone(phone)
.name(name)
.password(passwordEncoder.encode(reqPassword))
.build();
userDOMapper.insert(userDO);
StpUtil.login(userDO.getId());
return;
}
if (code != null && code.equals(reqCode)) {
StpUtil.login(userDO.getId());
return;
}
if (reqPassword != null && passwordEncoder.matches(reqPassword, userDO.getPassword())) {
StpUtil.login(userDO.getId());
return;
}
throw new RuntimeException("登录错误");
}
@Override
public void sendVerificationCode(String phone) {
String code = RandomUtil.randomNumbers(6);
String key = UserRedisConstants.buildUserLoginCode(phone);
redisTemplate.opsForValue().set(key, code);
redisTemplate.expire(key, 5, TimeUnit.MINUTES);
String signName = "短信测试";
String templateCode = "SMS_154950909";
String templateParam = String.format("{\"code\":\"%s\"}", code);
try {
sendMessage(phone, signName, templateCode, templateParam);
} catch (Exception e) {
log.error("==> 短信发送失败, phone: {}, signName: {}, templateCode: {}, templateParam: {}", phone, signName, templateCode, templateParam);
throw new RuntimeException(e);
}
}
/**
* 发送短信
*/
public void sendMessage(String signName, String templateCode, String phone, String templateParam) throws Exception {
SendSmsRequest sendSmsRequest = new SendSmsRequest()
.setSignName(signName)
.setTemplateCode(templateCode)
.setPhoneNumbers(phone)
.setTemplateParam(templateParam);
RuntimeOptions runtime = new RuntimeOptions();
log.info("==> 开始短信发送, phone: {}, signName: {}, templateCode: {}, templateParam: {}", phone, signName, templateCode, templateParam);
// 发送短信
SendSmsResponse response = client.sendSmsWithOptions(sendSmsRequest, runtime);
log.info("==> 短信发送成功, response: {}", JsonUtils.toJsonString(response));
}
}

View File

@@ -1,22 +1,26 @@
package com.yinlihupo.enlish.service.service.plan; package com.yinlihupo.enlish.service.service.plan;
import com.yinlihupo.enlish.service.constant.ExamWordsConstant;
import com.yinlihupo.enlish.service.constant.LessonPlanConstant; import com.yinlihupo.enlish.service.constant.LessonPlanConstant;
import com.yinlihupo.enlish.service.domain.dataobject.*; import com.yinlihupo.enlish.service.domain.dataobject.*;
import com.yinlihupo.enlish.service.domain.mapper.*; import com.yinlihupo.enlish.service.domain.mapper.*;
import com.yinlihupo.enlish.service.model.bo.Sentence;
import com.yinlihupo.enlish.service.model.bo.Word;
import com.yinlihupo.enlish.service.service.LessonPlansService; import com.yinlihupo.enlish.service.service.LessonPlansService;
import com.yinlihupo.enlish.service.utils.DifyArticleClient; import com.yinlihupo.enlish.service.utils.DifyClient;
import com.yinlihupo.enlish.service.utils.StringToPlanMapUtil; import com.yinlihupo.enlish.service.utils.StringToPlanMapUtil;
import com.yinlihupo.framework.common.util.JsonUtils; import com.yinlihupo.framework.common.util.JsonUtils;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.NonNull;
import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.io.IOException; import java.io.IOException;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.*; import java.util.*;
import java.util.concurrent.TimeUnit;
@Service @Service
@Slf4j @Slf4j
@@ -35,22 +39,34 @@ public class LessonPlansServiceImpl implements LessonPlansService {
@Resource @Resource
private GradeDOMapper gradeDOMapper; private GradeDOMapper gradeDOMapper;
@Resource @Resource
private DifyArticleClient difyArticleClient; private DifyClient difyClient;
@Resource
@Value("${templates.plan.weekday}") private ExamWordsDOMapper examWordsDOMapper;
private String planWeekday; @Resource
@Value("${templates.plan.weekend}") private StudentExamWordsDOMapper studentExamWordsDOMapper;
private String planWeekend; @Resource
private StudentDOMapper studentDOMapper;
@Resource
private PlanExamDOMapper planExamDOMapper;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private ClassDOMapper classDOMapper;
@Override @Override
@Transactional(rollbackFor = Exception.class) public void generateLessonPlans(Integer studentId, Integer unitId, Integer wordSize) {
public void generateLessonPlans(Integer studentId, Integer unitId) { String key = LessonPlanConstant.buildGeneratePlanContent(studentId);
redisTemplate.opsForValue().set(key, studentId);
redisTemplate.expire(key, 7, TimeUnit.MINUTES);
log.info("开始生成计划");
List<VocabularyBankDO> vocabularyBankDOS = vocabularyBankDOMapper.selectVocabularyBankDOAllByUnitId(unitId); List<VocabularyBankDO> vocabularyBankDOS = vocabularyBankDOMapper.selectVocabularyBankDOAllByUnitId(unitId);
UnitDO unitDO = unitDOMapper.selectByPrimaryKey(unitId); UnitDO unitDO = unitDOMapper.selectByPrimaryKey(unitId);
GradeUnitDO gradeUnitDO = gradeUnitDOMapper.selectByUnitId(unitId); GradeUnitDO gradeUnitDO = gradeUnitDOMapper.selectByUnitId(unitId);
GradeDO gradeDO = gradeDOMapper.selectById(gradeUnitDO.getGradeId()); GradeDO gradeDO = gradeDOMapper.selectById(gradeUnitDO.getGradeId());
List<VocabularyBankDO> totalWords = new ArrayList<>();
// 补差词汇所用词汇的 // 补差词汇所用词汇的
List<VocabularyBankDO> vocabularyBankListStudentNotMaster = getVocabListRandom(vocabularyBankDOMapper List<VocabularyBankDO> vocabularyBankListStudentNotMaster = getVocabListRandom(vocabularyBankDOMapper
.selectVocabularyBankListStudentNotMaster(gradeUnitDO.getGradeId(), studentId), 50); .selectVocabularyBankListStudentNotMaster(gradeUnitDO.getGradeId(), studentId), 50);
@@ -58,15 +74,25 @@ public class LessonPlansServiceImpl implements LessonPlansService {
int countGap = gapSize / 5; int countGap = gapSize / 5;
int syncSize = vocabularyBankDOS.size(); int syncSize = vocabularyBankDOS.size();
int countSync = syncSize / 5; wordSize = wordSize <= 0 ? syncSize / 5 : wordSize;
int checkTotal = 50; int checkTotal = 50;
List<List<VocabularyBankDO>> weeksSync = new ArrayList<>(); List<List<VocabularyBankDO>> weeksSync = new ArrayList<>();
List<List<VocabularyBankDO>> weeksGap = new ArrayList<>(); List<List<VocabularyBankDO>> weeksGap = new ArrayList<>();
int j = 0;
for (int i = 0; i < 5; i++) { for (int i = 0; i < 5; i++) {
List<VocabularyBankDO> syncVocabList = vocabularyBankDOS.subList(i * countSync, Math.min((i + 1) * countSync, syncSize)); List<VocabularyBankDO> syncVocabList;
if ((i + 1) * wordSize < syncSize) {
syncVocabList = vocabularyBankDOS.subList(i * wordSize, (i + 1) * wordSize);
} else {
syncVocabList = new ArrayList<>(weeksSync.get(Math.min(j++, weeksSync.size() - 1)));
}
List<VocabularyBankDO> gapVocabList = vocabularyBankListStudentNotMaster.subList(i * countGap, Math.min(i * countGap + countGap, gapSize)); List<VocabularyBankDO> gapVocabList = vocabularyBankListStudentNotMaster.subList(i * countGap, Math.min(i * countGap + countGap, gapSize));
weeksSync.add(syncVocabList); weeksSync.add(syncVocabList);
weeksGap.add(gapVocabList); weeksGap.add(gapVocabList);
totalWords.addAll(syncVocabList);
totalWords.addAll(gapVocabList);
List<VocabularyBankDO> reviewVocabList = new ArrayList<>(); List<VocabularyBankDO> reviewVocabList = new ArrayList<>();
List<VocabularyBankDO> checkList = new ArrayList<>(); List<VocabularyBankDO> checkList = new ArrayList<>();
// 艾宾浩斯遗忘曲线 // 艾宾浩斯遗忘曲线
@@ -116,21 +142,29 @@ public class LessonPlansServiceImpl implements LessonPlansService {
.build(); .build();
studentLessonPlansDOMapper.insert(studentLessonPlansDO); studentLessonPlansDOMapper.insert(studentLessonPlansDO);
Integer examId = (Integer) lessonPlanMap.get("examId");
PlanExamDO planExamDO = PlanExamDO.builder()
.planId(lessonPlansDO.getId())
.examId(examId)
.build();
planExamDOMapper.insert(planExamDO);
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException(e); log.info("生成第{}天计划失败,失败原因 {}", i + 1, e.getMessage());
} }
log.info("生成第{}天计划成功", i + 1); log.info("生成第{}天计划成功", i + 1);
} }
try { try {
int syncWeekender = syncSize / 2; log.info("开始生成周末计划");
int syncWeekendSize = totalWords.size() / 2;
for (int i = 0; i < 2; i++) { for (int i = 0; i < 2; i++) {
List<VocabularyBankDO> checkList = vocabularyBankDOS.subList(i * syncWeekender, Math.min((i + 1) * syncWeekender, syncSize)); List<VocabularyBankDO> checkList = totalWords.subList(i * syncWeekendSize, Math.min((i + 1) * syncWeekendSize, syncWeekendSize));
Map<String, Object> map = generateWeekendPlans(checkList, i + 6, gradeDO, unitDO, studentId); Map<String, Object> map = generateWeekendPlans(checkList, i + 6, gradeDO, unitDO, studentId);
LessonPlansDO lessonPlansDO = LessonPlansDO.builder() LessonPlansDO lessonPlansDO = LessonPlansDO.builder()
.title(map.get("title").toString()) .title(map.get("examStr").toString() + "复习")
.gradeId(gradeDO.getId().toString()) .gradeId(gradeDO.getId().toString())
.unitId(unitDO.getId()) .unitId(unitDO.getId())
.createdAt(LocalDateTime.now()) .createdAt(LocalDateTime.now())
@@ -138,6 +172,13 @@ public class LessonPlansServiceImpl implements LessonPlansService {
.build(); .build();
lessonPlansDOMapper.insert(lessonPlansDO); lessonPlansDOMapper.insert(lessonPlansDO);
Integer examId = (Integer) map.get("examId");
PlanExamDO planExamDO = PlanExamDO.builder()
.planId(lessonPlansDO.getId())
.examId(examId)
.build();
planExamDOMapper.insert(planExamDO);
StudentLessonPlansDO studentLessonPlansDO = StudentLessonPlansDO.builder() StudentLessonPlansDO studentLessonPlansDO = StudentLessonPlansDO.builder()
.studentId(studentId) .studentId(studentId)
.planId(lessonPlansDO.getId()) .planId(lessonPlansDO.getId())
@@ -160,14 +201,43 @@ public class LessonPlansServiceImpl implements LessonPlansService {
return lessonPlansDOMapper.selectByLessonId(id); return lessonPlansDOMapper.selectByLessonId(id);
} }
@Override
public List<LessonPlansDO> findLessonPlansByStudentId(Integer studentId) {
return lessonPlansDOMapper.selectByStudentId(studentId);
}
private Map<String, Object> generateWeekendPlans(List<VocabularyBankDO> checkList,
private Map<String, Object> generateWeekendPlans(List<VocabularyBankDO> words,
int day, int day,
GradeDO gradeDO, UnitDO unitDO, Integer studentId) throws IOException { GradeDO gradeDO, UnitDO unitDO, Integer studentId) throws IOException {
Map<String, Object> data = new HashMap<>(); Map<String, Object> data = new HashMap<>();
data.put("title", "" + day + "" + "复习" + gradeDO.getTitle() + unitDO.getTitle() + studentId); words.forEach(word -> word.setDefinition(word.getDefinition().length() > 5 ? word.getDefinition().substring(0, 5) : word.getDefinition()));
data.put("checkList", checkList); List<Integer> wordIds = words.stream().map(VocabularyBankDO::getId).toList();
StudentDO studentDO = studentDOMapper.selectStudentById(studentId);
String ExamTitle = gradeDO.getTitle() + unitDO.getTitle() + "教案小测第" + ExamWordsConstant.day2Chinese(day) + "" + studentDO.getName();
ExamWordsDO examWordsDO = ExamWordsDO.builder()
.gradeId(gradeDO.getId())
.level(1)
.wordIds(wordIds)
.type(ExamWordsConstant.EXAM_TYPE_TEST)
.title(ExamTitle)
.createdAt(LocalDateTime.now())
.build();
examWordsDOMapper.insert(examWordsDO);
studentExamWordsDOMapper.insertStudentsExam(studentId, examWordsDO.getId());
ClassDO classDO = classDOMapper.selectClassDOById(studentDOMapper.selectStudentById(studentId).getClassId());
data.put("examId", examWordsDO.getId());
data.put("studentId", studentId);
data.put("studentStr", gradeDO.getTitle() + " " + classDO.getTitle() + " " + studentDO.getName());
data.put("examStr", ExamTitle);
List<VocabularyBankDO> words1 = words.subList(0, words.size() / 2);
List<VocabularyBankDO> words2 = words.subList(words.size() / 2, words.size());
data.put("words1", words1);
data.put("words2", words2);
// LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy(); // LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
// Configure config = Configure.builder() // Configure config = Configure.builder()
// .bind("checkList", policy) // .bind("checkList", policy)
@@ -186,7 +256,8 @@ public class LessonPlansServiceImpl implements LessonPlansService {
List<VocabularyBankDO> checkList, List<VocabularyBankDO> checkList,
int day, int day,
GradeDO gradeDO, UnitDO unitDO, Integer studentId) throws Exception { GradeDO gradeDO, UnitDO unitDO, Integer studentId) throws Exception {
String title = gradeDO.getTitle() + " " + unitDO.getTitle() + " " + "" + day + "" + studentId;
String title = gradeDO.getTitle() + " " + unitDO.getTitle() + " " + "" + day + "";
Map<String, Object> data = new HashMap<>(); Map<String, Object> data = new HashMap<>();
data.put("title", title); data.put("title", title);
data.put("syncVocabList", syncVocabList); data.put("syncVocabList", syncVocabList);
@@ -194,15 +265,17 @@ public class LessonPlansServiceImpl implements LessonPlansService {
data.put("reviewVocabList", reviewVocabList); data.put("reviewVocabList", reviewVocabList);
data.put("checkList", checkList); data.put("checkList", checkList);
data.put("checkListAns", checkList); data.put("checkListAns", checkList);
// 中译英 // 中译英
List<VocabularyBankDO> drillRound1 = new ArrayList<>(syncVocabList); List<Word> list = syncVocabList.stream().map(vocabularyBankDO -> Word.builder().title(vocabularyBankDO.getWord()).definition(vocabularyBankDO.getDefinition()).build()).toList();
list.forEach(word -> word.setTitle(" "));
List<Word> drillRound1 = new ArrayList<>(list);
Collections.shuffle(drillRound1); Collections.shuffle(drillRound1);
data.put("drillRound1", drillRound1); data.put("drillRound1", drillRound1);
List<VocabularyBankDO> drillRound2 = new ArrayList<>(syncVocabList); List<Word> drillRound2 = new ArrayList<>(list);
Collections.shuffle(drillRound2); Collections.shuffle(drillRound2);
data.put("drillRound2", drillRound2); data.put("drillRound2", drillRound2);
List<VocabularyBankDO> drillRound3 = new ArrayList<>(syncVocabList); List<Word> drillRound3 = new ArrayList<>(list);
Collections.shuffle(drillRound3); Collections.shuffle(drillRound3);
data.put("drillRound3", drillRound3); data.put("drillRound3", drillRound3);
@@ -211,8 +284,10 @@ public class LessonPlansServiceImpl implements LessonPlansService {
mixedDrill.addAll(syncVocabList); mixedDrill.addAll(syncVocabList);
mixedDrill.addAll(gapVocabList); mixedDrill.addAll(gapVocabList);
mixedDrill.addAll(reviewVocabList); mixedDrill.addAll(reviewVocabList);
Collections.shuffle(mixedDrill); List<Word> mixedList = new ArrayList<>(mixedDrill.stream().map(vocabularyBankDO -> Word.builder().title(vocabularyBankDO.getWord()).definition(vocabularyBankDO.getDefinition()).build()).toList());
data.put("mixedDrill", mixedDrill); mixedList.forEach(word -> word.setDefinition(" "));
Collections.shuffle(mixedList);
data.put("mixedDrill", mixedList);
// 文章 A // 文章 A
log.info("生成文章 A 中文开始"); log.info("生成文章 A 中文开始");
@@ -239,6 +314,50 @@ public class LessonPlansServiceImpl implements LessonPlansService {
data.put("articleBans", mapB.get(LessonPlanConstant.ANSWER_KEY_EXPLANATION)); data.put("articleBans", mapB.get(LessonPlanConstant.ANSWER_KEY_EXPLANATION));
data.put("articleBtran", mapB.get(LessonPlanConstant.FULL_TRANSLATION)); data.put("articleBtran", mapB.get(LessonPlanConstant.FULL_TRANSLATION));
// 连词成句
List<Sentence> sentences = difyClient.sendSentenceAnalyze(syncVocabList.subList(0, Math.max(10, syncVocabList.size())), gradeDO.getTitle());
data.put("sentences", sentences);
data.put("sentencesAns", sentences);
log.info( "生成连词成句成功");
// 教案小测
List<VocabularyBankDO> words = new ArrayList<>(syncVocabList);
words.addAll(gapVocabList);
words.addAll(reviewVocabList);
if (words.size() < 100) {
words.addAll(vocabularyBankDOMapper.selectVocabularyBankListByGradeIdRandom(gradeDO.getId(), 100 - words.size()));
} else {
words = words.subList(0, 100);
}
words.forEach(word -> word.setDefinition(word.getDefinition().length() > 5 ? word.getDefinition().substring(0, 5) : word.getDefinition()));
List<Integer> wordIds = words.stream().map(VocabularyBankDO::getId).toList();
StudentDO studentDO = studentDOMapper.selectStudentById(studentId);
String ExamTitle = gradeDO.getTitle() + unitDO.getTitle() + "教案小测 第" + ExamWordsConstant.day2Chinese(day) + "" + studentDO.getName();
ExamWordsDO examWordsDO = ExamWordsDO.builder()
.gradeId(gradeDO.getId())
.level(1)
.wordIds(wordIds)
.type(ExamWordsConstant.EXAM_TYPE_TEST)
.title(ExamTitle)
.createdAt(LocalDateTime.now())
.build();
examWordsDOMapper.insert(examWordsDO);
studentExamWordsDOMapper.insertStudentsExam(studentId, examWordsDO.getId());
data.put("examId", examWordsDO.getId());
data.put("studentId", studentId);
data.put("studentStr", studentDO.getName());
data.put("examStr", ExamTitle);
List<VocabularyBankDO> words1 = words.subList(0, wordIds.size() / 2);
List<VocabularyBankDO> words2 = words.subList(wordIds.size() / 2, wordIds.size());
data.put("words1", words1);
data.put("words2", words2);
log.info("生成教案小测成功");
// LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy(); // LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
// Configure config = Configure.builder() // Configure config = Configure.builder()
// .bind("syncVocabList", policy) // .bind("syncVocabList", policy)
@@ -269,7 +388,7 @@ public class LessonPlansServiceImpl implements LessonPlansService {
int i = 0; int i = 0;
do { do {
log.info("第{}次生成文章中文开始", ++i); log.info("第{}次生成文章中文开始", ++i);
String answer = difyArticleClient.sendChat(string, String.valueOf(studentId) + UUID.randomUUID(), null).getAnswer(); String answer = difyClient.sendChat(string, String.valueOf(studentId) + UUID.randomUUID(), null).getAnswer();
map = StringToPlanMapUtil.parseTextToMap(answer); map = StringToPlanMapUtil.parseTextToMap(answer);
} while (map.get(LessonPlanConstant.TITLE) == null } while (map.get(LessonPlanConstant.TITLE) == null
|| map.get(LessonPlanConstant.PASSAGE) == null || map.get(LessonPlanConstant.PASSAGE) == null

View File

@@ -0,0 +1,66 @@
package com.yinlihupo.enlish.service.service.role;
import com.yinlihupo.enlish.service.constant.RoleConstants;
import com.yinlihupo.enlish.service.domain.dataobject.RoleDO;
import com.yinlihupo.enlish.service.domain.dataobject.UserRoleRelDO;
import com.yinlihupo.enlish.service.domain.mapper.RoleDOMapper;
import com.yinlihupo.enlish.service.domain.mapper.UserDOMapper;
import com.yinlihupo.enlish.service.domain.mapper.UserRoleRelDOMapper;
import com.yinlihupo.enlish.service.service.RoleService;
import com.yinlihupo.framework.common.util.JsonUtils;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
@Slf4j
public class RoleServiceImpl implements RoleService {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private RoleDOMapper roleDOMapper;
@Resource
private UserRoleRelDOMapper userRoleRelDOMapper;
@Override
public void pushRolePermission2Redis() {
List<RoleDO> roleDOS = roleDOMapper.selectAll();
List<String> roleKeys = roleDOS.stream().map(RoleDO::getRoleKey).toList();
log.info("将角色同步到 redis 中, {}", roleKeys);
redisTemplate.opsForValue().set(RoleConstants.ROLE, JsonUtils.toJsonString(roleKeys));
Map<Long, RoleDO> roleId2RoleDO = roleDOS.stream().collect(Collectors.toMap(RoleDO::getId, roleDO -> roleDO));
List<UserRoleRelDO> userRoleRelDOS = userRoleRelDOMapper.selectAll();
log.info("将用户角色关系同步到 redis 中, {}", userRoleRelDOS);
Map<Long, List<UserRoleRelDO>> userId2UserRoleRelDOs = userRoleRelDOS.stream().collect(Collectors.groupingBy(UserRoleRelDO::getUserId));
userId2UserRoleRelDOs.forEach((userId, userRoleRelDOs) -> {
List<Long> roleIds = userRoleRelDOs.stream().map(UserRoleRelDO::getRoleId).toList();
List<RoleDO> roleDOs = roleIds.stream().map(roleId2RoleDO::get).toList();
List<String> user2RoleKeys = roleDOs.stream().map(RoleDO::getRoleKey).toList();
log.info("将用户 {} 的角色同步到 redis 中, {}", userId, roleKeys);
// 不要使用 JsonUtils.toJsonString(user2RoleKeys); 会造成二次序列化
redisTemplate.opsForValue().set(RoleConstants.buildUserRoleKey(userId), user2RoleKeys);
});
}
@Override
public Map<Long, RoleDO> findUserId2RoleMap(List<Long> userIds) {
List<UserRoleRelDO> userRoleRelDOS = userRoleRelDOMapper.selectByUserIds(userIds);
Map<Long, UserRoleRelDO> userId2UserRoleRelDOs = userRoleRelDOS.stream().collect(Collectors.toMap(UserRoleRelDO::getUserId, userRoleRelDO -> userRoleRelDO));
List<RoleDO> roleDOS = roleDOMapper.selectByIds(userRoleRelDOS.stream().map(UserRoleRelDO::getRoleId).toList());
Map<Long, RoleDO> roleId2RoleDO = roleDOS.stream().collect(Collectors.toMap(RoleDO::getId, roleDO -> roleDO));
return userIds.stream().collect(Collectors.toMap(userId -> userId, userId -> roleId2RoleDO.get(userId2UserRoleRelDOs.get(userId).getRoleId())));
}
}

View File

@@ -1,19 +1,23 @@
package com.yinlihupo.enlish.service.service.student; package com.yinlihupo.enlish.service.service.student;
import com.yinlihupo.enlish.service.domain.dataobject.ClassDO; import com.yinlihupo.enlish.service.constant.StudentConstant;
import com.yinlihupo.enlish.service.domain.dataobject.GradeDO; import com.yinlihupo.enlish.service.domain.dataobject.*;
import com.yinlihupo.enlish.service.domain.dataobject.StudentDO;
import com.yinlihupo.enlish.service.domain.mapper.*; import com.yinlihupo.enlish.service.domain.mapper.*;
import com.yinlihupo.enlish.service.model.bo.StudentDetail; import com.yinlihupo.enlish.service.model.bo.StudentDetail;
import com.yinlihupo.enlish.service.model.bo.exam.ExamWordsJudgeResultDetail;
import com.yinlihupo.enlish.service.model.bo.exam.WordMasteryDetail;
import com.yinlihupo.enlish.service.model.vo.student.AddStudentReqVO; import com.yinlihupo.enlish.service.model.vo.student.AddStudentReqVO;
import com.yinlihupo.enlish.service.service.StudentService; import com.yinlihupo.enlish.service.service.StudentService;
import com.yinlihupo.enlish.service.utils.DifyClient;
import com.yinlihupo.framework.common.util.JsonUtils;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.*;
import java.util.Map; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Service @Service
@@ -29,6 +33,14 @@ public class StudentServiceImpl implements StudentService {
private VocabularyBankDOMapper vocabularyBankMapper; private VocabularyBankDOMapper vocabularyBankMapper;
@Resource @Resource
private WordMasteryLogDOMapper wordMasteryLogDOMapper; private WordMasteryLogDOMapper wordMasteryLogDOMapper;
@Resource
private ExamWordsJudgeResultDOMapper examWordsJudgeResultDOMapper;
@Resource
private DifyClient difyClient;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private StudentStageLearningRemarkDOMapper studentStageLearningRemarkDOMapper;
@Override @Override
public List<StudentDO> getStudentsByClassIdAndGradeId(Integer classId, Integer gradeId, String name, Integer pageNo, Integer pageSize) { public List<StudentDO> getStudentsByClassIdAndGradeId(Integer classId, Integer gradeId, String name, Integer pageNo, Integer pageSize) {
@@ -81,6 +93,7 @@ public class StudentServiceImpl implements StudentService {
.name(name) .name(name)
.classId(classId) .classId(classId)
.gradeId(gradeId) .gradeId(gradeId)
.actualGradeId(gradeId)
.startTime(createTime) .startTime(createTime)
.build(); .build();
studentDOMapper.insert(studentDO); studentDOMapper.insert(studentDO);
@@ -93,4 +106,94 @@ public class StudentServiceImpl implements StudentService {
public void deleteStudent(Integer studentId) { public void deleteStudent(Integer studentId) {
studentDOMapper.deleteById(studentId); studentDOMapper.deleteById(studentId);
} }
@Override
public String analyzeStudentStudy(Integer studentId) {
String key = StudentConstant.buildAnalyzeStudentStudyKey(studentId);
if (redisTemplate.hasKey(key)) {
Object ans = redisTemplate.opsForValue().get(key);
return JsonUtils.toJsonString(ans);
}
List<ExamWordsJudgeResultDO> examWordsJudgeResultDOS = examWordsJudgeResultDOMapper.selectByStudentIdAndLimitTime(studentId);
List<Integer> wordIds = new java.util.ArrayList<>(examWordsJudgeResultDOS.stream().map(ExamWordsJudgeResultDO::getCorrectWordIds).flatMap(List::stream).toList());
wordIds.addAll(examWordsJudgeResultDOS.stream().map(ExamWordsJudgeResultDO::getWrongWordIds).flatMap(List::stream).toList());
List<VocabularyBankDO> vocabularyBankDOS = new ArrayList<>();
if (!wordIds.isEmpty()) {
vocabularyBankDOS = vocabularyBankMapper.selectVocabularyBankDOListByIds(wordIds);
}
Map<Integer, VocabularyBankDO> id2Word = vocabularyBankDOS.stream().collect(Collectors.toMap(VocabularyBankDO::getId, vocabularyBankDO -> vocabularyBankDO));
List<ExamWordsJudgeResultDetail> examWordsJudgeResultDetails = new ArrayList<>();
for (ExamWordsJudgeResultDO examWordsJudgeResultDO : examWordsJudgeResultDOS) {
List<Integer> correctWordIds = examWordsJudgeResultDO.getCorrectWordIds();
List<String> correctWords = correctWordIds.stream().map(id2Word::get).map(VocabularyBankDO::getWord).toList();
List<Integer> wrongWordIds = examWordsJudgeResultDO.getWrongWordIds();
List<String> wrongWords = wrongWordIds.stream().map(id2Word::get).map(VocabularyBankDO::getWord).toList();
examWordsJudgeResultDetails.add(ExamWordsJudgeResultDetail.builder()
.correctWordCount(examWordsJudgeResultDO.getCorrectWordCount())
.wrongWordCount(examWordsJudgeResultDO.getWrongWordCount())
.startDate(examWordsJudgeResultDO.getStartDate())
.correctWords(correctWords)
.wrongWords(wrongWords)
.msg(examWordsJudgeResultDO.getMsg()).build()
);
}
Map<String, Object> studentStudyInfo = new HashMap<>();
studentStudyInfo.put("考试记录", examWordsJudgeResultDetails);
List<WordMasteryLogDO> wordMasteryLogDOS = wordMasteryLogDOMapper.selectByStudentIdAndLimitTime(studentId);
List<VocabularyBankDO> masteredWords = vocabularyBankMapper.selectVocabularyBankDOListByIds(wordMasteryLogDOS.stream().map(WordMasteryLogDO::getWordId).toList());
Map<Integer, VocabularyBankDO> id2MasteryWord = masteredWords.stream().collect(Collectors.toMap(VocabularyBankDO::getId, vocabularyBankDO -> vocabularyBankDO));
List<WordMasteryDetail> wordMasteryDetails = new ArrayList<>();
for (WordMasteryLogDO wordMasteryLogDO : wordMasteryLogDOS) {
wordMasteryDetails.add(WordMasteryDetail.builder()
.word(id2MasteryWord.get(wordMasteryLogDO.getWordId()).getWord())
.reviewCount(wordMasteryLogDO.getReviewCount())
.memoryStrength(wordMasteryLogDO.getMemoryStrength())
.update_time(wordMasteryLogDO.getUpdate_time())
.build());
}
studentStudyInfo.put("单词掌握情况", wordMasteryDetails);
try {
String analyze = difyClient.sendStudentAnalyze(JsonUtils.toJsonString(studentStudyInfo)).getAnswer();
// 设置过期时间 3 天
redisTemplate.opsForValue().set(key, analyze);
redisTemplate.expire(key, 3, TimeUnit.DAYS);
studentStageLearningRemarkDOMapper.insert(StudentStageLearningRemarkDO.builder()
.studentId(studentId)
.commentContent(analyze)
.createTime(LocalDateTime.now())
.build());
return analyze;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public List<WordMasteryDetail> findStudentWordMasteryDetail(Integer studentId) {
List<WordMasteryLogDO> wordMasteryLogDOS = wordMasteryLogDOMapper.selectAllByStudentId(studentId);
List<VocabularyBankDO> masteredWords = vocabularyBankMapper.selectVocabularyBankDOListByIds(wordMasteryLogDOS.stream().map(WordMasteryLogDO::getWordId).toList());
Map<Integer, VocabularyBankDO> id2MasteryWord = masteredWords.stream().collect(Collectors.toMap(VocabularyBankDO::getId, vocabularyBankDO -> vocabularyBankDO));
List<WordMasteryDetail> wordMasteryDetails = new ArrayList<>();
for (WordMasteryLogDO wordMasteryLogDO : wordMasteryLogDOS) {
wordMasteryDetails.add(WordMasteryDetail.builder()
.word(id2MasteryWord.get(wordMasteryLogDO.getWordId()).getWord())
.reviewCount(wordMasteryLogDO.getReviewCount())
.memoryStrength(wordMasteryLogDO.getMemoryStrength())
.update_time(wordMasteryLogDO.getUpdate_time())
.build());
}
return wordMasteryDetails;
}
} }

View File

@@ -0,0 +1,69 @@
package com.yinlihupo.enlish.service.service.user;
import cn.dev33.satoken.stp.StpUtil;
import com.yinlihupo.enlish.service.constant.UserRedisConstants;
import com.yinlihupo.enlish.service.domain.dataobject.UserDO;
import com.yinlihupo.enlish.service.domain.mapper.UserDOMapper;
import com.yinlihupo.enlish.service.service.UserService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Objects;
@Service
@Slf4j
public class UserServiceImpl implements UserService {
@Resource
private UserDOMapper userDOMapper;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private PasswordEncoder passwordEncoder;
@Override
public UserDO findUser() {
String loginIdStr =(String) StpUtil.getLoginId();
Long loginId = Long.parseLong(loginIdStr);
return userDOMapper.selectById(loginId);
}
@Override
public List<UserDO> findUsersList(int page, int limit, String name) {
return userDOMapper.selectUserDOList(name, (page - 1) * limit, limit);
}
@Override
public Integer findUserTotal() {
return userDOMapper.selectUserTotal();
}
@Override
public void createUser(UserDO userDO) {
userDOMapper.insert(userDO);
}
@Override
public void updateUserInfo(String password, String reqCode, String phone, String name) {
long id = Integer.parseInt(String.valueOf(StpUtil.getLoginId()));
UserDO userDO = userDOMapper.selectById(id);
String key = UserRedisConstants.buildUserLoginCode(userDO.getPhone());
String code = Objects.requireNonNull(redisTemplate.opsForValue().get(key)).toString();
if (code == null || !code.equals(reqCode)) {
throw new RuntimeException("验证码错误");
}
if (password != null) {
password = passwordEncoder.encode(password);
}
userDOMapper.updateUserInfo(id, name, password, phone);
}
}

View File

@@ -2,6 +2,8 @@ package com.yinlihupo.enlish.service.service.vocabulary;
import com.yinlihupo.enlish.service.domain.dataobject.VocabularyBankDO; import com.yinlihupo.enlish.service.domain.dataobject.VocabularyBankDO;
import com.yinlihupo.enlish.service.domain.mapper.VocabularyBankDOMapper; import com.yinlihupo.enlish.service.domain.mapper.VocabularyBankDOMapper;
import com.yinlihupo.enlish.service.domain.mapper.WordMasteryLogDOMapper;
import com.yinlihupo.enlish.service.model.vo.vocabulary.FindStudentWordDetailRspVO;
import com.yinlihupo.enlish.service.service.VocabularyService; import com.yinlihupo.enlish.service.service.VocabularyService;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -14,9 +16,19 @@ public class VocabularyServiceImpl implements VocabularyService {
@Resource @Resource
private VocabularyBankDOMapper vocabularyBankDOMapper; private VocabularyBankDOMapper vocabularyBankDOMapper;
@Resource
private WordMasteryLogDOMapper wordMasteryLogDOMapper;
@Override @Override
public List<VocabularyBankDO> findVocabularyBankDOListById(List<Integer> ids) { public List<VocabularyBankDO> findVocabularyBankDOListById(List<Integer> ids) {
return vocabularyBankDOMapper.selectVocabularyBankDOListByIds(ids); return vocabularyBankDOMapper.selectVocabularyBankDOListByIds(ids);
} }
@Override
public FindStudentWordDetailRspVO findStudentWordDetail(Integer studentId) {
Integer wordMastery = wordMasteryLogDOMapper.selectMasteryCount(studentId);
Integer wordNotMastery = wordMasteryLogDOMapper.selectNotMasteryCount(studentId);
Integer total = vocabularyBankDOMapper.selectWordTotal();
return FindStudentWordDetailRspVO.builder().masteredWordCount(wordMastery).unmasteredWordCount(wordNotMastery).pendingReviewWordCount(total - wordMastery - wordNotMastery).build();
}
} }

View File

@@ -1,13 +1,13 @@
package com.yinlihupo.enlish.service.job; package com.yinlihupo.enlish.service.task;
import com.yinlihupo.enlish.service.service.ExamWordsJudgeService; import com.yinlihupo.enlish.service.service.ExamWordsJudgeService;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component @Component
@Slf4j
public class AutoJudgeExamWordsTask { public class AutoJudgeExamWordsTask {
@Resource @Resource
@@ -15,7 +15,9 @@ public class AutoJudgeExamWordsTask {
@Scheduled(fixedRate = 5000) @Scheduled(fixedRate = 5000)
public void autoJudgeExamWords() { public void autoJudgeExamWords() {
System.out.println("【固定频率】开始自动判卷,时间:" + LocalDateTime.now()); if (examWordsJudgeService.getExamUnfinishedCount() != 0) {
log.info("有试卷待检测,开始检测");
examWordsJudgeService.judgeExamWords(5); examWordsJudgeService.judgeExamWords(5);
} }
}
} }

View File

@@ -0,0 +1,23 @@
package com.yinlihupo.enlish.service.task;
import com.yinlihupo.enlish.service.service.RoleService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class UserRoleTask {
@Resource
private RoleService roleService;
@Scheduled(cron = "0 0 1 * * ?")
public void PushRolePermissions2Redis() {
log.info("定时任务,将系统权限推送到 redis 中");
roleService.pushRolePermission2Redis();
}
}

View File

@@ -3,6 +3,9 @@ package com.yinlihupo.enlish.service.utils; // 修改为你的包名
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.yinlihupo.enlish.service.domain.dataobject.VocabularyBankDO;
import com.yinlihupo.enlish.service.model.bo.Sentence;
import com.yinlihupo.framework.common.util.JsonUtils;
import lombok.Data; import lombok.Data;
import lombok.ToString; import lombok.ToString;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@@ -14,20 +17,25 @@ import java.net.http.HttpRequest;
import java.net.http.HttpResponse; import java.net.http.HttpResponse;
import java.time.Duration; import java.time.Duration;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
@Component @Component
public class DifyArticleClient { public class DifyClient {
@Value("${ai.key}") @Value("${ai.key}")
private String apiKey; private String apiKey;
@Value("${ai.analyzeKey}")
private String analyzeKey;
@Value("${ai.sentenceKey}")
private String sentenceKey;
@Value("${ai.url}") @Value("${ai.url}")
private String baseUrl; private String baseUrl;
private final HttpClient httpClient; private final HttpClient httpClient;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
// 构造函数 // 构造函数
public DifyArticleClient() { public DifyClient() {
this.httpClient = HttpClient.newBuilder() this.httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10)) // 连接超时 .connectTimeout(Duration.ofSeconds(10)) // 连接超时
@@ -35,6 +43,82 @@ public class DifyArticleClient {
this.objectMapper = new ObjectMapper(); this.objectMapper = new ObjectMapper();
} }
public List<Sentence> sendSentenceAnalyze(List<VocabularyBankDO> list, String grade) throws Exception {
// 1. 构建请求体对象
ChatRequest payload = new ChatRequest();
payload.setQuery(JsonUtils.toJsonString(list.stream().map(VocabularyBankDO::getWord).toList()));
payload.setUser("admin");
HashMap<String, Object> objectObjectHashMap = new HashMap<>();
objectObjectHashMap.put("grade", grade);
payload.setResponseMode("blocking"); // 使用阻塞模式一次性返回
payload.setInputs(objectObjectHashMap);
// 2. 序列化为 JSON 字符串
String jsonBody = objectMapper.writeValueAsString(payload);
// 3. 构建 HTTP 请求
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl))
.header("Authorization", "Bearer " + sentenceKey)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.timeout(Duration.ofSeconds(30)) // 读取超时
.build();
// 4. 发送请求
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
// 5. 检查状态码
if (response.statusCode() != 200) {
throw new RuntimeException("Dify 请求失败: HTTP " + response.statusCode() + " | Body: " + response.body());
}
// 6. 反序列化响应体
DifyResponse difyResponse = objectMapper.readValue(response.body(), DifyResponse.class);
String answer = difyResponse.getAnswer();
answer = answer.replace("json", "");
answer = answer.replace("```", "");
return JsonUtils.parseList(answer, Sentence.class);
}
public DifyResponse sendStudentAnalyze(String query) throws Exception {
String endpoint = this.baseUrl;
// 1. 构建请求体对象
ChatRequest payload = new ChatRequest();
payload.setQuery(query);
payload.setUser(String.valueOf(1));
payload.setResponseMode("blocking"); // 使用阻塞模式一次性返回
payload.setInputs(new HashMap<>());
// 2. 序列化为 JSON 字符串
String jsonBody = objectMapper.writeValueAsString(payload);
// 3. 构建 HTTP 请求
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(endpoint))
.header("Authorization", "Bearer " + analyzeKey)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.timeout(Duration.ofSeconds(30)) // 读取超时
.build();
// 4. 发送请求
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
// 5. 检查状态码
if (response.statusCode() != 200) {
throw new RuntimeException("Dify 请求失败: HTTP " + response.statusCode() + " | Body: " + response.body());
}
// 6. 反序列化响应体
return objectMapper.readValue(response.body(), DifyResponse.class);
}
/** /**
* 发送对话请求 (阻塞模式) * 发送对话请求 (阻塞模式)
* *
@@ -64,7 +148,7 @@ public class DifyArticleClient {
// 3. 构建 HTTP 请求 // 3. 构建 HTTP 请求
HttpRequest request = HttpRequest.newBuilder() HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(endpoint)) .uri(URI.create(endpoint))
.header("Authorization", "Bearer " + this.apiKey) .header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody)) .POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.timeout(Duration.ofSeconds(30)) // 读取超时 .timeout(Duration.ofSeconds(30)) // 读取超时

View File

@@ -3,7 +3,6 @@ package com.yinlihupo.enlish.service.utils;
import com.yinlihupo.enlish.service.constant.ExamWordsConstant; import com.yinlihupo.enlish.service.constant.ExamWordsConstant;
import com.yinlihupo.enlish.service.model.bo.CoordinatesXY; import com.yinlihupo.enlish.service.model.bo.CoordinatesXY;
import com.yinlihupo.enlish.service.model.bo.StudentExamId; import com.yinlihupo.enlish.service.model.bo.StudentExamId;
import com.yinlihupo.enlish.service.model.bo.Word;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.sourceforge.tess4j.ITesseract; import net.sourceforge.tess4j.ITesseract;
import net.sourceforge.tess4j.Tesseract; import net.sourceforge.tess4j.Tesseract;
@@ -13,10 +12,10 @@ import org.checkerframework.checker.nullness.qual.NonNull;
import org.opencv.core.*; import org.opencv.core.*;
import org.opencv.imgcodecs.Imgcodecs; import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc; import org.opencv.imgproc.Imgproc;
import org.springframework.beans.factory.annotation.Value;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte; import java.awt.image.DataBufferByte;
import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
@@ -33,29 +32,11 @@ public class PngUtil {
// 获取起始坐标 // 获取起始坐标
public static List<CoordinatesXY> analysisXY(String imagePath) { public static List<CoordinatesXY> analysisXY(String imagePath) {
Mat binary = image2BinaryMath(imagePath);
Mat src = Imgcodecs.imread(imagePath); Mat src = Imgcodecs.imread(imagePath);
if (src.empty()) {
System.out.println("无法读取图片,请检查路径。");
return null;
}
// 3. 预处理
// 3.1 转换为灰度图
Mat gray = new Mat();
Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR2GRAY);
// 3.2 二值化处理 (Thresholding)
// 使用 THRESH_BINARY_INV (反转二值化),因为我们需要找的是白色背景上的黑色块。
// 反转后,黑色块变成白色(255),背景变成黑色(0),方便 findContours 查找。
Mat binary = new Mat();
// 阈值设为 50 左右即可,因为块是纯黑的
Imgproc.threshold(gray, binary, 50, 255, Imgproc.THRESH_BINARY_INV);
// 4. 查找轮廓 // 4. 查找轮廓
List<MatOfPoint> contours = new ArrayList<>(); List<MatOfPoint> contours = new ArrayList<>();
Mat hierarchy = new Mat(); Mat hierarchy = new Mat();
// RETR_EXTERNAL 只检测最外层轮廓,忽略块内部可能存在的噪点
Imgproc.findContours(binary, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE); Imgproc.findContours(binary, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);
System.out.println("检测到的轮廓总数: " + contours.size()); System.out.println("检测到的轮廓总数: " + contours.size());
@@ -91,67 +72,44 @@ public class PngUtil {
System.out.println("------------------------------------------------"); System.out.println("------------------------------------------------");
list.add(CoordinatesXY.builder().x(rect.x).y(rect.y).width(rect.width).height(rect.height).build()); list.add(CoordinatesXY.builder().x(rect.x).y(rect.y).width(rect.width).height(rect.height).build());
// 可选:在原图上画出框,用于调试验证 // 可选:在原图上画出框,用于调试验证
// Imgproc.rectangle(src, rect, new Scalar(0, 0, 255), 2); // 红色框 Imgproc.rectangle(src, rect, new Scalar(0, 0, 255), 2); // 红色框
// Imgproc.putText(src, "#" + blockCount, new Point(rect.x, rect.y - 5), Imgproc.FONT_HERSHEY_SIMPLEX, 0.5, new Scalar(0, 0, 255), 1); Imgproc.putText(src, "#" + blockCount, new Point(rect.x, rect.y - 5), Imgproc.FONT_HERSHEY_SIMPLEX, 0.5, new Scalar(0, 0, 255), 1);
// Imgcodecs.imwrite("output_red.png", src);
} }
} }
Imgcodecs.imwrite("output_red.png", src);
System.out.println("找到 " + blockCount + " 个黑色块。"); System.out.println("找到 " + blockCount + " 个黑色块。");
// 获取每一列的宽度 // 计算起始坐标
list.sort(Comparator.comparingInt(CoordinatesXY::getHeight));
int height = list.get(list.size() - 1).getHeight() / ExamWordsConstant.PGN_COL;
// 删除两列答题卡区块
list.sort(Comparator.comparingInt(CoordinatesXY::getWidth));
list.remove(list.size() - 1);
list.remove(list.size() - 1);
list.sort(Comparator.comparingInt(CoordinatesXY::getX)); list.sort(Comparator.comparingInt(CoordinatesXY::getX));
// 计算起始坐标 list.forEach(coordinatesXY -> coordinatesXY.setHeight(coordinatesXY.getHeight() / 51));
List<CoordinatesXY> ans = getCoordinatesXIES(list, height); list.forEach(coordinatesXY -> coordinatesXY.setWidth(coordinatesXY.getWidth() / 3));
list.forEach(coordinatesXY -> coordinatesXY.setX(coordinatesXY.getX() + coordinatesXY.getWidth() * 2));
src.release(); log.info("起始坐标: {}", list);
binary.release();
hierarchy.release();
binary.release();
return ans; return list;
} }
// 获取(未背熟)单词的 id // 获取(未背熟)单词的 id
public static List<Integer> analyzePngForUnmemorizedWordIds(String filePath, List<Integer> wordIds, List<CoordinatesXY> coordinatesXYList) { public static List<Integer> analyzePngForUnmemorizedWordIds(String filePath, List<Integer> wordIds, List<CoordinatesXY> coordinatesXYList) {
Mat src = Imgcodecs.imread(filePath);
if (src.empty()) {
log.error("无法读取图片,请检查路径: {}", filePath);
throw new RuntimeException("无法读取图片");
}
Mat gray = new Mat(); Mat binary = image2BinaryMath(filePath);
Mat binary = new Mat();
try { try {
Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR2GRAY);
// 建议:如果光照不均匀,考虑使用 THRESH_OTSU 自动阈值,或者自适应阈值
Imgproc.threshold(gray, binary, 150, 255, Imgproc.THRESH_BINARY_INV);
// 调试时打印
// Imgcodecs.imwrite("output_binary.png", binary);
List<Integer> answer = new ArrayList<>(); List<Integer> answer = new ArrayList<>();
int words_index = 0; int words_index = 0;
for (int i = 0; i < coordinatesXYList.size(); i++) { for (CoordinatesXY coordinatesXY : coordinatesXYList) {
CoordinatesXY coordinatesXY = coordinatesXYList.get(i);
int width = coordinatesXY.getWidth(); int width = coordinatesXY.getWidth();
int height = coordinatesXY.getHeight(); int height = coordinatesXY.getHeight();
int currentX = coordinatesXY.getX(); int currentX = coordinatesXY.getX();
int currentY = coordinatesXY.getY(); int currentY = coordinatesXY.getY() + height;
int count = i == 0 ? ExamWordsConstant.PGN_COL - 1 : ExamWordsConstant.PGN_COL;
// 内层循环:遍历这一列的每一行 // 内层循环:遍历这一列的每一行
for (int j = 0; j < count; j++) { for (int j = 0; j < 50; j++) {
// 安全检查:防止单词列表比格子少导致越界 // 安全检查:防止单词列表比格子少导致越界
if (words_index >= wordIds.size()) { if (words_index >= wordIds.size()) {
log.warn("单词列表耗尽,停止检测。格子数多于单词数。"); log.warn("单词列表耗尽,停止检测。格子数多于单词数。");
@@ -170,11 +128,14 @@ public class PngUtil {
Rect rect = new Rect(currentX + 1, currentY + 1, width - 2, height - 2); Rect rect = new Rect(currentX + 1, currentY + 1, width - 2, height - 2);
Mat region = binary.submat(rect); Mat region = binary.submat(rect);
int countNonZero = Core.countNonZero(region); int countNonZero = Core.countNonZero(region);
log.info("当前位置为 words_index={},坐标为 x={} y={} 当前区域非零像素数: {}", words_index, currentX, currentY, countNonZero);
if (countNonZero > 800) { if (countNonZero > 1000) {
Integer id = wordIds.get(words_index); Integer id = wordIds.get(words_index);
answer.add(id); answer.add(id);
log.info("检测到标记未背熟ID={}", id); log.info("检测到标记未背熟ID={}, 当前坐标 x = {} y = {} ", id, currentX + 1, currentY + 1);
}
if (countNonZero == 0) {
break;
} }
region.release(); region.release();
@@ -187,8 +148,6 @@ public class PngUtil {
} finally { } finally {
src.release();
gray.release();
binary.release(); binary.release();
} }
} }
@@ -208,21 +167,21 @@ public class PngUtil {
Rect roiRect = new Rect(0, 0, left.getX(), left.getY()); Rect roiRect = new Rect(0, 0, left.getX(), left.getY());
Mat roi = new Mat(src, roiRect); Mat roi = new Mat(src, roiRect);
// 3. 图像预处理 (提高 OCR 准确率) // // 3. 图像预处理 (提高 OCR 准确率)
// 3.1 转为灰度图 // // 3.1 转为灰度图
Mat gray = new Mat(); // Mat gray = new Mat();
Imgproc.cvtColor(roi, gray, Imgproc.COLOR_BGR2GRAY); // Imgproc.cvtColor(roi, gray, Imgproc.COLOR_BGR2GRAY);
//
// 3.2 二值化 (Thresholding) // // 3.2 二值化 (Thresholding)
// 使用 OTSU 算法自动寻找最佳阈值,或者手动指定阈值 // // 使用 OTSU 算法自动寻找最佳阈值,或者手动指定阈值
Mat binary = new Mat(); // Mat binary = new Mat();
Imgproc.threshold(gray, binary, 0, 255, Imgproc.THRESH_BINARY | Imgproc.THRESH_OTSU); // Imgproc.threshold(gray, binary, 0, 255, Imgproc.THRESH_BINARY | Imgproc.THRESH_OTSU);
// 可选:保存预处理后的图片查看效果 // 可选:保存预处理后的图片查看效果
// Imgcodecs.imwrite("debug_roi.jpg", binary); Imgcodecs.imwrite("debug_roi.jpg", src);
// 4. 将 OpenCV Mat 转换为 BufferedImage (供 Tess4J 使用) // 4. 将 OpenCV Mat 转换为 BufferedImage (供 Tess4J 使用)
BufferedImage processedImage = matToBufferedImage(binary); BufferedImage processedImage = matToBufferedImage(src);
// 5. 使用 Tesseract 进行 OCR 识别 // 5. 使用 Tesseract 进行 OCR 识别
ITesseract instance = new Tesseract(); ITesseract instance = new Tesseract();
@@ -247,6 +206,50 @@ public class PngUtil {
return null; return null;
} }
private static Mat image2BinaryMath(String imagePath) {
if (!new File(imagePath).exists()) {
log.error("图片不存在,请检查路径: {}", imagePath);
throw new RuntimeException("图片不存在");
}
Mat src = Imgcodecs.imread(imagePath);
if (src.empty()) {
log.info("无法读取图片,请检查路径: {}", imagePath);
throw new RuntimeException("无法读取图片");
}
Mat gray = new Mat();
//转换为灰度图
Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR2GRAY);
Imgproc.GaussianBlur(gray, gray, new Size(5, 5), 0);
Mat binary = new Mat();
Imgproc.adaptiveThreshold(gray, binary, 255,
Imgproc.ADAPTIVE_THRESH_GAUSSIAN_C,
Imgproc.THRESH_BINARY_INV, 25, 10);
Mat kernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(3, 3));
//开运算 (Open):先腐蚀后膨胀,用于去除背景中的微小噪点
Imgproc.morphologyEx(binary, binary, Imgproc.MORPH_OPEN, kernel);
// 闭运算 (Close):先膨胀后腐蚀,用于连接断裂的区域并填充块内部的空洞
// 如果块比较大且内部反光严重,可以将 Size(3,3) 改为 Size(5,5) 或更大
Mat closeKernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(7, 7));
Imgproc.morphologyEx(binary, binary, Imgproc.MORPH_CLOSE, closeKernel);
// 保存二值化过程图用于调试 (生产环境可注释)
Imgcodecs.imwrite("debug_binary_natural.png", binary);
src.release();
gray.release();
return binary;
}
private static @NonNull StudentExamId getStudentExamId(Pattern pattern, String result) { private static @NonNull StudentExamId getStudentExamId(Pattern pattern, String result) {
Matcher matcher = pattern.matcher(result); Matcher matcher = pattern.matcher(result);
StudentExamId studentExamId = new StudentExamId(0, 0); StudentExamId studentExamId = new StudentExamId(0, 0);
@@ -278,21 +281,4 @@ public class PngUtil {
return image; return image;
} }
private static @NonNull List<CoordinatesXY> getCoordinatesXIES(List<CoordinatesXY> list, int height) {
List<CoordinatesXY> ans = new ArrayList<>();
CoordinatesXY left = new CoordinatesXY();
left.setX(list.get(1).getX());
left.setWidth(list.get(1).getWidth());
left.setHeight(height);
left.setY(list.get(0).getY() + left.getHeight());
ans.add(left);
CoordinatesXY right = new CoordinatesXY();
right.setX(list.get(2).getX());
right.setY(list.get(0).getY());
right.setWidth(list.get(1).getWidth());
right.setHeight(height);
ans.add(right);
return ans;
}
} }

Some files were not shown because too many files have changed in this diff Show More