updates
This commit is contained in:
parent
94486dd149
commit
93bfb590d6
|
|
@ -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`: 用户成绩服务。
|
||||
- `UserScoreService`: 用户成绩服务。
|
||||
- `GetActiveByID(userID string)`: 获取用户当前激活的成绩,返回 `UserScoreVO`。
|
||||
- `ListByUser(userID string, page, size int)`: 获取用户的所有成绩分页列表。
|
||||
- `SaveUserScore(req *dto.SaveScoreRequest)`: 保存用户成绩,处理旧记录状态更新及 DTO 转换。
|
||||
- `UserScoreVO`: 用户成绩视图对象,包含基础信息、选课列表及子专业成绩映射。
|
||||
- `UserScoreVO`: 用户成绩视图对象,包含基础信息、选课列表及子专业成绩映射。
|
||||
|
|
|
|||
|
|
@ -56,3 +56,21 @@
|
|||
- 定义了 `UserScoreVO` 结构体,其字段设计参考了 `SaveScoreRequest`。
|
||||
- 在 `UserScoreService` 中实现了 `convertEntityToVo` 私有方法,处理了逗号分隔字符串到切片的转换,以及具体分数字段到 Map 的映射。
|
||||
- 更新 `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` 中的重复定义。
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
- `database.go`: 数据库连接配置。
|
||||
- `redis.go`: Redis连接配置。
|
||||
- `common/`: 通用工具包。
|
||||
- `constants.go`: 全局常量定义(Redis Key、业务状态等)。
|
||||
- `response.go`: 统一HTTP响应结构。
|
||||
- `context.go`: 上下文辅助函数。
|
||||
- `logger.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" // 艺术类
|
||||
)
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue