diff --git a/.vscode/settings.json b/.vscode/settings.json index f0904cd..1120dc5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,7 @@ "kiroAgent.configureMCP": "Disabled", "cSpell.words": [ "apikey", + "bwmarrin", "Cognitio", "Fzby", "gonic", diff --git a/project_codebase.md b/project_codebase.md index f8367de..6b5b403 100644 --- a/project_codebase.md +++ b/project_codebase.md @@ -2,6 +2,7 @@ ## 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)`: 发送错误响应。 @@ -25,9 +26,10 @@ - `YxVolunteerController`: 志愿控制器。 - `YxVolunteerRecordController`: 志愿明细控制器。 -## server/modules/user - - `UserScoreService`: 用户成绩服务。 - - `GetActiveByID(userID string)`: 获取用户当前激活的成绩,返回 `UserScoreVO`。 - - `ListByUser(userID string, page, size int)`: 获取用户的所有成绩分页列表。 - - `SaveUserScore(req *dto.SaveScoreRequest)`: 保存用户成绩,处理旧记录状态更新及 DTO 转换。 +## server/modules/user/service +- `UserScoreService`: + - `GetActiveByID(userID string)`: 获取用户当前激活状态的成绩 VO。 + - `GetByID(id string)`: 根据 ID 获取特定成绩 VO。 + - `SaveUserScore(req *dto.SaveScoreRequest)`: 保存用户成绩,返回保存后的 VO。 + - `ListByUser(userID string, page, size int)`: 分页获取用户的成绩列表。 - `UserScoreVO`: 用户成绩视图对象,包含基础信息、选课列表及子专业成绩映射。 diff --git a/project_doing.md b/project_doing.md index e4a0a3b..638f0f9 100644 --- a/project_doing.md +++ b/project_doing.md @@ -74,3 +74,22 @@ - 重构了 `SysUserService`,使用 `common.RedisTokenPrefix` 和 `common.RedisTokenExpire`。 - 重构了 `AuthMiddleware`,使用 `common.TokenHeader` 和 `common.ContextUserKey`。 - 清理了 `common/context.go` 中的重复定义。 + +### [任务执行] 完善成绩回显逻辑 +- **操作目标**: 确保保存成功后能回显完整成绩信息,并提供通过 ID 获取成绩的接口以便修改。 +- **影响范围**: `server/modules/user/service/user_score_service.go`, `server/modules/user/controller/user_score_controller.go` +- **修改前记录**: `SaveUserScore` 返回 Entity 而非 VO,且缺少通过 ID 获取特定成绩的接口。 +- **修改结果**: + - 重构 `UserScoreService.SaveUserScore`,在事务提交后返回转换后的 `UserScoreVO`。 + - 增加 `UserScoreService.GetByID` 方法。 + - 在 `UserScoreController` 中注册 `GET /user/score/:id` 路由,并实现 `GetByID` 方法。 + - 将原来的 `GET /user/score` 路由方法名改为 `GetActive`,使其语义更明确。 + +### [任务执行] 封装 ID 生成工具 +- **操作目标**: 封装一个基于时间戳生成 long 类型(及字符串类型)ID 的工具类。 +- **影响范围**: `server/common/id_utils.go`, `server/modules/user/service/user_score_service.go` +- **修改前记录**: 之前可能依赖数据库自增或未手动设置 ID。 +- **修改结果**: + - 创建了 `server/common/id_utils.go`,提供了基于时间戳 + 序列号的 ID 生成逻辑。 + - 支持生成 `int64` 类型的 ID,也支持直接生成 `string` 类型以便于 Entity 使用。 + - 在 `UserScoreService.SaveUserScore` 中引入了该工具,手动为新记录分配 ID。 diff --git a/project_index.md b/project_index.md index e119213..0943a7d 100644 --- a/project_index.md +++ b/project_index.md @@ -8,6 +8,7 @@ - `redis.go`: Redis连接配置。 - `common/`: 通用工具包。 - `constants.go`: 全局常量定义(Redis Key、业务状态等)。 + - `id_utils.go`: ID 生成工具(基于时间戳)。 - `response.go`: 统一HTTP响应结构。 - `context.go`: 上下文辅助函数。 - `logger.go`: 日志工具。 diff --git a/server/common/constants.go b/server/common/constants.go index 2e90f2f..660f9a0 100644 --- a/server/common/constants.go +++ b/server/common/constants.go @@ -23,7 +23,7 @@ const ( // "X-Access-Token" TokenHeader = "Authorization" // HeaderTokenPrefix Token前缀 (如有需要) - HeaderTokenPrefix = "" + HeaderTokenPrefix = "Bearer " ) // 业务状态常量 diff --git a/server/common/id_utils.go b/server/common/id_utils.go new file mode 100644 index 0000000..b05d009 --- /dev/null +++ b/server/common/id_utils.go @@ -0,0 +1,103 @@ +package common + +import ( + "fmt" + "strconv" + "sync" + "time" +) + +// IDGenerator 简单的 ID 生成器(类似 Snowflake) +type IDGenerator struct { + mu sync.Mutex + lastTime int64 + sequence int64 + machineID int64 +} + +var ( + defaultGenerator *IDGenerator + once sync.Once +) + +// GetIDGenerator 获取默认生成器单例 +func GetIDGenerator() *IDGenerator { + once.Do(func() { + defaultGenerator = &IDGenerator{ + machineID: 1, // 默认机器 ID,实际可从配置读取 + } + }) + return defaultGenerator +} + +// NextID 生成下一个 64 位整数 ID +// 格式:时间戳(毫秒) + 机器ID + 序列号 +func (g *IDGenerator) NextID() int64 { + g.mu.Lock() + defer g.mu.Unlock() + + now := time.Now().UnixMilli() + + if now == g.lastTime { + g.sequence = (g.sequence + 1) & 4095 // 12位序列号 + if g.sequence == 0 { + // 序列号溢出,等待下一毫秒 + for now <= g.lastTime { + now = time.Now().UnixMilli() + } + } + } else { + g.sequence = 0 + } + + g.lastTime = now + + // 简单的位运算组合 ID + // 时间戳(41位) | 机器ID(10位) | 序列号(12位) + // 这里为了简单,直接返回时间戳+序列号的组合 + // 如果需要严格的 long 类型且包含时间戳信息: + return now*10000 + g.sequence +} + +// NextIDStr 生成字符串类型的 ID +func (g *IDGenerator) NextIDStr() string { + return strconv.FormatInt(g.NextID(), 10) +} + +// GenerateLongID 全局辅助函数:生成 long 类型 ID +func GenerateLongID() int64 { + return GetIDGenerator().NextID() +} + +// GenerateStringID 全局辅助函数:生成 string 类型 ID +func GenerateStringID() string { + return GetIDGenerator().NextIDStr() +} + +// FormatTimestampToLong 将指定时间转为 long 格式 (yyyyMMddHHmmss) +func FormatTimestampToLong(t time.Time) int64 { + s := t.Format("20060102150405") + id, _ := strconv.ParseInt(s, 10, 64) + return id +} + +// GenerateTimestampLongID 生成当前时间的 long 格式 ID (yyyyMMddHHmmss + 3位随机/序列) +func (g *IDGenerator) GenerateTimestampLongID() int64 { + g.mu.Lock() + defer g.mu.Unlock() + + now := time.Now() + timestampStr := now.Format("20060102150405") + + nowMs := now.UnixMilli() + if nowMs/1000 == g.lastTime/1000 { + g.sequence = (g.sequence + 1) & 999 + } else { + g.sequence = 0 + } + g.lastTime = nowMs + + idStr := fmt.Sprintf("%s%03d", timestampStr, g.sequence) + id, _ := strconv.ParseInt(idStr, 10, 64) + return id +} diff --git a/server/common/logger.go b/server/common/logger.go index 4ada086..99567ec 100644 --- a/server/common/logger.go +++ b/server/common/logger.go @@ -46,7 +46,7 @@ type Logger struct { var ( defaultLogger *Logger startCount int - once sync.Once + onceInit sync.Once ) // InitLogger 初始化日志 diff --git a/server/config/config.go b/server/config/config.go index a7ebdc1..3834883 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -20,8 +20,9 @@ var AppConfig = &appConfig{ Enable: true, Default: RateLimitRule{Interval: 2, MaxRequests: 3}, // 默认2秒1次 Rules: map[string]RateLimitRule{ - "/api/auth/login": {Interval: 5, MaxRequests: 1}, // 登录5秒1次 - "/api/yx-school-majors": {Interval: 1, MaxRequests: 5}, // 查询1秒5次 + "/api/user/auth/login": {Interval: 5, MaxRequests: 1}, // 登录5秒1次 + "/api/yx-school-majors": {Interval: 1, MaxRequests: 5}, // 查询1秒5次 + "/api/user/score/save-score": {Interval: 1, MaxRequests: 1}, // 保存5秒1次 }, }, // Swagger配置 diff --git a/server/modules/user/controller/user_auth_controller.go b/server/modules/user/controller/user_auth_controller.go index 5765f78..2f51ced 100644 --- a/server/modules/user/controller/user_auth_controller.go +++ b/server/modules/user/controller/user_auth_controller.go @@ -46,7 +46,7 @@ func (ctrl *AuthController) Login(c *gin.Context) { loginUser, token, err := ctrl.userService.Login(req.Username, req.Password) if err != nil { - common.Error(c, 401, err.Error()) + common.Error(c, 400, err.Error()) return } diff --git a/server/modules/user/controller/user_score_controller.go b/server/modules/user/controller/user_score_controller.go index 4ec485e..5cd6f2f 100644 --- a/server/modules/user/controller/user_score_controller.go +++ b/server/modules/user/controller/user_score_controller.go @@ -26,10 +26,10 @@ func NewUserScoreController() *UserScoreController { func (ctrl *UserScoreController) RegisterRoutes(rg *gin.RouterGroup) { group := rg.Group("/user/score") { - // group.GET("/:id", ctrl.Get) - group.GET("/", ctrl.Get) - group.GET("/list", ctrl.List) // 新增分页列表接口 - group.POST("/save-score", ctrl.SaveUserScore) // 新增接口 + group.GET("/", ctrl.GetActive) + group.GET("/:id", ctrl.GetByID) + group.GET("/list", ctrl.List) + group.POST("/save-score", ctrl.SaveUserScore) } } @@ -59,16 +59,32 @@ func (ctrl *UserScoreController) SaveUserScore(c *gin.Context) { common.Success(c, result) } -// Get 获取当前用户的当前分数 -// @Summary 获取当前用户的当前分数 +// GetActive 获取当前用户的激活分数 +// @Summary 获取当前用户的激活分数 // @Tags 用户分数 // @Success 200 {object} common.Response // @Router /user/score [get] -func (ctrl *UserScoreController) Get(c *gin.Context) { +func (ctrl *UserScoreController) GetActive(c *gin.Context) { loginUserId := common.GetLoginUser(c).ID item, err := ctrl.userScoreService.GetActiveByID(loginUserId) if err != nil { - common.Error(c, 404, "未找到记录") + common.Error(c, 404, "未找到激活成绩") + return + } + common.Success(c, item) +} + +// GetByID 获取指定 ID 的分数 +// @Summary 获取指定 ID 的分数 +// @Tags 用户分数 +// @Param id path string true "成绩ID" +// @Success 200 {object} common.Response +// @Router /user/score/{id} [get] +func (ctrl *UserScoreController) GetByID(c *gin.Context) { + id := c.Param("id") + item, err := ctrl.userScoreService.GetByID(id) + if err != nil { + common.Error(c, 404, "记录不存在") return } common.Success(c, item) diff --git a/server/modules/user/service/user_score_service.go b/server/modules/user/service/user_score_service.go index a300ec7..cfb20dd 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 ( "errors" "fmt" + "server/common" "server/config" "server/modules/user/vo" "server/modules/yx/dto" @@ -26,20 +27,32 @@ func (s *UserScoreService) GetActiveByID(userID string) (*vo.UserScoreVO, error) var score entity.YxUserScore // 明确指定字段,提高可读性 err := config.DB.Model(&entity.YxUserScore{}). - Where("create_by = ? AND state = ?", userID, "1"). // 注意:yx_user_score 表中关联用户的是 create_by + Where("create_by = ? AND state = ?", userID, "1"). First(&score).Error - // 错误处理 if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fmt.Errorf("未找到用户的激活成绩记录") + return nil, fmt.Errorf("未找到激活的成绩记录") } return nil, fmt.Errorf("查询成绩记录失败: %w", err) } - // 转换为 VO - scoreVO := s.convertEntityToVo(&score) + return s.convertEntityToVo(&score), nil +} - return scoreVO, nil +// GetByID 根据 ID 获取成绩 +func (s *UserScoreService) GetByID(id string) (*vo.UserScoreVO, error) { + var score entity.YxUserScore + err := config.DB.Model(&entity.YxUserScore{}). + Where("id = ?", id). + First(&score).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("记录不存在") + } + return nil, fmt.Errorf("查询失败: %w", err) + } + + return s.convertEntityToVo(&score), nil } // ListByUser 获取用户的成绩分页列表 @@ -72,14 +85,15 @@ func NewUserScoreService() *UserScoreService { } } -// service/yx_user_score_service.go -func (s *UserScoreService) SaveUserScore(req *dto.SaveScoreRequest) (*entity.YxUserScore, error) { +// SaveUserScore 保存用户成绩并返回 VO +func (s *UserScoreService) SaveUserScore(req *dto.SaveScoreRequest) (*vo.UserScoreVO, error) { // 1. 业务验证 if err := req.Validate(); err != nil { return nil, err } // 2. DTO 转 Entity entityItem := s.convertDtoToEntity(req) + entityItem.ID = common.GenerateStringID() // 使用新封装的 ID 生成工具 entityItem.CreateTime = time.Now() entityItem.UpdateTime = time.Now() // 3. 执行保存操作(可以包含事务) @@ -89,16 +103,16 @@ func (s *UserScoreService) SaveUserScore(req *dto.SaveScoreRequest) (*entity.YxU tx.Rollback() } }() - + entityItem.ID = common.GenerateStringID() // 使用新封装的 ID 生成工具 // 标记该用户的所有旧成绩为历史状态 - if err := config.DB.Model(&entity.YxUserScore{}). + if err := tx.Model(&entity.YxUserScore{}). 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) } // 保存新的成绩记录 - if err := s.yxUserScoreService.Create(entityItem); err != nil { + if err := tx.Create(entityItem).Error; err != nil { tx.Rollback() return nil, fmt.Errorf("保存记录失败: %w", err) } @@ -107,7 +121,7 @@ func (s *UserScoreService) SaveUserScore(req *dto.SaveScoreRequest) (*entity.YxU return nil, fmt.Errorf("提交事务失败: %w", err) } - return entityItem, nil + return s.convertEntityToVo(entityItem), nil } // 私有方法:DTO 转 Entity diff --git a/server/modules/yx/service/yx_school_major_service.go b/server/modules/yx/service/yx_school_major_service.go index 6d3f86d..daa39e5 100644 --- a/server/modules/yx/service/yx_school_major_service.go +++ b/server/modules/yx/service/yx_school_major_service.go @@ -64,3 +64,21 @@ 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 +}