This commit is contained in:
zhouwentao 2025-12-20 13:05:27 +08:00
parent 93bfb590d6
commit ff2651c1eb
12 changed files with 205 additions and 30 deletions

View File

@ -2,6 +2,7 @@
"kiroAgent.configureMCP": "Disabled", "kiroAgent.configureMCP": "Disabled",
"cSpell.words": [ "cSpell.words": [
"apikey", "apikey",
"bwmarrin",
"Cognitio", "Cognitio",
"Fzby", "Fzby",
"gonic", "gonic",

View File

@ -2,6 +2,7 @@
## server/common ## server/common
- `constants`: 存放全局常量,如 `RedisTokenPrefix`, `TokenHeader`, `StateActive` 等。 - `constants`: 存放全局常量,如 `RedisTokenPrefix`, `TokenHeader`, `StateActive` 等。
- `id_utils`: ID 生成工具,提供 `GenerateLongID()``GenerateStringID()`
- `Response`: 统一的HTTP响应结构体 `{Code, Message, Data}` - `Response`: 统一的HTTP响应结构体 `{Code, Message, Data}`
- `Success(c *gin.Context, data interface{})`: 发送成功响应。 - `Success(c *gin.Context, data interface{})`: 发送成功响应。
- `Error(c *gin.Context, code int, msg string)`: 发送错误响应。 - `Error(c *gin.Context, code int, msg string)`: 发送错误响应。
@ -25,9 +26,10 @@
- `YxVolunteerController`: 志愿控制器。 - `YxVolunteerController`: 志愿控制器。
- `YxVolunteerRecordController`: 志愿明细控制器。 - `YxVolunteerRecordController`: 志愿明细控制器。
## server/modules/user ## server/modules/user/service
- `UserScoreService`: 用户成绩服务。 - `UserScoreService`:
- `GetActiveByID(userID string)`: 获取用户当前激活的成绩,返回 `UserScoreVO` - `GetActiveByID(userID string)`: 获取用户当前激活状态的成绩 VO。
- `ListByUser(userID string, page, size int)`: 获取用户的所有成绩分页列表。 - `GetByID(id string)`: 根据 ID 获取特定成绩 VO。
- `SaveUserScore(req *dto.SaveScoreRequest)`: 保存用户成绩,处理旧记录状态更新及 DTO 转换。 - `SaveUserScore(req *dto.SaveScoreRequest)`: 保存用户成绩,返回保存后的 VO。
- `ListByUser(userID string, page, size int)`: 分页获取用户的成绩列表。
- `UserScoreVO`: 用户成绩视图对象,包含基础信息、选课列表及子专业成绩映射。 - `UserScoreVO`: 用户成绩视图对象,包含基础信息、选课列表及子专业成绩映射。

View File

@ -74,3 +74,22 @@
- 重构了 `SysUserService`,使用 `common.RedisTokenPrefix``common.RedisTokenExpire` - 重构了 `SysUserService`,使用 `common.RedisTokenPrefix``common.RedisTokenExpire`
- 重构了 `AuthMiddleware`,使用 `common.TokenHeader``common.ContextUserKey` - 重构了 `AuthMiddleware`,使用 `common.TokenHeader``common.ContextUserKey`
- 清理了 `common/context.go` 中的重复定义。 - 清理了 `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。

View File

@ -8,6 +8,7 @@
- `redis.go`: Redis连接配置。 - `redis.go`: Redis连接配置。
- `common/`: 通用工具包。 - `common/`: 通用工具包。
- `constants.go`: 全局常量定义Redis Key、业务状态等 - `constants.go`: 全局常量定义Redis Key、业务状态等
- `id_utils.go`: ID 生成工具(基于时间戳)。
- `response.go`: 统一HTTP响应结构。 - `response.go`: 统一HTTP响应结构。
- `context.go`: 上下文辅助函数。 - `context.go`: 上下文辅助函数。
- `logger.go`: 日志工具。 - `logger.go`: 日志工具。

View File

@ -23,7 +23,7 @@ const (
// "X-Access-Token" // "X-Access-Token"
TokenHeader = "Authorization" TokenHeader = "Authorization"
// HeaderTokenPrefix Token前缀 (如有需要) // HeaderTokenPrefix Token前缀 (如有需要)
HeaderTokenPrefix = "" HeaderTokenPrefix = "Bearer "
) )
// 业务状态常量 // 业务状态常量

103
server/common/id_utils.go Normal file
View File

@ -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
}

View File

@ -46,7 +46,7 @@ type Logger struct {
var ( var (
defaultLogger *Logger defaultLogger *Logger
startCount int startCount int
once sync.Once onceInit sync.Once
) )
// InitLogger 初始化日志 // InitLogger 初始化日志

View File

@ -20,8 +20,9 @@ var AppConfig = &appConfig{
Enable: true, Enable: true,
Default: RateLimitRule{Interval: 2, MaxRequests: 3}, // 默认2秒1次 Default: RateLimitRule{Interval: 2, MaxRequests: 3}, // 默认2秒1次
Rules: map[string]RateLimitRule{ Rules: map[string]RateLimitRule{
"/api/auth/login": {Interval: 5, MaxRequests: 1}, // 登录5秒1次 "/api/user/auth/login": {Interval: 5, MaxRequests: 1}, // 登录5秒1次
"/api/yx-school-majors": {Interval: 1, MaxRequests: 5}, // 查询1秒5次 "/api/yx-school-majors": {Interval: 1, MaxRequests: 5}, // 查询1秒5次
"/api/user/score/save-score": {Interval: 1, MaxRequests: 1}, // 保存5秒1次
}, },
}, },
// Swagger配置 // Swagger配置

View File

@ -46,7 +46,7 @@ func (ctrl *AuthController) Login(c *gin.Context) {
loginUser, token, err := ctrl.userService.Login(req.Username, req.Password) loginUser, token, err := ctrl.userService.Login(req.Username, req.Password)
if err != nil { if err != nil {
common.Error(c, 401, err.Error()) common.Error(c, 400, err.Error())
return return
} }

View File

@ -26,10 +26,10 @@ func NewUserScoreController() *UserScoreController {
func (ctrl *UserScoreController) RegisterRoutes(rg *gin.RouterGroup) { func (ctrl *UserScoreController) RegisterRoutes(rg *gin.RouterGroup) {
group := rg.Group("/user/score") group := rg.Group("/user/score")
{ {
// group.GET("/:id", ctrl.Get) group.GET("/", ctrl.GetActive)
group.GET("/", ctrl.Get) group.GET("/:id", ctrl.GetByID)
group.GET("/list", ctrl.List) // 新增分页列表接口 group.GET("/list", ctrl.List)
group.POST("/save-score", ctrl.SaveUserScore) // 新增接口 group.POST("/save-score", ctrl.SaveUserScore)
} }
} }
@ -59,16 +59,32 @@ func (ctrl *UserScoreController) SaveUserScore(c *gin.Context) {
common.Success(c, result) common.Success(c, result)
} }
// Get 获取当前用户的当前分数 // GetActive 获取当前用户的激活分数
// @Summary 获取当前用户的当前分数 // @Summary 获取当前用户的激活分数
// @Tags 用户分数 // @Tags 用户分数
// @Success 200 {object} common.Response // @Success 200 {object} common.Response
// @Router /user/score [get] // @Router /user/score [get]
func (ctrl *UserScoreController) Get(c *gin.Context) { func (ctrl *UserScoreController) GetActive(c *gin.Context) {
loginUserId := common.GetLoginUser(c).ID loginUserId := common.GetLoginUser(c).ID
item, err := ctrl.userScoreService.GetActiveByID(loginUserId) item, err := ctrl.userScoreService.GetActiveByID(loginUserId)
if err != nil { 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 return
} }
common.Success(c, item) common.Success(c, item)

View File

@ -4,6 +4,7 @@ package service
import ( import (
"errors" "errors"
"fmt" "fmt"
"server/common"
"server/config" "server/config"
"server/modules/user/vo" "server/modules/user/vo"
"server/modules/yx/dto" "server/modules/yx/dto"
@ -26,20 +27,32 @@ func (s *UserScoreService) GetActiveByID(userID string) (*vo.UserScoreVO, error)
var score entity.YxUserScore var score entity.YxUserScore
// 明确指定字段,提高可读性 // 明确指定字段,提高可读性
err := config.DB.Model(&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 First(&score).Error
// 错误处理
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("未找到用户的激活成绩记录") return nil, fmt.Errorf("未找到激活成绩记录")
} }
return nil, fmt.Errorf("查询成绩记录失败: %w", err) return nil, fmt.Errorf("查询成绩记录失败: %w", err)
} }
// 转换为 VO return s.convertEntityToVo(&score), nil
scoreVO := s.convertEntityToVo(&score) }
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 获取用户的成绩分页列表 // ListByUser 获取用户的成绩分页列表
@ -72,14 +85,15 @@ func NewUserScoreService() *UserScoreService {
} }
} }
// service/yx_user_score_service.go // SaveUserScore 保存用户成绩并返回 VO
func (s *UserScoreService) SaveUserScore(req *dto.SaveScoreRequest) (*entity.YxUserScore, error) { func (s *UserScoreService) SaveUserScore(req *dto.SaveScoreRequest) (*vo.UserScoreVO, error) {
// 1. 业务验证 // 1. 业务验证
if err := req.Validate(); err != nil { if err := req.Validate(); err != nil {
return nil, err return nil, err
} }
// 2. DTO 转 Entity // 2. DTO 转 Entity
entityItem := s.convertDtoToEntity(req) entityItem := s.convertDtoToEntity(req)
entityItem.ID = common.GenerateStringID() // 使用新封装的 ID 生成工具
entityItem.CreateTime = time.Now() entityItem.CreateTime = time.Now()
entityItem.UpdateTime = time.Now() entityItem.UpdateTime = time.Now()
// 3. 执行保存操作(可以包含事务) // 3. 执行保存操作(可以包含事务)
@ -89,16 +103,16 @@ func (s *UserScoreService) SaveUserScore(req *dto.SaveScoreRequest) (*entity.YxU
tx.Rollback() 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"). Where("create_by = ? AND state = ?", req.CreateBy, "1").
Updates(map[string]interface{}{"state": "2"}).Error; err != nil { Updates(map[string]interface{}{"state": "2"}).Error; err != nil {
tx.Rollback() tx.Rollback()
return nil, fmt.Errorf("更新旧记录失败: %w", err) return nil, fmt.Errorf("更新旧记录失败: %w", err)
} }
// 保存新的成绩记录 // 保存新的成绩记录
if err := s.yxUserScoreService.Create(entityItem); err != nil { if err := tx.Create(entityItem).Error; err != nil {
tx.Rollback() tx.Rollback()
return nil, fmt.Errorf("保存记录失败: %w", err) 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 nil, fmt.Errorf("提交事务失败: %w", err)
} }
return entityItem, nil return s.convertEntityToVo(entityItem), nil
} }
// 私有方法DTO 转 Entity // 私有方法DTO 转 Entity

View File

@ -64,3 +64,21 @@ func (s *YxSchoolMajorService) BatchUpsert(items []entity.YxSchoolMajor, updateC
func (s *YxSchoolMajorService) BatchDelete(ids []string) error { func (s *YxSchoolMajorService) BatchDelete(ids []string) error {
return s.mapper.BatchDelete(ids) 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
}