diff --git a/.vscode/settings.json b/.vscode/settings.json index 1120dc5..05751d5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,6 +7,7 @@ "Fzby", "gonic", "gorm", + "kslx", "swaggo", "Xjysby", "Xjysdy", diff --git a/Task5.md b/Task5.md new file mode 100644 index 0000000..9236bfa --- /dev/null +++ b/Task5.md @@ -0,0 +1,35 @@ +``` +select +sm.school_code, +sc.school_name, +sm.major_code, +sm.major_name, +sm.major_type, +sm.major_type_child, +sm.plan_num, +sm.main_subjects, +sm.limitation, +sm.chinese_score_limitation, +sm.english_score_limitation, +sm.cultural_score_limitation, +sm.professional_score_limitation, +sm.enrollment_code, +sm.tuition, +sm.detail, +sm.category, +sm.batch, +sm.rules_enroll_probability as rulesEnrollProbability, +sm.probability_operator as probabilityOperator, +sm.private_rules_enroll_probability as privateRulesEnrollProbability, +sm.private_probability_operator as privateProbabilityOperator, +sm.rules_enroll_probability_sx, +sm.kslx, +sm.state +FROM yitisheng.yx_school_major sm +LEFT JOIN (SELECT school_id,school_name,school_code FROM yitisheng.yx_school_child group by school_code) sc ON sc.school_code = sm.school_code +LEFT JOIN yitisheng.yx_school s ON s.id = sc.school_id +where 1=1 and sm.state >0 +and sm.major_type = '音乐类' and sm.category = '文科' and major_type_child in ('音乐教育') and sm.main_subjects = '器乐' +``` +请你参考这个查询SQL,帮我在#yx_school_major_mapper.go中实现这个查询,Where后条件是传递过去的。 +这个返回结果封装成DTO。 \ No newline at end of file diff --git a/project_doing.md b/project_doing.md index 638f0f9..8fd8bad 100644 --- a/project_doing.md +++ b/project_doing.md @@ -93,3 +93,19 @@ - 创建了 `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 生成器的测试用例。 diff --git a/project_index.md b/project_index.md index 0943a7d..3ae19f5 100644 --- a/project_index.md +++ b/project_index.md @@ -6,6 +6,9 @@ - `config.go`: 应用全局配置。 - `database.go`: 数据库连接配置。 - `redis.go`: Redis连接配置。 +- `tests/`: 单元测试与集成测试目录。 + - `init_test.go`: 测试环境初始化(自动连接 DB 和 Redis)。 + - `service_test.go`: 业务逻辑与数据访问测试用例。 - `common/`: 通用工具包。 - `constants.go`: 全局常量定义(Redis Key、业务状态等)。 - `id_utils.go`: ID 生成工具(基于时间戳)。 diff --git a/server/modules/yx/dto/yx_school_major_dto.go b/server/modules/yx/dto/yx_school_major_dto.go new file mode 100644 index 0000000..d33999a --- /dev/null +++ b/server/modules/yx/dto/yx_school_major_dto.go @@ -0,0 +1,50 @@ +package dto + +// SchoolMajorDTO 院校专业查询结果 DTO +type SchoolMajorDTO struct { + SchoolCode string `json:"schoolCode"` + SchoolName string `json:"schoolName"` + MajorCode string `json:"majorCode"` + MajorName string `json:"majorName"` + MajorType string `json:"majorType"` + MajorTypeChild string `json:"majorTypeChild"` + PlanNum int `json:"planNum"` + MainSubjects string `json:"mainSubjects"` + Limitation string `json:"limitation"` + ChineseScoreLimitation float64 `json:"chineseScoreLimitation"` + EnglishScoreLimitation float64 `json:"englishScoreLimitation"` + CulturalScoreLimitation float64 `json:"culturalScoreLimitation"` + ProfessionalScoreLimitation float64 `json:"professionalScoreLimitation"` + EnrollmentCode string `json:"enrollmentCode"` + Tuition string `json:"tuition"` + Detail string `json:"detail"` + Category string `json:"category"` + Batch string `json:"batch"` + RulesEnrollProbability string `json:"rulesEnrollProbability"` + ProbabilityOperator string `json:"probabilityOperator"` + PrivateRulesEnrollProbability string `json:"privateRulesEnrollProbability"` + PrivateProbabilityOperator string `json:"privateProbabilityOperator"` + RulesEnrollProbabilitySx string `json:"rulesEnrollProbabilitySx"` + Kslx string `json:"kslx"` + State string `json:"state"` + HistoryMajorEnrollMap map[string]YxHistoryMajorEnrollDTO `json:"historyMajorEnrollMap"` +} + +type YxHistoryMajorEnrollDTO struct { + Year string `json:"year"` + EnrollmentCode string `json:"enrollmentCode"` + EnrollmentCount int `json:"enrollmentCount"` + RulesEnrollProbability string `json:"rulesEnrollProbability"` + ProbabilityOperator string `json:"probabilityOperator"` + AdmissionLine float64 `json:"admissionLine"` + ControlLine float64 `json:"controlLine"` + // 其他字段... +} + +// SchoolMajorQuery 院校专业查询条件 +type SchoolMajorQuery struct { + MajorType string `json:"majorType"` // 对应 major_type + Category string `json:"category"` // 对应 category + MajorTypeChildren []string `json:"majorTypeChildren"` // 对应 major_type_child in (...) + MainSubjects string `json:"mainSubjects"` // 对应 main_subjects +} diff --git a/server/modules/yx/mapper/yx_school_major_mapper.go b/server/modules/yx/mapper/yx_school_major_mapper.go index 52c12d7..0ec2184 100644 --- a/server/modules/yx/mapper/yx_school_major_mapper.go +++ b/server/modules/yx/mapper/yx_school_major_mapper.go @@ -3,6 +3,7 @@ package mapper import ( "server/config" + "server/modules/yx/dto" "server/modules/yx/entity" "gorm.io/gorm/clause" @@ -62,3 +63,61 @@ func (m *YxSchoolMajorMapper) BatchUpsert(items []entity.YxSchoolMajor, updateCo func (m *YxSchoolMajorMapper) BatchDelete(ids []string) error { return config.DB.Delete(&entity.YxSchoolMajor{}, "id IN ?", ids).Error } + +func (m *YxSchoolMajorMapper) SelectSchoolMajor(query dto.SchoolMajorQuery) ([]dto.SchoolMajorDTO, error) { + var items []dto.SchoolMajorDTO + + sql := ` + SELECT + sm.school_code, + sc.school_name, + sm.major_code, + sm.major_name, + sm.major_type, + sm.major_type_child, + sm.plan_num, + sm.main_subjects, + sm.limitation, + sm.chinese_score_limitation, + sm.english_score_limitation, + sm.cultural_score_limitation, + sm.professional_score_limitation, + sm.enrollment_code, + sm.tuition, + sm.detail, + sm.category, + sm.batch, + sm.rules_enroll_probability, + sm.probability_operator, + sm.private_rules_enroll_probability, + sm.private_probability_operator, + sm.rules_enroll_probability_sx, + sm.kslx, + sm.state + FROM yx_school_major sm + LEFT JOIN (SELECT school_id,school_name,school_code FROM yx_school_child group by school_code) sc ON sc.school_code = sm.school_code + LEFT JOIN yx_school s ON s.id = sc.school_id + WHERE 1=1 AND sm.state > 0 + ` + params := []interface{}{} + + if query.MajorType != "" { + sql += " AND sm.major_type = ?" + params = append(params, query.MajorType) + } + if query.Category != "" { + sql += " AND sm.category = ?" + params = append(params, query.Category) + } + if len(query.MajorTypeChildren) > 0 { + sql += " AND sm.major_type_child IN ?" + params = append(params, query.MajorTypeChildren) + } + if query.MainSubjects != "" { + sql += " AND sm.main_subjects = ?" + params = append(params, query.MainSubjects) + } + + err := config.DB.Raw(sql, params...).Scan(&items).Error + return items, err +} diff --git a/server/modules/yx/service/yx_calculation_major_service.go b/server/modules/yx/service/yx_calculation_major_service.go index ceed489..f65ddae 100644 --- a/server/modules/yx/service/yx_calculation_major_service.go +++ b/server/modules/yx/service/yx_calculation_major_service.go @@ -2,6 +2,8 @@ package service import ( + "server/modules/user/vo" + "server/modules/yx/dto" "server/modules/yx/entity" "server/modules/yx/mapper" @@ -9,11 +11,15 @@ import ( ) type YxCalculationMajorService struct { - mapper *mapper.YxCalculationMajorMapper + historyMajorEnrollService *YxHistoryMajorEnrollService + mapper *mapper.YxCalculationMajorMapper } func NewYxCalculationMajorService() *YxCalculationMajorService { - return &YxCalculationMajorService{mapper: mapper.NewYxCalculationMajorMapper()} + return &YxCalculationMajorService{ + historyMajorEnrollService: NewYxHistoryMajorEnrollService(), + mapper: mapper.NewYxCalculationMajorMapper(), + } } func (s *YxCalculationMajorService) List(page, size int) ([]entity.YxCalculationMajor, int64, error) { @@ -72,3 +78,113 @@ func (s *YxCalculationMajorService) BatchDelete(ids []string) error { func (s *YxCalculationMajorService) DeleteByScoreID(scoreID string) error { return s.mapper.DeleteByScoreID(scoreID) } + +// 函数名 根据用户查询类型获取专业列表 +// 详细描述(可选) +// +// 参数说明: +// +// professionalCategory - 专业分类 +// cognitioPolyclinic - 文理文科 +// professionalCategoryChildren - 专业分类子项 +// +// 返回值说明: +// +// 返回值类型 - 返回值描述 +func (s *YxCalculationMajorService) ListByUserQueryType(professionalCategory string, cognitioPolyclinic string, + professionalCategoryChildren []string) ([]dto.SchoolMajorDTO, error) { + // 构造查询条件 + query := dto.SchoolMajorQuery{ + MajorType: professionalCategory, + Category: cognitioPolyclinic, + // MainSubjects: "器乐", + // MajorTypeChildren: []string{ + // "音乐教育", + // }, + } + years := []string{"2025", "2024"} + + // 执行院校查询 + majorItems, err := mapper.NewYxSchoolMajorMapper().SelectSchoolMajor(query) + if err != nil { + return nil, err + } + // 分别收集专业名称和院校代码集合(去重) + majorNameSet := make(map[string]bool) + schoolCodeSet := make(map[string]bool) + for _, dto := range majorItems { + majorNameSet[dto.MajorName] = true + schoolCodeSet[dto.SchoolCode] = true + } + // 转换为切片用于查询 + majorNames := make([]string, 0, len(majorNameSet)) + schoolCodes := make([]string, 0, len(schoolCodeSet)) + for name := range majorNameSet { + majorNames = append(majorNames, name) + } + for code := range schoolCodeSet { + schoolCodes = append(schoolCodes, code) + } + // 执行查询院校专业的历年数据 - 类似Java中的in查询 + historyItems, err := s.historyMajorEnrollService.ListBySchoolCodesAndMajorNames( + schoolCodes, + majorNames, + query.Category, // 文科/理科 + query.MajorType, // 专业类型 + years, // 年份 + ) + + if err != nil { + return nil, err + } + + // 构建历史数据映射:schoolCode_majorName_batch_year -> historyItem + allHistoryMajorEnrollMap := make(map[string]entity.YxHistoryMajorEnroll) + for _, historyItem := range historyItems { + key := historyItem.SchoolCode + "_" + historyItem.MajorName + "_" + historyItem.Batch + "_" + historyItem.Year + allHistoryMajorEnrollMap[key] = historyItem + } + // 将历史数据填充到每个专业数据中 + for i, majorItem := range majorItems { + // 为每个majorItem创建独立的历史数据映射 + historyMap := make(map[string]dto.YxHistoryMajorEnrollDTO) + + for _, year := range years { + key := majorItem.SchoolCode + "_" + majorItem.MajorName + "_" + majorItem.Batch + "_" + year + if historyItem, ok := allHistoryMajorEnrollMap[key]; ok { + // 类型转换:entity -> dto + dtoItem := dto.YxHistoryMajorEnrollDTO{ + Year: historyItem.Year, + EnrollmentCode: historyItem.EnrollmentCode, + RulesEnrollProbability: historyItem.RulesEnrollProbability, + ProbabilityOperator: historyItem.ProbabilityOperator, + AdmissionLine: historyItem.AdmissionLine, + ControlLine: historyItem.ControlLine, + // 复制其他需要的字段 + } + historyMap[year] = dtoItem + } + } + majorItems[i].HistoryMajorEnrollMap = historyMap + } + + return majorItems, nil +} + +// 函数名 给专业列表计算录取率 +// 详细描述(可选) +// +// 参数说明: +// +// schoolMajorDTOList - 专业列表 +// professionalCategory - 专业分类 +// cognitioPolyclinic - 文理文科 +// professionalCategoryChildren - 专业分类子项 +// +// 返回值说明: +// +// 返回值类型 - 返回值描述 +func (s *YxCalculationMajorService) CheckEnrollProbability(schoolMajorDTOList *[]dto.SchoolMajorDTO, userScoreVO vo.UserScoreVO) error { + + return nil +} diff --git a/server/modules/yx/service/yx_history_major_enroll_service.go b/server/modules/yx/service/yx_history_major_enroll_service.go index b6aaaf1..3b7041d 100644 --- a/server/modules/yx/service/yx_history_major_enroll_service.go +++ b/server/modules/yx/service/yx_history_major_enroll_service.go @@ -2,6 +2,7 @@ package service import ( + "server/config" "server/modules/yx/entity" "server/modules/yx/mapper" @@ -12,6 +13,46 @@ type YxHistoryMajorEnrollService struct { mapper *mapper.YxHistoryMajorEnrollMapper } +// HistoryMajorEnrollService 中的方法 +func (s *YxHistoryMajorEnrollService) ListBySchoolCodesAndMajorNames( + schoolCodes []string, + majorNames []string, + category string, + majorType string, + years []string, +) ([]entity.YxHistoryMajorEnroll, error) { + var items []entity.YxHistoryMajorEnroll + sql := ` + SELECT hme.* + FROM yx_history_major_enroll hme + WHERE 1=1 AND hme.rules_enroll_probability is not null AND hme.admission_line > 0 + ` + params := []interface{}{} + if majorType != "" { + sql += " AND hme.major_type = ?" + params = append(params, majorType) + } + if category != "" { + sql += " AND hme.category = ?" + params = append(params, category) + } + if len(schoolCodes) > 0 { + sql += " AND hme.school_code IN ?" + params = append(params, schoolCodes) + } + if len(majorNames) > 0 { + sql += " AND hme.major_name IN ?" + params = append(params, majorNames) + } + if len(years) > 0 { + sql += " AND hme.year IN ?" + params = append(params, years) + } + sql += " ORDER BY hme.year DESC" + err := config.DB.Raw(sql, params...).Scan(&items).Error + return items, err +} + func NewYxHistoryMajorEnrollService() *YxHistoryMajorEnrollService { return &YxHistoryMajorEnrollService{mapper: mapper.NewYxHistoryMajorEnrollMapper()} } diff --git a/server/modules/yx/service/yx_school_major_service.go b/server/modules/yx/service/yx_school_major_service.go index daa39e5..6d3f86d 100644 --- a/server/modules/yx/service/yx_school_major_service.go +++ b/server/modules/yx/service/yx_school_major_service.go @@ -64,21 +64,3 @@ func (s *YxSchoolMajorService) BatchUpsert(items []entity.YxSchoolMajor, updateC func (s *YxSchoolMajorService) BatchDelete(ids []string) error { return s.mapper.BatchDelete(ids) } - -// 函数名 根据用户查询类型获取专业列表 -// 详细描述(可选) -// -// 参数说明: -// -// professionalCategory - 专业分类 -// cognitioPolyclinic - 文理文科 -// -// 返回值说明: -// -// 返回值类型 - 返回值描述 -func (s *YxCalculationMajorService) ListByUserQueryType(professionalCategory string, cognitioPolyclinic string, - professionalCategoryChildren []string) (*entity.YxCalculationMajor, error) { - - // return s.mapper.FindByProfessionalCategoryAndCognitioPolyclinic(professionalCategory, cognitioPolyclinic, professionalCategoryChildren) - return &entity.YxCalculationMajor{}, nil -} diff --git a/server/tests/help.md b/server/tests/help.md new file mode 100644 index 0000000..b83867d --- /dev/null +++ b/server/tests/help.md @@ -0,0 +1,6 @@ +``` +cd server/tests +go test -v -run TestRedisOperation # 运行 Redis 测试 +go test -v -run TestSchoolMajorQuery # 运行 SQL 查询测试 +go test -v # 运行所有测试 +``` \ No newline at end of file diff --git a/server/tests/init_test.go b/server/tests/init_test.go new file mode 100644 index 0000000..59b904d --- /dev/null +++ b/server/tests/init_test.go @@ -0,0 +1,25 @@ +package tests + +import ( + "log" + "os" + "path/filepath" + "runtime" + "server/config" +) + +func init() { + // 切换工作目录到 server 根目录,以便读取配置文件(如果有) + // 这里直接初始化配置,不依赖文件,但保留目录切换逻辑以防万一 + _, filename, _, _ := runtime.Caller(0) + dir := filepath.Join(filepath.Dir(filename), "..") + err := os.Chdir(dir) + if err != nil { + log.Fatal("切换工作目录失败:", err) + } + + // 初始化数据库 + config.InitDB() + // 初始化Redis + config.InitRedis() +} diff --git a/server/tests/service_test.go b/server/tests/service_test.go new file mode 100644 index 0000000..5e4d209 --- /dev/null +++ b/server/tests/service_test.go @@ -0,0 +1,69 @@ +package tests + +import ( + "context" + "server/common" + "server/config" + "server/modules/yx/service" + "testing" + "time" +) + +// TestRedisOperation 测试 Redis 读写 +func TestRedisOperation(t *testing.T) { + ctx := context.Background() + key := "test:hello" + value := "world_" + time.Now().Format("2006-01-02 15:04:05") + + // 写 Redis + err := config.RDB.Set(ctx, key, value, time.Minute).Err() + if err != nil { + t.Fatalf("Redis 写入失败: %v", err) + } + t.Logf("Redis 写入成功: %s -> %s", key, value) + + // 读 Redis + val, err := config.RDB.Get(ctx, key).Result() + if err != nil { + t.Fatalf("Redis 读取失败: %v", err) + } + t.Logf("Redis 读取成功: %s", val) + + if val != value { + t.Errorf("Redis 值不匹配: 期望 %s, 实际 %s", value, val) + } +} + +// TestSchoolMajorQuery 测试院校专业复杂查询 Mapper +func TestSchoolMajorQuery(t *testing.T) { + cs := service.NewYxCalculationMajorService() + start := time.Now() + items, err := cs.ListByUserQueryType("美术与设计类", "文科", []string{}) + if err != nil { + t.Fatalf("查询失败: %v", err) + } + duration := time.Since(start) + t.Logf("查询耗时: %v", duration) + t.Logf("查询成功,共找到 %d 条记录", len(items)) + + // 打印前几条结果 + // for i, item := range items { + // if i >= 3 { + // break + // } + // jsonBytes, _ := json.Marshal(item) + // t.Logf("记录 #%d: %s", i+1, string(jsonBytes)) + // } +} + +// TestIDGenerator 测试 ID 生成器 +func TestIDGenerator(t *testing.T) { + idLong := common.GenerateLongID() + t.Logf("生成的 Long ID: %d", idLong) + + idStr := common.GenerateStringID() + t.Logf("生成的 String ID: %s", idStr) + + idTimestamp := common.GetIDGenerator().GenerateTimestampLongID() + t.Logf("生成的 Timestamp Long ID: %d", idTimestamp) +}