diff --git a/project_codebase.md b/project_codebase.md index 0beac28..f8367de 100644 --- a/project_codebase.md +++ b/project_codebase.md @@ -1,6 +1,7 @@ # 代码库函数概览 ## server/common +- `constants`: 存放全局常量,如 `RedisTokenPrefix`, `TokenHeader`, `StateActive` 等。 - `Response`: 统一的HTTP响应结构体 `{Code, Message, Data}`。 - `Success(c *gin.Context, data interface{})`: 发送成功响应。 - `Error(c *gin.Context, code int, msg string)`: 发送错误响应。 @@ -25,7 +26,8 @@ - `YxVolunteerRecordController`: 志愿明细控制器。 ## server/modules/user -- `UserScoreService`: 用户成绩服务。 - - `GetActiveByID(userID string)`: 获取用户当前激活的成绩,返回 `UserScoreVO`。 - - `SaveUserScore(req *dto.SaveScoreRequest)`: 保存用户成绩,处理旧记录状态更新及 DTO 转换。 -- `UserScoreVO`: 用户成绩视图对象,包含基础信息、选课列表及子专业成绩映射。 + - `UserScoreService`: 用户成绩服务。 + - `GetActiveByID(userID string)`: 获取用户当前激活的成绩,返回 `UserScoreVO`。 + - `ListByUser(userID string, page, size int)`: 获取用户的所有成绩分页列表。 + - `SaveUserScore(req *dto.SaveScoreRequest)`: 保存用户成绩,处理旧记录状态更新及 DTO 转换。 + - `UserScoreVO`: 用户成绩视图对象,包含基础信息、选课列表及子专业成绩映射。 diff --git a/project_doing.md b/project_doing.md index 5ef466b..e4a0a3b 100644 --- a/project_doing.md +++ b/project_doing.md @@ -55,4 +55,22 @@ - **修改结果**: - 定义了 `UserScoreVO` 结构体,其字段设计参考了 `SaveScoreRequest`。 - 在 `UserScoreService` 中实现了 `convertEntityToVo` 私有方法,处理了逗号分隔字符串到切片的转换,以及具体分数字段到 Map 的映射。 - - 更新 `GetActiveByID` 返回 `UserScoreVO` 对象。 + - 更新 `GetActiveByID` 返回 `UserScoreVO` 对象。 + +### [任务执行] 增加用户成绩分页列表接口 +- **操作目标**: 在 `UserScoreController` 和 `UserScoreService` 中增加获取当前用户所有成绩的分页列表接口。 +- **影响范围**: `server/modules/user/service/user_score_service.go`, `server/modules/user/controller/user_score_controller.go` +- **修改前记录**: 仅支持获取当前激活的单条成绩。 +- **修改结果**: + - 在 `UserScoreService` 中实现了 `ListByUser` 方法,支持分页查询并返回 VO 列表。 + - 在 `UserScoreController` 中增加了 `List` 方法,并注册了 `GET /user/score/list` 路由。 + +### [任务执行] 统一管理 Redis Key 及全局常量 +- **操作目标**: 创建专门存放 Redis Key 和业务常量的地方,并重构相关代码以引用这些常量。 +- **影响范围**: `server/common/constants.go`, `server/modules/system/service/sys_user_service.go`, `server/middleware/auth.go`, `server/common/context.go` +- **修改前记录**: 常量散落在各个业务文件和中间件中,存在硬编码和重复定义。 +- **修改结果**: + - 创建了 `server/common/constants.go`,集中管理 Redis 前缀、Token 请求头、业务状态码等。 + - 重构了 `SysUserService`,使用 `common.RedisTokenPrefix` 和 `common.RedisTokenExpire`。 + - 重构了 `AuthMiddleware`,使用 `common.TokenHeader` 和 `common.ContextUserKey`。 + - 清理了 `common/context.go` 中的重复定义。 diff --git a/project_index.md b/project_index.md index 9e8f4ff..e119213 100644 --- a/project_index.md +++ b/project_index.md @@ -7,6 +7,7 @@ - `database.go`: 数据库连接配置。 - `redis.go`: Redis连接配置。 - `common/`: 通用工具包。 + - `constants.go`: 全局常量定义(Redis Key、业务状态等)。 - `response.go`: 统一HTTP响应结构。 - `context.go`: 上下文辅助函数。 - `logger.go`: 日志工具。 diff --git a/server/common/constants.go b/server/common/constants.go new file mode 100644 index 0000000..2e90f2f --- /dev/null +++ b/server/common/constants.go @@ -0,0 +1,40 @@ +package common + +import "time" + +// Redis 相关常量 +const ( + // RedisTokenPrefix Redis中Token前缀 + RedisTokenPrefix = "login:token:" + // RedisTokenExpire Token过期时间 + RedisTokenExpire = 24 * time.Hour + + // RedisUserScorePrefix Redis中用户成绩前缀 + RedisUserScorePrefix = "user:score:" + // RedisUserScoreExpire 用户成绩过期时间 + RedisUserScoreExpire = 8 * time.Hour +) + +// HTTP/Context 相关常量 +const ( + // ContextUserKey 上下文中存储用户信息的key + ContextUserKey = "loginUser" + // TokenHeader 请求头中Token的key + // "X-Access-Token" + TokenHeader = "Authorization" + // HeaderTokenPrefix Token前缀 (如有需要) + HeaderTokenPrefix = "" +) + +// 业务状态常量 +const ( + StateActive = "1" // 使用中 + StateInactive = "0" // 未使用/已删除 + StateHistory = "2" // 历史记录 +) + +// 数据类型常量 +const ( + TypeNormal = "1" // 普通类 + TypeArt = "2" // 艺术类 +) diff --git a/server/common/context.go b/server/common/context.go index 8301629..74258af 100644 --- a/server/common/context.go +++ b/server/common/context.go @@ -7,8 +7,6 @@ import ( "github.com/gin-gonic/gin" ) -const ContextUserKey = "loginUser" - // GetLoginUser 从上下文获取当前登录用户 // 在Controller中使用: user := common.GetLoginUser(c) func GetLoginUser(c *gin.Context) *entity.LoginUser { diff --git a/server/middleware/auth.go b/server/middleware/auth.go index 7a0ee34..83259d0 100644 --- a/server/middleware/auth.go +++ b/server/middleware/auth.go @@ -10,15 +10,6 @@ import ( "github.com/gin-gonic/gin" ) -const ( - // ContextUserKey 上下文中存储用户信息的key - ContextUserKey = "loginUser" - // TokenHeader 请求头中Token的key - TokenHeader = "Authorization" - // TokenPrefix Token前缀 - TokenPrefix = "Bearer " -) - // 白名单路径 (不需要登录即可访问) var whiteList = []string{ "/api/sys/auth/login", @@ -46,29 +37,28 @@ func AuthMiddleware() gin.HandlerFunc { } // 获取Token - token := c.GetHeader(TokenHeader) + token := c.GetHeader(common.TokenHeader) if token == "" { common.Error(c, 401, "未登录") c.Abort() return } - // 去除Bearer前缀 - token = strings.TrimPrefix(token, TokenPrefix) - // if strings.HasPrefix(token, TokenPrefix) { - // token = token[len(TokenPrefix):] - // } + // 如果有前缀则处理前缀 + if common.HeaderTokenPrefix != "" && strings.HasPrefix(token, common.HeaderTokenPrefix) { + token = token[len(common.HeaderTokenPrefix):] + } // 验证Token并获取用户信息 loginUser, err := userService.GetLoginUser(token) if err != nil { - common.Error(c, 401, err.Error()) + common.Error(c, 401, "登录已失效,请重新登录") c.Abort() return } - // 将用户信息存入上下文 - c.Set(ContextUserKey, loginUser) + // 存入上下文 + c.Set(common.ContextUserKey, loginUser) c.Next() } diff --git a/server/modules/system/service/sys_user_service.go b/server/modules/system/service/sys_user_service.go index f0de896..52da083 100644 --- a/server/modules/system/service/sys_user_service.go +++ b/server/modules/system/service/sys_user_service.go @@ -17,11 +17,6 @@ import ( "github.com/google/uuid" ) -const ( - TokenPrefix = "login:token:" // Redis中Token前缀 - TokenExpire = 24 * time.Hour // Token过期时间 -) - type SysUserService struct { mapper *mapper.SysUserMapper } @@ -117,13 +112,13 @@ func (s *SysUserService) SysLogin(username, password string) (*entity.LoginUser, // Logout 用户登出 func (s *SysUserService) Logout(token string) error { ctx := context.Background() - return config.RDB.Del(ctx, TokenPrefix+token).Err() + return config.RDB.Del(ctx, common.RedisTokenPrefix+token).Err() } // GetLoginUser 根据Token获取登录用户信息 func (s *SysUserService) GetLoginUser(token string) (*entity.LoginUser, error) { ctx := context.Background() - data, err := config.RDB.Get(ctx, TokenPrefix+token).Result() + data, err := config.RDB.Get(ctx, common.RedisTokenPrefix+token).Result() if err != nil { return nil, errors.New("未登录或登录已过期") } @@ -134,7 +129,7 @@ func (s *SysUserService) GetLoginUser(token string) (*entity.LoginUser, error) { } // 刷新过期时间 - config.RDB.Expire(ctx, TokenPrefix+token, TokenExpire) + config.RDB.Expire(ctx, common.RedisTokenPrefix+token, common.RedisTokenExpire) return &loginUser, nil } @@ -142,7 +137,7 @@ func (s *SysUserService) GetLoginUser(token string) (*entity.LoginUser, error) { // RefreshToken 刷新Token过期时间 func (s *SysUserService) RefreshToken(token string) error { ctx := context.Background() - return config.RDB.Expire(ctx, TokenPrefix+token, TokenExpire).Err() + return config.RDB.Expire(ctx, common.RedisTokenPrefix+token, common.RedisTokenExpire).Err() } // 保存登录用户到Redis @@ -152,7 +147,7 @@ func (s *SysUserService) saveLoginUser(token string, user *entity.LoginUser) err if err != nil { return err } - return config.RDB.Set(ctx, TokenPrefix+token, data, TokenExpire).Err() + return config.RDB.Set(ctx, common.RedisTokenPrefix+token, data, common.RedisTokenExpire).Err() } // 生成Token diff --git a/server/modules/user/controller/user_score_controller.go b/server/modules/user/controller/user_score_controller.go index 14979d1..4ec485e 100644 --- a/server/modules/user/controller/user_score_controller.go +++ b/server/modules/user/controller/user_score_controller.go @@ -5,6 +5,7 @@ import ( user_service "server/modules/user/service" "server/modules/yx/dto" yx_service "server/modules/yx/service" + "strconv" "github.com/gin-gonic/gin" ) @@ -27,6 +28,7 @@ func (ctrl *UserScoreController) RegisterRoutes(rg *gin.RouterGroup) { { // group.GET("/:id", ctrl.Get) group.GET("/", ctrl.Get) + group.GET("/list", ctrl.List) // 新增分页列表接口 group.POST("/save-score", ctrl.SaveUserScore) // 新增接口 } } @@ -71,3 +73,25 @@ func (ctrl *UserScoreController) Get(c *gin.Context) { } common.Success(c, item) } + +// List 获取当前用户的成绩列表 +// @Summary 获取当前用户的成绩列表 +// @Tags 用户分数 +// @Param page query int false "页码" default(1) +// @Param size query int false "每页数量" default(10) +// @Success 200 {object} common.Response +// @Router /user/score/list [get] +func (ctrl *UserScoreController) List(c *gin.Context) { + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + size, _ := strconv.Atoi(c.DefaultQuery("size", "10")) + loginUserId := common.GetLoginUser(c).ID + items, total, err := ctrl.userScoreService.ListByUser(loginUserId, page, size) + if err != nil { + common.Error(c, 500, err.Error()) + return + } + common.Success(c, gin.H{ + "items": items, + "total": total, + }) +} diff --git a/server/modules/user/service/user_score_service.go b/server/modules/user/service/user_score_service.go index a32e431..a300ec7 100644 --- a/server/modules/user/service/user_score_service.go +++ b/server/modules/user/service/user_score_service.go @@ -11,6 +11,7 @@ import ( "server/modules/yx/mapper" "server/modules/yx/service" "strings" + "time" "gorm.io/gorm" ) @@ -41,6 +42,29 @@ func (s *UserScoreService) GetActiveByID(userID string) (*vo.UserScoreVO, error) return scoreVO, nil } +// ListByUser 获取用户的成绩分页列表 +func (s *UserScoreService) ListByUser(userID string, page, size int) ([]*vo.UserScoreVO, int64, error) { + var scores []entity.YxUserScore + var total int64 + + query := config.DB.Model(&entity.YxUserScore{}).Where("create_by = ?", userID) + + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("查询总数失败: %w", err) + } + + if err := query.Offset((page - 1) * size).Limit(size).Order("create_time desc").Find(&scores).Error; err != nil { + return nil, 0, fmt.Errorf("查询成绩列表失败: %w", err) + } + + vos := make([]*vo.UserScoreVO, 0, len(scores)) + for i := range scores { + vos = append(vos, s.convertEntityToVo(&scores[i])) + } + + return vos, total, nil +} + func NewUserScoreService() *UserScoreService { return &UserScoreService{ yxUserScoreService: service.NewYxUserScoreService(), @@ -56,7 +80,8 @@ func (s *UserScoreService) SaveUserScore(req *dto.SaveScoreRequest) (*entity.YxU } // 2. DTO 转 Entity entityItem := s.convertDtoToEntity(req) - + entityItem.CreateTime = time.Now() + entityItem.UpdateTime = time.Now() // 3. 执行保存操作(可以包含事务) tx := config.DB.Begin() defer func() { @@ -67,7 +92,7 @@ func (s *UserScoreService) SaveUserScore(req *dto.SaveScoreRequest) (*entity.YxU // 标记该用户的所有旧成绩为历史状态 if err := config.DB.Model(&entity.YxUserScore{}). - Where("user_id = ? AND state = ?", entityItem.CreateBy, "1"). + Where("create_by = ? AND state = ?", req.CreateBy, "1"). Updates(map[string]interface{}{"state": "2"}).Error; err != nil { tx.Rollback() return nil, fmt.Errorf("更新旧记录失败: %w", err) diff --git a/server/modules/yx/dto/yx_user_score_dto.go b/server/modules/yx/dto/yx_user_score_dto.go index 89491f1..1895a00 100644 --- a/server/modules/yx/dto/yx_user_score_dto.go +++ b/server/modules/yx/dto/yx_user_score_dto.go @@ -23,9 +23,19 @@ func (req *SaveScoreRequest) Validate() error { if req.CognitioPolyclinic != "文科" && req.CognitioPolyclinic != "理科" { return errors.New("考试类型必须是'物理组'或'历史组'") } - if len(req.SubjectList) != 2 { - return errors.New("选考科目有且只能传两个值") + + if req.ProfessionalCategory == "表演类" || req.ProfessionalCategory == "音乐类" { + if len(req.ProfessionalCategoryChildren) == 0 { + return errors.New("表演类或音乐类必须至少选一个专业子级") + } + } else { + req.ProfessionalCategoryChildren = []string{} } + + if len(req.SubjectList) > 3 { + return errors.New("选考科目有且最多只能传三个值") + } + validSubjects := map[string]bool{"地理": true, "政治": true, "历史": true, "化学": true, "生物": true} for _, s := range req.SubjectList { if !validSubjects[s] { @@ -33,16 +43,16 @@ func (req *SaveScoreRequest) Validate() error { } } if !isValidScore(*req.ProfessionalScore, 300) { - return errors.New("ProfessionalScore必须在0-300之间") + return errors.New("统考成绩必须在0-300之间") } - if !isValidScore(*req.CulturalScore, 300) { - return errors.New("CulturalScore必须在0-300之间") + if !isValidScore(*req.CulturalScore, 750) { + return errors.New("文化成绩必须在0-750之间") } if !isValidScore(*req.EnglishScore, 150) { - return errors.New("EnglishScore必须在0-150之间") + return errors.New("英文成绩必须在0-150之间") } if !isValidScore(*req.ChineseScore, 150) { - return errors.New("ChineseScore必须在0-150之间") + return errors.New("中文成绩必须在0-150之间") } // TODO 在这里判断一下 专业子级,如 表演类只有:"服装表演", "戏剧影视表演", "戏剧影视导演"。音乐类只有:音乐表演声乐、音乐表演器乐、音乐教育。 validProfessionalChildren := map[string]string{ // 子级 -> 父级分类 @@ -79,6 +89,51 @@ func (req *SaveScoreRequest) Validate() error { if count > 3 { // 假设每类最多3个 return errors.New(category + "最多只能有3个子级") } + for _, key := range req.ProfessionalCategoryChildren { + if req.ProfessionalCategoryChildrenScore[key] == 0 { + return errors.New(key + "成绩不能为空") + } + } + } + + // 专业子级和成绩数据一致性校验 + if err := req.validateProfessionalConsistency(); err != nil { + return err + } + + return nil +} + +func (req *SaveScoreRequest) validateProfessionalConsistency() error { + // 特殊情况:如果不需要专业子级,则两个都应该为空 + if len(req.ProfessionalCategoryChildren) == 0 { + if len(req.ProfessionalCategoryChildrenScore) != 0 { + return errors.New("未选择专业子级时,成绩数据应为空") + } + return nil + } + // 创建映射用于双向验证 + childrenMap := make(map[string]bool) + for _, child := range req.ProfessionalCategoryChildren { + if childrenMap[child] { + return errors.New("专业子级 '" + child + "' 重复") + } + childrenMap[child] = true + } + // 验证长度一致 + if len(childrenMap) != len(req.ProfessionalCategoryChildrenScore) { + return errors.New("专业子级列表与成绩数据数量不匹配") + } + // 双向验证一致性 + for childName := range req.ProfessionalCategoryChildrenScore { + if !childrenMap[childName] { + return errors.New("成绩数据中的专业子级 '" + childName + "' 未在选中列表中") + } + } + for _, child := range req.ProfessionalCategoryChildren { + if _, exists := req.ProfessionalCategoryChildrenScore[child]; !exists { + return errors.New("选中的专业子级 '" + child + "' 缺少成绩数据") + } } return nil }