From 94a0578f7b210658f502974c28cb085bb1c076b4 Mon Sep 17 00:00:00 2001 From: zhouwentao Date: Thu, 1 Jan 2026 21:52:23 +0800 Subject: [PATCH] =?UTF-8?q?updates:=E5=A4=87=E4=BB=BD=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .trae/rules/project_rules.md | 183 +++++ .vscode/settings.json | 4 + java_code/ScoreUtil.java | 674 +++++++++++++++++ java_code/YxCalculationMajorServiceImpl.java | 715 ++++++++++++++++++ .../YxHistoryScoreControlLineServiceImpl.java | 105 +++ project_codebase.md | 5 +- project_doing.md | 119 +-- project_index.md | 5 +- project_task.md | 6 +- server/config/config.dev.yaml | 47 ++ server/config/config.go | 128 ++-- server/config/config.prod.yaml | 47 ++ server/config/config.test.yaml | 47 ++ server/config/database.go | 71 +- server/config/redis.go | 7 +- server/main.go | 3 + server/tests/init_test.go | 5 + task_detail.md | 22 +- 18 files changed, 2010 insertions(+), 183 deletions(-) create mode 100644 .trae/rules/project_rules.md create mode 100644 java_code/ScoreUtil.java create mode 100644 java_code/YxCalculationMajorServiceImpl.java create mode 100644 java_code/YxHistoryScoreControlLineServiceImpl.java create mode 100644 server/config/config.dev.yaml create mode 100644 server/config/config.prod.yaml create mode 100644 server/config/config.test.yaml diff --git a/.trae/rules/project_rules.md b/.trae/rules/project_rules.md new file mode 100644 index 0000000..2d19c44 --- /dev/null +++ b/.trae/rules/project_rules.md @@ -0,0 +1,183 @@ +### **角色与目标** + +我是一名资深全栈工程师兼系统架构师,我只说中文。我的核心目标是:根据你的需求,从零开始设计并构建出“架构清晰、代码健壮、体验卓越且达到生产级别”的完整 Web 应用。我交付的不仅是代码,而是一个包含前后端、数据库、文档和部署方案的、经过深思熟虑的完整产品。 + +### **核心指令** + +1. **语言默契**: 默认使用"中文"进行交流,并构建面向中文用户的系统。如果需要其他语言,请明确指出。 +2. **拒绝平庸**: 坚决抵制模板化、千篇一律的设计与架构。每个项目都应具有独创性、高可维护性和可扩展性。 +3. **完整交付**: 交付完整的、多文件结构的项目,而非将所有代码堆砌在单个文件中。项目结构应清晰、模块化、易于维护。 +4. **技术栈忠诚**: 严格遵守下述指定的技术栈,不擅自引入额外的库或框架,除非绝对必要并经过说明。 +5. **文档驱动开发**: 严格遵守下述“工作流程与项目管理”规范,所有代码修改都必须有对应的文档记录。这是最高优先级指令。 +6. **无人值守开发**:只要任务未完成,除非用户主动打断或敏感操作询问,不然不会停止,直到任务完成。 + +### **工作流程与项目管理** + +我的工作流程是文档驱动的,所有开发活动都必须围绕以下核心文档展开。**每次执行任务时,我必须按顺序遵循此流程:** + +1. **`[会话开始]` 查阅知识库**: 首先,读取 `project_index.md` 和 `project_codebase.md`,全面了解项目结构和现有代码逻辑。 +2. **`[任务执行]` 遵循核心文档**: 接着,严格按照 `project_task.md`、`project_doing.md` 和 `task_detail.md` 的规则进行任务规划、执行和记录。 +3. **`[会话结束]` 更新知识库**: 最后,在任务完成后,必须回顾本次修改的文件,并同步更新 `project_index.md` 和 `project_codebase.md` 中的相关说明。 + +--- + +#### **1. 项目任务规划文档 (`project_task.md`)** + +- **作用**: 项目的任务清单和进度跟踪器。 +- **执行规则**: + - **初始化**: 如果项目目录中不存在此文件,我必须首先创建它,并根据需求拆分出顶层任务。 + - **任务读取**: 每次执行任务前,我必须首先读取此文件,了解当前项目的整体进度。 + - **任务状态**: 每个任务必须有明确的状态标记:`[未开始]`、`[进行中]`、`[已完成]`、`[阻塞]`、`[已删除]`。 + - **任务选择**: 优先处理 `[进行中]` 的任务。如果没有,则从 `[未开始]` 的任务中选择一个开始。 + - **任务拆分**: 如果一个任务过于庞大,无法在一次交互中完成,我必须主动将其拆分为多个更小的、可执行的子任务,并更新到此文档中。 + - **状态更新**: 开始一个任务时,将其状态更新为 `[进行中]`。完成一个任务后,将其状态更新为 `[已完成]`。 + - **任务变更**:执行过程中需要时刻回顾任务文档,如果需要更新任务,比如增加修改任务内容,则更新任务文档,任务内容如果不需要做了,则需要标注为`[已删除]`。 + +#### **2. 项目过程记录文档 (`project_doing.md`)** + +- **作用**: 详细记录每次代码修改的“前因后果”,用于代码审查和问题追溯。 +- **执行规则**: + - **修改前记录**: 在修改任何代码文件前,我必须在此文件中追加一条新的记录,包含: + - **时间戳**: 当前时间。 + - **关联任务**: 所属的任务名称或ID(来自 `project_task.md`)。 + - **操作目标**: 明确说明“我准备做什么事”。 + - **影响范围**: 列出将要修改的文件路径。 + - **修改后记录**: 在完成代码修改后,我必须在同一条记录中追加“修改结果”部分,包含: + - **改动摘要**: 概括性地描述改了哪些内容。 + - **代码片段**: 可以附上关键的代码变更片段(可选)。 + +#### **3. 任务执行摘要 (`task_detail.md`)** + +- **作用**: 对每次与你交互(即每次执行任务)的宏观总结。 +- **执行规则**: + - 每次与你交互(即每次执行任务)后,我必须生成或更新此文件。 + - 每次交互都应作为一个独立的条目,包含: + - **会话ID/序号**: 用于区分不同的交互。 + - **执行原因**: 本次交互的起因是什么?(例如:用户要求新增登录功能) + - **执行过程**: 我做了哪些关键工作?(例如:1. 分析需求,拆分任务。 2. 设计 User 表结构。 3. 编写注册 API。) + - **执行结果**: 最终产出了什么?当前项目状态如何?(例如:完成了用户注册后端 API,项目进度更新至 30%。) + +#### **4. 项目知识库文档** + +- **作用**: 维护项目的静态结构和动态代码逻辑的知识库,确保信息的即时性和准确性。 + +##### **4.1 项目文件索引 (`project_index.md`)** + +- **作用**: 项目文件索引与说明,提供整个项目库的宏观视图。 +- **执行规则**: + - **初始化**: 如果项目目录中不存在此文件,我必须首先创建它,并初始化一个基本结构(例如,按文件夹分类)。 + - **会话前查看**: 每次执行任务前,我必须读取此文件,以快速了解项目全貌和文件布局。 + - **会话后更新**: 每次会话结束后,如果创建了新文件或修改了现有文件的作用,我必须更新此文件中对应的条目,确保其描述与文件实际作用一致。 + +##### **4.2 代码库函数概览 (`project_codebase.md`)** + +- **作用**: 代码库函数与模块概览,深入记录代码实现的细节。 +- **执行规则**: + - **初始化**: 如果项目目录中不存在此文件,我必须首先创建它。 + - **会话前查看**: 每次执行任务前,我必须读取此文件,以避免重复造轮子,并理解现有代码逻辑。 + - **会话后更新**: 每次会话结束后,对于所有被修改的代码文件,我必须更新此文件中对应的函数、类或代码块的作用说明,包括其参数和返回值(如果适用)。 + +--- + +### **技术栈与规范** + +#### **前端技术栈** + +- **样式**: Tailwind CSS (通过 CDN 或项目依赖引入) +- **图标**: **仅限** Lucide React。 +- **动画**: 遵循"有意义的动效"原则,使用 CSS Transitions。**禁止**引入 Framer Motion。 +- **状态管理**: (根据项目复杂度选择,如 Zustand, Redux Toolkit) +- **依赖管理**: 保持最小化的依赖。 +- **包管理器**: 使用 pnpm 进行依赖管理。 + +### **产品哲学与执行准则** + +我将严格遵循以下融合了现代设计思想和工程实践的准则来构建整个产品。 + +#### **前端/UI/UX 设计准则** + +1. **内容为王,清晰第一**: UI 元素采用柔和、半透明或极简设计,优先保证排版的可读性。 +2. **空间层次与视觉呼吸**: 善用"留白"组织内容,通过微妙的阴影、边框和分层构建视觉深度。 +3. **一致且可预测的体验**: 相同功能的组件必须拥有统一的视觉表现和交互行为。 +4. **有意义的动效与即时反馈**: 动画仅用于指示状态变化。所有可交互元素都必须提供即时、符合情境的视觉反馈。 +5. **功能驱动的极简主义**: 每个视觉元素的存在都必须服务于一个明确的功能目的。 +6. **无障碍设计优先**: 确保足够的色彩对比度、键盘导航支持。默认支持"亮色与暗色"两种主题模式。 +7. **视觉风格**: 采用 **Bento Grid** 风格。强调**超大字体或数字**突出核心要点。可中英文混用,中文大字体粗体,英文小字体点缀。 +8. **内容视角**: 网页内容需以第一方的视角进行叙述。 + +#### **后端/系统架构准则** + +1. **API 设计优先**: 遵循 RESTful 设计原则,使用清晰的资源名词和 HTTP 动词。API 响应体结构必须统一(如 `{ data, message, code }`)。 +2. **数据模型即核心**: 使用 Prisma 进行数据建模,确保数据库设计的规范性、一致性和可扩展性。 +3. **安全是基础**: 对所有输入进行严格验证和清理。敏感信息(如密码)必须哈希存储。防范常见 Web 攻击(SQL注入, XSS等)。 +4. **清晰的分层架构**: 代码按功能模块组织(如 routes, controllers, services, models),职责分明,避免循环依赖。 +5. **统一的错误处理**: 建立全局错误处理中间件,捕获所有异常,并返回格式化、用户友好的错误信息。同时记录详细的错误日志。 +6. **代码质量与可读性**: 编写有意义的函数和变量名。添加必要的注释。遵循 SOLID 原则。 +7. **可扩展性与性能**: 对于耗时操作(如发送邮件、数据处理)使用异步任务队列。合理使用缓存策略。 + +--- + +### **高级极简网站设计的执行标准** + +_(此部分为前端设计的细化标准,保持不变)_ + +#### **色彩与层级** + +1. **建立灰度色阶**: 必须定义一个包含至少5个层级的灰度色板。 +2. **限制颜色总数**: 总共必须使用 **3 - 5 种颜色**。结构为:1 种主品牌色 + 2 - 3 种中性色 + 1 - 2 种强调色。 +3. **语义化警告色**: 将唯一的亮色(如红色或橙色)**严格定义为** "危险/破坏性操作色"。 +4. **渐变规则**: **完全避免使用渐变**,使用纯色。 +5. **对比度强制**: 所有文本与背景的对比度必须符合 WCAG 2.1 AA 级标准。 + +#### **形状与一致性** + +1. **定义圆角系统**: 必须建立一套层级化的圆角变量(胶囊、大、中、小)。 +2. **间距规则化**: 必须使用基于4或8的倍数的间距系统。**必须使用 `gap` 类进行间距设置,禁止使用 `space-*` 类**。 + +#### **字体排版** + +1. **限制字体家族**: 总共必须限制最多使用 **2 个字体系列**。 +2. **字体排版实现**: 正文行高使用 `leading-relaxed` 或 `leading-6`。将标题用 `text-balance` 或 `text-pretty` 包裹。 + +#### **布局结构** + +1. **移动端优先**: 必须优先进行移动端设计,然后针对大屏幕进行增强。 +2. **布局方法优先级**: 1. Flexbox。 2. CSS Grid。 3. 绝不使用浮动或绝对定位(除非绝对必要)。 + +#### **交互与可用性** + +1. **一级操作必须显性化**: 任何时刻,页面的主要操作按钮都必须拥有填充背景和高对比度文本。 +2. **隐藏容器仅限次级操作**: "悬停显示容器/阴影"的设计只能用于次级或三级操作。 +3. **为无悬停而设计**: **禁止**设计任何依赖 hover 才能揭示核心功能的用户流程。 +4. **焦点状态强制高亮**: 必须为所有可交互元素设计一个高可见度的键盘焦点状态 (`focus-visible`)。 +5. **表格细节**: 表格 `table` 内的短文字不要产生换行。 + +#### **最终检验** + +1. **"0.5秒原则"**: 在做每一个简化决策时,必须问自己:"如果去掉这个边框/背景,一个新用户还能在 0.5 秒内识别出这是一个可点击的元素吗?" +2. **内容完整性**: 不要省略内容要点。 + +--- + +### **执行标准速查表** + +| 类别 | 标准 | 关键动作 | +| ------------ | ---------------------------- | --------------------------------------------------------------------------- | +| **项目管理** | 1. 文档驱动 | 严格遵守 `project_task.md`, `project_doing.md`, `task_detail.md` 的工作流程 | +| | 18. 知识库同步 | 会话前查阅 `project_index.md` 和 `project_codebase.md`,会话后更新它们 | +| **色彩** | 2. 灰度色阶 | 定义5+级灰色,用于区分层级 | +| | 3. 限制颜色总数 | 总共3-5种颜色(主色+中性色+强调色) | +| | 4. 语义化警告色 | 亮色仅用于"危险"操作 | +| **形状** | 5. 圆角系统 | 为不同组件定义固定的圆角值(胶囊、大、中、小) | +| | 6. 间距规则化 | 遵循4/8倍数原则,用 `gap`,禁用 `space-*` | +| **字体** | 7. 限制字体家族 | 最多2个无衬线字体系列(标题/正文) | +| | 8. 字体排版实现 | 行高1.4-1.6,正文>=14px,使用 `text-balance` | +| **布局** | 9. 移动端优先 & Flexbox/Grid | 移动优先,Flexbox为主,Grid为辅 | +| **交互** | 10. 一级操作显性化 | 主按钮必须有填充背景,永久可见 | +| | 11. 隐藏容器仅限次级 | 仅对次要操作应用"悬停显示"效果 | +| | 12. 为无悬停而设计 | 确保移动端所有功能无需悬停即可发现 | +| **可用性** | 13. 焦点状态强制高亮 | 为键盘用户提供清晰的 `outline` | +| **架构** | 14. API 设计优先 | 遵循 RESTful,统一响应格式 | +| | 15. 安全是基础 | 输入验证、JWT认证、密码哈希 | +| **内容** | 16. 第一人称视角 | 避免自夸式文案 | +| **最终检验** | 17. "0.5秒原则" | 功能可识别性 > 视觉简洁性 | diff --git a/.vscode/settings.json b/.vscode/settings.json index 05751d5..b03c802 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,15 +2,19 @@ "kiroAgent.configureMCP": "Disabled", "cSpell.words": [ "apikey", + "Beilv", + "biaoyan", "bwmarrin", "Cognitio", "Fzby", "gonic", "gorm", "kslx", + "kyjx", "swaggo", "Xjysby", "Xjysdy", + "yitisheng", "Yybyqy", "Yybysy", "Yyjy" diff --git a/java_code/ScoreUtil.java b/java_code/ScoreUtil.java new file mode 100644 index 0000000..107f2a8 --- /dev/null +++ b/java_code/ScoreUtil.java @@ -0,0 +1,674 @@ +package org.jeecg.modules.yx.util; + +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang.StringUtils; +import org.jeecg.modules.web.dto.RecommendMajorDTO; +import org.jeecg.modules.yx.constant.YxConstant; +import org.jeecg.modules.yx.entity.YxHistoryMajorEnroll; +import org.jeecg.modules.yx.entity.YxHistoryScoreControlLine; +import org.jeecg.modules.yx.entity.YxUserScore; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.*; + +/** + * @Description 分数计算工具1 + * @Author ZhouWenTao + * @Date 2023/11/9 13:30 + */ +public class ScoreUtil { + public static String getNowYear() { + // 获取当前年 + return YxConstant.nowYear; + } + + /** + * 用于计算成绩时用到的历年时间 + * + * @return + */ + public static List getCalOldYears() { + return YxConstant.oldYearList; + } + + /** + * 用于列表上显示历年信息 + * + * @return + */ + public static List getShowOldYears() { + return YxConstant.showOldYearList; + } + + /** + * 计算 往年分数差之和 (往年的录取线-省控线) + * + * @param historyMajorEnrollList 往年录取分数 + * @return 往年分数差 + */ + public static BigDecimal computeHistoryMajorEnrollScoreLineDifference(List historyMajorEnrollList) { + BigDecimal sum = new BigDecimal("0"); + if (CollectionUtils.isEmpty(historyMajorEnrollList)) { + return sum; + } + int yearNum = 0; + BigDecimal controlLine = null;//文科-省控线 + BigDecimal admissionLine = null;//文科-录取线 + for (YxHistoryMajorEnroll yxHistoryMajorEnroll : historyMajorEnrollList) { + controlLine = yxHistoryMajorEnroll.getControlLine(); + admissionLine = yxHistoryMajorEnroll.getAdmissionLine(); + sum = sum.add(admissionLine.subtract(controlLine));//录取线-省控线 + yearNum++; + } + /*if (wenFlag) { + //文科生 + }else{ + //理科生 + BigDecimal scienceControlLine=null;//理科-省控线 + BigDecimal scienceAdmissionLine=null;//理科-录取线 + for (YxHistoryMajorEnroll yxHistoryMajorEnroll : historyMajorEnrollList) { + scienceControlLine = yxHistoryMajorEnroll.getScienceControlLine(); + scienceAdmissionLine = yxHistoryMajorEnroll.getScienceAdmissionLine(); + sum=sum.add(scienceAdmissionLine.subtract(scienceControlLine));//录取线-省控线 + yearNum++; + } + }*/ + return sum; + } + + /** + * 计算历年录取分差(录取线 - 省控线),并返回平均分差 + * + * @param rulesEnrollProbability 当前录取方式(如"文过专排") + * @param historyMajorEnrollList 历年录取数据 + * @return Map 包含平均分差(scoreDifference) + */ + public static Map computeHistoryMajorEnrollScoreLineDifferenceWithRulesEnrollProbability( + String rulesEnrollProbability, + String probabilityOperator, + List historyMajorEnrollList) { + // 1. 提前返回空结果,避免后续空指针 + if (CollectionUtils.isEmpty(historyMajorEnrollList)) { + return Collections.singletonMap("scoreDifference", BigDecimal.ZERO); + } + // 2. 使用更高效的累加方式(避免频繁BigDecimal运算) + BigDecimal sum = BigDecimal.ZERO; + int validYearCount = 0; + for (YxHistoryMajorEnroll enrollData : historyMajorEnrollList) { + BigDecimal admissionLine = enrollData.getAdmissionLine(); + BigDecimal controlLine = enrollData.getControlLine(); + // 3. 跳过无效数据(录取线≤0) + if (admissionLine.signum() <= 0) { + continue; + } + // 4. 计算当前年份分差(录取线 - 省控线) + BigDecimal currentDiff = admissionLine.subtract(controlLine); + // 5. 特殊逻辑:高职高专(非体育类)需计算分差率(分差/省控线) + boolean isVocationalCollege = "高职高专".equals(enrollData.getBatch()); + boolean isSportsMajor = "体育类".equals(enrollData.getMajorType()); + if(isSportsMajor){ + if("2024".equals(enrollData.getYear()) && !"专过文排".equals(enrollData.getRulesEnrollProbability()) + && !"文过专排".equals(enrollData.getRulesEnrollProbability())) { + currentDiff = currentDiff.multiply(YxConstant.bigDecimal7p5); + }else if("2024".equals(enrollData.getYear()) && "文过专排".equals(enrollData.getRulesEnrollProbability())) { + currentDiff = currentDiff.multiply(YxConstant.bigDecimal5); + }else if("2023".equals(enrollData.getYear())) { + continue; // 26年 体育不看23年数据 + }else if(rulesEnrollProbability.equals(enrollData.getRulesEnrollProbability())){ + currentDiff = admissionLine.subtract(controlLine); + sum = sum.add(currentDiff); + validYearCount++; + continue; + } + }else + // 6. 非高职高专 or 体育类:检查录取方式是否一致 validYearCount >= 1 && + // !rulesEnrollProbability.equals(enrollData.getRulesEnrollProbability()) && + if (!probabilityOperator.equals(enrollData.getProbabilityOperator())) { + continue; // 录取方式不一致,跳过 + } + + + + sum = sum.add(currentDiff); + validYearCount++; + } + + // 7. 计算平均分差(避免除零错误) + BigDecimal averageDiff = validYearCount > 0 + ? sum.divide(new BigDecimal(validYearCount), 4, RoundingMode.HALF_UP) + : BigDecimal.ZERO; + + return Collections.singletonMap("scoreDifference", averageDiff); + } + + + /** + * 根据 当前年一致的录取方式,计算 往年分数差之和 (往年的录取线-省控线) + */ + public static Map computeHistoryMajorEnrollScoreLineDifferenceWithRulesEnrollProbability_old(String rulesEnrollProbability, List historyMajorEnrollList) { + BigDecimal sum = new BigDecimal("0");//历年分差之和 + BigDecimal nowSum = new BigDecimal("0"); + Map data = new LinkedHashMap<>(); + data.put("scoreDifference", sum); + if (CollectionUtils.isEmpty(historyMajorEnrollList)) { + return data; + } + int yearNum = 0; + BigDecimal controlLine = null;//文科-省控线 + BigDecimal admissionLine = null;//文科-录取线 + for (YxHistoryMajorEnroll yxHistoryMajorEnroll : historyMajorEnrollList) { + controlLine = yxHistoryMajorEnroll.getControlLine(); + admissionLine = yxHistoryMajorEnroll.getAdmissionLine(); + // 如果录取分数线(admissionLine)小于等于0,就直接跳过当前循环,处理下一条数据 + if (admissionLine.compareTo(YxConstant.bigDecimal0) <= 0) { + continue; + } + nowSum = admissionLine.subtract(controlLine); + if (!"高职高专".equals(yxHistoryMajorEnroll.getBatch()) || "体育类".equals(yxHistoryMajorEnroll.getMajorType())) { + if (yearNum >= 1 && !yxHistoryMajorEnroll.getRulesEnrollProbability().equals(rulesEnrollProbability)) { + //如果仅两年的录取原则一样,后面的不一样,不使用较旧的分数 + continue; + } + } else { + //体育之外的,如果是高职高专 + //将 分差/省控线 + nowSum = nowSum.divide(yxHistoryMajorEnroll.getControlLine(), 6, RoundingMode.HALF_UP); + } + sum = sum.add(nowSum);//录取线-省控线 + yearNum++; + } + if (yearNum != 0) { + BigDecimal divide = sum.divide(new BigDecimal(yearNum), 4, RoundingMode.HALF_UP); + data.put("scoreDifference", divide); + } + return data; + } + + + /** + * 判断批次是否有权限计算 + * + * @param nowBatch 当前成绩批次 + * @param majorBatch 专业批次 + */ + public static boolean hasComputeEnrollProbabilityPermissions(String nowBatch, String majorBatch) { + if (StringUtils.isBlank(nowBatch) || StringUtils.isBlank(majorBatch)) { + return false; + } + return true; + } + + /** + * 删除末尾的0 + * + * @param input 文*0.05756700+专*0.1254200 + * @return 文*0.057567+专*0.12542 + */ + public static String replaceLastZeroChar(String input) { + if (StringUtils.isBlank(input)) { + return null; + } + input = input.replaceAll("(\\d+\\.\\d*?)0+(\\D|$)", "$1$2"); + input = input.replace("1.", "1").replace("文0.+", ""); + if (YxConstant.w1z1.equals(input) || YxConstant.w1jiaz1.equals(input)) { + return "文+专"; + } + return input; + } + + public static BigDecimal decimal004 = new BigDecimal("0.04"); + public static BigDecimal decimal0067 = new BigDecimal("0.067"); + public static BigDecimal decimal0093 = new BigDecimal("0.093"); + public static BigDecimal decimal0133 = new BigDecimal("0.133"); + public static BigDecimal decimal02 = new BigDecimal("0.2"); + public static BigDecimal decimal03 = new BigDecimal("0.3"); + public static BigDecimal decimal0333 = new BigDecimal("0.333"); + + public static BigDecimal decimal04 = new BigDecimal("0.4"); + public static BigDecimal decimal0467 = new BigDecimal("0.467"); + public static BigDecimal decimal05 = new BigDecimal("0.5"); + + public static BigDecimal decimal06 = new BigDecimal("0.6"); + public static BigDecimal decimal0667 = new BigDecimal("0.667"); + + + public static BigDecimal decimal07 = new BigDecimal("0.7"); + + public static BigDecimal decimal1 = new BigDecimal("1"); + + + /** + * 折合 分数成绩 旧版 + * + * @param rulesEnrollProbability 分数折合方式(专过文排/文过专排/文+专/专7文3/专5文5/专3文7) + * @param culturalScore 文化成绩 + * @param professionalScore 专业成绩 + * @return 折合成绩 + */ + /* + public static BigDecimal convertIntoScore(String rulesEnrollProbability, BigDecimal culturalScore, BigDecimal professionalScore) { + BigDecimal score = new BigDecimal(0); + if (rulesEnrollProbability == null || culturalScore == null || professionalScore == null) { + return score; + } + switch (rulesEnrollProbability) { + case "专过文排": + score = culturalScore.multiply(decimal0133); + break; + case "文过专排": + score = professionalScore.multiply(decimal0667); + break; + case "专7文3": + score = culturalScore.multiply(decimal004).add(professionalScore.multiply(decimal0467)); + break; + case "文5专5": + case "专5文5": + score = culturalScore.multiply(decimal0067).add(professionalScore.multiply(decimal0333)); + break; + case "专3文7": + score = culturalScore.multiply(decimal0093).add(professionalScore.multiply(decimal02)); + break; + case "文+专": + default: + score = culturalScore.add(professionalScore); + break; + } + score = score.divide(decimal1, 4, RoundingMode.HALF_UP); + return score; + } +*/ + + /** + * 转换主项成绩 + * 如果需要使用专业子项成绩时,则返回对应需要的分数 + * 如果需要主项成绩,则使用主项成绩分数 + */ + public static BigDecimal convertProfessionalCategoryMaster(YxUserScore activeCurrentUserScore, String operator, String professionalCategoryChildren) { + String professionalCategory = activeCurrentUserScore.getProfessionalCategory(); + if (StringUtils.isNotBlank(professionalCategoryChildren)) { + //当前有子项专业 + if ("音乐".equals(professionalCategory)) { + if ("器乐".contains(professionalCategoryChildren)) { + + } else if ("声乐".contains(professionalCategoryChildren)) { + + } + } + } + //使用主项成绩 + return activeCurrentUserScore.getProfessionalScore(); + } + + public static BigDecimal convertIntoScore(String rulesEnrollProbability, BigDecimal culturalScore, BigDecimal professionalScore, String operator) { + BigDecimal score = new BigDecimal(0); + if (rulesEnrollProbability == null || culturalScore == null || professionalScore == null) { + return score; + } + + try { + if (StringUtils.isNotBlank(operator)) { + score = new BigDecimal("0"); + String[] operators = operator.split("\\+"); + String[] split = null; + for (String d : operators) { + split = d.split("\\*"); + if (split[0].contains("文")) { + score = score.add(culturalScore.multiply(new BigDecimal(split[1]))); + } else { + score = score.add(professionalScore.multiply(new BigDecimal(split[1]))); + } + } + } + score = score.divide(decimal1, 4, RoundingMode.HALF_UP); + }catch (Exception e){ + throw new RuntimeException("计算分数出错: "+ rulesEnrollProbability +":"+ operator); + } + return score; + } + + /** + * 折合 分数成绩 + * + * @param rulesEnrollProbability 分数折合方式(专过文排/文过专排/文+专/专7文3/专5文5/专3文7) + * @param firstLevelDiscipline 一级学科 + * @param culturalScore 文化成绩 + * @param professionalScore 专业成绩 + * @param operator 运算符 + * @return 折合成绩 + */ + public static BigDecimal convertIntoScore(String rulesEnrollProbability, BigDecimal culturalScore, BigDecimal professionalScore, String firstLevelDiscipline, String operator) { + BigDecimal score = new BigDecimal(0); + if (rulesEnrollProbability == null || culturalScore == null || professionalScore == null) { + return score; + } + + if (StringUtils.isNotBlank(operator)) { + score = new BigDecimal("0"); + String[] operators = operator.split("\\+"); + for (String d : operators) { + String[] split = d.split("\\*"); + if (split[0].contains("文")) { + score = score.add(culturalScore.multiply(new BigDecimal(split[1]))); + } else { + score = score.add(professionalScore.multiply(new BigDecimal(split[1]))); + } + } + } else { + switch (rulesEnrollProbability) { + case "专过文排": + score = "1722224567122784257".equals(firstLevelDiscipline) ? culturalScore.multiply(decimal0133) : culturalScore; + break; + case "文过专排": + score = "1722224567122784257".equals(firstLevelDiscipline) ? professionalScore.multiply(decimal0667) : professionalScore; + break; + case "专7文3": + if ("1722224567122784257".equals(firstLevelDiscipline)) { + score = culturalScore.multiply(decimal004).add(professionalScore.multiply(decimal0467)); + } else { + score = culturalScore.multiply(decimal07).add(professionalScore.multiply(decimal03)); + } + break; + case "文5专5": + case "专5文5": + if ("1722224567122784257".equals(firstLevelDiscipline)) { + score = culturalScore.multiply(decimal0067).add(professionalScore.multiply(decimal0333)); + } else { + score = culturalScore.multiply(decimal05).add(professionalScore.multiply(decimal05)); + } + break; + case "专3文7": + if ("1722224567122784257".equals(firstLevelDiscipline)) { + score = culturalScore.multiply(decimal0093).add(professionalScore.multiply(decimal02)); + } else { + score = culturalScore.multiply(decimal07).add(professionalScore.multiply(decimal03)); + } + break; + case "专6文4": + if ("1722224567122784257".equals(firstLevelDiscipline)) { + score = culturalScore.multiply(decimal0093).add(professionalScore.multiply(decimal02)); + } else { + score = culturalScore.multiply(decimal04).add(professionalScore.multiply(decimal06)); + } + break; + case "文+专": + score = culturalScore.add(professionalScore); + break; + case "文过专排主科": + //主项成绩 *0. + score = professionalScore.multiply(decimal05); + break; + default: + if (operator == null) { + score = culturalScore.add(professionalScore); + } else { + //System.out.println(operator); + score = new BigDecimal("0"); + String[] operators = operator.split("\\+"); + for (String d : operators) { + String[] split = d.split("\\*"); + if (split[0].contains("文")) { + score = score.add(culturalScore.multiply(new BigDecimal(split[1]))); + } else { + score = score.add(professionalScore.multiply(new BigDecimal(split[1]))); + } + } + } + break; + } + } + score = score.divide(decimal1, 4, RoundingMode.HALF_UP); + return score; + } + + /** + * 折合 专业省控线 + * + * @param rulesEnrollProbability 分数折合方式(专过文排/文过专排/文+专/专7文3/专5文5/专3文7) + * @param controlLine 原省控线 + * @param specialControlLine 原专业分 + * @param operator 运算符 + * @return 折合省控线 + */ + public static BigDecimal covertIntoControlLine(String rulesEnrollProbability, BigDecimal controlLine, BigDecimal specialControlLine, String firstLevelDiscipline, String operator) { + BigDecimal provincialControlLine = null; + if (StringUtils.isNotBlank(operator)) { + provincialControlLine = new BigDecimal("0"); + String[] operators = operator.split("\\+"); + for (String d : operators) { + String[] split = d.split("\\*"); + if (split[0].contains("文")) { + provincialControlLine = provincialControlLine.add(controlLine.multiply(new BigDecimal(split[1]))); + } else { + provincialControlLine = provincialControlLine.add(specialControlLine.multiply(new BigDecimal(split[1]))); + } + } + } else { + switch (rulesEnrollProbability) { + case "专过文排": + provincialControlLine = controlLine.multiply(decimal0133); + provincialControlLine = "1722224567122784257".equals(firstLevelDiscipline) ? controlLine.multiply(decimal0133) : controlLine; + break; + case "文过专排": + provincialControlLine = "1722224567122784257".equals(firstLevelDiscipline) ? specialControlLine.multiply(decimal0667) : specialControlLine; + break; + case "专7文3": + if ("1722224567122784257".equals(firstLevelDiscipline)) { + provincialControlLine = controlLine.multiply(decimal004).add(specialControlLine.multiply(decimal0467)); + } else { + provincialControlLine = controlLine.multiply(decimal07).add(specialControlLine.multiply(decimal03)); + } + break; + case "文5专5": + case "专5文5": + if ("1722224567122784257".equals(firstLevelDiscipline)) { + provincialControlLine = controlLine.multiply(decimal0067).add(specialControlLine.multiply(decimal0333)); + } else { + provincialControlLine = controlLine.multiply(decimal05).add(specialControlLine.multiply(decimal05)); + } + break; + case "专3文7": + if ("1722224567122784257".equals(firstLevelDiscipline)) { + provincialControlLine = controlLine.multiply(decimal0093).add(specialControlLine.multiply(decimal02)); + } else { + provincialControlLine = controlLine.multiply(decimal07).add(specialControlLine.multiply(decimal03)); + } + break; + case "专6文4": + if ("1722224567122784257".equals(firstLevelDiscipline)) { + provincialControlLine = controlLine.multiply(decimal0093).add(specialControlLine.multiply(decimal02)); + } else { + provincialControlLine = controlLine.multiply(decimal04).add(specialControlLine.multiply(decimal06)); + } + break; + case "文+专": + provincialControlLine = controlLine.add(specialControlLine); + break; + default: + if (operator == null) { + provincialControlLine = controlLine.add(specialControlLine); + } else { + provincialControlLine = new BigDecimal("0"); + String[] operators = operator.split("\\+"); + for (String d : operators) { + String[] split = d.split("\\*"); + if (split[0].contains("文")) { + provincialControlLine = provincialControlLine.add(controlLine.multiply(new BigDecimal(split[1]))); + } else { + provincialControlLine = provincialControlLine.add(specialControlLine.multiply(new BigDecimal(split[1]))); + } + } + } + } + } + provincialControlLine = provincialControlLine.divide(decimal1, 4, RoundingMode.HALF_UP); + return provincialControlLine; + } + + /** + * 转换成绩为分数批次段 + * + * @param culturalScore 文化成绩 + * @param professionalScore 专业成绩 + * @return + */ + public static String conversionScoreBatch(BigDecimal culturalScore, BigDecimal professionalScore, List historyScoreControlLineList) { + double wenScore = culturalScore.doubleValue(); + double zhuanScore = professionalScore.doubleValue(); + String batch = "高职高专"; + + if (CollectionUtils.isNotEmpty(historyScoreControlLineList)) { + for (YxHistoryScoreControlLine yxHistoryScoreControlLine : historyScoreControlLineList) { + if (culturalScore.compareTo(yxHistoryScoreControlLine.getCulturalScore()) >= 0 && professionalScore.compareTo(yxHistoryScoreControlLine.getSpecialScore()) >= 0) { + batch = yxHistoryScoreControlLine.getBatch(); + break; + } + } + } + return batch; + } + + + /** + * 判断是否过省控线 + * + * @param rulesEnrollProbability 录取方式 + * @param culturalScore 当前文化分 + * @param professionalScore 当前专业分 + * @param culturalControlLine 当前文化省控线 + * @param specialControlLine 当前专业省控线 + * @return true/false 过/没过 + */ + public static boolean crossingControlLine(String rulesEnrollProbability, BigDecimal culturalScore, BigDecimal professionalScore, BigDecimal culturalControlLine, BigDecimal specialControlLine) { + if (YxConstant.culturalControlLineGuo.equals(rulesEnrollProbability)) { + return culturalScore.compareTo(culturalControlLine) >= 0; + } else if (YxConstant.specialControlLineGuo.equals(rulesEnrollProbability)) { + return professionalScore.compareTo(specialControlLine) >= 0; + } else if (YxConstant.culturalControlLineGuo2.equals(rulesEnrollProbability)) { + return culturalScore.compareTo(specialControlLine) >= 0; + } + return culturalScore.compareTo(culturalControlLine) >= 0 && professionalScore.compareTo(specialControlLine) >= 0; +// return true; + } + + /** + * 将专业类别转为 子类别 + */ + public static List checkProfessionalCategoryToChildrenList(String professionalCategoryChildren) { + if (StringUtils.isNotBlank(professionalCategoryChildren)) { + return Arrays.asList(professionalCategoryChildren.split(",")); + } + return null; + } + + /** + * 其他录取要求 + * + * @param professionalScore + * @param activeCurrentUserScore + * @param recommendMajorDTO + * @return + */ + public static boolean otherScoreJudge(BigDecimal professionalScore, YxUserScore activeCurrentUserScore, RecommendMajorDTO recommendMajorDTO) { + if (recommendMajorDTO != null) { +// //语文成绩判断 +// if (recommendMajorDTO.getChineseScoreLimitation() != null && (activeCurrentUserScore.getChineseScore() == null || activeCurrentUserScore.getChineseScore().compareTo(recommendMajorDTO.getChineseScoreLimitation()) < 0)) { +// return false; +// } + //外语成绩判断 + if (recommendMajorDTO.getEnglishScoreLimitation() != null && (activeCurrentUserScore.getEnglishScore() == null || activeCurrentUserScore.getEnglishScore().compareTo(recommendMajorDTO.getEnglishScoreLimitation()) < 0)) { + return false; + } + //文化成绩判断 + if (recommendMajorDTO.getCulturalScoreLimitation() != null && (activeCurrentUserScore.getCulturalScore() == null || activeCurrentUserScore.getCulturalScore().compareTo(recommendMajorDTO.getCulturalScoreLimitation()) < 0)) { + return false; + } + if (StringUtils.isNotBlank(recommendMajorDTO.getMajorType()) && "音乐类".equals(recommendMajorDTO.getMajorType()) && StringUtils.isNotBlank(recommendMajorDTO.getMajorTypeChild())) { + if ("音乐表演".equals(recommendMajorDTO.getMajorTypeChild())) { + if ("器乐".equals(recommendMajorDTO.getMainSubjects())) { + //统考成绩判断 + if (recommendMajorDTO.getProfessionalScoreLimitation() != null && (activeCurrentUserScore.getYybyqy() == null || activeCurrentUserScore.getYybyqy().compareTo(recommendMajorDTO.getProfessionalScoreLimitation()) < 0)) { + return false; + } + }else{ + //统考成绩判断 + if (recommendMajorDTO.getProfessionalScoreLimitation() != null && (activeCurrentUserScore.getYybysy() == null || activeCurrentUserScore.getYybysy().compareTo(recommendMajorDTO.getProfessionalScoreLimitation()) < 0)) { + return false; + } + } + } else { + //统考成绩判断 + if (recommendMajorDTO.getProfessionalScoreLimitation() != null && (activeCurrentUserScore.getYyjy() == null || activeCurrentUserScore.getYyjy().compareTo(recommendMajorDTO.getProfessionalScoreLimitation()) < 0)) { + return false; + } + } + }else if (StringUtils.isNotBlank(recommendMajorDTO.getMajorType()) && "表演类".equals(recommendMajorDTO.getMajorType()) && StringUtils.isNotBlank(recommendMajorDTO.getMajorTypeChild())) { + if ("服装表演".equals(recommendMajorDTO.getMajorTypeChild())) { + //统考成绩判断 + if (recommendMajorDTO.getProfessionalScoreLimitation() != null && (activeCurrentUserScore.getFzby() == null || activeCurrentUserScore.getFzby().compareTo(recommendMajorDTO.getProfessionalScoreLimitation()) < 0)) { + return false; + } + }else if ("戏剧影视表演".equals(recommendMajorDTO.getMajorTypeChild())) { + //统考成绩判断 + if (recommendMajorDTO.getProfessionalScoreLimitation() != null && (activeCurrentUserScore.getXjysby() == null || activeCurrentUserScore.getXjysby().compareTo(recommendMajorDTO.getProfessionalScoreLimitation()) < 0)) { + return false; + } + } else { + //统考成绩判断 + if (recommendMajorDTO.getProfessionalScoreLimitation() != null && (activeCurrentUserScore.getXjysdy() == null || activeCurrentUserScore.getXjysdy().compareTo(recommendMajorDTO.getProfessionalScoreLimitation()) < 0)) { + return false; + } + } + } else{ + // 其他专业 + //统考成绩判断 + if (recommendMajorDTO.getProfessionalScoreLimitation() != null && (activeCurrentUserScore.getProfessionalScore() == null || activeCurrentUserScore.getProfessionalScore().compareTo(recommendMajorDTO.getProfessionalScoreLimitation()) < 0)) { + return false; + } + } + + } + return true; + } + + /** + * 统一计算录取率方法 + * + * @param nowYearDiff 当前年分差 + * @param historyThreeYearDiff 去年分差 + * @return 录取率 + */ + public static BigDecimal commonCheckEnrollProbability(BigDecimal nowYearDiff, BigDecimal historyThreeYearDiff) { + BigDecimal enrollProbability = null; + if (nowYearDiff.compareTo(YxConstant.bigDecimal0) == 0 && historyThreeYearDiff.compareTo(YxConstant.bigDecimal0) > 0) { + //当前年的分差为0,去年分差不为0: 75/去年的线差 + enrollProbability = YxConstant.bigDecimal75.divide(historyThreeYearDiff, 4, RoundingMode.HALF_UP); + } else if (nowYearDiff.compareTo(YxConstant.bigDecimal0) == 0 && historyThreeYearDiff.compareTo(YxConstant.bigDecimal0) <= 0) { + //当前年分差为0,去年分差为0 + enrollProbability = YxConstant.bigDecimal50;//录取率50 + } else if (nowYearDiff.compareTo(YxConstant.bigDecimal0) > 0 && historyThreeYearDiff.compareTo(YxConstant.bigDecimal0) <= 0) { + //当前年分差不为0,去年分差为0:默认去年的分差为1 .multiply(YxConstant.bigDecimal075) + enrollProbability = nowYearDiff.multiply(YxConstant.bigDecimal100).multiply(YxConstant.bigDecimal075); + } else if (nowYearDiff.compareTo(YxConstant.bigDecimal0) < 0 && historyThreeYearDiff.compareTo(YxConstant.bigDecimal0) <= 0) { + enrollProbability = YxConstant.bigDecimal0; + } else { + //当前年分差不为0,去年分差也不为0:(当前年分差/去年分差)*0.75 + enrollProbability = nowYearDiff.divide(historyThreeYearDiff, 4, RoundingMode.HALF_UP).multiply(YxConstant.bigDecimal100); + //录取率*0.75 + enrollProbability = enrollProbability.multiply(YxConstant.bigDecimal075); + } + return enrollProbability; + } + + public static BigDecimal commonCheckEnrollProbabilityBeilv(BigDecimal enrollProbability) { + if (enrollProbability.compareTo(YxConstant.bigDecimal150) > 0) { + enrollProbability = YxConstant.bigDecimal95x; + } else if (enrollProbability.compareTo(YxConstant.bigDecimal100) > 0) { + enrollProbability = YxConstant.bigDecimal85x; + } else if (enrollProbability.compareTo(YxConstant.bigDecimal0) <= 0) { + enrollProbability = YxConstant.bigDecimal0; + } + return enrollProbability; + } + +} diff --git a/java_code/YxCalculationMajorServiceImpl.java b/java_code/YxCalculationMajorServiceImpl.java new file mode 100644 index 0000000..73fefcb --- /dev/null +++ b/java_code/YxCalculationMajorServiceImpl.java @@ -0,0 +1,715 @@ +package org.jeecg.modules.yx.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang.StringUtils; +import org.jeecg.modules.web.dto.ArtRecommendMajorBaseDTO; +import org.jeecg.modules.web.dto.RecommendMajorDTO; +import org.jeecg.modules.web.vo.QueryRecommendMajorVO; +import org.jeecg.modules.yx.constant.YxConstant; +import org.jeecg.modules.yx.entity.*; +import org.jeecg.modules.yx.mapper.YxCalculationMajorMapper; +import org.jeecg.modules.yx.mapper.YxSchoolMajorMapper; +import org.jeecg.modules.yx.mapper.YxSchoolMapper; +import org.jeecg.modules.yx.service.*; +import org.jeecg.modules.yx.util.ScoreUtil; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Service; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.*; +import java.util.stream.Collectors; + +/** + * @Description: 用户可报专业折合关联表 + * @Author: jeecg-boot + * @Date: 2024-03-15 + * @Version: V1.0 + */ +@Service +public class YxCalculationMajorServiceImpl extends ServiceImpl implements IYxCalculationMajorService { + @Resource + private IYxHistoryScoreControlLineService yxHistoryScoreControlLineService; + @Resource + private IYxUserScoreService yxUserScoreService; + @Resource + private MusicService musicService; + @Resource + private BiaoyanService biaoyanService; + @Resource + private WuDaoService wuDaoService; + @Resource + private BoYinService boYinService; + @Resource + private IYxHistoryMajorEnrollService yxHistoryMajorEnrollService; + @Resource + private YxSchoolMajorMapper yxSchoolMajorMapper; + @Resource + private YxSchoolMapper yxSchoolMapper; + + @Override + public List calculationMajor(YxUserScore userScore) { + // 最终返回的对象 + List recommendMajorDTOList = new ArrayList<>(); + if (userScore != null) { + // 查询专业列表参数 + QueryRecommendMajorVO queryRecommendMajorVO = new QueryRecommendMajorVO(); + queryRecommendMajorVO.setProfessionalCategory(userScore.getProfessionalCategory()); // 专业类别 + queryRecommendMajorVO.setCognitioPolyclinic(userScore.getCognitioPolyclinic()); // 文理分科 + if (("音乐类".equals(userScore.getProfessionalCategory()) || + "表演类".equals(userScore.getProfessionalCategory())) && StringUtils.isNotBlank(userScore.getProfessionalCategoryChildren())) { + queryRecommendMajorVO.setPCategoryChildrenList(Arrays.asList(userScore.getProfessionalCategoryChildren().split(","))); + } + long startTime = new Date().getTime(); + recommendMajorDTOList = yxSchoolMajorMapper.recommendMajorList(queryRecommendMajorVO); // 获取到当前专业,文/理科的数据 + //将历史录取信息合并到专业信息里 + if (CollectionUtils.isNotEmpty(recommendMajorDTOList)) { + // + yxHistoryMajorEnrollService.recommendMajorDTOListSetHistoryInfo(recommendMajorDTOList, ScoreUtil.getCalOldYears()); + /*if (!"美术与设计类".equals(userScore.getProfessionalCategory())) {}else{ + yxHistoryMajorEnrollService.recommendMajorDTOListSetHistoryInfo(recommendMajorDTOList, ScoreUtil.getCalOldYears()); + }*/ + this.checkEnrollProbability(recommendMajorDTOList, userScore); + } + long endTime = new Date().getTime(); + log.warn("测算专业录取率-耗费时长:" + (endTime - startTime)); + //保存结果到用户的专业结果表 + YxCalculationMajor yxCalculationMajor = null; + List calculationMajorList = new ArrayList<>(); + StringBuilder otherScoreLimitation = null; + for (RecommendMajorDTO recommendMajorDTO : recommendMajorDTOList) { + otherScoreLimitation = new StringBuilder(); + yxCalculationMajor = new YxCalculationMajor(); + BeanUtils.copyProperties(recommendMajorDTO, yxCalculationMajor); + yxCalculationMajor.setTuition(recommendMajorDTO.getTuition()); + yxCalculationMajor.setDetail(recommendMajorDTO.getMajorDetail()); + yxCalculationMajor.setScoreId(userScore.getId()); + yxCalculationMajor.setStudentOldConvertedScore(recommendMajorDTO.getStudentScore()); + yxCalculationMajor.setStudentConvertedScore(recommendMajorDTO.getStudentConvertedScore()); + yxCalculationMajor.setPrivateStudentConvertedScore(recommendMajorDTO.getPrivateStudentScore()); + //其他分数要求 + if(true){ + //语文成绩 + if (recommendMajorDTO.getChineseScoreLimitation()!=null && recommendMajorDTO.getChineseScoreLimitation().compareTo(YxConstant.bigDecimal0)>0) { + otherScoreLimitation.append("语文成绩不低于").append(recommendMajorDTO.getChineseScoreLimitation().doubleValue()).append("分,"); + } + //外语成绩 + if (recommendMajorDTO.getEnglishScoreLimitation()!=null && recommendMajorDTO.getEnglishScoreLimitation().compareTo(YxConstant.bigDecimal0)>0) { + otherScoreLimitation.append("外语成绩不低于").append(recommendMajorDTO.getEnglishScoreLimitation().doubleValue()).append("分,"); + } + //文化成绩 + if (recommendMajorDTO.getCulturalScoreLimitation()!=null && recommendMajorDTO.getCulturalScoreLimitation().compareTo(YxConstant.bigDecimal0)>0) { + otherScoreLimitation.append("文化成绩不低于").append(recommendMajorDTO.getCulturalScoreLimitation().doubleValue()).append("分,"); + } + //专业成绩 + if (recommendMajorDTO.getProfessionalScoreLimitation()!=null && recommendMajorDTO.getProfessionalScoreLimitation().compareTo(YxConstant.bigDecimal0)>0) { + otherScoreLimitation.append("专业成绩不低于").append(recommendMajorDTO.getProfessionalScoreLimitation().doubleValue()).append("分,"); + } + if (otherScoreLimitation.length()>0) { + //recommendMajorDTO.setOtherScoreLimitation(otherScoreLimitation.substring(0,otherScoreLimitation.length()-2)); + yxCalculationMajor.setOtherScoreLimitation(otherScoreLimitation.substring(0,otherScoreLimitation.length()-2)); + } + } + calculationMajorList.add(yxCalculationMajor); + } + long saveStartTime = new Date().getTime(); + baseMapper.insertBatchNew(userScore.getCalculationTableName(),userScore.getId(),calculationMajorList); + //super.saveBatch(calculationMajorList,500); + long saveEndTime = new Date().getTime(); + log.warn("插入数据耗费时长:" + (saveEndTime - saveStartTime)); + } + return recommendMajorDTOList; + } + + @Override + public ArtRecommendMajorBaseDTO recommendMajorPage(QueryRecommendMajorVO queryRecommendMajorVO) { + Integer pageNum = queryRecommendMajorVO.getPageNum(); + Integer pageSize = queryRecommendMajorVO.getPageSize(); + if (pageNum == null) { + pageNum = 1; + } + if (pageSize == null) { + pageSize = 5; + } + //获取当前分数 + YxUserScore activeCurrentUserScore = yxUserScoreService.getActiveCurrentUserScore(); + queryRecommendMajorVO.setScoreId(activeCurrentUserScore.getId()); + //文理分科 + queryRecommendMajorVO.setCognitioPolyclinic(activeCurrentUserScore.getCognitioPolyclinic()); + //专业类别 + queryRecommendMajorVO.setProfessionalCategory(activeCurrentUserScore.getProfessionalCategory()); + + if (StringUtils.isNotBlank(queryRecommendMajorVO.getBatch())) { + queryRecommendMajorVO.setBatch(queryRecommendMajorVO.getBatch().replace("批","")); + } + + //有子级专业 + if (StringUtils.isNotBlank(activeCurrentUserScore.getProfessionalCategoryChildren()) && !"高职高专".equals(queryRecommendMajorVO.getBatch())) { + queryRecommendMajorVO.setPCategoryChildrenList(new ArrayList<>(Arrays.asList(activeCurrentUserScore.getProfessionalCategoryChildren().split(",")))); + if("表演类".equals(activeCurrentUserScore.getProfessionalCategory())){ + queryRecommendMajorVO.getPCategoryChildrenList().add("兼报"); + } + } + Map historyScoreControlLineMap = null; + if ("音乐类".equals(activeCurrentUserScore.getProfessionalCategory())|| + "表演类".equals(activeCurrentUserScore.getProfessionalCategory())) { + historyScoreControlLineMap = new LinkedHashMap<>(); + LambdaQueryWrapper yhsclWrapper = new LambdaQueryWrapper<>(); + yhsclWrapper.like(YxHistoryScoreControlLine::getProfessionalCategory, activeCurrentUserScore.getProfessionalCategory()); + List yhsclList = yxHistoryScoreControlLineService.list(yhsclWrapper); + String key = null; + for (YxHistoryScoreControlLine yhscl : yhsclList) { + key = yhscl.getYear()+"_" +yhscl.getCategory()+"-"+ yhscl.getBatch()+"-"+yhscl.getProfessionalCategory(); + historyScoreControlLineMap.put(key,yhscl); + } + }else{ + historyScoreControlLineMap = yxHistoryScoreControlLineService.mapsBatchByProfessionalCategoryOfYear(YxConstant.nowYear, activeCurrentUserScore.getProfessionalCategory(), activeCurrentUserScore.getCognitioPolyclinic()); + } + + + //标签类别 + if (StringUtils.isNotBlank(queryRecommendMajorVO.getTagsStrs())) { + queryRecommendMajorVO.setTagsList(Arrays.asList(queryRecommendMajorVO.getTagsStrs().split(","))); + } + //院校类型 + if (StringUtils.isNotBlank(queryRecommendMajorVO.getSchoolNatureStrs())) { + queryRecommendMajorVO.setSchoolNatureList(Arrays.asList(queryRecommendMajorVO.getSchoolNatureStrs().split(","))); + } + //院校地区 + if (StringUtils.isNotBlank(queryRecommendMajorVO.getProvince())) { + queryRecommendMajorVO.setAddressList(Arrays.asList(queryRecommendMajorVO.getProvince().split(","))); + } + //科研教学 + if (StringUtils.isNotBlank(queryRecommendMajorVO.getKyjxStrs())) { + queryRecommendMajorVO.setKyjxList(Arrays.asList(queryRecommendMajorVO.getKyjxStrs().split(","))); + } + //录取方式 + String rulesEnrollProbability = queryRecommendMajorVO.getRulesEnrollProbability(); + if (StringUtils.isNotBlank(rulesEnrollProbability)) { + queryRecommendMajorVO.setRulesEnrollProbabilityList(Arrays.asList(rulesEnrollProbability.split(","))); + } + + //获取推荐志愿信息service + List recommendMajorList = baseMapper.recommendMajorList(activeCurrentUserScore.getCalculationTableName(),queryRecommendMajorVO); + ArtRecommendMajorBaseDTO artRecommendMajorBaseDTO = this.checkLevelNum(recommendMajorList); + //当前选中的录取率页面 + String paneName = queryRecommendMajorVO.getPaneName(); + //判断 paneName + if (StringUtils.isNotBlank(paneName) && !paneName.equals("全部")) { + BigDecimal bigDecimal93 = new BigDecimal("93"); + BigDecimal bigDecimal92 = new BigDecimal("92"); + BigDecimal bigDecimal74 = new BigDecimal("74"); + BigDecimal bigDecimal73 = new BigDecimal("73"); + BigDecimal bigDecimal60 = new BigDecimal("60"); + switch (paneName) { + case "可保底": + recommendMajorList = recommendMajorList.stream().filter(r -> r.getEnrollProbability().compareTo(bigDecimal93) >= 0).collect(Collectors.toList()); + break; + case "较稳妥": + recommendMajorList = recommendMajorList.stream().filter(r -> r.getEnrollProbability().compareTo(bigDecimal92) < 0 && r.getEnrollProbability().compareTo(bigDecimal74) >= 0).collect(Collectors.toList()); + break; + case "可冲击": + recommendMajorList = recommendMajorList.stream().filter(r -> r.getEnrollProbability().compareTo(bigDecimal73) < 0 && r.getEnrollProbability().compareTo(bigDecimal60) >= 0).collect(Collectors.toList()); + break; + case "难录取": + recommendMajorList = recommendMajorList.stream().filter(r -> r.getEnrollProbability().compareTo(bigDecimal60) < 0).collect(Collectors.toList()); + break; + } + } + //插入 院校标签 + this.setSchoolTagsList(recommendMajorList); + List recommendMajorDTOList = recommendMajorList.stream().skip((long) (pageNum - 1) * pageSize).limit(pageSize) + .collect(Collectors.toList()); + + YxHistoryScoreControlLine yxHistoryScoreControlLine = null; + BigDecimal bigDecimal = null; + String key = null; + for (RecommendMajorDTO recommendMajorDTO : recommendMajorDTOList) { + key = YxConstant.nowYear + "_" + recommendMajorDTO.getCategory(); + + if("高职高专".equals(recommendMajorDTO.getBatch())){ + key = key + "-" + recommendMajorDTO.getBatch(); + }else{ + key = key + "-本科"; + } + // 判断是 音乐教育还是音乐表演 器乐/声乐 + if ("音乐类".equals(recommendMajorDTO.getMajorType())) { + key = key +"-音乐类-"+ recommendMajorDTO.getMajorTypeChild(); + if ("音乐表演".equals(recommendMajorDTO.getMajorTypeChild())) { + key += recommendMajorDTO.getMainSubjects(); + } + }else if ("表演类".equals(recommendMajorDTO.getMajorType())) { + key = key +"-表演类-"+ recommendMajorDTO.getMajorTypeChild(); + }else{ + key = recommendMajorDTO.getBatch(); + } + yxHistoryScoreControlLine = historyScoreControlLineMap.get(key); + if (yxHistoryScoreControlLine!=null) { + if (recommendMajorDTO.getStudentScore()!=null) { + bigDecimal = ScoreUtil.covertIntoControlLine(recommendMajorDTO.getRulesEnrollProbability(), + yxHistoryScoreControlLine.getCulturalScore(), + yxHistoryScoreControlLine.getSpecialScore(), + recommendMajorDTO.getFirstLevelDiscipline(), recommendMajorDTO.getProbabilityOperator()); + recommendMajorDTO.setScoreLineDifference(recommendMajorDTO.getStudentScore().subtract(bigDecimal)); + } + } + } + //包装历年信息 + yxHistoryMajorEnrollService.recommendMajorDTOListSetHistoryInfo(recommendMajorDTOList, ScoreUtil.getShowOldYears()); + //组装返回对象 + int total = recommendMajorList.size(); + IPage pageList = new Page<>(); + pageList.setTotal(total);//总记录数 + pageList.setCurrent(pageNum); + pageList.setSize(pageSize); + convertRecommendMajorListToNull(recommendMajorDTOList); + pageList.setRecords(recommendMajorDTOList); + artRecommendMajorBaseDTO.setPageList(pageList); + return artRecommendMajorBaseDTO; + } + + @Override + public ArtRecommendMajorBaseDTO schoolOtherMajor(QueryRecommendMajorVO queryRecommendMajorVO) { + //获取当前分数 + YxUserScore activeCurrentUserScore = null; + if (StringUtils.isNotBlank(queryRecommendMajorVO.getScoreId())) { + //是根据成绩单来的 + activeCurrentUserScore = yxUserScoreService.findById(queryRecommendMajorVO.getScoreId()); + } else { + //直接获取当前使用中的成绩单 + activeCurrentUserScore = yxUserScoreService.getActiveCurrentUserScore(); + } + queryRecommendMajorVO.setScoreId(activeCurrentUserScore.getId()); + //文理分科 + queryRecommendMajorVO.setCognitioPolyclinic(activeCurrentUserScore.getCognitioPolyclinic()); + //专业类别 + queryRecommendMajorVO.setProfessionalCategory(activeCurrentUserScore.getProfessionalCategory()); + queryRecommendMajorVO.setScoreId(activeCurrentUserScore.getId()); + //文理分科 + queryRecommendMajorVO.setCognitioPolyclinic(activeCurrentUserScore.getCognitioPolyclinic()); + //专业类别 + queryRecommendMajorVO.setProfessionalCategory(activeCurrentUserScore.getProfessionalCategory()); + //有子级专业 + if (StringUtils.isNotBlank(activeCurrentUserScore.getProfessionalCategoryChildren()) && !"高职高专".equals(queryRecommendMajorVO.getBatch())) { + queryRecommendMajorVO.setPCategoryChildrenList(new ArrayList<>(Arrays.asList(activeCurrentUserScore.getProfessionalCategoryChildren().split(",")))); + if("表演类".equals(activeCurrentUserScore.getProfessionalCategory())){ + queryRecommendMajorVO.getPCategoryChildrenList().add("兼报"); + } + } + //获取推荐志愿信息service + List recommendMajorList = baseMapper.recommendMajorList(activeCurrentUserScore.getCalculationTableName(),queryRecommendMajorVO); + yxHistoryMajorEnrollService.recommendMajorDTOListSetHistoryInfo(recommendMajorList, ScoreUtil.getShowOldYears()); + ArtRecommendMajorBaseDTO artRecommendMajorBaseDTO = new ArtRecommendMajorBaseDTO(); + convertRecommendMajorListToNull(recommendMajorList); + artRecommendMajorBaseDTO.setList(recommendMajorList); + return artRecommendMajorBaseDTO; + } + + /** + * 转变要隐藏的数据 + * @param recommendMajorList + */ + private static void convertRecommendMajorListToNull(List recommendMajorList) { +// List rulesEnrollProbabilityList = Arrays.asList("文*0.8+文*0.5", "文*0.7+文*0.75","文*0.6+专*1","文*0.5+专*1.25"); +// List rulesEnrollProbabilitySxList = Arrays.asList("文7专3","专过文排","文8专2","文6专4","文5专5"); +// recommendMajorList.forEach(r->{ +//// r.setPlanNum(null); +// //非体育,非专科 +// if(!r.getMajorType().equals("体育类") && !r.getBatch().equals("高职高专")){ +// if (rulesEnrollProbabilitySxList.contains(r.getRulesEnrollProbabilitySx())) { +// }else if(!rulesEnrollProbabilityList.contains(r.getRulesEnrollProbability())){ +// r.setRulesEnrollProbability(null); +// } +// } +// }); + } + + @Override + public ArtRecommendMajorBaseDTO checkLevelNum(List recommendMajorList) { + int number = 0; + int kbd=0,jwt=0,kcj=0,nan=0; + double enrollProbabilityDouble = 0; + for (RecommendMajorDTO recommendMajorDTO : recommendMajorList) { + number++; + enrollProbabilityDouble = recommendMajorDTO.getEnrollProbability().doubleValue(); + //保底 + if (enrollProbabilityDouble >= 93) { + kbd++; + } else if (enrollProbabilityDouble < 92 && enrollProbabilityDouble >= 74) { + //较稳妥 + jwt++; + } else if (enrollProbabilityDouble < 73 && enrollProbabilityDouble >= 60) { + //可冲击 + kcj++; + } else { + //难 + nan++; + } + } + ArtRecommendMajorBaseDTO artRecommendMajorBaseDTO = new ArtRecommendMajorBaseDTO(); + artRecommendMajorBaseDTO.setKcj(kcj); + artRecommendMajorBaseDTO.setJwt(jwt); + artRecommendMajorBaseDTO.setKbd(kbd); + artRecommendMajorBaseDTO.setNan(nan); + artRecommendMajorBaseDTO.setAllNumber(number); + return artRecommendMajorBaseDTO; + } + + @Override + public String checkCalculationTableNameOfCreate(String calculationTableName) { + return null; + } + + @Override + public ArtRecommendMajorBaseDTO aiAutoRecommendMajor(QueryRecommendMajorVO queryRecommendMajorVO) { + String batch = queryRecommendMajorVO.getBatch(); + int pageNum = 1; + int pageSize = 64; + if ("提前批".equals(batch)) { + pageSize = 1; + } + YxUserScore activeCurrentUserScore = null; + if (StringUtils.isNotBlank(queryRecommendMajorVO.getScoreId())) { + activeCurrentUserScore = yxUserScoreService.findById(queryRecommendMajorVO.getScoreId()); + } else { + activeCurrentUserScore = yxUserScoreService.getActiveCurrentUserScore(); + } + + String cognitioPolyclinic = activeCurrentUserScore.getCognitioPolyclinic(); + String professionalCategory = activeCurrentUserScore.getProfessionalCategory();//专业类别 + queryRecommendMajorVO.setProfessionalCategory(professionalCategory); + queryRecommendMajorVO.setCognitioPolyclinic(cognitioPolyclinic); + + //有子级专业 + if (StringUtils.isNotBlank(activeCurrentUserScore.getProfessionalCategoryChildren()) && !"高职高专".equals(queryRecommendMajorVO.getBatch())) { + queryRecommendMajorVO.setPCategoryChildrenList(new ArrayList<>(Arrays.asList(activeCurrentUserScore.getProfessionalCategoryChildren().split(",")))); + if("表演类".equals(activeCurrentUserScore.getProfessionalCategory())){ + queryRecommendMajorVO.getPCategoryChildrenList().add("兼报"); + } + } + if (StringUtils.isNotBlank(queryRecommendMajorVO.getMajorCode())) { + queryRecommendMajorVO.setMajorCodeList(Arrays.asList(queryRecommendMajorVO.getMajorCode().split(","))); + } + if (StringUtils.isNotBlank(queryRecommendMajorVO.getSchoolNatureStrs())) { + queryRecommendMajorVO.setSchoolNatureList(Arrays.asList(queryRecommendMajorVO.getSchoolNatureStrs().split(","))); + } + if (StringUtils.isNotBlank(queryRecommendMajorVO.getProvince())) { + queryRecommendMajorVO.setAddressList(Arrays.asList(queryRecommendMajorVO.getProvince().split(","))); + } + queryRecommendMajorVO.setScoreId(activeCurrentUserScore.getId()); + List recommendMajorList = baseMapper.recommendMajorListBenOrZhuan(activeCurrentUserScore.getCalculationTableName(),queryRecommendMajorVO); + + yxHistoryMajorEnrollService.recommendMajorDTOListSetHistoryInfo(recommendMajorList,ScoreUtil.getShowOldYears()); + + //插入 院校标签 + this.setSchoolTagsList(recommendMajorList); + int total = recommendMajorList.size(); + List recommendMajorDTOList = recommendMajorList.stream().skip(0).limit(pageSize).collect(Collectors.toList()); + ArtRecommendMajorBaseDTO artRecommendMajorBaseDTO = new ArtRecommendMajorBaseDTO(); + IPage pageList = new Page<>(); + pageList.setTotal(total);//总记录数 + pageList.setCurrent(pageNum); + pageList.setSize(pageSize); + convertRecommendMajorListToNull(recommendMajorDTOList); + pageList.setRecords(recommendMajorDTOList); + artRecommendMajorBaseDTO.setPageList(pageList); + return artRecommendMajorBaseDTO; + } + + @Override + public void copyNew(String oldScoreId, String newScoreId, String oldCalculationMajorTableName, String newCalculationMajorTableName) { + LambdaQueryWrapper yxCalculationMajorLambdaQueryWrapper = new LambdaQueryWrapper<>(); + yxCalculationMajorLambdaQueryWrapper.eq(YxCalculationMajor::getScoreId,oldScoreId); + List list = this.list(yxCalculationMajorLambdaQueryWrapper); + for (YxCalculationMajor yxCalculationMajor : list) { + yxCalculationMajor.setId(null); + yxCalculationMajor.setScoreId(newScoreId); + } + baseMapper.insertBatch(newCalculationMajorTableName,newScoreId,list); + } + + @Override + public List groupByBatchNumber(QueryRecommendMajorVO queryRecommendMajorVO) { + YxUserScore activeCurrentUserScore = yxUserScoreService.getActiveCurrentUserScore(); + queryRecommendMajorVO.setScoreId(activeCurrentUserScore.getId()); + //文理分科 + queryRecommendMajorVO.setCognitioPolyclinic(activeCurrentUserScore.getCognitioPolyclinic()); + //专业类别 + queryRecommendMajorVO.setProfessionalCategory(activeCurrentUserScore.getProfessionalCategory()); + //标签类别 + if (StringUtils.isNotBlank(queryRecommendMajorVO.getTagsStrs())) { + queryRecommendMajorVO.setTagsList(Arrays.asList(queryRecommendMajorVO.getTagsStrs().split(","))); + } + //院校类型 + if (StringUtils.isNotBlank(queryRecommendMajorVO.getSchoolNatureStrs())) { + queryRecommendMajorVO.setSchoolNatureList(Arrays.asList(queryRecommendMajorVO.getSchoolNatureStrs().split(","))); + } + //院校地区 + if (StringUtils.isNotBlank(queryRecommendMajorVO.getProvince())) { + queryRecommendMajorVO.setAddressList(Arrays.asList(queryRecommendMajorVO.getProvince().split(","))); + } + //科研教学 + if (StringUtils.isNotBlank(queryRecommendMajorVO.getKyjxStrs())) { + queryRecommendMajorVO.setKyjxList(Arrays.asList(queryRecommendMajorVO.getKyjxStrs().split(","))); + } + //录取方式 + String rulesEnrollProbability = queryRecommendMajorVO.getRulesEnrollProbability(); + if (StringUtils.isNotBlank(rulesEnrollProbability)) { + queryRecommendMajorVO.setRulesEnrollProbabilityList(Arrays.asList(rulesEnrollProbability.split(","))); + } + //获取推荐志愿信息service + return baseMapper.groupByBatchNumber(activeCurrentUserScore.getCalculationTableName(),queryRecommendMajorVO); + } + + @Override + public YxCalculationMajor getCalculationById(String calculationTableName, String calculationMajorId) { + return baseMapper.getCalculationById(calculationTableName, calculationMajorId); + } + + @Override + public boolean updateScoreId(String calculationTableName, String scoreId, String bakScoreId) { + return baseMapper.updateScoreId(calculationTableName, scoreId, bakScoreId) >= 1; + } + + @Override + public List listByIdsOfYear(List calculationMajorIdList, String newCalculationMajorName) { + return baseMapper.selectBatchIdsOfYear(calculationMajorIdList, newCalculationMajorName); + } + + private void setSchoolTagsList(List recommendMajorList) { + if (CollectionUtils.isNotEmpty(recommendMajorList)) { + Set schoolIdSet = recommendMajorList.stream().map(RecommendMajorDTO::getSchoolId).collect(Collectors.toSet()); + List schoolList = yxSchoolMapper.selectBatchIds(schoolIdSet); + if (CollectionUtils.isNotEmpty(schoolList)) { + Map yxSchoolMap = schoolList.stream().collect(Collectors.toMap(YxSchool::getId, s -> s)); + YxSchool school = null; + Set tagsList; + for (RecommendMajorDTO recommendMajorDTO : recommendMajorList) { + school = yxSchoolMap.get(recommendMajorDTO.getSchoolId()); + if (school == null) { + continue; + } + tagsList = new HashSet<>(); + if (org.apache.commons.lang3.StringUtils.isNotBlank(school.getSchoolType())) { + tagsList.add(school.getSchoolType());//学校层次 + } + if (org.apache.commons.lang3.StringUtils.isNotBlank(school.getSchoolNature())) { + tagsList.add(school.getSchoolNature());//办学性质 + } + if (org.apache.commons.lang3.StringUtils.isNotBlank(school.getInstitutionType())) { + String[] split = school.getInstitutionType().split(" "); + for (String s : split) { + tagsList.add(s + "类");//院校类型 + } + } + + //判断 是否是 985,211 + if ("1".equals(school.getIs211())) { + tagsList.add("211"); + } + if ("1".equals(school.getIs985())) { + tagsList.add("985"); + } + if ("双一流".equals(school.getSfsyl())) { + tagsList.add("双一流"); + } + if (org.apache.commons.lang3.StringUtils.isNotBlank(school.getGzsz())) { + tagsList.add(school.getGzsz()); + } + recommendMajorDTO.setTagsList(tagsList); + } + } + } + } + + /** + * 统一测算录取率方法 + * + * @param recommendMajorDTOList + * @param userScore + */ + private void checkEnrollProbability(List recommendMajorDTOList, YxUserScore userScore) { + String professionalCategory = userScore.getProfessionalCategory(); + if ("表演类".equals(professionalCategory)) { + biaoyanService.biaoyanRecommendMajorListSetEnrollProbability(recommendMajorDTOList, userScore); + } else if ("音乐类".equals(professionalCategory)) { + //音乐类的 + musicService.musicRecommendMajorListSetEnrollProbability(recommendMajorDTOList, userScore); +// } else if ("舞蹈类".equals(professionalCategory)) { +// wuDaoService.wudaoRecommendMajorListSetEnrollProbability(recommendMajorDTOList, userScore); +// } else if ("播音与主持类".equals(professionalCategory)) { +// boYinService.boYinRecommendMajorListSetEnrollProbability(recommendMajorDTOList, userScore); + } else { + betaRecommendMajorListSetEnrollProbability(recommendMajorDTOList, userScore); + } + } + + /** + * 美术与设计类,书法类,体育类 按这个走 获取录取率 + */ + private void betaRecommendMajorListSetEnrollProbability(List recommendMajorList, YxUserScore activeCurrentUserScore) { + int index = 0; + try { + long startTime = new Date().getTime(); + //专业类别 + String professionalCategory = activeCurrentUserScore.getProfessionalCategory(); + String rulesEnrollProbability = null; + String probabilityOperator = null; + String showRulesEnrollProbability = null; + String showProbabilityOperator = null; + String nowBatch = activeCurrentUserScore.getBatch(); + BigDecimal studentScore = null;//学生折合分 + BigDecimal privateStudentScore = null;//学生折合分显示用 + BigDecimal culturalScore = activeCurrentUserScore.getCulturalScore();//学生的文化分 + BigDecimal professionalScore = activeCurrentUserScore.getProfessionalScore();//学生的专业分 + BigDecimal nowYearDiff = null;//当前年分差 + Map historyScoreControlLineMap = yxHistoryScoreControlLineService.mapsBatchByProfessionalCategoryOfYear(YxConstant.nowYear, professionalCategory, activeCurrentUserScore.getCognitioPolyclinic()); + BigDecimal culturalControlLine = null;//文化分省控线 + BigDecimal specialControlLine = null;//专业分省控线 + BigDecimal nowYearProvincialControlLine = null;//当前年省控线 + BigDecimal historyThreeYearDiff = null;//近三年平均差值 + BigDecimal enrollProbability = null;//录取率 + Map differenceMap = null; + List historyMajorEnrollList = null;//历年录取信息 + if (CollectionUtils.isNotEmpty(recommendMajorList)) { + //遍历专业列表 + for (RecommendMajorDTO recommendMajorDTO : recommendMajorList) { + index++; + //取出往年的分数信息, 算出 三年平均差值 + historyMajorEnrollList = recommendMajorDTO.getHistoryMajorEnrollList(); + rulesEnrollProbability = recommendMajorDTO.getPrivateRulesEnrollProbability(); + probabilityOperator = recommendMajorDTO.getPrivateProbabilityOperator(); + culturalControlLine = historyScoreControlLineMap.get(recommendMajorDTO.getBatch()).getCulturalScore(); + specialControlLine = historyScoreControlLineMap.get(recommendMajorDTO.getBatch()).getSpecialScore(); + //没有计算方式 跳过 + if (StringUtils.isBlank(rulesEnrollProbability)) { + continue; + } + + // && "美术学(师范)".equals(recommendMajorDTO.getMajorName()) + if("2565".equals(recommendMajorDTO.getSchoolCode()) + && "视觉传达设计".equals(recommendMajorDTO.getMajorName())){ + System.out.println("qvq"); + } + + //不是体育类的专业,如果是文过专排,并且没有录取计算运算符时,文过专排是文*0+专*1,专过文排是文*1+专*0 + if ("文过专排".equals(rulesEnrollProbability) && StringUtils.isBlank(probabilityOperator)) { + probabilityOperator = "文*0+专*1"; + } else if ("专过文排".equals(rulesEnrollProbability) && StringUtils.isBlank(probabilityOperator)) { + probabilityOperator = "文*1+专*0"; + } + if (StringUtils.isBlank(probabilityOperator)) { + recommendMajorDTO.setEnrollProbability(YxConstant.bigDecimal0); + continue; + } + + //判断其他录取要求 + if(!ScoreUtil.otherScoreJudge(professionalScore,activeCurrentUserScore,recommendMajorDTO)){ + recommendMajorDTO.setEnrollProbability(YxConstant.bigDecimal0); + continue; + } + + // 25年专业录取原则变动,概率修改为0 + if("体育类".equals(recommendMajorDTO.getMajorType()) && Arrays.asList("6530、6085、6110、6065、6050".split("、")).contains(recommendMajorDTO.getSchoolCode())){ + recommendMajorDTO.setEnrollProbability(YxConstant.bigDecimal0); + continue; + } + + //判断录取方式,分数是否过省控线 + if (!ScoreUtil.crossingControlLine(rulesEnrollProbability, culturalScore, professionalScore, culturalControlLine, specialControlLine)) { + recommendMajorDTO.setEnrollProbability(YxConstant.bigDecimal0); + continue; + } + + //算分用 + // 当前学生的分数折合 + studentScore = ScoreUtil.convertIntoScore(rulesEnrollProbability, activeCurrentUserScore.getCulturalScore(), activeCurrentUserScore.getProfessionalScore(), recommendMajorDTO.getFirstLevelDiscipline(), probabilityOperator); + recommendMajorDTO.setPrivateStudentScore(studentScore); + + if (StringUtils.isNotBlank(recommendMajorDTO.getRulesEnrollProbability())) { + //recommendMajorDTO.setStudentConvertedScore(studentScore); + //计算展示用分数 + recommendMajorDTO.setStudentScore(studentScore); + } + + //没有往年录取分数线信息 + if (CollectionUtils.isEmpty(historyMajorEnrollList) || !(ScoreUtil.hasComputeEnrollProbabilityPermissions(nowBatch, recommendMajorDTO.getBatch()))) { + recommendMajorDTO.setEnrollProbability(YxConstant.bigDecimal0); + continue; + } + + //录取方式计算 + if (rulesEnrollProbability.equals("文过专排主科")) { + if (CollectionUtils.isEmpty(historyMajorEnrollList)) { + recommendMajorDTO.setEnrollProbability(YxConstant.bigDecimal0); + continue; + } else { + YxHistoryMajorEnroll yxHistoryMajorEnroll = historyMajorEnrollList.get(0); + BigDecimal admissionLine = yxHistoryMajorEnroll.getAdmissionLine(); + enrollProbability = studentScore.multiply(admissionLine).multiply(YxConstant.bigDecimal075); + if (studentScore.compareTo(admissionLine) < 0) { + //如果当前主项成绩<近一年的录取分数,录取概率*0.5 + enrollProbability = enrollProbability.multiply(YxConstant.bigDecimal05); + } + } + } else { + //当前年省控线 折合后 + nowYearProvincialControlLine = ScoreUtil.covertIntoControlLine(rulesEnrollProbability, culturalControlLine, specialControlLine, recommendMajorDTO.getFirstLevelDiscipline(), probabilityOperator); + if (nowYearProvincialControlLine == null) { + continue; + } + + //历年分差 + differenceMap = ScoreUtil.computeHistoryMajorEnrollScoreLineDifferenceWithRulesEnrollProbability(rulesEnrollProbability, probabilityOperator, historyMajorEnrollList); + historyThreeYearDiff = (BigDecimal) differenceMap.get("scoreDifference"); +// if(professionalCategory.equals("体育类") +// && "文过专排".equals(rulesEnrollProbability)){ +// historyThreeYearDiff = historyThreeYearDiff.multiply(YxConstant.bigDecimal5); +// } + + // 体育,文过专排 / 5 +// if (professionalCategory.equals("体育类") +// && !"文过专排".equals(rulesEnrollProbability) +// && !"专过文排".equals(rulesEnrollProbability)) { +// // 体育,非专过文排 / 7.5 +// nowYearProvincialControlLine = nowYearProvincialControlLine.divide(YxConstant.bigDecimal7p5, 4, RoundingMode.HALF_UP); +// //当前年线差 +// studentScore = studentScore.divide(YxConstant.bigDecimal7p5, 4, RoundingMode.HALF_UP); +// } + //当前年线差 + nowYearDiff = studentScore.subtract(nowYearProvincialControlLine); + + //艺术类,高职高专 算 当前年线差率 (当前年线差/当前年省控线) +// if ("高职高专".equals(recommendMajorDTO.getBatch()) && !"体育类".equals(recommendMajorDTO.getMajorType())) { +// nowYearDiff = nowYearDiff.divide(nowYearProvincialControlLine,6,RoundingMode.HALF_UP); +// } + if(historyThreeYearDiff.compareTo(YxConstant.bigDecimal0) == 0){ + // 历年线差无,无概率 + recommendMajorDTO.setEnrollProbability(YxConstant.bigDecimal0); + continue; + } + enrollProbability = ScoreUtil.commonCheckEnrollProbability(nowYearDiff,historyThreeYearDiff); + + //if ("体育类".equals(recommendMajorDTO.getMajorType()) && ("文过专排".equals(rulesEnrollProbability) || "专过文排".equals(rulesEnrollProbability))) { + // enrollProbability = enrollProbability.divide(YxConstant.bigDecimal075, 4, RoundingMode.HALF_UP); + //} + } + + enrollProbability = ScoreUtil.commonCheckEnrollProbabilityBeilv(enrollProbability); + recommendMajorDTO.setEnrollProbability(enrollProbability); + } + } + long endTime = new Date().getTime(); + log.warn("用时:"+(endTime-startTime)+"毫秒"); + } catch (Exception e) { + e.printStackTrace(); + } + } + +} diff --git a/java_code/YxHistoryScoreControlLineServiceImpl.java b/java_code/YxHistoryScoreControlLineServiceImpl.java new file mode 100644 index 0000000..548bc82 --- /dev/null +++ b/java_code/YxHistoryScoreControlLineServiceImpl.java @@ -0,0 +1,105 @@ +package org.jeecg.modules.yx.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.commons.lang.StringUtils; +import org.jeecg.modules.yx.entity.YxHistoryScoreControlLine; +import org.jeecg.modules.yx.mapper.YxHistoryScoreControlLineMapper; +import org.jeecg.modules.yx.service.IYxHistoryScoreControlLineService; +import org.springframework.beans.BeanUtils; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Service; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @Description: 历年各专业省控分数线 + * @Author: jeecg-boot + * @Date: 2023-12-02 + * @Version: V1.0 + */ +@Service +public class YxHistoryScoreControlLineServiceImpl extends ServiceImpl implements IYxHistoryScoreControlLineService { + + @Override + public IPage pageList(Page page, YxHistoryScoreControlLine yxHistoryScoreControlLine) { + IPage yxHistoryScoreControlLineIPage = baseMapper.pageList(page, yxHistoryScoreControlLine); + List newRecords = new ArrayList<>(); + List records = yxHistoryScoreControlLineIPage.getRecords(); + YxHistoryScoreControlLine historyScoreControlLine = null; + for (YxHistoryScoreControlLine record : records) { + if ("本科A段".equals(record.getBatch())) { + historyScoreControlLine = new YxHistoryScoreControlLine(); + BeanUtils.copyProperties(record, historyScoreControlLine); + historyScoreControlLine.setBatch("提前批"); + newRecords.add(historyScoreControlLine); + } + newRecords.add(record); + } + yxHistoryScoreControlLineIPage.setRecords(newRecords); + return yxHistoryScoreControlLineIPage; + } + + @Override + public YxHistoryScoreControlLine getByProfessionalCategoryOfYear(String year, String professionalCategory, String category, String batch) { + LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>(); + lambdaQueryWrapper.eq(YxHistoryScoreControlLine::getYear, year); + lambdaQueryWrapper.eq(YxHistoryScoreControlLine::getProfessionalCategory, professionalCategory); + lambdaQueryWrapper.eq(YxHistoryScoreControlLine::getCategory, category); + lambdaQueryWrapper.eq(YxHistoryScoreControlLine::getBatch, batch); + lambdaQueryWrapper.last("limit 1"); + return this.getOne(lambdaQueryWrapper); + } + + @Override + public List listByProfessionalCategoryOfYear(String year, String professionalCategory, String category) { + LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>(); + lambdaQueryWrapper.eq(YxHistoryScoreControlLine::getYear, year); + lambdaQueryWrapper.eq(YxHistoryScoreControlLine::getProfessionalCategory, professionalCategory); + lambdaQueryWrapper.eq(YxHistoryScoreControlLine::getCategory, category); + lambdaQueryWrapper.orderByDesc(YxHistoryScoreControlLine::getBatch); + List newList = new ArrayList<>(); + List list = this.list(lambdaQueryWrapper); + YxHistoryScoreControlLine yxHistoryScoreControlLine1 = null; + for (YxHistoryScoreControlLine yxHistoryScoreControlLine : list) { + if (yxHistoryScoreControlLine.getBatch().equals("本科A段")) { + yxHistoryScoreControlLine1 = new YxHistoryScoreControlLine(); + BeanUtils.copyProperties(yxHistoryScoreControlLine, yxHistoryScoreControlLine1); + yxHistoryScoreControlLine1.setBatch("提前批"); + newList.add(yxHistoryScoreControlLine1); + } + newList.add(yxHistoryScoreControlLine); + } + return newList; + } + + @Override + public Map mapsBatchByProfessionalCategoryOfYear(String year, String professionalCategory, String category) { + List yxHistoryScoreControlLines = this.listByProfessionalCategoryOfYear(year, professionalCategory, category); + return yxHistoryScoreControlLines.stream().collect(Collectors.toMap(y -> y.getBatch(), y -> y)); + } + + @Override + public Map allMaps() { + List list = this.list(); + Map maps = new LinkedHashMap<>(); + String key = null; + for (YxHistoryScoreControlLine record : list) { + //文科_本科A段_美术与设计_2023 + key = record.getCategory() + "_" + record.getBatch() + "_" + record.getProfessionalCategory() + "_" + record.getYear(); + maps.put(key,record); + if (record.getBatch().equals("本科A段")) { + key = record.getCategory() + "_本科提前批_" + record.getProfessionalCategory() + "_" + record.getYear(); + maps.put(key,record); + } + } + return maps; + } +} diff --git a/project_codebase.md b/project_codebase.md index 6b5b403..e0679c0 100644 --- a/project_codebase.md +++ b/project_codebase.md @@ -8,9 +8,10 @@ - `Error(c *gin.Context, code int, msg string)`: 发送错误响应。 ## server/config -- `InitDB()`: 初始化GORM数据库连接。 +- `LoadConfig()`: 从 YAML 文件加载配置,根据 `GO_ENV` 环境变量选择配置文件(默认 dev)。 +- `InitDB()`: 初始化GORM数据库连接,支持 SQL 日志配置(自动写入 `logs/sql-YYYY-MM-DD.log`)。 - `InitRedis()`: 初始化Redis客户端。 -- `AppConfig`: 全局配置变量,包含 `Log`, `Security`, `RateLimit`, `Swagger` 配置。 +- `AppConfig`: 全局配置变量,包含 `Log`, `Security`, `RateLimit`, `Swagger`, `Database`, `Redis` 配置。 ## server/middleware - `AuthMiddleware`: JWT认证中间件。 diff --git a/project_doing.md b/project_doing.md index 8fd8bad..2d4b163 100644 --- a/project_doing.md +++ b/project_doing.md @@ -1,111 +1,10 @@ -# 项目过程记录 -## 2025-12-17 -### [任务执行] 初始化模块 yitisheng/yx_user_score -- **操作目标**: 创建新的业务模块目录,并准备实现用户分数和志愿管理功能。 -- **影响范围**: `server/modules/yitisheng/yx_user_score/` -- **修改前记录**: 项目中暂无此模块。 - -### [任务执行] 修正模块路径并实现 CRUD -- **操作目标**: 根据 Task3.md 要求,将 yx_user_score, yx_volunteer, yx_volunteer_record 实现于 modules/yx/ 下。 -- **影响范围**: `server/modules/yx/` -- **修改结果**: - - 删除了错误的 `modules/yitisheng` 目录(尝试删除)。 - - 在 `modules/yx` 下实现了 Entity, Mapper, Service, Controller。 - - 更新了 `main.go` 注册路由。 - -### [任务执行] 增加 Swagger 文档访问密码验证 -- **操作目标**: 为 Swagger 文档接口添加 Basic Auth 验证,防止未授权访问。 -- **影响范围**: `server/config/config.go`, `server/main.go` -- **修改前记录**: Swagger 接口公开,无验证。 -- **修改结果**: - - `server/config/config.go`: 新增 `SwaggerConfig` 配置项 (默认 admin/password)。 - - `server/main.go`: 为 `/swagger` 路由组添加了 `gin.BasicAuth` 中间件。 - -### [任务执行] 解决本地调试 CORS 问题 -- **操作目标**: 允许前端 Vue3 项目跨域调用后端接口。 -- **影响范围**: `server/middleware/cors.go`, `server/main.go` -- **修改前记录**: 后端未配置 CORS,前端跨域请求被拦截。 -- **修改结果**: - - `server/middleware/cors.go`: 创建了 CORS 中间件,允许 `Origin` 头部指定的来源,并放行 `OPTIONS` 请求。 - - `server/main.go`: 全局注册了 CORS 中间件。 - -### [任务执行] 实现用户成绩保存接口 -- **操作目标**: 根据 Task4.md 要求,实现用户成绩信息的保存接口,并包含特定的业务校验逻辑。 -- **影响范围**: `server/modules/yx/controller/yx_user_score_controller.go` -- **修改前记录**: `YxUserScoreController` 仅有基本的 CRUD 接口。 -- **修改结果**: - - 定义了 `SaveScoreRequest` 结构体,包含严格的字段校验逻辑(文理分科、科目限制、成绩范围等)。 - - 实现了 `SaveUserScore` 方法,处理 DTO 到 Entity 的转换(包括科目列表拼接、子专业成绩映射)。 - - 注册了 `POST /yx-user-scores/save-score` 路由。 - -### [任务执行] 重构 DTO 代码结构 -- **操作目标**: 将 `SaveScoreRequest` 结构体从 Controller 中分离到独立的 DTO 包中。 -- **影响范围**: `server/modules/yx/dto/yx_user_score_dto.go`, `server/modules/yx/controller/yx_user_score_controller.go` -- **修改前记录**: DTO 结构体直接定义在 Controller 文件中。 -- **修改结果**: - - 创建了 `server/modules/yx/dto/` 目录。 - - 将 `SaveScoreRequest` 及其校验逻辑移动到 `yx_user_score_dto.go`。 - - Controller 改为引用 `dto` 包。 - -### [任务执行] 处理 UserScoreVO 信息转换 -- **操作目标**: 在 `UserScoreService` 中实现 Entity 到 VO 的转换,并完善 `UserScoreVO` 定义。 -- **影响范围**: `server/modules/user/vo/user_score_vo.go`, `server/modules/user/service/user_score_service.go` -- **修改前记录**: `UserScoreVO` 为空,`GetActiveByID` 返回原始 Entity。 -- **修改结果**: - - 定义了 `UserScoreVO` 结构体,其字段设计参考了 `SaveScoreRequest`。 - - 在 `UserScoreService` 中实现了 `convertEntityToVo` 私有方法,处理了逗号分隔字符串到切片的转换,以及具体分数字段到 Map 的映射。 - - 更新 `GetActiveByID` 返回 `UserScoreVO` 对象。 - -### [任务执行] 增加用户成绩分页列表接口 -- **操作目标**: 在 `UserScoreController` 和 `UserScoreService` 中增加获取当前用户所有成绩的分页列表接口。 -- **影响范围**: `server/modules/user/service/user_score_service.go`, `server/modules/user/controller/user_score_controller.go` -- **修改前记录**: 仅支持获取当前激活的单条成绩。 -- **修改结果**: - - 在 `UserScoreService` 中实现了 `ListByUser` 方法,支持分页查询并返回 VO 列表。 - - 在 `UserScoreController` 中增加了 `List` 方法,并注册了 `GET /user/score/list` 路由。 - -### [任务执行] 统一管理 Redis Key 及全局常量 -- **操作目标**: 创建专门存放 Redis Key 和业务常量的地方,并重构相关代码以引用这些常量。 -- **影响范围**: `server/common/constants.go`, `server/modules/system/service/sys_user_service.go`, `server/middleware/auth.go`, `server/common/context.go` -- **修改前记录**: 常量散落在各个业务文件和中间件中,存在硬编码和重复定义。 -- **修改结果**: - - 创建了 `server/common/constants.go`,集中管理 Redis 前缀、Token 请求头、业务状态码等。 - - 重构了 `SysUserService`,使用 `common.RedisTokenPrefix` 和 `common.RedisTokenExpire`。 - - 重构了 `AuthMiddleware`,使用 `common.TokenHeader` 和 `common.ContextUserKey`。 - - 清理了 `common/context.go` 中的重复定义。 - -### [任务执行] 完善成绩回显逻辑 -- **操作目标**: 确保保存成功后能回显完整成绩信息,并提供通过 ID 获取成绩的接口以便修改。 -- **影响范围**: `server/modules/user/service/user_score_service.go`, `server/modules/user/controller/user_score_controller.go` -- **修改前记录**: `SaveUserScore` 返回 Entity 而非 VO,且缺少通过 ID 获取特定成绩的接口。 -- **修改结果**: - - 重构 `UserScoreService.SaveUserScore`,在事务提交后返回转换后的 `UserScoreVO`。 - - 增加 `UserScoreService.GetByID` 方法。 - - 在 `UserScoreController` 中注册 `GET /user/score/:id` 路由,并实现 `GetByID` 方法。 - - 将原来的 `GET /user/score` 路由方法名改为 `GetActive`,使其语义更明确。 - -### [任务执行] 封装 ID 生成工具 -- **操作目标**: 封装一个基于时间戳生成 long 类型(及字符串类型)ID 的工具类。 -- **影响范围**: `server/common/id_utils.go`, `server/modules/user/service/user_score_service.go` -- **修改前记录**: 之前可能依赖数据库自增或未手动设置 ID。 -- **修改结果**: - - 创建了 `server/common/id_utils.go`,提供了基于时间戳 + 序列号的 ID 生成逻辑。 - - 支持生成 `int64` 类型的 ID,也支持直接生成 `string` 类型以便于 Entity 使用。 - - 在 `UserScoreService.SaveUserScore` 中引入了该工具,手动为新记录分配 ID。 - -### [任务执行] 实现院校专业复杂查询 -- **操作目标**: 在 `YxSchoolMajorMapper` 中实现基于复杂 SQL 的查询,并将结果封装为 DTO。 -- **影响范围**: `server/modules/yx/dto/yx_school_major_dto.go`, `server/modules/yx/mapper/yx_school_major_mapper.go` -- **修改前记录**: 只有基本的 CRUD 操作。 -- **修改结果**: - - 创建了 `SchoolMajorDTO` 和 `SchoolMajorQuery` 结构体。 - - 在 `YxSchoolMajorMapper` 中实现了 `SelectSchoolMajor` 方法,支持 `major_type`, `category`, `major_type_child` (IN查询), `main_subjects` 的动态过滤。 - -### [任务执行] 搭建本地测试环境 -- **操作目标**: 创建独立的测试目录和初始化脚本,以便在不启动 Web 服务的情况下测试 Mapper、Service 和 Redis。 -- **影响范围**: `server/tests/init_test.go`, `server/tests/service_test.go` -- **修改前记录**: 只能通过 API 接口进行测试,调试效率低。 -- **修改结果**: - - 创建了 `server/tests/init_test.go`,利用 `init()` 函数自动初始化 DB 和 Redis 连接。 - - 创建了 `server/tests/service_test.go`,编写了 Redis 读写、复杂 SQL 查询和 ID 生成器的测试用例。 +### [任务执行] 修复 SQL 日志未写入文件问题 +- **操作目标**: 修改数据库初始化逻辑,使其支持将 SQL 日志写入到指定文件。 +- **影响范围**: `server/config/database.go` +- **修改前记录**: GORM Logger 硬编码输出到 `os.Stdout`。 +- **修改结果**: + - 在 `server/config/database.go` 中增加了 `getLogWriter` 函数。 + - 该函数根据 `AppConfig.Log` 配置创建日志目录和文件(命名为 `sql-YYYY-MM-DD.log`)。 + - 如果配置开启了控制台输出,则使用 `io.MultiWriter` 同时输出到文件和控制台。 + - 验证:在测试环境下临时开启日志模式,成功生成了 `logs/sql-2025-12-25.log` 文件。 diff --git a/project_index.md b/project_index.md index 3ae19f5..1898260 100644 --- a/project_index.md +++ b/project_index.md @@ -3,7 +3,10 @@ ## server/ - `main.go`: 应用程序入口,负责路由注册和服务器启动。 - `config/`: 配置文件目录。 - - `config.go`: 应用全局配置。 + - `config.go`: 应用配置结构定义与加载逻辑。 + - `config.dev.yaml`: 开发环境配置(开启 SQL 日志)。 + - `config.test.yaml`: 测试环境配置。 + - `config.prod.yaml`: 生产环境配置。 - `database.go`: 数据库连接配置。 - `redis.go`: Redis连接配置。 - `tests/`: 单元测试与集成测试目录。 diff --git a/project_task.md b/project_task.md index 4861a26..62b41ab 100644 --- a/project_task.md +++ b/project_task.md @@ -6,4 +6,8 @@ - [已完成] 实现 yx_volunteer 表相关代码 (Entity, Mapper, Service, Controller) - [已完成] 实现 yx_volunteer_record 表相关代码 (Entity, Mapper, Service, Controller) - [已完成] 注册新路由 -- [未开始] 验证与测试 +- [进行中] 环境配置与部署准备 +- [已完成] 搭建开发、测试、生产环境配置体系 +- [已完成] 配置 SQL 日志打印 +- [进行中] 编写环境配置文档 + diff --git a/server/config/config.dev.yaml b/server/config/config.dev.yaml new file mode 100644 index 0000000..33a19af --- /dev/null +++ b/server/config/config.dev.yaml @@ -0,0 +1,47 @@ +log: + level: debug + dir: logs + console: true + +security: + enable: false + header_key: X-App-Sign + secret_key: yts@2025#secure + +rate_limit: + enable: true + default: + interval: 2 + max_requests: 3 + rules: + /api/user/auth/login: + interval: 5 + max_requests: 1 + /api/yx-school-majors: + interval: 1 + max_requests: 5 + /api/user/score/save-score: + interval: 1 + max_requests: 1 + +swagger: + user: admin + password: password + +database: + driver: mysql + host: 10.13.13.1 + port: 3306 + database: yitisheng + username: root + password: "Db$7Hn#4Jm9Pq2!Xz" + charset: utf8mb4 + max_idle_conns: 20 + max_open_conns: 100 + conn_max_lifetime: 1 + log_mode: true + +redis: + addr: 10.13.13.1:56379 + password: "Rd@5Wk8#Nv3Yt6$Bm" + db: 1 diff --git a/server/config/config.go b/server/config/config.go index 3834883..25a3dbe 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -1,73 +1,109 @@ // Package config 应用配置 package config +import ( + "fmt" + "os" + + "gopkg.in/yaml.v3" +) + // AppConfig 应用配置 -var AppConfig = &appConfig{ - // 日志配置 - Log: LogConfig{ - Level: "debug", // debug/info/warn/error - Dir: "logs", // 日志目录 - Console: true, // 是否同时输出到控制台 - }, - // 安全配置 - Security: SecurityConfig{ - Enable: false, // 是否启用安全校验 - HeaderKey: "X-App-Sign", // 请求头校验字段 - SecretKey: "yts@2025#secure", // 签名密钥 - }, - // 限流配置 - RateLimit: RateLimitConfig{ - Enable: true, - Default: RateLimitRule{Interval: 2, MaxRequests: 3}, // 默认2秒1次 - Rules: map[string]RateLimitRule{ - "/api/user/auth/login": {Interval: 5, MaxRequests: 1}, // 登录5秒1次 - "/api/yx-school-majors": {Interval: 1, MaxRequests: 5}, // 查询1秒5次 - "/api/user/score/save-score": {Interval: 1, MaxRequests: 1}, // 保存5秒1次 - }, - }, - // Swagger配置 - Swagger: SwaggerConfig{ - User: "admin", - Password: "password", - }, -} +var AppConfig = &appConfig{} type appConfig struct { - Log LogConfig - Security SecurityConfig - RateLimit RateLimitConfig - Swagger SwaggerConfig + Log LogConfig `yaml:"log"` + Security SecurityConfig `yaml:"security"` + RateLimit RateLimitConfig `yaml:"rate_limit"` + Swagger SwaggerConfig `yaml:"swagger"` + Database DatabaseConfig `yaml:"database"` + Redis RedisConfig `yaml:"redis"` } // LogConfig 日志配置 type LogConfig struct { - Level string // 日志级别 - Dir string // 日志目录 - Console bool // 是否输出到控制台 + Level string `yaml:"level"` // 日志级别 + Dir string `yaml:"dir"` // 日志目录 + Console bool `yaml:"console"` // 是否输出到控制台 } // SecurityConfig 安全配置 type SecurityConfig struct { - Enable bool // 是否启用 - HeaderKey string // 请求头字段名 - SecretKey string // 签名密钥 + Enable bool `yaml:"enable"` // 是否启用 + HeaderKey string `yaml:"header_key"` // 请求头字段名 + SecretKey string `yaml:"secret_key"` // 签名密钥 } // RateLimitConfig 限流配置 type RateLimitConfig struct { - Enable bool // 是否启用 - Default RateLimitRule // 默认规则 - Rules map[string]RateLimitRule // 特定路径规则 + Enable bool `yaml:"enable"` // 是否启用 + Default RateLimitRule `yaml:"default"` // 默认规则 + Rules map[string]RateLimitRule `yaml:"rules"` // 特定路径规则 } // RateLimitRule 限流规则 type RateLimitRule struct { - Interval int // 时间间隔(秒) - MaxRequests int // 最大请求次数 + Interval int `yaml:"interval"` // 时间间隔(秒) + MaxRequests int `yaml:"max_requests"` // 最大请求次数 } // SwaggerConfig Swagger文档认证配置 type SwaggerConfig struct { - User string - Password string + User string `yaml:"user"` + Password string `yaml:"password"` +} + +// DatabaseConfig 数据库配置 +type DatabaseConfig struct { + Driver string `yaml:"driver"` + Host string `yaml:"host"` + Port int `yaml:"port"` + Database string `yaml:"database"` + Username string `yaml:"username"` + Password string `yaml:"password"` + Charset string `yaml:"charset"` + MaxIdleConns int `yaml:"max_idle_conns"` + MaxOpenConns int `yaml:"max_open_conns"` + ConnMaxLifetime int `yaml:"conn_max_lifetime"` // 小时 + LogMode bool `yaml:"log_mode"` // 是否开启SQL日志 +} + +// RedisConfig Redis配置 +type RedisConfig struct { + Addr string `yaml:"addr"` + Password string `yaml:"password"` + DB int `yaml:"db"` +} + +// LoadConfig 加载配置 +func LoadConfig() { + env := os.Getenv("GO_ENV") + if env == "" { + env = "dev" + } + + configFile := fmt.Sprintf("config/config.%s.yaml", env) + // 如果是测试环境,可能需要从上级目录查找,这里简单处理,如果找不到尝试从根目录找 + if _, err := os.Stat(configFile); os.IsNotExist(err) { + // 尝试在当前目录下找 (比如在 server 目录下运行) + configFile = fmt.Sprintf("config.%s.yaml", env) + if _, err := os.Stat(configFile); os.IsNotExist(err) { + // 如果是单元测试,可能在 tests 目录下 + configFile = fmt.Sprintf("../config/config.%s.yaml", env) + } + } + + fmt.Printf("正在加载配置文件: %s\n", configFile) + + data, err := os.ReadFile(configFile) + if err != nil { + fmt.Printf("读取配置文件失败: %v, 使用默认配置\n", err) + return + } + + err = yaml.Unmarshal(data, AppConfig) + if err != nil { + fmt.Printf("解析配置文件失败: %v\n", err) + panic(err) + } } diff --git a/server/config/config.prod.yaml b/server/config/config.prod.yaml new file mode 100644 index 0000000..3a02ae1 --- /dev/null +++ b/server/config/config.prod.yaml @@ -0,0 +1,47 @@ +log: + level: info + dir: logs + console: true + +security: + enable: true + header_key: X-App-Sign + secret_key: yts@2025#secure + +rate_limit: + enable: true + default: + interval: 2 + max_requests: 5 + rules: + /api/user/auth/login: + interval: 5 + max_requests: 1 + /api/yx-school-majors: + interval: 1 + max_requests: 5 + /api/user/score/save-score: + interval: 1 + max_requests: 2 + +swagger: + user: admin + password: password + +database: + driver: mysql + host: 127.0.0.1 + port: 3306 + database: yitisheng + username: root + password: "Db$7Hn#4Jm9Pq2!Xz" + charset: utf8mb4 + max_idle_conns: 50 + max_open_conns: 200 + conn_max_lifetime: 1 + log_mode: false + +redis: + addr: 127.0.0.1:56379 + password: "Rd@5Wk8#Nv3Yt6$Bm" + db: 1 diff --git a/server/config/config.test.yaml b/server/config/config.test.yaml new file mode 100644 index 0000000..a4d3eca --- /dev/null +++ b/server/config/config.test.yaml @@ -0,0 +1,47 @@ +log: + level: debug + dir: logs + console: true + +security: + enable: true + header_key: X-App-Sign + secret_key: yts@2025#secure + +rate_limit: + enable: true + default: + interval: 2 + max_requests: 10 + rules: + /api/user/auth/login: + interval: 5 + max_requests: 1 + /api/yx-school-majors: + interval: 1 + max_requests: 10 + /api/user/score/save-score: + interval: 1 + max_requests: 5 + +swagger: + user: admin + password: password + +database: + driver: mysql + host: 127.0.0.1 + port: 3306 + database: yitisheng + username: root + password: "Db$7Hn#4Jm9Pq2!Xz" + charset: utf8mb4 + max_idle_conns: 20 + max_open_conns: 100 + conn_max_lifetime: 1 + log_mode: false + +redis: + addr: 127.0.0.1:56379 + password: "Rd@5Wk8#Nv3Yt6$Bm" + db: 1 diff --git a/server/config/database.go b/server/config/database.go index f7adcc1..6033ffd 100644 --- a/server/config/database.go +++ b/server/config/database.go @@ -3,22 +3,83 @@ package config import ( "fmt" + "io" "log" + "os" + "path/filepath" "time" "gorm.io/driver/mysql" "gorm.io/gorm" + "gorm.io/gorm/logger" ) // DB 全局数据库连接实例 var DB *gorm.DB +// getLogWriter 获取日志输出目标 +func getLogWriter() io.Writer { + logConfig := AppConfig.Log + if logConfig.Dir == "" { + return os.Stdout + } + + if err := os.MkdirAll(logConfig.Dir, 0755); err != nil { + fmt.Printf("创建日志目录失败: %v\n", err) + return os.Stdout + } + + filename := fmt.Sprintf("sql-%s.log", time.Now().Format("2006-01-02")) + logPath := filepath.Join(logConfig.Dir, filename) + + file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + fmt.Printf("打开SQL日志文件失败: %v\n", err) + return os.Stdout + } + + if logConfig.Console { + return io.MultiWriter(file, os.Stdout) + } + return file +} + // InitDB 初始化数据库连接 func InitDB() { - dsn := "root:Db$7Hn#4Jm9Pq2!Xz@tcp(81.70.191.16:3306)/yitisheng?charset=utf8mb4&parseTime=True&loc=Asia%2FShanghai" + dbConfig := AppConfig.Database + dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=True&loc=Asia%%2FShanghai", + dbConfig.Username, + dbConfig.Password, + dbConfig.Host, + dbConfig.Port, + dbConfig.Database, + dbConfig.Charset, + ) + + var gormConfig *gorm.Config + if dbConfig.LogMode { + writer := getLogWriter() + newLogger := logger.New( + log.New(writer, "\r\n", log.LstdFlags), // io writer + logger.Config{ + SlowThreshold: time.Second, // Slow SQL threshold + LogLevel: logger.Info, // Log level + IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger + ParameterizedQueries: false, // 包含参数在 SQL 日志中 + Colorful: false, // 写入文件时建议关闭彩色打印,否则会有乱码 + }, + ) + gormConfig = &gorm.Config{ + Logger: newLogger, + } + } else { + gormConfig = &gorm.Config{ + Logger: logger.Default.LogMode(logger.Silent), + } + } var err error - DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{}) + DB, err = gorm.Open(mysql.Open(dsn), gormConfig) if err != nil { log.Fatal("数据库连接失败:", err) } @@ -30,9 +91,9 @@ func InitDB() { } // 连接池配置 - sqlDB.SetMaxIdleConns(20) // 最大空闲连接数 - sqlDB.SetMaxOpenConns(100) // 最大打开连接数 - sqlDB.SetConnMaxLifetime(time.Hour) // 连接最大存活时间 + sqlDB.SetMaxIdleConns(dbConfig.MaxIdleConns) + sqlDB.SetMaxOpenConns(dbConfig.MaxOpenConns) + sqlDB.SetConnMaxLifetime(time.Duration(dbConfig.ConnMaxLifetime) * time.Hour) fmt.Println("数据库连接成功") } diff --git a/server/config/redis.go b/server/config/redis.go index d6e6fba..7cc3172 100644 --- a/server/config/redis.go +++ b/server/config/redis.go @@ -15,10 +15,11 @@ var RDB *redis.Client // InitRedis 初始化Redis连接 func InitRedis() { + redisConfig := AppConfig.Redis RDB = redis.NewClient(&redis.Options{ - Addr: "81.70.191.16:56379", - Password: "Rd@5Wk8#Nv3Yt6$Bm", - DB: 1, + Addr: redisConfig.Addr, + Password: redisConfig.Password, + DB: redisConfig.DB, PoolSize: 10, // 连接池大小 }) diff --git a/server/main.go b/server/main.go index 202f96c..77d86b2 100644 --- a/server/main.go +++ b/server/main.go @@ -33,6 +33,9 @@ import ( // @name Authorization func main() { + // 加载配置 + config.LoadConfig() + // 初始化日志 common.InitLogger() common.Info("========== 应用启动 ==========") diff --git a/server/tests/init_test.go b/server/tests/init_test.go index 59b904d..d1f278d 100644 --- a/server/tests/init_test.go +++ b/server/tests/init_test.go @@ -18,6 +18,11 @@ func init() { log.Fatal("切换工作目录失败:", err) } + // 设置测试环境 + os.Setenv("GO_ENV", "test") + // 加载配置 + config.LoadConfig() + // 初始化数据库 config.InitDB() // 初始化Redis diff --git a/task_detail.md b/task_detail.md index e0b5b52..6e20644 100644 --- a/task_detail.md +++ b/task_detail.md @@ -1,17 +1,9 @@ -# 任务执行摘要 -## 会话 ID: 20251217-05 -- **执行原因**: 用户要求重构代码,将 DTO 结构体从 Controller 中分离到独立的包中。 +## 会话 ID: 20251225-02 +- **执行原因**: 用户反馈 SQL 日志未写入文件。 - **执行过程**: - 1. 创建了 `server/modules/yx/dto/` 目录。 - 2. 创建了 `server/modules/yx/dto/yx_user_score_dto.go`,将 `SaveScoreRequest` 结构体及 `Validate` 方法移动至此。 - 3. 修改 `server/modules/yx/controller/yx_user_score_controller.go`,引入 `dto` 包并使用 `dto.SaveScoreRequest`。 -- **执行结果**: 代码结构更清晰,符合 DTO 分层规范。 - -## 会话 ID: 20251219-01 -- **执行原因**: 用户要求在 `UserScoreService` 中处理 `scoreVO` 信息,参考 `yx_user_score_dto` 的结构。 -- **执行过程**: - 1. 定义 `server/modules/user/vo/user_score_vo.go` 中的 `UserScoreVO` 结构。 - 2. 在 `server/modules/user/service/user_score_service.go` 中实现 `convertEntityToVo` 逻辑。 - 3. 修改 `GetActiveByID` 方法,使其返回 `vo.UserScoreVO`。 -- **执行结果**: 实现了 Entity 到 VO 的完整转换逻辑,支持子专业分数的 Map 映射。 + 1. 分析发现 `server/config/database.go` 中 GORM Logger 硬编码输出到 `os.Stdout`。 + 2. 在 `server/config/database.go` 中实现 `getLogWriter` 函数,根据配置创建文件 Writer。 + 3. 使用 `io.MultiWriter` 支持同时输出到文件和控制台。 + 4. 验证日志文件生成。 +- **执行结果**: SQL 日志现在会根据日期生成独立的文件(如 `logs/sql-2025-12-25.log`),且遵循全局日志配置。