Compare commits

..

6 Commits

Author SHA1 Message Date
zhouwentao 74ff031144 updates 2026-01-24 20:22:11 +08:00
zhouwentao e6efa896a1 根据批次 查询专业 2026-01-24 16:44:24 +08:00
zhouwentao 6b6389394e 志愿单明细接口 2026-01-24 14:20:30 +08:00
zhouwentao 26ce858f7a 保持志愿明细接口 2026-01-24 13:56:16 +08:00
zhouwentao c1a0b60218 修改成绩单会创建新志愿单 2026-01-24 12:06:16 +08:00
zhouwentao fff2833d29 根据SchoolCode查询专业列表 2026-01-24 11:09:47 +08:00
14 changed files with 762 additions and 26 deletions

187
.agent/rules/rule1.md Normal file
View File

@ -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秒原则" | 功能可识别性 > 视觉简洁性 |

View File

@ -0,0 +1,14 @@
package common
import "regexp"
// 验证表名格式的辅助函数
func IsValidTableName(tableName string) bool {
if tableName == "" {
return false
}
// 表名只能包含字母、数字、下划线和点号,且长度合理
matched, err := regexp.MatchString(`^[a-zA-Z_][a-zA-Z0-9_.]{0,100}$`, tableName)
return err == nil && matched
}

View File

@ -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

View File

@ -14,6 +14,8 @@ type UserMajorController struct {
userScoreService *user_service.UserScoreService
yxUserScoreService *yx_service.YxUserScoreService
yxCalculationMajorService *yx_service.YxCalculationMajorService
yxVolunteerService *yx_service.YxVolunteerService
yxVolunteerRecordService *yx_service.YxVolunteerRecordService
}
func NewUserMajorController() *UserMajorController {
@ -21,6 +23,8 @@ func NewUserMajorController() *UserMajorController {
yxUserScoreService: yx_service.NewYxUserScoreService(),
userScoreService: user_service.NewUserScoreService(),
yxCalculationMajorService: yx_service.NewYxCalculationMajorService(),
yxVolunteerService: yx_service.NewYxVolunteerService(),
yxVolunteerRecordService: yx_service.NewYxVolunteerRecordService(),
}
}
@ -31,26 +35,67 @@ func (ctrl *UserMajorController) RegisterRoutes(rg *gin.RouterGroup) {
// group.GET("/", ctrl.GetActive)
// group.GET("/:id", ctrl.GetByID)
group.GET("/list", ctrl.List)
group.GET("/list_by_school", ctrl.ListBySchool)
}
}
// List 获取当前用户可检索列表
// @Summary 获取当前用户可检索列表
// ListBySchool 获取当前院校下其他专业数据
// @Summary 获取当前院校下其他专业数据
// @Tags 用户专业
// @Param page query int false "页码" default(1)
// @Param size query int false "每页数量" default(10)
// @Param batch query string false "批次(本科提前批/本科A段/本科B段/本科/高职高专)" default("")
// @Param probability query string false "录取概率类型(难录取/可冲击/较稳妥/可保底)" default("")
// @Param school_code query string true "院校代码"
// @Param probability query string false "录取概率类型(难录取/可冲击/较稳妥/可保底)" default("")
// @Success 200 {object} common.Response
// @Router /user/major/list [get]
func (ctrl *UserMajorController) List(c *gin.Context) {
// @Router /user/major/list_by_school [get]
func (ctrl *UserMajorController) ListBySchool(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("size", "10"))
schoolMajorQuery := yxDto.SchoolMajorQuery{
Keyword: c.DefaultQuery("keyword", ""),
Page: page,
Size: size,
Batch: c.DefaultQuery("batch", ""),
SchoolCode: c.Query("school_code"),
Probability: c.DefaultQuery("probability", ""),
LoginUserId: common.GetLoginUser(c).ID,
}
userScoreVO, err := ctrl.userScoreService.GetActiveByID(schoolMajorQuery.LoginUserId)
if err != nil {
common.Error(c, 500, err.Error())
return
}
schoolMajorQuery.UserScoreVO = userScoreVO
items, total, probCount, err := ctrl.yxCalculationMajorService.RecommendMajorList(schoolMajorQuery)
if err != nil {
common.Error(c, 500, err.Error())
return
}
newMap := map[string]interface{}{
"items": items,
"total": total,
"probCount": probCount,
}
common.SuccessPage(c, newMap, total, page, size)
}
// List 获取当前用户可检索列表
// @Summary 获取当前用户可检索列表
// @Tags 用户专业
// @Param page query int false "页码" default(1)
// @Param size query int false "每页数量" default(10)
// @Param batch query string false "批次(本科提前批/本科A段/本科B段/本科/高职高专)" default("")
// @Param probability query string false "录取概率类型(难录取/可冲击/较稳妥/可保底)" default("")
// @Success 200 {object} common.Response
// @Router /user/major/list [get]
func (ctrl *UserMajorController) List(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("size", "10"))
schoolMajorQuery := yxDto.SchoolMajorQuery{
Keyword: c.DefaultQuery("keyword", ""),
Page: page,
Size: size,
SchoolCode: c.Query("schoolCode"),
Batch: c.DefaultQuery("batch", ""),
Batch2: c.DefaultQuery("batch2", ""),
Probability: c.DefaultQuery("probability", ""),
LoginUserId: common.GetLoginUser(c).ID,
}

