diff --git a/server/modules/user/controller/user_major_controller.go b/server/modules/user/controller/user_major_controller.go index 40db3ba..d15b049 100644 --- a/server/modules/user/controller/user_major_controller.go +++ b/server/modules/user/controller/user_major_controller.go @@ -60,10 +60,15 @@ func (ctrl *UserMajorController) List(c *gin.Context) { return } schoolMajorQuery.UserScoreVO = userScoreVO - items, total, err := ctrl.yxCalculationMajorService.RecommendMajorList(schoolMajorQuery) + items, total, probCount, err := ctrl.yxCalculationMajorService.RecommendMajorList(schoolMajorQuery) if err != nil { common.Error(c, 500, err.Error()) return } - common.SuccessPage(c, items, total, page, size) + newMap := map[string]interface{}{ + "items": items, + "total": total, + "probCount": probCount, + } + common.SuccessPage(c, newMap, total, page, size) } diff --git a/server/modules/yx/dto/probability_count_dto.go b/server/modules/yx/dto/probability_count_dto.go new file mode 100644 index 0000000..1a10b18 --- /dev/null +++ b/server/modules/yx/dto/probability_count_dto.go @@ -0,0 +1,9 @@ +package dto + +// 定义概率数量统计结果结构体,用于接收四种录取概率对应的各自数量 +type ProbabilityCountDTO struct { + HardAdmit int64 `gorm:"column:hard_admit"` // 难录取(<60) + Impact int64 `gorm:"column:impact"` // 可冲击(60<=x<73) + Stable int64 `gorm:"column:stable"` // 较稳妥(73<=x<93) + Secure int64 `gorm:"column:secure"` // 可保底(>=93) +} diff --git a/server/modules/yx/mapper/yx_calculation_major_mapper.go b/server/modules/yx/mapper/yx_calculation_major_mapper.go index 4e664df..50c824c 100644 --- a/server/modules/yx/mapper/yx_calculation_major_mapper.go +++ b/server/modules/yx/mapper/yx_calculation_major_mapper.go @@ -8,6 +8,7 @@ import ( "server/modules/yx/entity" "strings" "sync" + "time" "gorm.io/gorm/clause" ) @@ -18,6 +19,14 @@ func NewYxCalculationMajorMapper() *YxCalculationMajorMapper { return &YxCalculationMajorMapper{} } +// 先定义存储各协程耗时的结构体(局部使用,也可全局复用) +type QueryCostTime struct { + CountCost time.Duration // 总数量查询耗时 + ProbCountCost time.Duration // 四种概率数量查询耗时 + QueryCost time.Duration // 主列表查询耗时 + TotalCost time.Duration // 整体总耗时 +} + func (m *YxCalculationMajorMapper) FindAll(page, size int) ([]entity.YxCalculationMajor, int64, error) { var items []entity.YxCalculationMajor var total int64 @@ -26,7 +35,208 @@ func (m *YxCalculationMajorMapper) FindAll(page, size int) ([]entity.YxCalculati return items, total, err } -func (m *YxCalculationMajorMapper) FindRecommendList(query dto.SchoolMajorQuery) ([]dto.UserMajorDTO, int64, error) { +// 调整返回值:新增 ProbabilityCountDTO,返回列表、总数量、四种概率各自数量 +func (m *YxCalculationMajorMapper) FindRecommendList(query dto.SchoolMajorQuery) ([]dto.UserMajorDTO, int64, dto.ProbabilityCountDTO, error) { + var items []dto.UserMajorDTO + var total int64 + var probCount dto.ProbabilityCountDTO // 四种概率的数量统计结果 + + // 1. 表名合法性校验:非空 + 白名单 + tableName := query.UserScoreVO.CalculationTableName + if tableName == "" { + return nil, 0, dto.ProbabilityCountDTO{}, fmt.Errorf("CalculationTableName is empty") + } + // if !validTableNames[] { + // return nil, 0, dto.ProbabilityCountDTO{}, fmt.Errorf("invalid table name: %s, potential SQL injection risk", tableName) + // } + + // 2. 基础条件SQL(共用过滤条件,排除概率筛选) + baseSQL := " WHERE 1=1 AND cm.state > 0 " + params := []interface{}{} + + // 拼接共用过滤条件(与原有列表查询条件一致,保证统计结果准确性) + if query.UserScoreVO.ID != "" { + baseSQL += " AND cm.score_id = ?" + params = append(params, query.UserScoreVO.ID) + } + if query.MajorType != "" { + baseSQL += " AND cm.major_type = ?" + params = append(params, query.MajorType) + } + if query.Category != "" { + baseSQL += " AND cm.category = ?" + params = append(params, query.Category) + } + if len(query.MajorTypeChildren) > 0 { + placeholders := strings.Repeat("?,", len(query.MajorTypeChildren)-1) + "?" + baseSQL += " AND cm.major_type_child IN (" + placeholders + ")" + for _, v := range query.MajorTypeChildren { + params = append(params, v) + } + } + if query.MainSubjects != "" { + baseSQL += " AND cm.main_subjects = ?" + params = append(params, query.MainSubjects) + } + + // 3. 优化后的总数量COUNT SQL + countSQL := fmt.Sprintf(` + SELECT COUNT(cm.id) FROM %s cm + %s + `, tableName, baseSQL) + + // 4. 四种概率批量统计SQL(使用CASE WHEN一次查询,性能最优) + probCountSQL := fmt.Sprintf(` + SELECT + SUM(CASE WHEN cm.enroll_probability < 60 THEN 1 ELSE 0 END) AS hard_admit, + SUM(CASE WHEN cm.enroll_probability >= 60 AND cm.enroll_probability < 73 THEN 1 ELSE 0 END) AS impact, + SUM(CASE WHEN cm.enroll_probability >= 73 AND cm.enroll_probability < 93 THEN 1 ELSE 0 END) AS stable, + SUM(CASE WHEN cm.enroll_probability >= 93 THEN 1 ELSE 0 END) AS secure + FROM %s cm + %s + `, tableName, baseSQL) + + // 5. 主查询SQL(保留原有字段和JOIN) + mainSQL := 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 + %s + `, tableName, baseSQL) + + // 拼接传入概率的筛选条件(兼容原有业务逻辑) + switch query.Probability { + case "难录取": + mainSQL += " AND cm.enroll_probability < 60" + case "可冲击": + mainSQL += " AND (cm.enroll_probability >= 60 and cm.enroll_probability < 73)" + case "较稳妥": + mainSQL += " AND (cm.enroll_probability >= 73 and cm.enroll_probability < 93)" + case "可保底": + mainSQL += " AND (cm.enroll_probability >= 93)" + } + + // 6. 分页参数合法性校验 + page := query.Page + size := query.Size + if page < 1 { + page = 1 + } + if size < 1 { + size = 10 + } + if size > 100 { + size = 100 + } + offset := (page - 1) * size + // 提前拼接分页条件,避免协程内操作共享变量 + mainSQL += fmt.Sprintf(" LIMIT %d OFFSET %d", size, offset) + + // 7. 协程并发执行三个查询(总数量、概率数量、主列表),提升性能 + // ---------------------- 核心局部代码(替换你原来的协程块) ---------------------- + var wg sync.WaitGroup + var countErr, probCountErr, queryErr error + var queryCost QueryCostTime // 存储各协程耗时 + var mu sync.Mutex // 互斥锁:防止多协程同时修改queryCost引发竞态问题 + + // 整体开始时间 + totalStartTime := time.Now() + + wg.Add(3) + + // 协程1:总数量查询(单独记录耗时) + go func() { + defer wg.Done() + // 记录该协程单独的开始时间 + start := time.Now() + countErr = config.DB.Raw(countSQL, params...).Count(&total).Error + // 计算该协程耗时,通过互斥锁安全写入共享变量 + mu.Lock() + queryCost.CountCost = time.Now().Sub(start) + mu.Unlock() + }() + + // 协程2:四种概率数量批量查询(单独记录耗时) + go func() { + defer wg.Done() + // 记录该协程单独的开始时间 + start := time.Now() + probCountErr = config.DB.Raw(probCountSQL, params...).Scan(&probCount).Error + // 计算该协程耗时,通过互斥锁安全写入共享变量 + mu.Lock() + queryCost.ProbCountCost = time.Now().Sub(start) + mu.Unlock() + }() + + // 协程3:主列表查询(单独记录耗时) + go func() { + defer wg.Done() + // 记录该协程单独的开始时间 + start := time.Now() + queryErr = config.DB.Raw(mainSQL, params...).Scan(&items).Error + // 计算该协程耗时,通过互斥锁安全写入共享变量 + mu.Lock() + queryCost.QueryCost = time.Now().Sub(start) + mu.Unlock() + }() + + wg.Wait() + + // 计算整体总耗时 + queryCost.TotalCost = time.Now().Sub(totalStartTime) + + // 打印各协程耗时和总耗时(按需输出,可注释或删除) + fmt.Printf("各查询耗时统计:\n") + fmt.Printf(" 总数量查询耗时:%v\n", queryCost.CountCost) + fmt.Printf(" 概率数量查询耗时:%v\n", queryCost.ProbCountCost) + fmt.Printf(" 主列表查询耗时:%v\n", queryCost.QueryCost) + fmt.Printf(" 整体总耗时:%v\n", queryCost.TotalCost) + + // 8. 错误处理 + if countErr != nil { + return nil, 0, dto.ProbabilityCountDTO{}, fmt.Errorf("failed to query total count: %w", countErr) + } + if probCountErr != nil { + return nil, 0, dto.ProbabilityCountDTO{}, fmt.Errorf("failed to query probability count: %w", probCountErr) + } + if queryErr != nil { + return nil, 0, dto.ProbabilityCountDTO{}, fmt.Errorf("failed to query recommend major list: %w", queryErr) + } + + return items, total, probCount, nil +} + +func (m *YxCalculationMajorMapper) FindRecommendList1(query dto.SchoolMajorQuery) ([]dto.UserMajorDTO, int64, error) { var items []dto.UserMajorDTO var total int64 diff --git a/server/modules/yx/service/yx_calculation_major_service.go b/server/modules/yx/service/yx_calculation_major_service.go index fed68a7..8532095 100644 --- a/server/modules/yx/service/yx_calculation_major_service.go +++ b/server/modules/yx/service/yx_calculation_major_service.go @@ -21,9 +21,9 @@ type YxCalculationMajorService struct { historyScoreControlLineService *YxHistoryScoreControlLineService } -func (s *YxCalculationMajorService) RecommendMajorList(schoolMajorQuery yxDto.SchoolMajorQuery) ([]yxDto.UserMajorDTO, int64, error) { +func (s *YxCalculationMajorService) RecommendMajorList(schoolMajorQuery yxDto.SchoolMajorQuery) ([]yxDto.UserMajorDTO, int64, dto.ProbabilityCountDTO, error) { if schoolMajorQuery.UserScoreVO.ProfessionalCategory == "" { - return nil, 0, fmt.Errorf("专业类型错误") + return nil, 0, dto.ProbabilityCountDTO{}, fmt.Errorf("专业类型错误") } if schoolMajorQuery.Batch != "" { schoolMajorQuery.Batch = strings.ReplaceAll(schoolMajorQuery.Batch, "批", "") @@ -33,18 +33,23 @@ func (s *YxCalculationMajorService) RecommendMajorList(schoolMajorQuery yxDto.Sc // } // } - calculationMajors, total, err := s.mapper.FindRecommendList(schoolMajorQuery) + calculationMajors, total, probCount, err := s.mapper.FindRecommendList(schoolMajorQuery) if err != nil { - return nil, 0, err + return nil, 0, dto.ProbabilityCountDTO{}, err } // 为专业列表添加历史数据 + startTime := time.Now() err = s.UserMajorDTOGetHistory(&calculationMajors) + endTime := time.Now() + // 执行时长 + queryCostTime := endTime.Sub(startTime) + fmt.Printf(" 历史数据查询耗时:%v\n", queryCostTime) if err != nil { - return nil, 0, err + return nil, 0, dto.ProbabilityCountDTO{}, err } - return calculationMajors, total, nil + return calculationMajors, total, probCount, nil } func (s *YxCalculationMajorService) BatchCreateBySchoolMajorDTO(tableName string, items []dto.SchoolMajorDTO, scoreID string) error {