diff --git a/.iflow/settings.json b/.iflow/settings.json new file mode 100644 index 0000000..d7c5681 --- /dev/null +++ b/.iflow/settings.json @@ -0,0 +1,12 @@ +{ + "mcpServers": { + "'codebase-mcp'": { + "command": "npx", + "args": [ + "-y", + "@iflow-mcp/codebase-mcp", + "start" + ] + } + } +} \ No newline at end of file diff --git a/.iflow/IFLOW.md b/IFLOW.md similarity index 95% rename from .iflow/IFLOW.md rename to IFLOW.md index 835bf55..ea88afd 100644 --- a/.iflow/IFLOW.md +++ b/IFLOW.md @@ -55,7 +55,16 @@ 如果是从0到1开发新的项目,尽可能使用下方给出的技术栈: -### 后端 - Java(主力) +### 后端 - Go(主力) + +| 配置项 | 要求 | +| -------- | -------------------------------------- | +| 语言版本 | Go 1.21+ | +| 开发框架 | Gin | +| ORM框架 | GORM | +| 代码规范 | Google Go 编程规范 | + +### 后端 - Java | 配置项 | 要求 | | -------- | -------------------------------------- | diff --git a/project_codebase.md b/project_codebase.md deleted file mode 100644 index c746b67..0000000 --- a/project_codebase.md +++ /dev/null @@ -1,54 +0,0 @@ -# 代码库函数概览 - -## server/common -- `constants`: 存放全局常量,如 `RedisTokenPrefix`, `TokenHeader`, `StateActive` 等。 -- `id_utils`: ID 生成工具,提供 `GenerateLongID()` 和 `GenerateStringID()`。 -- `Response`: 统一的HTTP响应结构体 `{Code, Message, Data}`。 -- `Success(c *gin.Context, data interface{})`: 发送成功响应。 -- `Error(c *gin.Context, code int, msg string)`: 发送错误响应。 - -## server/config -- `LoadConfig()`: 加载配置。 - - 优先级1:命令行参数 `-c` 或 `-config` 指定的文件。 - - 优先级2:环境变量 `GO_ENV` 决定的文件(查找顺序:`./config.{env}.yaml` -> `config/config.{env}.yaml` -> `../config/config.{env}.yaml`)。 -- `AppConfig`: 全局配置对象,包含 Log, Server, Database 等配置。 -- `InitDB()`: 初始化GORM数据库连接,支持 SQL 日志配置(自动写入 `logs/sql-YYYY-MM-DD.log`)。 -- `InitRedis()`: 初始化Redis客户端。 -- `AppConfig`: 全局配置变量,包含 `Log`, `Security`, `RateLimit`, `Swagger`, `Database`, `Redis` 配置。 - -## server/middleware -- `AuthMiddleware`: JWT认证中间件。 -- `SecurityMiddleware`: 安全校验中间件(请求头签名)。 -- `RateLimitMiddleware`: 接口限流中间件。 -- `CorsMiddleware`: 跨域资源共享中间件。 - -## server/modules/yx -- `YxSchoolMajorController`: 院校专业控制器。 -- `YxHistoryMajorEnrollController`: 历年招生记录控制器。 -- `YxCalculationMajorController`: 计算专业控制器。 -- `YxUserScoreController`: 用户分数控制器。 -- `YxVolunteerController`: 志愿控制器。 -- `YxVolunteerRecordController`: 志愿明细控制器。 - -## server/modules/yx/service -- `YxCalculationMajorService`: - - `BatchCreateBySchoolMajorDTO(tableName string, items []dto.SchoolMajorDTO, scoreID string)`: 根据 SchoolMajorDTO 列表批量创建计算专业数据,支持动态表名。 - - `BatchCreate(tableName string, items []entity.YxCalculationMajor)`: 批量创建计算专业数据,支持动态表名。 - -## server/modules/user/service -- `UserScoreService`: - - `GetActiveByID(userID string)`: 获取用户当前激活状态的成绩 VO。 - - `GetByID(id string)`: 根据 ID 获取特定成绩 VO。 - - `GetActiveScoreID(userID string)`: 获取用户当前激活的成绩ID。 - - `UpdateFields(id string, fields map[string]interface{})`: 更新成绩字段。 - - `Delete(id string)`: 删除成绩记录。 - - `SaveUserScore(req *dto.SaveScoreRequest)`: 保存用户成绩,返回保存后的 VO。 - - `ListByUser(userID string, page, size int)`: 分页获取用户的成绩列表。 - - `UserScoreVO`: 用户成绩视图对象,包含基础信息、选课列表及子专业成绩映射。 - -## server/modules/yx/service -- `YxVolunteerService`: - - `UpdateName(id, name, userID string)`: 编辑志愿单名称(权属校验)。 - - `ListByUser(userID string, page, size int)`: 获取当前用户志愿单列表(关联成绩数据)。 - - `DeleteVolunteer(...)`: 删除志愿单(级联删除计算表、成绩、明细)。 - - `SwitchVolunteer(id, userID string, scoreService IScoreService)`: 切换当前志愿单(同步同步成绩状态)。 diff --git a/project_doing.md b/project_doing.md deleted file mode 100644 index 1209c72..0000000 --- a/project_doing.md +++ /dev/null @@ -1,14 +0,0 @@ - -### [任务执行] 服务端口配置化 -- **时间戳**: 2026-01-02 -- **关联任务**: 端口配置化 -- **操作目标**: 将服务启动端口从硬编码改为从配置文件读取。 -- **影响范围**: - - `server/config/config.go` - - `server/config/config.*.yaml` - - `server/main.go` -- **修改前记录**: `main.go` 中硬编码使用 `:8080`。 -- **修改结果**: - - `AppConfig` 增加 `Server.Port` 字段。 - - 所有环境配置文件增加 `server.port: 8080` 配置。 - - `main.go` 读取配置端口启动,若未配置则默认为 8080。 diff --git a/project_index.md b/project_index.md deleted file mode 100644 index 1898260..0000000 --- a/project_index.md +++ /dev/null @@ -1,38 +0,0 @@ -# 项目文件索引 - -## server/ -- `main.go`: 应用程序入口,负责路由注册和服务器启动。 -- `config/`: 配置文件目录。 - - `config.go`: 应用配置结构定义与加载逻辑。 - - `config.dev.yaml`: 开发环境配置(开启 SQL 日志)。 - - `config.test.yaml`: 测试环境配置。 - - `config.prod.yaml`: 生产环境配置。 - - `database.go`: 数据库连接配置。 - - `redis.go`: Redis连接配置。 -- `tests/`: 单元测试与集成测试目录。 - - `init_test.go`: 测试环境初始化(自动连接 DB 和 Redis)。 - - `service_test.go`: 业务逻辑与数据访问测试用例。 -- `common/`: 通用工具包。 - - `constants.go`: 全局常量定义(Redis Key、业务状态等)。 - - `id_utils.go`: ID 生成工具(基于时间戳)。 - - `response.go`: 统一HTTP响应结构。 - - `context.go`: 上下文辅助函数。 - - `logger.go`: 日志工具。 - - `password.go`: 密码加密工具。 -- `middleware/`: HTTP中间件。 - - `auth.go`: JWT认证中间件。 - - `security.go`: 安全相关的中间件。 - - `ratelimit.go`: 限流中间件。 - - `cors.go`: CORS跨域中间件。 -- `modules/`: 业务模块目录。 - - `system/`: 系统管理模块(用户、权限等)。 - - `yx/`: 艺考相关业务模块。 - - `dto/`: 数据传输对象,用于接口请求/响应定义。 - - `entity/`: 实体定义。 - - `mapper/`: 数据访问层实现。 - - `service/`: 业务逻辑层实现。 - - `controller/`: 控制层实现。 - - `user/`: 用户相关业务扩展。 - - `vo/`: 视图对象,用于数据展示。 - - `service/`: 业务逻辑层。 -- `docs/`: Swagger API文档。 diff --git a/project_task.md b/project_task.md deleted file mode 100644 index 21dc728..0000000 --- a/project_task.md +++ /dev/null @@ -1,18 +0,0 @@ -# 项目任务规划 - -## [进行中] 模块开发: yx -- [已完成] 初始化模块目录结构 (yx_user_score, yx_volunteer, yx_volunteer_record 归入 yx 模块) -- [已完成] 实现 yx_user_score 表相关代码 (Entity, Mapper, Service, Controller) -- [已完成] 实现 yx_volunteer 表相关代码 (Entity, Mapper, Service, Controller) -- [已完成] 实现 yx_volunteer_record 表相关代码 (Entity, Mapper, Service, Controller) -- [已完成] 注册新路由 -- [进行中] 环境配置与部署准备 -- [已完成] 搭建开发、测试、生产环境配置体系 -- [已完成] 配置 SQL 日志打印 -- [进行中] 编写环境配置文档 -- [已完成] 完善用户志愿管理接口 - - [已完成] 编辑志愿单名称 (权属校验) - - [已完成] 获取用户志愿单列表 (分页, 降序, 关联成绩) - - [已完成] 删除志愿单 (级联删除, 状态校验) - - [已完成] 切换当前志愿单 (状态切换, Redis 同步) - diff --git a/server/modules/user/controller/user_major_controller.go b/server/modules/user/controller/user_major_controller.go index 4443386..d0876ea 100644 --- a/server/modules/user/controller/user_major_controller.go +++ b/server/modules/user/controller/user_major_controller.go @@ -58,7 +58,7 @@ func (ctrl *UserMajorController) ListBySchool(c *gin.Context) { Probability: c.DefaultQuery("probability", ""), LoginUserId: common.GetLoginUser(c).ID, } - userScoreVO, err := ctrl.userScoreService.GetActiveByID(schoolMajorQuery.LoginUserId) + userScoreVO, err := ctrl.userScoreService.GetActiveScoreByUserID(schoolMajorQuery.LoginUserId) if err != nil { common.Error(c, 500, err.Error()) return @@ -99,11 +99,12 @@ func (ctrl *UserMajorController) List(c *gin.Context) { Probability: c.DefaultQuery("probability", ""), LoginUserId: common.GetLoginUser(c).ID, } - userScoreVO, err := ctrl.userScoreService.GetActiveByID(schoolMajorQuery.LoginUserId) + userScoreVO, err := ctrl.userScoreService.GetActiveScoreByUserID(schoolMajorQuery.LoginUserId) if err != nil { - common.Error(c, 500, err.Error()) + common.Error(c, 500, err.Error()) // 获取用户成绩信息失败 return } + schoolMajorQuery.UserScoreVO = userScoreVO items, total, probCount, err := ctrl.yxCalculationMajorService.RecommendMajorList(schoolMajorQuery) if err != nil { diff --git a/server/modules/user/controller/user_score_controller.go b/server/modules/user/controller/user_score_controller.go index 5cd6f2f..7ca0224 100644 --- a/server/modules/user/controller/user_score_controller.go +++ b/server/modules/user/controller/user_score_controller.go @@ -66,7 +66,7 @@ func (ctrl *UserScoreController) SaveUserScore(c *gin.Context) { // @Router /user/score [get] func (ctrl *UserScoreController) GetActive(c *gin.Context) { loginUserId := common.GetLoginUser(c).ID - item, err := ctrl.userScoreService.GetActiveByID(loginUserId) + item, err := ctrl.userScoreService.GetActiveScoreByUserID(loginUserId) if err != nil { common.Error(c, 404, "未找到激活成绩") return diff --git a/server/modules/user/controller/user_volunteer_controller.go b/server/modules/user/controller/user_volunteer_controller.go index 5a195f1..a075bcc 100644 --- a/server/modules/user/controller/user_volunteer_controller.go +++ b/server/modules/user/controller/user_volunteer_controller.go @@ -3,6 +3,7 @@ package controller import ( "server/common" "server/modules/user/service" + "server/modules/user/vo" yxDto "server/modules/yx/dto" "server/modules/yx/entity" yx_service "server/modules/yx/service" @@ -64,9 +65,14 @@ func (ctrl *UserVolunteerController) SaveVolunteer(c *gin.Context) { keys = uniqueKeys loginUserId := common.GetLoginUser(c).ID - userScoreVO, err := ctrl.userScoreService.GetActiveByID(loginUserId) + scoreObj, err := ctrl.userScoreService.GetActiveScoreID(loginUserId) if err != nil { - common.Error(c, 500, err.Error()) + common.Error(c, 500, err.Error()) // 获取用户成绩信息失败 + return + } + userScoreVO, ok := scoreObj.(vo.UserScoreVO) + if !ok { + common.Error(c, 500, "成绩信息格式错误") return } @@ -144,11 +150,16 @@ func (ctrl *UserVolunteerController) SaveVolunteer(c *gin.Context) { // @Router /user/volunteer/detail [get] func (ctrl *UserVolunteerController) GetVolunteerDetail(c *gin.Context) { loginUserId := common.GetLoginUser(c).ID - userScoreVO, err := ctrl.userScoreService.GetActiveByID(loginUserId) + scoreObj, err := ctrl.userScoreService.GetActiveScoreID(loginUserId) if err != nil { common.Error(c, 500, err.Error()) return } + userScoreVO, ok := scoreObj.(vo.UserScoreVO) + if !ok { + common.Error(c, 500, "成绩信息格式错误") + return + } // 查找当前激活的志愿表 volunteer, err := ctrl.yxVolunteerService.FindActiveByScoreId(userScoreVO.ID) @@ -250,9 +261,6 @@ func (ctrl *UserVolunteerController) GetVolunteerDetail(c *gin.Context) { 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, @@ -321,7 +329,7 @@ func (ctrl *UserVolunteerController) DeleteVolunteer(c *gin.Context) { } loginUserID := common.GetLoginUser(c).ID - err := ctrl.yxVolunteerService.DeleteVolunteer(id, loginUserID, ctrl.userScoreService, ctrl.yxCalculationMajorService, ctrl.yxVolunteerRecordService) + err := ctrl.yxVolunteerService.DeleteVolunteer(id, loginUserID) if err != nil { common.Error(c, 500, err.Error()) return @@ -345,7 +353,17 @@ func (ctrl *UserVolunteerController) SwitchVolunteer(c *gin.Context) { loginUserID := common.GetLoginUser(c).ID // 1. 先判断是否已是该志愿单 (从 cache 或 db 中查找激活的) - userScoreVO, _ := ctrl.userScoreService.GetActiveByID(loginUserID) + scoreObj, err := ctrl.userScoreService.GetActiveScoreID(loginUserID) + if err != nil { + common.Error(c, 500, "获取用户成绩信息失败: "+err.Error()) + return + } + userScoreVO, ok := scoreObj.(vo.UserScoreVO) + if !ok { + common.Error(c, 500, "成绩信息格式错误") + return + } + if userScoreVO.ID != "" { activeVolunteer, _ := ctrl.yxVolunteerService.FindActiveByScoreId(userScoreVO.ID) if activeVolunteer != nil && activeVolunteer.ID == id { diff --git a/server/modules/user/service/user_score_service.go b/server/modules/user/service/user_score_service.go index 6882a36..7a04bb2 100644 --- a/server/modules/user/service/user_score_service.go +++ b/server/modules/user/service/user_score_service.go @@ -4,6 +4,7 @@ package service import ( "context" "encoding/json" + "errors" "fmt" "server/common" "server/config" @@ -14,6 +15,8 @@ import ( "server/modules/yx/service" "strings" "time" + + "gorm.io/gorm" ) type UserScoreService struct { @@ -23,24 +26,63 @@ type UserScoreService struct { mapper *mapper.YxUserScoreMapper } -func (s *UserScoreService) GetUserScoreByUserId(id string) { - panic("unimplemented") -} - -func (s *UserScoreService) GetActiveScoreID(userID string) string { - obj, err := s.GetActiveByID(userID) +// GetActiveScoreID 获取用户的激活成绩信息 +func (s *UserScoreService) GetActiveScoreID(userID string) (interface{}, error) { + vo, err := s.GetActiveScoreByUserID(userID) if err != nil { - return "" + return nil, err } - if voItem, ok := obj.(vo.UserScoreVO); ok { - return voItem.ID - } - return "" + return vo, nil } -func (s *UserScoreService) GetActiveByID(userID string) (interface{}, error) { +// GetActiveScoreByID 获取用户的激活成绩实体(实现 IScoreService 接口) +func (s *UserScoreService) GetActiveScoreByID(userID string) (entity.YxUserScore, error) { var score entity.YxUserScore - // ... (logic remains same) + // 明确指定字段,提高可读性 + err := config.DB.Model(&entity.YxUserScore{}). + Where("create_by = ? AND state = ?", userID, "1"). + First(&score).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return entity.YxUserScore{}, fmt.Errorf("未找到激活的成绩记录") + } + return entity.YxUserScore{}, fmt.Errorf("查询成绩记录失败: %w", err) + } + return score, nil +} + +func (s *UserScoreService) GetActiveScoreByUserID(userID string) (vo.UserScoreVO, error) { + var score entity.YxUserScore + // 先从Redis获取是否存在 + scoreRedisData, err := config.RDB.Get(context.Background(), common.RedisUserScorePrefix+userID).Result() + if err != nil { + // 明确指定字段,提高可读性 + err := config.DB.Model(&entity.YxUserScore{}). + Where("create_by = ? AND state = ?", userID, "1"). + First(&score).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return vo.UserScoreVO{}, fmt.Errorf("未找到激活的成绩记录") + } + return vo.UserScoreVO{}, fmt.Errorf("查询成绩记录失败: %w", err) + } + // 缓存到 Redis + scoreRedisSetData, err := json.Marshal(score) + if err != nil { + return vo.UserScoreVO{}, fmt.Errorf("序列化成绩记录失败: %w", err) + } + err = config.RDB.Set(context.Background(), common.RedisUserScorePrefix+userID, scoreRedisSetData, common.RedisUserScoreExpire).Err() + if err != nil { + return vo.UserScoreVO{}, fmt.Errorf("缓存成绩记录失败: %w", err) + } + } else { + if err := json.Unmarshal([]byte(scoreRedisData), &score); err != nil { + return vo.UserScoreVO{}, fmt.Errorf("解析 Redis 数据失败: %w", err) + } + // 刷新过期时间 + config.RDB.Expire(context.Background(), common.RedisUserScorePrefix+userID, common.RedisUserScoreExpire) + } + return s.convertEntityToVo(score), nil } diff --git a/server/modules/yx/mapper/yx_volunteer_record_mapper.go b/server/modules/yx/mapper/yx_volunteer_record_mapper.go index fe5bf2d..486bbe0 100644 --- a/server/modules/yx/mapper/yx_volunteer_record_mapper.go +++ b/server/modules/yx/mapper/yx_volunteer_record_mapper.go @@ -10,6 +10,10 @@ import ( type YxVolunteerRecordMapper struct{} +func (m *YxVolunteerRecordMapper) BatchDeleteByVolunteerId(id string) { + config.DB.Delete(&entity.YxVolunteerRecord{}, "volunteer_id = ?", id) +} + func NewYxVolunteerRecordMapper() *YxVolunteerRecordMapper { return &YxVolunteerRecordMapper{} } diff --git a/server/modules/yx/service/yx_volunteer_service.go b/server/modules/yx/service/yx_volunteer_service.go index a830d45..fc38f6c 100644 --- a/server/modules/yx/service/yx_volunteer_service.go +++ b/server/modules/yx/service/yx_volunteer_service.go @@ -13,18 +13,19 @@ import ( // IScoreService 定义成绩服务的接口,用于解耦 type IScoreService interface { GetByID(id string) (interface{}, error) - GetActiveByID(userID string) (interface{}, error) - GetActiveScoreID(userID string) string + GetActiveScoreByID(userID string) (entity.YxUserScore, error) + GetActiveScoreID(userID string) (interface{}, error) UpdateFields(id string, fields map[string]interface{}) error Delete(id string) error } type YxVolunteerService struct { - mapper *mapper.YxVolunteerMapper + mapper *mapper.YxVolunteerMapper + volunteerRecordMapper *mapper.YxVolunteerRecordMapper } func NewYxVolunteerService() *YxVolunteerService { - return &YxVolunteerService{mapper: mapper.NewYxVolunteerMapper()} + return &YxVolunteerService{mapper: mapper.NewYxVolunteerMapper(), volunteerRecordMapper: mapper.NewYxVolunteerRecordMapper()} } func (s *YxVolunteerService) List(page, size int) ([]entity.YxVolunteer, int64, error) { @@ -116,7 +117,7 @@ func (s *YxVolunteerService) ListByUser(userID string, page, size int) ([]dto.Us return s.mapper.ListByUser(userID, page, size) } -func (s *YxVolunteerService) DeleteVolunteer(id, userID string, scoreService IScoreService, calculationService *YxCalculationMajorService, recordService *YxVolunteerRecordService) error { +func (s *YxVolunteerService) DeleteVolunteer(id, userID string) error { volunteer, err := s.mapper.FindByID(id) if err != nil { return err @@ -127,13 +128,14 @@ func (s *YxVolunteerService) DeleteVolunteer(id, userID string, scoreService ISc if volunteer.State == "1" { return fmt.Errorf("激活状态的志愿单不可删除") } - + s.volunteerRecordMapper.BatchDeleteByVolunteerId(id) + s.mapper.Delete(id) // 1. 获取成绩单信息 - scoreObj, err := scoreService.GetByID(volunteer.ScoreId) - if err != nil { - return fmt.Errorf("获取成绩单失败: %w", err) - } - _ = scoreObj // temporarily ignore until cascade logic is refined + //scoreObj, err := scoreService.GetByID(volunteer.ScoreId) + // if err != nil { + // return fmt.Errorf("获取成绩单失败: %w", err) + // } + // _ = scoreObj // temporarily ignore until cascade logic is refined // 这里需要从 scoreObj 中获取 CalculationTableName 和 ID // 由于是 interface{},可以通过反射或简单断言(如果能在 yx 定义共用结构最好) @@ -141,7 +143,7 @@ func (s *YxVolunteerService) DeleteVolunteer(id, userID string, scoreService ISc // 简化:这里我们假设 scoreObj 其实包含了这些信息。 // 为了真正的解耦,我们可以在 scoreService 中增加专门的方法。 - return fmt.Errorf("暂不支持级联删除,请先手动处理关联数据") // 暂时回退复杂逻辑 + return nil // 暂时回退复杂逻辑 } func (s *YxVolunteerService) SwitchVolunteer(id, userID string, scoreService IScoreService) error { @@ -158,7 +160,21 @@ func (s *YxVolunteerService) SwitchVolunteer(id, userID string, scoreService ISc } // 获取之前的激活成绩并关闭 - activeScoreID := scoreService.GetActiveScoreID(userID) + scoreObj, err := scoreService.GetActiveScoreID(userID) + if err != nil { + return fmt.Errorf("获取激活成绩失败: %w", err) + } + var activeScoreID string + if scoreObj != nil { + // 尝试从 VO 对象中提取 ID + if vo, ok := scoreObj.(map[string]interface{}); ok { + if id, ok := vo["id"].(string); ok { + activeScoreID = id + } + } else if vo, ok := scoreObj.(struct{ ID string }); ok { + activeScoreID = vo.ID + } + } if activeScoreID != "" && activeScoreID != volunteer.ScoreId { scoreService.UpdateFields(activeScoreID, map[string]interface{}{"state": "0"}) } diff --git a/task_detail.md b/task_detail.md deleted file mode 100644 index 6150d72..0000000 --- a/task_detail.md +++ /dev/null @@ -1,8 +0,0 @@ - -## 会话 ID: 20260102-03 -- **执行原因**: 用户希望将服务端口号配置到配置文件中,避免硬编码。 -- **执行过程**: - 1. 修改 `server/config/config.go`,在 `AppConfig` 中添加 `ServerConfig` 结构体。 - 2. 更新 `config.dev.yaml`、`config.prod.yaml` 和 `config.test.yaml`,添加 `server: port: 8080` 配置。 - 3. 修改 `server/main.go`,使其从 `config.AppConfig.Server.Port` 读取端口号,并保留默认值 8080。 -- **执行结果**: 服务现在会使用配置文件中指定的端口启动。