feat:普通专业计算录取率

This commit is contained in:
zhouwentao 2025-12-20 22:50:38 +08:00
parent 60dc668f1d
commit 523f323cd8
9 changed files with 522 additions and 6 deletions

View File

@ -33,8 +33,34 @@ const (
StateHistory = "2" // 历史记录
)
// 数值常量
const (
Number0 = 0
Number0p75 = 0.75
Number0p5 = 0.5
Number5 = 5
Number7p5 = 7.5
Number100 = 100
)
// 数据类型常量
const (
TypeNormal = "1" // 普通类
TypeArt = "2" // 艺术类
// YxConstant 相关常量
NowYear = "2026"
// 录取方式常量
CulturalControlLineGuo = "文线专排"
SpecialControlLineGuo = "专过文排"
CulturalControlLineGuoMain = "文过专排主科"
W1Z1 = "文*1+专*1"
W1JiaZ1 = "文+专"
)
var (
OldYearList = []string{"2025", "2024"}
ShowOldYearList = []string{"2025", "2024", "2023"}
)

View File

@ -1,5 +1,7 @@
package dto
import "server/modules/yx/entity"
// SchoolMajorDTO 院校专业查询结果 DTO
type SchoolMajorDTO struct {
SchoolCode string `json:"schoolCode"`
@ -28,6 +30,13 @@ type SchoolMajorDTO struct {
Kslx string `json:"kslx"`
State string `json:"state"`
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"` // 一级学科 (需确认来源)
}
type YxHistoryMajorEnrollDTO struct {

View File

@ -0,0 +1,22 @@
package entity
import "time"
// YxHistoryScoreControlLine 历年各专业省控分数线实体
type YxHistoryScoreControlLine struct {
ID string `gorm:"column:id;primaryKey" json:"id"`
Year string `gorm:"column:year" json:"year"` // 年份
ProfessionalCategory string `gorm:"column:professional_category" json:"professionalCategory"` // 专业类别
Category string `gorm:"column:category" json:"category"` // 文理科
Batch string `gorm:"column:batch" json:"batch"` // 批次
CulturalScore float64 `gorm:"column:cultural_score" json:"culturalScore"` // 文化分
SpecialScore float64 `gorm:"column:special_score" json:"specialScore"` // 专业分
CreateBy string `gorm:"column:create_by" json:"createBy"`
CreateTime time.Time `gorm:"column:create_time" json:"createTime"`
UpdateBy string `gorm:"column:update_by" json:"updateBy"`
UpdateTime time.Time `gorm:"column:update_time" json:"updateTime"`
}
func (YxHistoryScoreControlLine) TableName() string {
return "yx_history_score_control_line"
}

View File

@ -0,0 +1,23 @@
package mapper
import (
"server/config"
"server/modules/yx/entity"
)
type YxHistoryScoreControlLineMapper struct{}
func NewYxHistoryScoreControlLineMapper() *YxHistoryScoreControlLineMapper {
return &YxHistoryScoreControlLineMapper{}
}
func (m *YxHistoryScoreControlLineMapper) SelectByYearAndCategory(year, professionalCategory, category string) ([]entity.YxHistoryScoreControlLine, error) {
var items []entity.YxHistoryScoreControlLine
err := config.DB.Model(&entity.YxHistoryScoreControlLine{}).
Where("year = ?", year).
Where("professional_category = ?", professionalCategory).
Where("category = ?", category).
Order("batch desc").
Find(&items).Error
return items, err
}

View File

@ -0,0 +1,202 @@
package service
import (
"math"
"regexp"
"server/common"
"server/modules/user/vo"
"server/modules/yx/dto"
"strconv"
"strings"
)
// ScoreUtil 分数计算工具函数集合
// 移植自 Java 版本的 ScoreUtil.java
var (
BigDecimal0 = 0.0
BigDecimal05 = 0.5
BigDecimal075 = 0.75
BigDecimal0133 = 0.133
BigDecimal0667 = 0.667
BigDecimal004 = 0.04
BigDecimal0467 = 0.467
BigDecimal0067 = 0.067
BigDecimal0333 = 0.333
BigDecimal0093 = 0.093
BigDecimal02 = 0.2
BigDecimal07 = 0.7
BigDecimal03 = 0.3
BigDecimal04 = 0.4
BigDecimal06 = 0.6
BigDecimal1 = 1.0
BigDecimal100 = 100.0
BigDecimal150 = 150.0
BigDecimal95x = 95.0
BigDecimal85x = 85.0
BigDecimal7p5 = 7.5
BigDecimal5 = 5.0
)
// ComputeHistoryMajorEnrollScoreLineDifferenceWithRulesEnrollProbability 计算历年录取分差
func ComputeHistoryMajorEnrollScoreLineDifferenceWithRulesEnrollProbability(majorType string,
rulesEnrollProbability, probabilityOperator string,
HistoryMajorEnrollMap map[string]dto.YxHistoryMajorEnrollDTO) map[string]any {
if len(HistoryMajorEnrollMap) == 0 {
return map[string]any{"scoreDifference": 0.0}
}
sum := 0.0
validYearCount := 0
for _, enrollData := range HistoryMajorEnrollMap {
admissionLine := enrollData.AdmissionLine
controlLine := enrollData.ControlLine
if admissionLine <= 0 || controlLine <= 0 {
continue
}
// 计算分差
currentDiff := admissionLine - controlLine
// currentDiff := enrollData.ScoreLineDifference
// 特殊逻辑:高职高专(非体育类)需计算分差率(分差/省控线)
// Java: boolean isVocationalCollege = "高职高专".equals(enrollData.getBatch());
// boolean isSportsMajor = "体育类".equals(enrollData.getMajorType());
isSportsMajor := "体育类" == majorType
if isSportsMajor {
if "2024" == enrollData.Year && "专过文排" != enrollData.EnrollmentCode && "文过专排" != enrollData.EnrollmentCode { // enrollment_code 对应 rulesEnrollProbability? No.
// Java uses rulesEnrollProbability field in entity.
// Go entity YxHistoryMajorEnroll doesn't have RulesEnrollProbability field?
// Let's check entity definition again.
currentDiff = currentDiff * common.Number7p5
} else if "2024" == enrollData.Year && "文过专排" == enrollData.EnrollmentCode { // Placeholder
currentDiff = currentDiff * common.Number5
} else if "2023" == enrollData.Year {
continue
} else if rulesEnrollProbability == enrollData.RulesEnrollProbability { // Need field
sum += currentDiff
validYearCount++
continue
}
} else {
// 非高职高专 or 体育类:检查录取方式是否一致
if probabilityOperator != enrollData.ProbabilityOperator {
continue
}
}
sum += currentDiff
validYearCount++
}
averageDiff := 0.0
if validYearCount > 0 {
averageDiff = sum / float64(validYearCount)
}
return map[string]any{"scoreDifference": averageDiff}
}
// ConvertIntoScore 计算折合分
func ConvertIntoScore(rulesEnrollProbability string, culturalScore, professionalScore float64, operator string) float64 {
score := 0.0
if operator != "" {
// operator: 例如: "文*0.5+专*0.5"
parts := strings.Split(operator, "+")
for _, part := range parts {
subParts := strings.Split(part, "*")
if len(subParts) == 2 {
ratio, _ := strconv.ParseFloat(subParts[1], 64)
if strings.Contains(subParts[0], "文") {
score += culturalScore * ratio
} else {
score += professionalScore * ratio
}
}
}
}
return round(score, 4)
}
// CrossingControlLine 判断是否过省控线
func CrossingControlLine(rulesEnrollProbability string, culturalScore, professionalScore, culturalControlLine, specialControlLine float64) bool {
if rulesEnrollProbability == common.CulturalControlLineGuo {
return culturalScore >= culturalControlLine
} else if rulesEnrollProbability == common.SpecialControlLineGuo {
return professionalScore >= specialControlLine
} else if rulesEnrollProbability == common.CulturalControlLineGuoMain {
return culturalScore >= specialControlLine
}
return culturalScore >= culturalControlLine && professionalScore >= specialControlLine
}
// CommonCheckEnrollProbability 计算录取率
func CommonCheckEnrollProbability(nowYearDiff, historyThreeYearDiff float64) float64 {
enrollProbability := 0.0
if nowYearDiff == 0 && historyThreeYearDiff > 0 {
enrollProbability = 75.0 / historyThreeYearDiff
} else if nowYearDiff == 0 && historyThreeYearDiff <= 0 {
enrollProbability = 50.0
} else if nowYearDiff > 0 && historyThreeYearDiff <= 0 {
enrollProbability = nowYearDiff * 100 * 0.75
} else if nowYearDiff < 0 && historyThreeYearDiff <= 0 {
enrollProbability = 0.0
} else {
// (当前年分差/去年分差)*0.75 * 100
if historyThreeYearDiff != 0 {
enrollProbability = (nowYearDiff / historyThreeYearDiff) * 100 * 0.75
}
}
return enrollProbability
}
// CommonCheckEnrollProbabilityBeilv 录取率倍率调整
func CommonCheckEnrollProbabilityBeilv(enrollProbability float64) float64 {
if enrollProbability > 150 {
return 95.0
} else if enrollProbability > 100 {
return 85.0
} else if enrollProbability <= 0 {
return 0.0
}
return enrollProbability
}
// OtherScoreJudge 其他录取要求
func OtherScoreJudge(professionalScore float64, userScoreVO vo.UserScoreVO, schoolMajorDTO dto.SchoolMajorDTO) bool {
// 简单实现,参考 Java 逻辑
if schoolMajorDTO.EnglishScoreLimitation > 0 && userScoreVO.EnglishScore < schoolMajorDTO.EnglishScoreLimitation {
return false
}
if schoolMajorDTO.CulturalScoreLimitation > 0 && userScoreVO.CulturalScore < schoolMajorDTO.CulturalScoreLimitation {
return false
}
// 专业分限制等...
return true
}
// HasComputeEnrollProbabilityPermissions 判断是否有权限计算
func HasComputeEnrollProbabilityPermissions(nowBatch, majorBatch string) bool {
return "" != nowBatch && "" != majorBatch
}
// round 四舍五入
func round(val float64, precision int) float64 {
ratio := math.Pow(10, float64(precision))
return math.Round(val*ratio) / ratio
}
// ReplaceLastZeroChar 格式化操作符字符串 (Java: replaceLastZeroChar)
func ReplaceLastZeroChar(input string) string {
if input == "" {
return ""
}
re := regexp.MustCompile(`(\d+\.\d*?)0+(\D|$)`)
input = re.ReplaceAllString(input, "$1$2")
input = strings.ReplaceAll(input, "1.", "1")
// 简化处理
return input
}

View File

@ -2,6 +2,7 @@
package service
import (
"server/common"
"server/modules/user/vo"
"server/modules/yx/dto"
"server/modules/yx/entity"
@ -13,12 +14,14 @@ import (
type YxCalculationMajorService struct {
historyMajorEnrollService *YxHistoryMajorEnrollService
mapper *mapper.YxCalculationMajorMapper
historyScoreControlLineService *YxHistoryScoreControlLineService
}
func NewYxCalculationMajorService() *YxCalculationMajorService {
return &YxCalculationMajorService{
historyMajorEnrollService: NewYxHistoryMajorEnrollService(),
mapper: mapper.NewYxCalculationMajorMapper(),
historyScoreControlLineService: NewYxHistoryScoreControlLineService(),
}
}
@ -186,5 +189,150 @@ func (s *YxCalculationMajorService) ListByUserQueryType(professionalCategory str
// 返回值类型 - 返回值描述
func (s *YxCalculationMajorService) CheckEnrollProbability(schoolMajorDTOList *[]dto.SchoolMajorDTO, userScoreVO vo.UserScoreVO) error {
professionalCategory := userScoreVO.ProfessionalCategory
if "表演类" == professionalCategory {
// TODO: biaoyanService
} else if "音乐类" == professionalCategory {
// TODO: musicService
} else {
s.betaRecommendMajorListSetEnrollProbability(schoolMajorDTOList, userScoreVO)
}
return nil
}
// betaRecommendMajorListSetEnrollProbability 美术与设计类,书法类,体育类 按这个走 获取录取率
func (s *YxCalculationMajorService) betaRecommendMajorListSetEnrollProbability(recommendMajorList *[]dto.SchoolMajorDTO, userScoreVO vo.UserScoreVO) {
if recommendMajorList == nil || len(*recommendMajorList) == 0 {
return
}
professionalCategory := userScoreVO.ProfessionalCategory
nowBatch := "本科" // TODO 默认为本科需根据实际情况获取Java中是从 userScore 获取,这里 VO 中似乎没有 Batch? 假设为本科或从 VO 获取
// userScoreVO.Batch? 检查 VO 定义
// 假设 userScoreVO 没有 Batch需要确认。Entity YxUserScore 有 Batch。VO 可能需要补充。
// 暂时假设为 "本科" 或 ""
// 获取省控线 Map
historyScoreControlLineMap, err := s.historyScoreControlLineService.MapsBatchByProfessionalCategoryOfYear(common.NowYear, professionalCategory, userScoreVO.CognitioPolyclinic)
if err != nil {
// Log error
return
}
culturalScore := userScoreVO.CulturalScore
professionalScore := userScoreVO.ProfessionalScore
for i := range *recommendMajorList {
item := &(*recommendMajorList)[i]
rulesEnrollProbability := item.PrivateRulesEnrollProbability
probabilityOperator := item.PrivateProbabilityOperator
// 获取对应批次的省控线
controlLineData, ok := historyScoreControlLineMap[item.Batch]
if !ok {
// 尝试默认批次
if val, okDefault := historyScoreControlLineMap["本科"]; okDefault {
controlLineData = val
} else {
continue // 没有省控线无法计算
}
}
culturalControlLine := controlLineData.CulturalScore
specialControlLine := controlLineData.SpecialScore
if rulesEnrollProbability == "" {
continue
}
// 补全 probabilityOperator 逻辑
if rulesEnrollProbability == "文过专排" && probabilityOperator == "" {
probabilityOperator = "文*0+专*1"
} else if rulesEnrollProbability == "专过文排" && probabilityOperator == "" {
probabilityOperator = "文*1+专*0"
}
if probabilityOperator == "" {
item.EnrollProbability = common.Number5
continue
}
// 判断其他录取要求
if !OtherScoreJudge(professionalScore, userScoreVO, *item) {
item.EnrollProbability = common.Number0
continue
}
// 25年专业录取原则变动 (体育类特殊逻辑,硬编码 ID 列表)
if item.MajorType == "体育类" {
specialSchoolCodes := []string{"6530", "6085", "6110", "6065", "6050"}
isSpecial := false
for _, code := range specialSchoolCodes {
if item.SchoolCode == code {
isSpecial = true
break
}
}
if isSpecial {
item.EnrollProbability = common.Number0
continue
}
}
// 判断是否过省控线
if !CrossingControlLine(rulesEnrollProbability, culturalScore, professionalScore, culturalControlLine, specialControlLine) {
item.EnrollProbability = common.Number0
continue
}
// 计算学生折合分
studentScore := ConvertIntoScore(rulesEnrollProbability, culturalScore, professionalScore, probabilityOperator)
item.PrivateStudentScore = studentScore
item.StudentScore = studentScore // 展示用
// 权限检查
if !HasComputeEnrollProbabilityPermissions(nowBatch, item.Batch) {
item.EnrollProbability = common.Number0
continue
}
// 录取方式计算
if common.CulturalControlLineGuoMain == rulesEnrollProbability {
if len(item.HistoryMajorEnrollList) == 0 {
item.EnrollProbability = common.Number0
continue
}
item.EnrollProbability = (studentScore * item.HistoryMajorEnrollList[0].AdmissionLine) * common.Number0p75
if studentScore >= item.HistoryMajorEnrollList[0].AdmissionLine {
item.EnrollProbability *= common.Number0p5
}
continue
} else {
// 当前年省控线 折合后
nowYearProvincialControlLine := ConvertIntoScore(rulesEnrollProbability, culturalControlLine, specialControlLine, probabilityOperator)
if nowYearProvincialControlLine <= 0 {
item.EnrollProbability = common.Number0
continue
}
// 历年分差
diffMap := ComputeHistoryMajorEnrollScoreLineDifferenceWithRulesEnrollProbability(item.MajorType, rulesEnrollProbability, probabilityOperator, item.HistoryMajorEnrollMap)
historyThreeYearDiff := diffMap["scoreDifference"].(float64)
if historyThreeYearDiff == 0 {
item.EnrollProbability = common.Number0
continue
}
// 当前年线差
nowYearDiff := studentScore - nowYearProvincialControlLine
// 计算录取率
enrollProbability := CommonCheckEnrollProbability(nowYearDiff, historyThreeYearDiff)
item.EnrollProbability = CommonCheckEnrollProbabilityBeilv(enrollProbability)
}
}
// log time...
}

View File

@ -3,6 +3,7 @@ package service
import (
"server/config"
"server/modules/yx/dto"
"server/modules/yx/entity"
"server/modules/yx/mapper"
@ -13,6 +14,43 @@ type YxHistoryMajorEnrollService struct {
mapper *mapper.YxHistoryMajorEnrollMapper
}
// RecommendMajorDTOListSetHistoryInfo 填充历史录取信息
func (s *YxHistoryMajorEnrollService) RecommendMajorDTOListSetHistoryInfo(dtoList *[]dto.SchoolMajorDTO, years []string) {
if dtoList == nil || len(*dtoList) == 0 {
return
}
schoolCodes := make([]string, 0)
majorNames := make([]string, 0)
category := (*dtoList)[0].Category // 假设同一批次查询类别相同
majorType := (*dtoList)[0].MajorType
for _, item := range *dtoList {
schoolCodes = append(schoolCodes, item.SchoolCode)
majorNames = append(majorNames, item.MajorName)
}
historyList, err := s.ListBySchoolCodesAndMajorNames(schoolCodes, majorNames, category, majorType, years)
if err != nil {
return // Log error
}
// Group by SchoolCode + MajorName
historyMap := make(map[string][]entity.YxHistoryMajorEnroll)
for _, h := range historyList {
key := h.SchoolCode + "_" + h.MajorName
historyMap[key] = append(historyMap[key], h)
}
for i := range *dtoList {
item := &(*dtoList)[i]
key := item.SchoolCode + "_" + item.MajorName
if list, ok := historyMap[key]; ok {
item.HistoryMajorEnrollList = list
}
}
}
// HistoryMajorEnrollService 中的方法
func (s *YxHistoryMajorEnrollService) ListBySchoolCodesAndMajorNames(
schoolCodes []string,

View File

@ -0,0 +1,36 @@
package service
import (
"server/modules/yx/entity"
"server/modules/yx/mapper"
)
type YxHistoryScoreControlLineService struct {
mapper *mapper.YxHistoryScoreControlLineMapper
}
func NewYxHistoryScoreControlLineService() *YxHistoryScoreControlLineService {
return &YxHistoryScoreControlLineService{
mapper: mapper.NewYxHistoryScoreControlLineMapper(),
}
}
// MapsBatchByProfessionalCategoryOfYear 获取指定年份、专业类别、文理科的省控线,并以批次为 Key 返回 Map
func (s *YxHistoryScoreControlLineService) MapsBatchByProfessionalCategoryOfYear(year, professionalCategory, category string) (map[string]entity.YxHistoryScoreControlLine, error) {
list, err := s.mapper.SelectByYearAndCategory(year, professionalCategory, category)
if err != nil {
return nil, err
}
result := make(map[string]entity.YxHistoryScoreControlLine)
for _, item := range list {
// 复制对象逻辑,处理"本科A段"映射为"提前批"
if item.Batch == "本科A段" {
newItem := item
newItem.Batch = "提前批"
result[newItem.Batch] = newItem
}
result[item.Batch] = item
}
return result, nil
}

View File

@ -4,6 +4,7 @@ import (
"context"
"server/common"
"server/config"
"server/modules/user/vo"
"server/modules/yx/service"
"testing"
"time"
@ -37,15 +38,26 @@ func TestRedisOperation(t *testing.T) {
// TestSchoolMajorQuery 测试院校专业复杂查询 Mapper
func TestSchoolMajorQuery(t *testing.T) {
cs := service.NewYxCalculationMajorService()
userScoreVO := vo.UserScoreVO{
ProfessionalCategory: "美术与设计类",
CognitioPolyclinic: "文科",
CulturalScore: 500,
ProfessionalScore: 250,
}
start := time.Now()
items, err := cs.ListByUserQueryType("美术与设计类", "文科", []string{})
if err != nil {
t.Fatalf("查询失败: %v", err)
}
duration := time.Since(start)
t.Logf("查询耗时: %v", duration)
duration := time.Now().UnixMilli() - start.UnixMilli()
common.LogInfo("CheckEnrollProbability elapsed: %v", duration)
t.Logf("查询成功,共找到 %d 条记录", len(items))
startTime := time.Now()
cs.CheckEnrollProbability(&items, userScoreVO)
elapsed := time.Now().UnixMilli() - startTime.UnixMilli()
common.LogInfo("CheckEnrollProbability elapsed: %v", elapsed)
// 打印前几条结果
// for i, item := range items {
// if i >= 3 {