This commit is contained in:
zhouwentao 2026-01-31 13:03:24 +08:00
parent 44ddefd4c6
commit 9167b6a661
13 changed files with 141 additions and 171 deletions

12
.iflow/settings.json Normal file
View File

@ -0,0 +1,12 @@
{
"mcpServers": {
"'codebase-mcp'": {
"command": "npx",
"args": [
"-y",
"@iflow-mcp/codebase-mcp",
"start"
]
}
}
}

View File

@ -55,7 +55,16 @@
如果是从0到1开发新的项目尽可能使用下方给出的技术栈
### 后端 - Java主力
### 后端 - Go主力
| 配置项 | 要求 |
| -------- | -------------------------------------- |
| 语言版本 | Go 1.21+ |
| 开发框架 | Gin |
| ORM框架 | GORM |
| 代码规范 | Google Go 编程规范 |
### 后端 - Java
| 配置项 | 要求 |
| -------- | -------------------------------------- |

View File

@ -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)`: 切换当前志愿单(同步同步成绩状态)。

View File

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

View File

@ -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文档。

View File

@ -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 同步)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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。
- **执行结果**: 服务现在会使用配置文件中指定的端口启动。