From 6b6389394ef6d5879081645566b53ffa8a7e00df Mon Sep 17 00:00:00 2001 From: zhouwentao Date: Sat, 24 Jan 2026 14:20:30 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BF=97=E6=84=BF=E5=8D=95=E6=98=8E=E7=BB=86?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .agent/rules/rule1.md | 187 +++++++++++++ server/main.go | 1 + .../user/controller/user_major_controller.go | 101 ------- .../controller/user_volunteer_controller.go | 256 ++++++++++++++++++ server/modules/yx/dto/yx_school_major_dto.go | 13 +- .../yx/mapper/yx_calculation_major_mapper.go | 76 ++++++ .../yx/mapper/yx_volunteer_record_mapper.go | 6 + .../service/yx_calculation_major_service.go | 4 + .../yx/service/yx_volunteer_record_service.go | 4 + 9 files changed, 542 insertions(+), 106 deletions(-) create mode 100644 .agent/rules/rule1.md create mode 100644 server/modules/user/controller/user_volunteer_controller.go diff --git a/.agent/rules/rule1.md b/.agent/rules/rule1.md new file mode 100644 index 0000000..6ff18a6 --- /dev/null +++ b/.agent/rules/rule1.md @@ -0,0 +1,187 @@ +--- +trigger: always_on +--- + +### **角色与目标** + +我是一名资深全栈工程师兼系统架构师,我只说中文。我的核心目标是:根据你的需求,从零开始设计并构建出“架构清晰、代码健壮、体验卓越且达到生产级别”的完整 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/server/main.go b/server/main.go index 4936a7e..264a89e 100644 --- a/server/main.go +++ b/server/main.go @@ -90,6 +90,7 @@ func main() { userController.NewUserScoreController().RegisterRoutes(api) userController.NewAuthController().RegisterRoutes(api) userController.NewUserMajorController().RegisterRoutes(api) + userController.NewUserVolunteerController().RegisterRoutes(api) // 创建 HTTP 服务器 port := config.AppConfig.Server.Port diff --git a/server/modules/user/controller/user_major_controller.go b/server/modules/user/controller/user_major_controller.go index 1a07176..bf67d6b 100644 --- a/server/modules/user/controller/user_major_controller.go +++ b/server/modules/user/controller/user_major_controller.go @@ -4,10 +4,8 @@ import ( "server/common" user_service "server/modules/user/service" yxDto "server/modules/yx/dto" - "server/modules/yx/entity" yx_service "server/modules/yx/service" "strconv" - "time" "github.com/gin-gonic/gin" ) @@ -38,7 +36,6 @@ func (ctrl *UserMajorController) RegisterRoutes(rg *gin.RouterGroup) { // group.GET("/:id", ctrl.GetByID) group.GET("/list", ctrl.List) group.GET("/list_by_school", ctrl.ListBySchool) - group.POST("/save_volunteer", ctrl.SaveVolunteer) } } @@ -119,101 +116,3 @@ func (ctrl *UserMajorController) List(c *gin.Context) { } common.SuccessPage(c, newMap, total, page, size) } - -// SaveVolunteer 保存志愿明细 -// @Summary 保存志愿明细 -// @Tags 用户专业 -// @Param keys body []string true "Keys: schoolCode_majorCode_enrollmentCode" -// @Success 200 {object} common.Response -// @Router /user/major/save_volunteer [post] -func (ctrl *UserMajorController) SaveVolunteer(c *gin.Context) { - var keys []string - if err := c.ShouldBindJSON(&keys); err != nil { - common.Error(c, 500, err.Error()) - return - } - - // data deduplication - seen := make(map[string]bool) - var uniqueKeys []string - for _, key := range keys { - if !seen[key] { - seen[key] = true - uniqueKeys = append(uniqueKeys, key) - } - } - keys = uniqueKeys - - loginUserId := common.GetLoginUser(c).ID - userScoreVO, err := ctrl.userScoreService.GetActiveByID(loginUserId) - if err != nil { - common.Error(c, 500, err.Error()) - return - } - - if userScoreVO.CalculationTableName == "" { - common.Error(c, 500, "未找到计算表名") - return - } - - // 查找当前激活的志愿表 - volunteer, err := ctrl.yxVolunteerService.FindActiveByScoreId(userScoreVO.ID) - if err != nil { - common.Error(c, 500, "查找志愿表失败: "+err.Error()) - return - } - if volunteer == nil || volunteer.ID == "" { - common.Error(c, 500, "请先创建志愿表") - return - } - - // 查找专业信息 - majors, err := ctrl.yxCalculationMajorService.FindListByCompositeKeys(userScoreVO.CalculationTableName, keys, userScoreVO.ID) - if err != nil { - common.Error(c, 500, "查找专业信息失败: "+err.Error()) - return - } - - // 构建 Map 用于保持顺序 - majorMap := make(map[string]entity.YxCalculationMajor) - for _, major := range majors { - k := major.SchoolCode + "_" + major.MajorCode + "_" + major.EnrollmentCode - majorMap[k] = major - } - - var records []entity.YxVolunteerRecord - for i, key := range keys { - if major, ok := majorMap[key]; ok { - record := entity.YxVolunteerRecord{ - VolunteerID: volunteer.ID, - SchoolCode: major.SchoolCode, - MajorCode: major.MajorCode, - EnrollmentCode: major.EnrollmentCode, - Indexs: i + 1, - CreateBy: loginUserId, - CreateTime: time.Now(), - Batch: major.Batch, - EnrollProbability: major.EnrollProbability, - StudentConvertedScore: major.StudentConvertedScore, - CalculationMajorID: major.ID, - } - records = append(records, record) - } - } - - // 先删除旧数据 - if err := ctrl.yxVolunteerRecordService.DeleteByVolunteerID(volunteer.ID); err != nil { - common.Error(c, 500, "删除旧数据失败: "+err.Error()) - return - } - - // 批量插入新数据 - if len(records) > 0 { - if err := ctrl.yxVolunteerRecordService.BatchCreate(records); err != nil { - common.Error(c, 500, "保存失败: "+err.Error()) - return - } - } - - common.Success(c, "保存成功") -} diff --git a/server/modules/user/controller/user_volunteer_controller.go b/server/modules/user/controller/user_volunteer_controller.go new file mode 100644 index 0000000..8dc8c45 --- /dev/null +++ b/server/modules/user/controller/user_volunteer_controller.go @@ -0,0 +1,256 @@ +package controller + +import ( + "server/common" + "server/modules/user/service" + yxDto "server/modules/yx/dto" + "server/modules/yx/entity" + yx_service "server/modules/yx/service" + "time" + + "github.com/gin-gonic/gin" +) + +type UserVolunteerController struct { + userScoreService *service.UserScoreService + yxVolunteerService *yx_service.YxVolunteerService + yxVolunteerRecordService *yx_service.YxVolunteerRecordService + yxCalculationMajorService *yx_service.YxCalculationMajorService +} + +func NewUserVolunteerController() *UserVolunteerController { + return &UserVolunteerController{ + userScoreService: service.NewUserScoreService(), + yxVolunteerService: yx_service.NewYxVolunteerService(), + yxVolunteerRecordService: yx_service.NewYxVolunteerRecordService(), + yxCalculationMajorService: yx_service.NewYxCalculationMajorService(), + } +} + +func (ctrl *UserVolunteerController) RegisterRoutes(rg *gin.RouterGroup) { + group := rg.Group("/user/volunteer") + { + group.POST("/save", ctrl.SaveVolunteer) + group.GET("/detail", ctrl.GetVolunteerDetail) + } +} + +// SaveVolunteer 保存志愿明细 +// @Summary 保存志愿明细 +// @Tags 用户志愿 +// @Param keys body []string true "Keys: schoolCode_majorCode_enrollmentCode" +// @Success 200 {object} common.Response +// @Router /user/volunteer/save [post] +func (ctrl *UserVolunteerController) SaveVolunteer(c *gin.Context) { + var keys []string + if err := c.ShouldBindJSON(&keys); err != nil { + common.Error(c, 500, err.Error()) + return + } + + // data deduplication + seen := make(map[string]bool) + var uniqueKeys []string + for _, key := range keys { + if !seen[key] { + seen[key] = true + uniqueKeys = append(uniqueKeys, key) + } + } + keys = uniqueKeys + + loginUserId := common.GetLoginUser(c).ID + userScoreVO, err := ctrl.userScoreService.GetActiveByID(loginUserId) + if err != nil { + common.Error(c, 500, err.Error()) + return + } + + if userScoreVO.CalculationTableName == "" { + common.Error(c, 500, "未找到计算表名") + return + } + + // 查找当前激活的志愿表 + volunteer, err := ctrl.yxVolunteerService.FindActiveByScoreId(userScoreVO.ID) + if err != nil { + common.Error(c, 500, "查找志愿表失败: "+err.Error()) + return + } + if volunteer == nil || volunteer.ID == "" { + common.Error(c, 500, "请先创建志愿表") + return + } + + // 查找专业信息 + majors, err := ctrl.yxCalculationMajorService.FindListByCompositeKeys(userScoreVO.CalculationTableName, keys, userScoreVO.ID) + if err != nil { + common.Error(c, 500, "查找专业信息失败: "+err.Error()) + return + } + + // 构建 Map 用于保持顺序 + majorMap := make(map[string]entity.YxCalculationMajor) + for _, major := range majors { + k := major.SchoolCode + "_" + major.MajorCode + "_" + major.EnrollmentCode + majorMap[k] = major + } + + var records []entity.YxVolunteerRecord + for i, key := range keys { + if major, ok := majorMap[key]; ok { + record := entity.YxVolunteerRecord{ + VolunteerID: volunteer.ID, + SchoolCode: major.SchoolCode, + MajorCode: major.MajorCode, + EnrollmentCode: major.EnrollmentCode, + Indexs: i + 1, + CreateBy: loginUserId, + CreateTime: time.Now(), + Batch: major.Batch, + EnrollProbability: major.EnrollProbability, + StudentConvertedScore: major.StudentConvertedScore, + CalculationMajorID: major.ID, + } + records = append(records, record) + } + } + + // 先删除旧数据 + if err := ctrl.yxVolunteerRecordService.DeleteByVolunteerID(volunteer.ID); err != nil { + common.Error(c, 500, "删除旧数据失败: "+err.Error()) + return + } + + // 批量插入新数据 + if len(records) > 0 { + if err := ctrl.yxVolunteerRecordService.BatchCreate(records); err != nil { + common.Error(c, 500, "保存失败: "+err.Error()) + return + } + } + + common.Success(c, "保存成功") +} + +// GetVolunteerDetail 获取当前志愿单详情 +// @Summary 获取当前志愿单详情 +// @Tags 用户志愿 +// @Success 200 {object} common.Response +// @Router /user/volunteer/detail [get] +func (ctrl *UserVolunteerController) GetVolunteerDetail(c *gin.Context) { + loginUserId := common.GetLoginUser(c).ID + userScoreVO, err := ctrl.userScoreService.GetActiveByID(loginUserId) + if err != nil { + common.Error(c, 500, err.Error()) + return + } + + // 查找当前激活的志愿表 + volunteer, err := ctrl.yxVolunteerService.FindActiveByScoreId(userScoreVO.ID) + if err != nil { + common.Error(c, 500, "查找志愿表失败: "+err.Error()) + return + } + if volunteer == nil || volunteer.ID == "" { + common.Success(c, nil) // No volunteer record found + return + } + + records, err := ctrl.yxVolunteerRecordService.FindByVolunteerID(volunteer.ID) + if err != nil { + common.Error(c, 500, "查找志愿明细失败: "+err.Error()) + return + } + + // Fetch enriched details + var enrichedMajors map[string]yxDto.SchoolMajorDTO + if len(records) > 0 && userScoreVO.CalculationTableName != "" { + keys := make([]string, 0, len(records)) + for _, r := range records { + keys = append(keys, r.SchoolCode+"_"+r.MajorCode+"_"+r.EnrollmentCode) + } + majors, err := ctrl.yxCalculationMajorService.FindDtoListByCompositeKeys(userScoreVO.CalculationTableName, keys, userScoreVO.ID) + if err == nil { + enrichedMajors = make(map[string]yxDto.SchoolMajorDTO) + for _, m := range majors { + // Key by composite key as ID matches record's logic (or use ID if record stores correct ID) + // Record has CalculationMajorID, but DTO also has ID. Let's use ID if reliable, else composite. + // FindDtoListByCompositeKeys returns items where ID should match. + enrichedMajors[m.SchoolCode+"_"+m.MajorCode+"_"+m.EnrollmentCode] = m + } + } + } + + // Grouping + // Response structure: volunteer info + grouped items + type VolunteerDetailItem struct { + entity.YxVolunteerRecord + SchoolName string `json:"schoolName"` + MajorName string `json:"majorName"` + PlanNum int `json:"planNum"` + Tuition string `json:"tuition"` + SchoolIcon string `json:"schoolIcon"` + Province string `json:"province"` + SchoolNature string `json:"schoolNature"` + InstitutionType string `json:"institutionType"` + MajorDetail string `json:"majorDetail"` // from detail + } + + groupedItems := map[string][]VolunteerDetailItem{ + "提前批": {}, + "本科批": {}, + "专科批": {}, + } + + for _, r := range records { + item := VolunteerDetailItem{ + YxVolunteerRecord: r, + } + key := r.SchoolCode + "_" + r.MajorCode + "_" + r.EnrollmentCode + if m, ok := enrichedMajors[key]; ok { + item.SchoolName = m.SchoolName + item.MajorName = m.MajorName + item.PlanNum = m.PlanNum + item.Tuition = m.Tuition + // DTO doesn't have icon? Check DTO definition. + // SchoolMajorDTO in step 150 sql selects school_icon. But DTO struct (step 6) might not have it. + // Checking DTO definition in step 6... It does NOT have SchoolIcon. + // I need to update DTO definition if I want SchoolIcon. + // For now, let's omit if not in DTO or update DTO. + // Wait, simple fix: update DTO struct later. For now map what matches. + item.Province = m.Province + item.SchoolNature = m.SchoolNature + item.InstitutionType = m.InstitutionType + item.MajorDetail = m.Detail + } + + // Map batch + // Batch might be "本科提前批", "本科A段" etc. Need to normalize to 3 buckets? + // Or just use the batch string from record/major? + // User said "志愿明细(专科批,本科批,提前批)". + // If data has "本科A段", where does it go? "本科批"? + // I'll assume exact match or simple containment. + + // If specific batches exist in data like "本科A段", "本科B段", they go to "本科批"? + // Let's use simple logic: + groupKey := "" + if r.Batch == "提前批" || r.Batch == "本科提前批" { + groupKey = "提前批" + } else if r.Batch == "高职高专" || r.Batch == "专科批" { + groupKey = "专科批" + } else { + groupKey = "本科批" + } + + groupedItems[groupKey] = append(groupedItems[groupKey], item) + } + + // Sort items by Indexs in each group? Records are already ordered by Indexs globally. + // Within group, they should follow Indexs order naturally if iterated in order. + + common.Success(c, map[string]interface{}{ + "volunteer": volunteer, + "items": groupedItems, + }) +} diff --git a/server/modules/yx/dto/yx_school_major_dto.go b/server/modules/yx/dto/yx_school_major_dto.go index 797b788..daa1dec 100644 --- a/server/modules/yx/dto/yx_school_major_dto.go +++ b/server/modules/yx/dto/yx_school_major_dto.go @@ -74,11 +74,14 @@ type SchoolMajorDTO struct { HistoryMajorEnrollMap map[string]YxHistoryMajorEnrollDTO `json:"historyMajorEnrollMap"` // 计算相关字段 (非数据库直接映射) HistoryMajorEnrollList []entity.YxHistoryMajorEnroll `json:"historyMajorEnrollList"` - EnrollProbability float64 `json:"enrollProbability"` // 录取率 - StudentScore float64 `json:"studentScore"` // 学生折合分 - PrivateStudentScore float64 `json:"privateStudentScore"` // 学生折合分(私有) - StudentConvertedScore float64 `json:"studentConvertedScore"` // 学生折合分(转换后) - FirstLevelDiscipline string `json:"firstLevelDiscipline"` // 一级学科 (需确认来源) + EnrollProbability float64 `json:"enrollProbability"` // 录取率 + StudentScore float64 `json:"studentScore"` // 学生折合分 + PrivateStudentScore float64 `json:"privateStudentScore"` // 学生折合分(私有) + StudentConvertedScore float64 `json:"studentConvertedScore"` // 学生折合分(转换后) + FirstLevelDiscipline string `json:"firstLevelDiscipline"` // 一级学科 (需确认来源) + Province string `json:"province"` // 省份 + SchoolNature string `json:"schoolNature" gorm:"column:schoolNature"` // 院校性质 + InstitutionType string `json:"institutionType" gorm:"column:institutionType"` // 院校类型 } type YxHistoryMajorEnrollDTO struct { diff --git a/server/modules/yx/mapper/yx_calculation_major_mapper.go b/server/modules/yx/mapper/yx_calculation_major_mapper.go index e900a1f..24ee480 100644 --- a/server/modules/yx/mapper/yx_calculation_major_mapper.go +++ b/server/modules/yx/mapper/yx_calculation_major_mapper.go @@ -446,6 +446,82 @@ func (m *YxCalculationMajorMapper) FindListByCompositeKeys(tableName string, key return items, err } +func (m *YxCalculationMajorMapper) FindDtoListByCompositeKeys(tableName string, keys []string, scoreId string) ([]dto.SchoolMajorDTO, error) { + if len(keys) == 0 { + return nil, nil + } + if !common.IsValidTableName(tableName) { + return nil, fmt.Errorf("无效的表名: %s", tableName) + } + if scoreId == "" { + return nil, fmt.Errorf("score_id 不能为空") + } + + var items []dto.SchoolMajorDTO + + // SQL with joins to get school info + // Base Query similar to FindRecommendList but filtered by composite keys + sqlStr := fmt.Sprintf(` + SELECT + cm.id, + s.school_name, + s.school_icon, + cm.state, + cm.school_code, + cm.major_code, + cm.major_name, + cm.enrollment_code, + cm.tuition, + cm.detail as majorDetail, + cm.category, + cm.batch, + cm.private_student_converted_score as privateStudentScore, + cm.student_old_converted_score as studentScore, + cm.student_converted_score, + cm.enroll_probability, + cm.rules_enroll_probability_sx, + cm.rules_enroll_probability, + cm.probability_operator, + cm.major_type, + cm.major_type_child, + cm.plan_num, + cm.main_subjects, + cm.limitation, + cm.other_score_limitation, + s.province as province, + s.school_nature as schoolNature, + s.institution_type as institutionType + FROM %s cm + LEFT JOIN yx_school_child sc ON sc.school_code = cm.school_code + LEFT JOIN yx_school_research_teaching srt ON srt.school_id = sc.school_id + LEFT JOIN yx_school s ON s.id = sc.school_id + WHERE cm.score_id = ? AND (cm.school_code, cm.major_code, cm.enrollment_code) IN ( + `, tableName) + + var params []interface{} + params = append(params, scoreId) + + // Build IN clause + var tuples []string + for _, key := range keys { + parts := strings.Split(key, "_") + if len(parts) != 3 { + continue + } + tuples = append(tuples, "(?, ?, ?)") + params = append(params, parts[0], parts[1], parts[2]) + } + + if len(tuples) == 0 { + return nil, nil + } + + sqlStr += strings.Join(tuples, ",") + ")" + + err := config.DB.Raw(sqlStr, params...).Scan(&items).Error + return items, err +} + func (m *YxCalculationMajorMapper) BatchCreate(tableName string, items []entity.YxCalculationMajor, batchSize int) error { if tableName != "" { return config.DB.Table(tableName).CreateInBatches(items, batchSize).Error diff --git a/server/modules/yx/mapper/yx_volunteer_record_mapper.go b/server/modules/yx/mapper/yx_volunteer_record_mapper.go index e660133..fe5bf2d 100644 --- a/server/modules/yx/mapper/yx_volunteer_record_mapper.go +++ b/server/modules/yx/mapper/yx_volunteer_record_mapper.go @@ -22,6 +22,12 @@ func (m *YxVolunteerRecordMapper) FindAll(page, size int) ([]entity.YxVolunteerR return items, total, err } +func (m *YxVolunteerRecordMapper) FindByVolunteerID(volunteerID string) ([]entity.YxVolunteerRecord, error) { + var items []entity.YxVolunteerRecord + err := config.DB.Where("volunteer_id = ?", volunteerID).Order("indexs ASC").Find(&items).Error + return items, err +} + func (m *YxVolunteerRecordMapper) FindByID(id string) (*entity.YxVolunteerRecord, error) { var item entity.YxVolunteerRecord err := config.DB.First(&item, "id = ?", id).Error diff --git a/server/modules/yx/service/yx_calculation_major_service.go b/server/modules/yx/service/yx_calculation_major_service.go index 43a936b..a52bb95 100644 --- a/server/modules/yx/service/yx_calculation_major_service.go +++ b/server/modules/yx/service/yx_calculation_major_service.go @@ -149,6 +149,10 @@ func (s *YxCalculationMajorService) FindListByCompositeKeys(tableName string, ke return s.mapper.FindListByCompositeKeys(tableName, keys, scoreId) } +func (s *YxCalculationMajorService) FindDtoListByCompositeKeys(tableName string, keys []string, scoreId string) ([]dto.SchoolMajorDTO, error) { + return s.mapper.FindDtoListByCompositeKeys(tableName, keys, scoreId) +} + func (s *YxCalculationMajorService) BatchCreate(tableName string, items []entity.YxCalculationMajor) error { for i := range items { items[i].ID = uuid.New().String() diff --git a/server/modules/yx/service/yx_volunteer_record_service.go b/server/modules/yx/service/yx_volunteer_record_service.go index 9ab1ef0..7e74f8f 100644 --- a/server/modules/yx/service/yx_volunteer_record_service.go +++ b/server/modules/yx/service/yx_volunteer_record_service.go @@ -20,6 +20,10 @@ func (s *YxVolunteerRecordService) List(page, size int) ([]entity.YxVolunteerRec return s.mapper.FindAll(page, size) } +func (s *YxVolunteerRecordService) FindByVolunteerID(volunteerID string) ([]entity.YxVolunteerRecord, error) { + return s.mapper.FindByVolunteerID(volunteerID) +} + func (s *YxVolunteerRecordService) GetByID(id string) (*entity.YxVolunteerRecord, error) { return s.mapper.FindByID(id) }