View File

@ -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,
})
}

View File

@ -21,6 +21,7 @@ import (
type UserScoreService struct {
yxUserScoreService *service.YxUserScoreService
yxVolunteerService *service.YxVolunteerService
yxCalculationMajorService *service.YxCalculationMajorService
mapper *mapper.YxUserScoreMapper
}
@ -108,6 +109,7 @@ func NewUserScoreService() *UserScoreService {
return &UserScoreService{
yxUserScoreService: service.NewYxUserScoreService(),
yxCalculationMajorService: service.NewYxCalculationMajorService(),
yxVolunteerService: service.NewYxVolunteerService(),
mapper: mapper.NewYxUserScoreMapper(),
}
}
@ -177,6 +179,13 @@ func (s *UserScoreService) SaveUserScore(req *dto.SaveScoreRequest) (vo.UserScor
return vo.UserScoreVO{}, fmt.Errorf("保存专业信息失败: %w", err)
}
// 创建志愿表
err = s.yxVolunteerService.CreateByScoreId(entityItem.ID, req.CreateBy)
if err != nil {
tx.Rollback()
return vo.UserScoreVO{}, fmt.Errorf("创建志愿表失败: %w", err)
}
// 提交事务
if err := tx.Commit().Error; err != nil {
tx.Rollback()
@ -192,7 +201,6 @@ func (s *UserScoreService) SaveUserScore(req *dto.SaveScoreRequest) (vo.UserScor
if err != nil {
return vo.UserScoreVO{}, fmt.Errorf("缓存成绩记录失败: %w", err)
}
return userScoreVO, nil
}

View File

@ -33,8 +33,8 @@ type UserMajorDTO struct {
State string `json:"state"`
HistoryMajorEnrollMap map[string]YxHistoryMajorEnrollDTO `json:"historyMajorEnrollMap"`
// 计算相关字段 (非数据库直接映射)
EnrollProbability float64 `json:"enrollProbability"` // 录取率
StudentScore float64 `json:"studentScore"` // 学生折合分
EnrollProbability float64 `json:"enrollProbability"` // 录取率
StudentScore float64 `json:"studentScore" gorm:"column:studentScore"` // 学生折合分
// PrivateStudentScore float64 `json:"privateStudentScore"` // 学生折合分(私有)
// StudentConvertedScore float64 `json:"studentConvertedScore"` // 学生折合分(转换后)
// FirstLevelDiscipline string `json:"firstLevelDiscipline"` // 一级学科 (需确认来源)
@ -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 {
@ -100,6 +103,7 @@ type SchoolMajorQuery struct {
MajorType string `json:"majorType"` // 对应 major_type
Category string `json:"category"` // 对应 category
Batch string `json:"batch"` // 对应 批次
Batch2 string `json:"batch2"` // 对应 批次2
MajorTypeChildren []string `json:"majorTypeChildren"` // 对应 major_type_child in (...)
MainSubjects string `json:"mainSubjects"` // 对应 main_subjects
Probability string `json:"probability"` // 对应 录取概率类型
@ -112,4 +116,5 @@ type SchoolMajorQuery struct {
LoginUserId string `json:"loginUserId"` // 登录用户 ID
UserScoreVO userVO.UserScoreVO `json:"userScoreVO"` // 用户成绩 VO
CalculationTableName string `json:"calculationTableName"` // 对应 calculation_table_name
SchoolCode string `json:"schoolCode"` // 对应的 院校代码
}

View File

@ -6,7 +6,7 @@ import "time"
type YxVolunteer struct {
ID string `gorm:"column:id;primaryKey" json:"id"`
VolunteerName string `gorm:"column:volunteer_name" json:"volunteerName"` // 志愿单名称
ScoreID string `gorm:"column:score_id" json:"scoreId"` // 使用成绩id
ScoreId string `gorm:"column:score_id" json:"scoreId"` // 使用成绩id
CreateType string `gorm:"column:create_type;default:1" json:"createType"` // 生成类型(1.手动生成,2.智能生成)
State string `gorm:"column:state;default:0" json:"state"` // 志愿单状态(0-否1.正在使用2-历史)
CreateBy string `gorm:"column:create_by" json:"createBy"` // 创建人

View File

@ -3,6 +3,7 @@ package mapper
import (
"fmt"
"server/common"
"server/config"
"server/modules/yx/dto"
"server/modules/yx/entity"
@ -59,6 +60,10 @@ func (m *YxCalculationMajorMapper) FindRecommendList(query dto.SchoolMajorQuery)
baseSQL += " AND cm.score_id = ?"
params = append(params, query.UserScoreVO.ID)
}
if query.SchoolCode != "" {
baseSQL += " AND cm.school_code = ?"
params = append(params, query.SchoolCode)
}
if query.MajorType != "" {
baseSQL += " AND cm.major_type = ?"
params = append(params, query.MajorType)
@ -74,6 +79,12 @@ func (m *YxCalculationMajorMapper) FindRecommendList(query dto.SchoolMajorQuery)
params = append(params, v)
}
}
if "" != query.Batch {
baseSQL += " AND cm.batch = ?"
params = append(params, query.Batch)
}
if query.MainSubjects != "" {
baseSQL += " AND cm.main_subjects = ?"
params = append(params, query.MainSubjects)
@ -124,7 +135,7 @@ func (m *YxCalculationMajorMapper) FindRecommendList(query dto.SchoolMajorQuery)
cm.main_subjects,
cm.limitation,
cm.other_score_limitation,
s.province as province,
s.province,
s.school_nature,
s.institution_type
FROM %s cm
@ -233,7 +244,9 @@ func (m *YxCalculationMajorMapper) FindRecommendList(query dto.SchoolMajorQuery)
if queryErr != nil {
return nil, 0, dto.ProbabilityCountDTO{}, fmt.Errorf("failed to query recommend major list: %w", queryErr)
}
if items == nil {
items = []dto.UserMajorDTO{}
}
return items, total, probCount, nil
}
@ -397,6 +410,126 @@ func (m *YxCalculationMajorMapper) FindByScoreID(scoreID string) ([]entity.YxCal
return items, err
}
func (m *YxCalculationMajorMapper) FindListByCompositeKeys(tableName string, keys []string, scoreId string) ([]entity.YxCalculationMajor, error) {
if len(keys) == 0 {
return nil, nil
}
// 验证表名格式(防止表名注入)
if !common.IsValidTableName(tableName) {
return nil, fmt.Errorf("无效的表名: %s", tableName)
}
// 验证和转义 score_id
if scoreId == "" {
return nil, fmt.Errorf("score_id 不能为空")
}
var items []entity.YxCalculationMajor
db := config.DB
if tableName != "" {
db = db.Table(tableName)
}
sql := "SELECT * FROM " + tableName + " WHERE score_id = ? AND (school_code, major_code, enrollment_code) IN ("
var params []interface{}
// 将 score_id 作为第一个参数
params = append(params, scoreId)
for i, key := range keys {
parts := strings.Split(key, "_")
if len(parts) != 3 {
continue
}
if i > 0 {
sql += ","
}
sql += "(?, ?, ?)"
params = append(params, parts[0], parts[1], parts[2])
}
sql += ")"
err := db.Raw(sql, params...).Scan(&items).Error
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

View File

@ -10,6 +10,17 @@ import (
type YxVolunteerMapper struct{}
func (m *YxVolunteerMapper) CloseOtherVolunteer(userId string) error {
result := config.DB.Model(&entity.YxVolunteer{}).
Where("create_by = ?", userId).
Updates(map[string]interface{}{"state": "0"})
if result.Error != nil {
return result.Error
}
return nil
}
func NewYxVolunteerMapper() *YxVolunteerMapper {
return &YxVolunteerMapper{}
}
@ -28,6 +39,12 @@ func (m *YxVolunteerMapper) FindByID(id string) (*entity.YxVolunteer, error) {
return &item, err
}
func (m *YxVolunteerMapper) FindActiveByScoreId(scoreId string) (*entity.YxVolunteer, error) {
var item entity.YxVolunteer
err := config.DB.Where("score_id = ? AND state = ?", scoreId, "1").First(&item).Error
return &item, err
}
func (m *YxVolunteerMapper) Create(item *entity.YxVolunteer) error {
return config.DB.Create(item).Error
}

View File

@ -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
@ -44,6 +50,10 @@ func (m *YxVolunteerRecordMapper) Delete(id string) error {
return config.DB.Delete(&entity.YxVolunteerRecord{}, "id = ?", id).Error
}
func (m *YxVolunteerRecordMapper) DeleteByVolunteerID(volunteerID string) error {
return config.DB.Delete(&entity.YxVolunteerRecord{}, "volunteer_id = ?", volunteerID).Error
}
func (m *YxVolunteerRecordMapper) BatchCreate(items []entity.YxVolunteerRecord, batchSize int) error {
return config.DB.CreateInBatches(items, batchSize).Error
}

View File

@ -25,9 +25,26 @@ func (s *YxCalculationMajorService) RecommendMajorList(schoolMajorQuery yxDto.Sc
if schoolMajorQuery.UserScoreVO.ProfessionalCategory == "" {
return nil, 0, dto.ProbabilityCountDTO{}, fmt.Errorf("专业类型错误")
}
if schoolMajorQuery.Batch != "" {
schoolMajorQuery.Batch = strings.ReplaceAll(schoolMajorQuery.Batch, "批", "")
// 根据批次类型设置批次 985/211/双一流 -> 提前批, 公办本科 -> 本科A段, 民办本科 -> 本科B段, 体育类 -> 本科
if schoolMajorQuery.Batch != "" && "本科提前" == schoolMajorQuery.Batch {
return []yxDto.UserMajorDTO{}, 0, dto.ProbabilityCountDTO{}, nil
} else if schoolMajorQuery.Batch != "" && "高职高专" != schoolMajorQuery.Batch {
if schoolMajorQuery.Batch2 == "双一流" {
schoolMajorQuery.Batch = "提前批"
} else if schoolMajorQuery.Batch2 == "公办本科" {
schoolMajorQuery.Batch = "本科A段"
} else if schoolMajorQuery.Batch2 == "民办本科" {
schoolMajorQuery.Batch = "本科B段"
} else if schoolMajorQuery.MajorType == "体育类" {
schoolMajorQuery.Batch = "本科"
} else {
schoolMajorQuery.Batch = ""
}
} else {
schoolMajorQuery.Batch = "高职高专"
}
// if len(schoolMajorQuery.MajorTypeChildren) > 0 && "高职高专" != schoolMajorQuery.Batch {
// if "表演类" == schoolMajorQuery.MajorType {
@ -145,6 +162,14 @@ func (s *YxCalculationMajorService) GetByScoreID(scoreID string) ([]entity.YxCal
return s.mapper.FindByScoreID(scoreID)
}
func (s *YxCalculationMajorService) FindListByCompositeKeys(tableName string, keys []string, scoreId string) ([]entity.YxCalculationMajor, error) {
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()

View File

@ -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)
}
@ -41,6 +45,10 @@ func (s *YxVolunteerRecordService) Delete(id string) error {
return s.mapper.Delete(id)
}
func (s *YxVolunteerRecordService) DeleteByVolunteerID(volunteerID string) error {
return s.mapper.DeleteByVolunteerID(volunteerID)
}
func (s *YxVolunteerRecordService) BatchCreate(items []entity.YxVolunteerRecord) error {
for i := range items {
items[i].ID = uuid.New().String()

View File

@ -2,10 +2,11 @@
package service
import (
"fmt"
"server/common"
"server/modules/yx/entity"
"server/modules/yx/mapper"
"github.com/google/uuid"
"time"
)
type YxVolunteerService struct {
@ -25,10 +26,14 @@ func (s *YxVolunteerService) GetByID(id string) (*entity.YxVolunteer, error) {
}
func (s *YxVolunteerService) Create(item *entity.YxVolunteer) error {
item.ID = uuid.New().String()
item.ID = common.GenerateStringID()
return s.mapper.Create(item)
}
func (s *YxVolunteerService) FindActiveByScoreId(scoreId string) (*entity.YxVolunteer, error) {
return s.mapper.FindActiveByScoreId(scoreId)
}
func (s *YxVolunteerService) Update(item *entity.YxVolunteer) error {
return s.mapper.Update(item)
}
@ -43,7 +48,7 @@ func (s *YxVolunteerService) Delete(id string) error {
func (s *YxVolunteerService) BatchCreate(items []entity.YxVolunteer) error {
for i := range items {
items[i].ID = uuid.New().String()
items[i].ID = common.GenerateStringID()
}
return s.mapper.BatchCreate(items, 100)
}
@ -55,7 +60,7 @@ func (s *YxVolunteerService) BatchUpdate(items []entity.YxVolunteer) error {
func (s *YxVolunteerService) BatchUpsert(items []entity.YxVolunteer, updateColumns []string) error {
for i := range items {
if items[i].ID == "" {
items[i].ID = uuid.New().String()
items[i].ID = common.GenerateStringID()
}
}
return s.mapper.BatchUpsert(items, updateColumns)
@ -64,3 +69,25 @@ func (s *YxVolunteerService) BatchUpsert(items []entity.YxVolunteer, updateColum
func (s *YxVolunteerService) BatchDelete(ids []string) error {
return s.mapper.BatchDelete(ids)
}
// 根据ScoreId创建新志愿表
func (s *YxVolunteerService) CreateByScoreId(scoreId string, userId string) error {
volunteer := entity.YxVolunteer{}
volunteer.ID = common.GenerateStringID()
// 志愿表名称格式 时间戳 20260101134501志愿表
volunteer.VolunteerName = time.Now().Format("20060102150405") + "志愿表"
volunteer.ScoreId = scoreId
volunteer.CreateType = "1"
volunteer.State = "1"
volunteer.CreateBy = userId
volunteer.CreateTime = time.Now()
volunteer.UpdateTime = time.Now()
// 先关闭当前用户其他志愿单 - ✅ 检查错误
if err := s.mapper.CloseOtherVolunteer(userId); err != nil {
return fmt.Errorf("关闭其他志愿表失败: %w", err)
}
// 创建志愿表
return s.mapper.Create(&volunteer)
}