default
This commit is contained in:
parent
4de6f185b3
commit
7433724a54
|
|
@ -0,0 +1,217 @@
|
||||||
|
# 艺考招生管理系统 API
|
||||||
|
|
||||||
|
## 项目概述
|
||||||
|
|
||||||
|
艺考招生管理系统是一个基于 Go + Gin + GORM + Redis 的 RESTful API 服务,用于管理和计算艺考志愿填报、成绩统计和录取概率等功能。
|
||||||
|
|
||||||
|
### 核心技术栈
|
||||||
|
|
||||||
|
- **Go 1.21+** - 主要开发语言
|
||||||
|
- **Gin** - Web 框架
|
||||||
|
- **GORM** - ORM 框架
|
||||||
|
- **MySQL 8.0** - 数据存储
|
||||||
|
- **Redis** - 会话存储和限流
|
||||||
|
- **Swaggo** - API 文档生成
|
||||||
|
|
||||||
|
### 项目架构
|
||||||
|
|
||||||
|
项目采用典型的分层架构模式:
|
||||||
|
|
||||||
|
```
|
||||||
|
server/
|
||||||
|
├── main.go # 应用程序入口
|
||||||
|
├── config/ # 配置文件
|
||||||
|
│ ├── config.go # 应用配置
|
||||||
|
│ ├── database.go # MySQL 配置
|
||||||
|
│ └── redis.go # Redis 配置
|
||||||
|
├── common/ # 公共组件
|
||||||
|
│ ├── response.go # 统一响应格式
|
||||||
|
│ ├── context.go # 上下文工具
|
||||||
|
│ ├── logger.go # 日志工具
|
||||||
|
│ └── password.go # 密码加密
|
||||||
|
├── middleware/ # 中间件
|
||||||
|
│ ├── auth.go # 登录鉴权
|
||||||
|
│ ├── security.go # 安全校验
|
||||||
|
│ ├── ratelimit.go # 接口限流
|
||||||
|
│ └── cors.go # 跨域处理
|
||||||
|
├── modules/ # 业务模块
|
||||||
|
│ ├── system/ # 系统模块 (用户认证、权限管理)
|
||||||
|
│ ├── user/ # 用户模块 (用户相关功能)
|
||||||
|
│ └── yx/ # 艺考模块 (核心业务)
|
||||||
|
├── docs/ # API 文档
|
||||||
|
└── logs/ # 日志目录
|
||||||
|
```
|
||||||
|
|
||||||
|
### 业务模块
|
||||||
|
|
||||||
|
#### 1. 系统模块 (system)
|
||||||
|
- 用户认证 (登录/登出/用户信息)
|
||||||
|
- 用户管理
|
||||||
|
|
||||||
|
#### 2. 用户模块 (user)
|
||||||
|
- 成绩管理
|
||||||
|
- 专业选择
|
||||||
|
- 志愿填报
|
||||||
|
- 用户认证
|
||||||
|
|
||||||
|
#### 3. 艺考模块 (yx)
|
||||||
|
- 院校专业管理
|
||||||
|
- 历年招生数据
|
||||||
|
- 计算专业逻辑
|
||||||
|
- 用户成绩管理
|
||||||
|
- 志愿管理
|
||||||
|
- 志愿记录管理
|
||||||
|
|
||||||
|
## 部署与运行
|
||||||
|
|
||||||
|
### 环境要求
|
||||||
|
- Go 1.21+
|
||||||
|
- MySQL 8.0
|
||||||
|
- Redis
|
||||||
|
|
||||||
|
### 快速启动
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd server
|
||||||
|
go mod tidy
|
||||||
|
swag init
|
||||||
|
go run main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置说明
|
||||||
|
|
||||||
|
系统支持多环境配置文件:
|
||||||
|
|
||||||
|
- `config/config.dev.yaml` - 开发环境
|
||||||
|
- `config/config.test.yaml` - 测试环境
|
||||||
|
- `config/config.prod.yaml` - 生产环境
|
||||||
|
|
||||||
|
配置文件格式示例:
|
||||||
|
```yaml
|
||||||
|
server:
|
||||||
|
port: 8081
|
||||||
|
|
||||||
|
database:
|
||||||
|
driver: mysql
|
||||||
|
host: localhost
|
||||||
|
port: 3306
|
||||||
|
database: yitisheng
|
||||||
|
username: root
|
||||||
|
password: "password"
|
||||||
|
|
||||||
|
redis:
|
||||||
|
addr: localhost:6379
|
||||||
|
password: "password"
|
||||||
|
db: 1
|
||||||
|
```
|
||||||
|
|
||||||
|
## 安全机制
|
||||||
|
|
||||||
|
### 1. 登录鉴权
|
||||||
|
- 基于 JWT Token 的身份验证
|
||||||
|
- 支持白名单配置,允许特定接口无需登录访问
|
||||||
|
- Token 存储在 Redis 中,支持过期时间设置
|
||||||
|
|
||||||
|
### 2. 安全校验
|
||||||
|
- 请求头需携带签名进行验证
|
||||||
|
- 防暴力破解机制
|
||||||
|
- 支持白名单配置
|
||||||
|
|
||||||
|
### 3. 接口限流
|
||||||
|
- 基于 Redis 滑动窗口算法
|
||||||
|
- 支持按用户ID或IP限流
|
||||||
|
- 可为不同接口配置不同限流规则
|
||||||
|
|
||||||
|
## API 接口
|
||||||
|
|
||||||
|
### 认证相关
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| POST | /api/user/auth/login | 用户登录 |
|
||||||
|
| POST | /api/user/auth/logout | 用户登出 |
|
||||||
|
| GET | /api/user/auth/info | 获取当前用户信息 |
|
||||||
|
|
||||||
|
### 系统管理
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| GET | /api/sys-users | 获取用户列表 |
|
||||||
|
| POST | /api/sys-users | 创建用户 |
|
||||||
|
| PUT | /api/sys-users/:id | 更新用户 |
|
||||||
|
| DELETE | /api/sys-users/:id | 删除用户 |
|
||||||
|
|
||||||
|
### 艺考相关
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| GET | /api/yx-school-majors | 获取院校专业列表 |
|
||||||
|
| GET | /api/yx-history-enrolls | 获取历年招生数据 |
|
||||||
|
| GET | /api/yx-calculation-majors | 获取计算专业数据 |
|
||||||
|
| GET | /api/yx-user-scores | 获取用户成绩 |
|
||||||
|
| GET | /api/yx-volunteers | 获取志愿列表 |
|
||||||
|
| POST | /api/yx-volunteers | 创建志愿 |
|
||||||
|
| PUT | /api/yx-volunteers/:id | 更新志愿 |
|
||||||
|
| DELETE | /api/yx-volunteers/:id | 删除志愿 |
|
||||||
|
|
||||||
|
### 中间件执行顺序
|
||||||
|
|
||||||
|
```
|
||||||
|
请求 -> 安全校验 -> 限流 -> 登录鉴权 -> Controller
|
||||||
|
```
|
||||||
|
|
||||||
|
## 数据实体
|
||||||
|
|
||||||
|
### YxVolunteer (志愿表)
|
||||||
|
- ID: 主键
|
||||||
|
- VolunteerName: 志愿单名称
|
||||||
|
- ScoreId: 关联成绩ID
|
||||||
|
- CreateType: 生成类型 (1.手动生成, 2.智能生成)
|
||||||
|
- State: 状态 (0-未使用, 1-使用中, 2-历史)
|
||||||
|
- CreateBy: 创建人
|
||||||
|
- CreateTime: 创建时间
|
||||||
|
- UpdateBy: 更新人
|
||||||
|
- UpdateTime: 更新时间
|
||||||
|
|
||||||
|
## 常量定义
|
||||||
|
|
||||||
|
### 业务状态常量
|
||||||
|
- StateActive ("1") - 使用中
|
||||||
|
- StateInactive ("0") - 未使用/已删除
|
||||||
|
- StateHistory ("2") - 历史记录
|
||||||
|
|
||||||
|
### 令牌相关
|
||||||
|
- TokenHeader: "Authorization"
|
||||||
|
- HeaderTokenPrefix: "Bearer "
|
||||||
|
- RedisTokenExpire: 24小时
|
||||||
|
|
||||||
|
## 日志系统
|
||||||
|
|
||||||
|
- 支持 debug/info/warn/error 级别
|
||||||
|
- 输出到 HTML 文件,按日期和启动次数命名
|
||||||
|
- 可配置是否同时输出到控制台
|
||||||
|
|
||||||
|
## 开发规范
|
||||||
|
|
||||||
|
### 代码风格
|
||||||
|
- 使用 Go 语言标准格式 (go fmt)
|
||||||
|
- 遵循 Go 语言命名规范
|
||||||
|
- 使用有意义的变量和函数名
|
||||||
|
|
||||||
|
### 错误处理
|
||||||
|
- 统一使用 common.Error(c, code, message) 返回错误
|
||||||
|
- 业务错误码使用 HTTP 标准码或自定义业务码
|
||||||
|
- 记录详细错误日志便于调试
|
||||||
|
|
||||||
|
### 接口文档
|
||||||
|
- 使用 Swaggo 注解生成 API 文档
|
||||||
|
- 所有公开接口都应有完整的文档注释
|
||||||
|
- 包含请求参数、响应格式和错误码说明
|
||||||
|
|
||||||
|
## 额外说明
|
||||||
|
|
||||||
|
项目还包含一些 Java 代码文件,如 `ScoreUtil.java`,这表明项目可能正在从 Java 版本迁移到 Go 版本,或者 Java 版本作为参考。Go 版本中的 `server/modules/yx/service/score_util.go` 文件可能包含与 Java 版本相同功能的实现。
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 开发环境中的安全校验默认关闭 (enable: false)
|
||||||
|
- 生产环境务必开启安全校验
|
||||||
|
- 需要配置合适的限流规则防止恶意请求
|
||||||
|
- 定期清理日志文件避免磁盘空间不足
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
#### 关于 Swagger 文档更新流程:
|
||||||
|
|
||||||
|
新增接口后更新文档步骤:
|
||||||
|
|
||||||
|
在 Controller 方法上添加 Swagger 注解(@Summary、@Param、@Router 等)
|
||||||
|
在 server 目录运行 swag init 重新生成文档
|
||||||
|
重启服务器
|
||||||
|
常用 Swagger 注解说明:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// @Summary 接口简介
|
||||||
|
// @Description 详细描述
|
||||||
|
// @Tags 分组标签
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param name query/path/body 类型 是否必填 "描述"
|
||||||
|
// @Success 200 {object} Response "成功描述"
|
||||||
|
// @Failure 400 {object} Response "失败描述"
|
||||||
|
// @Router /path [get/post/put/delete]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 批量修改/新增/删除
|
||||||
|
|
||||||
|
Mapper 层: | 方法 | 说明 | |------|------| | UpdateFields(id, fields) | 动态字段更新,只更新指定字段 | | BatchCreate(items, batchSize) | 批量插入,分批处理 | | BatchUpdate(items) | 批量更新 (根据主键) | | BatchUpsert(items, updateColumns) | 批量插入或更新 (存在则更新) | | BatchDelete(ids) | 批量删除 |
|
||||||
|
|
||||||
|
Service 层: 同样的方法,自动处理 UUID 生成。
|
||||||
|
|
||||||
|
使用示例:
|
||||||
|
|
||||||
|
```
|
||||||
|
// 动态字段更新 - 只更新指定字段
|
||||||
|
service.UpdateFields("xxx-id", map[string]interface{}{
|
||||||
|
"school_name": "新名称",
|
||||||
|
"plan_num": 100,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 批量插入
|
||||||
|
items := []entity.SchoolMajor{{...}, {...}}
|
||||||
|
service.BatchCreate(items)
|
||||||
|
|
||||||
|
// 批量插入或更新 (存在则更新指定字段)
|
||||||
|
service.BatchUpsert(items, []string{"school_name", "plan_num"})
|
||||||
|
|
||||||
|
// 批量删除
|
||||||
|
service.BatchDelete([]string{"id1", "id2", "id3"})
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,226 @@
|
||||||
|
# IFLOW.md - 核心工作规则
|
||||||
|
|
||||||
|
## Global Protocols
|
||||||
|
|
||||||
|
所有操作必须严格遵循以下系统约束:
|
||||||
|
|
||||||
|
- **交互语言**:技术术语、工具与模型交互强制使用 **English**;用户输出强制使用 **中文**。
|
||||||
|
- **最小改动**:仅对需求做针对性改动,严禁影响用户现有的其他功能。
|
||||||
|
- **风格一致**:遵循项目现有的代码风格,使用项目已有的工具函数。
|
||||||
|
|
||||||
|
## Tool Priority
|
||||||
|
|
||||||
|
在执行任何操作前,必须按照以下顺序选择工具,严禁跳级使用:
|
||||||
|
|
||||||
|
**1. MCP 工具**:当 MCP 工具能够完成任务时,必须使用 MCP,禁止降级到内置工具或 Shell 命令。
|
||||||
|
|
||||||
|
**2. 内置工具**:仅当 MCP 工具**无法覆盖**该功能时,使用内置工具。
|
||||||
|
|
||||||
|
**3. Shell 命令**:Shell 命令是最后手段,同时遵循以下规则:
|
||||||
|
|
||||||
|
- 只读类安全操作允许直接执行
|
||||||
|
|
||||||
|
| 类别 | 安全操作示例 |
|
||||||
|
| ---------------- | ------------------------------------------------- |
|
||||||
|
| Git 只读操作 | `git status`、`git log`、`git diff`、`git branch` |
|
||||||
|
| 包管理器只读操作 | `npm list`、`pnpm why`、`pip show` |
|
||||||
|
| 容器只读操作 | `docker ps`、`docker logs` |
|
||||||
|
| 环境检查 | `node -v`、`python -version`、`which xxx` |
|
||||||
|
|
||||||
|
- 写入/删除/修改/安装等危险操作必须征得用户同意
|
||||||
|
|
||||||
|
| 类别 | 危险操作示例 |
|
||||||
|
| ------------ | ------------------------------------------------------------ |
|
||||||
|
| Git 写操作 | `commit`、`push`、`pull`、`merge`、`rebase`、`reset`、`checkout <branch>` |
|
||||||
|
| 文件删除 | `rm`、`rmdir`、清空目录 |
|
||||||
|
| 批量文件修改 | `sed -i`(多文件)、批量重命名 |
|
||||||
|
| 包管理写操作 | `pnpm install/uninstall`、`pnpm add/remove`、`uv add/remove` |
|
||||||
|
| 容器写操作 | `docker rm`、`docker rmi`、`docker-compose down` |
|
||||||
|
| 系统级操作 | 修改环境变量、修改系统配置文件 |
|
||||||
|
|
||||||
|
- 触发危险操作时告知用户
|
||||||
|
|
||||||
|
```
|
||||||
|
# 告知示例
|
||||||
|
!!!即将执行危险操作!!!:
|
||||||
|
命令:git push origin main
|
||||||
|
影响:将本地 main 分支的提交推送到远程仓库
|
||||||
|
|
||||||
|
是否继续?请回复"确认"或"取消"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Technology Stack
|
||||||
|
|
||||||
|
如果是对已有项目二次开发/修改bug,则遵循项目已有技术栈。
|
||||||
|
|
||||||
|
如果是从0到1开发新的项目,尽可能使用下方给出的技术栈:
|
||||||
|
|
||||||
|
### 后端 - Go(主力)
|
||||||
|
|
||||||
|
| 配置项 | 要求 |
|
||||||
|
| -------- | -------------------------------------- |
|
||||||
|
| 语言版本 | Go 1.21+ |
|
||||||
|
| 开发框架 | Gin |
|
||||||
|
| ORM框架 | GORM |
|
||||||
|
| 代码规范 | Google Go 编程规范 |
|
||||||
|
|
||||||
|
### 后端 - Java
|
||||||
|
|
||||||
|
| 配置项 | 要求 |
|
||||||
|
| -------- | -------------------------------------- |
|
||||||
|
| 语言版本 | Java 17 |
|
||||||
|
| 开发框架 | Spring Boot 3.x + Spring Cloud Alibaba |
|
||||||
|
| ORM框架 | MyBatis Plus |
|
||||||
|
| 包管理器 | Maven |
|
||||||
|
| 代码规范 | 阿里巴巴Java开发手册(嵩山版) |
|
||||||
|
|
||||||
|
### 后端 - Python(辅助/小工具)
|
||||||
|
|
||||||
|
| 配置项 | 要求 |
|
||||||
|
| ---------- | ------------------------------------------------------------ |
|
||||||
|
| 语言版本 | Python 3.10+ |
|
||||||
|
| 开发框架 | FastAPI(轻量级API)/ Typer(CLI工具)/ Streamlit(数据可视化) |
|
||||||
|
| 包管理工具 | uv |
|
||||||
|
| 代码规范 | PEP 8 + Google Python Style Guide |
|
||||||
|
| 虚拟环境 | **强制启用**(uv venv) |
|
||||||
|
|
||||||
|
### 后端 - 其他组件
|
||||||
|
|
||||||
|
| 组件 | 选型 |
|
||||||
|
| -------- | --------- |
|
||||||
|
| 数据库 | MySQL 8.x |
|
||||||
|
| 缓存 | Redis |
|
||||||
|
|
||||||
|
### 前端 - TypeScript + Vue 3
|
||||||
|
|
||||||
|
| 配置项 | 要求 |
|
||||||
|
| -------- | ---------------------------- |
|
||||||
|
| 语言版本 | TypeScript 5.x |
|
||||||
|
| 开发框架 | Vue 3(Composition API) |
|
||||||
|
| UI组件库 | TailWind CSS |
|
||||||
|
| 包管理器 | pnpm |
|
||||||
|
| 构建工具 | Vite |
|
||||||
|
| 代码规范 | ESLint(严格模式)+ Prettier |
|
||||||
|
|
||||||
|
### 桌面端 - Electron
|
||||||
|
|
||||||
|
| 配置项 | 要求 |
|
||||||
|
| -------- | ------------------ |
|
||||||
|
| 基础框架 | Vue 3 + TypeScript |
|
||||||
|
| 打包工具 | electron-builder |
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
在开发过程中,严格按照以下阶段顺序执行任务。
|
||||||
|
|
||||||
|
**格式要求**: 每次回复必须在开头标注 `【当前阶段: [阶段名称]】`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 0:上下文全量检索
|
||||||
|
|
||||||
|
**执行条件**:在生成任何建议或代码前。
|
||||||
|
|
||||||
|
**调用工具**:`mcp__auggie-mcp__codebase-retrieval`
|
||||||
|
|
||||||
|
**检索策略**:
|
||||||
|
|
||||||
|
- 禁止基于假设(Assumption)回答。
|
||||||
|
- 使用自然语言(NL)构建语义查询(Where/What/How)。
|
||||||
|
- **完整性检查**:必须获取相关类、函数、变量的完整定义与签名。若上下文不足,触发递归检索。
|
||||||
|
|
||||||
|
**需求对齐**:若检索后需求仍有模糊空间,**必须**向用户输出引导性问题列表,直至需求边界清晰(无遗漏、无冗余)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 1: 产品需求分析
|
||||||
|
|
||||||
|
**角色**:产品经理
|
||||||
|
|
||||||
|
**方法**:通过`AskUserQuestion`工具进行多轮提问引导,直到需求完全量化。
|
||||||
|
|
||||||
|
**最小维度**:
|
||||||
|
|
||||||
|
- 目标用户与使用场景。
|
||||||
|
- 核心功能清单(按优先级 P0/P1/P2 排列)。
|
||||||
|
- 业务规则与约束条件。
|
||||||
|
|
||||||
|
**输出**:`requirement.md`(需求规格书)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 2: UI/UX 设计
|
||||||
|
|
||||||
|
**角色**:UI/UX 设计师
|
||||||
|
|
||||||
|
**方法**:基于`requirement.md`,通过多轮提问引导,定义交互与视觉规范。
|
||||||
|
|
||||||
|
**最小维度**:
|
||||||
|
|
||||||
|
- 核心用户流程。
|
||||||
|
- 页面结构与布局。
|
||||||
|
- 组件状态定义。
|
||||||
|
|
||||||
|
**冲突检测**:与`requirement.md`中的约束进行一致性校验,如有冲突,必须提问澄清后再继续。
|
||||||
|
|
||||||
|
**输出**:`ui_ux_specifications.md`(UI/UX 规范)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 3: 架构设计
|
||||||
|
|
||||||
|
**角色**:系统架构师
|
||||||
|
|
||||||
|
**方法**:基于`requirement.md`和`ui_ux_specifications.md`,通过多轮提问引导,设计技术方案。
|
||||||
|
|
||||||
|
**最小维度**:
|
||||||
|
|
||||||
|
- 技术栈选型(遵循本文档`Technology Stack`章节)。
|
||||||
|
- 系统分层、模块划分、目录结构。
|
||||||
|
- API 契约定义。
|
||||||
|
|
||||||
|
**冲突检测**:与`requirement.md`中的约束进行一致性校验,如有冲突,必须提问澄清后再继续。
|
||||||
|
|
||||||
|
**输出**:`architecture_design_document.md`(架构设计文档)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 4: 代码实现
|
||||||
|
|
||||||
|
**角色**:全栈开发工程师
|
||||||
|
|
||||||
|
**方法**:
|
||||||
|
|
||||||
|
1. 根据 `requirement.md` 和 `architecture_design_document.md`,拆分开发任务
|
||||||
|
2. 在 `task_list.md` 中记录任务清单,将**待开发/已开发/跳过**的任务通过不同的复选框进行标记
|
||||||
|
3. 逐个任务开发,每个任务完成后更新状态
|
||||||
|
|
||||||
|
**输出**:`task_list.md`(任务清单,持续更新)、`deployment.md`(部署文档)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 5: 代码审计
|
||||||
|
|
||||||
|
**执行条件**:每个任务模块开发完成后进行增量审计,全部完成后进行最终审计。
|
||||||
|
|
||||||
|
**角色**:代码审计工程师
|
||||||
|
|
||||||
|
**方法**:根据`task_list.md`,逐个对已完成代码进行 Code Review。
|
||||||
|
|
||||||
|
**审计范围**:
|
||||||
|
|
||||||
|
- 功能完整性:是否覆盖`requirement.md`对应功能的全部需求
|
||||||
|
- 代码质量:命名规范、无重复代码、适当抽象、注释完整
|
||||||
|
- 安全检查:输入验证、SQL注入防护、XSS防护、敏感数据处理、权限控制
|
||||||
|
- 性能检查:算法效率、数据库查询优化、资源释放
|
||||||
|
|
||||||
|
**问题分级与处理**:
|
||||||
|
|
||||||
|
| 级别 | 定义 | 处理方式 |
|
||||||
|
| ---- | -------------------------------- | ------------------ |
|
||||||
|
| P0 | 安全漏洞、数据风险、核心功能缺失 | 阻断发布,立即修复 |
|
||||||
|
| P1 | 功能不完整、明显性能问题 | 当前迭代必须修复 |
|
||||||
|
| P2 | 代码规范、可维护性问题 | 可选 |
|
||||||
|
| P3 | 优化建议 | 可选 |
|
||||||
|
|
||||||
|
**输出**:`audit_report.md`(审计报告)、`fix_changelog.md`(修复记录)
|
||||||
184
README.md
184
README.md
|
|
@ -1,2 +1,184 @@
|
||||||
# golang-api-server
|
# 艺考招生管理系统 API
|
||||||
|
|
||||||
|
基于 Go + Gin + GORM + Redis 的 RESTful API 服务。
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- Go 1.21+
|
||||||
|
- Gin (Web框架)
|
||||||
|
- GORM (ORM框架)
|
||||||
|
- MySQL 8.0
|
||||||
|
- Redis (会话存储/限流)
|
||||||
|
- Swaggo (API文档)
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
server/
|
||||||
|
├── main.go
|
||||||
|
├── config/
|
||||||
|
│ ├── config.go # 应用配置 (日志/安全/限流)
|
||||||
|
│ ├── database.go # MySQL配置
|
||||||
|
│ └── redis.go # Redis配置
|
||||||
|
├── common/
|
||||||
|
│ ├── response.go # 统一响应
|
||||||
|
│ ├── context.go # 上下文工具
|
||||||
|
│ ├── logger.go # 日志工具
|
||||||
|
│ └── password.go # 密码加密 (PBE)
|
||||||
|
├── middleware/
|
||||||
|
│ ├── auth.go # 登录鉴权
|
||||||
|
│ ├── security.go # 安全校验 (防暴力入侵)
|
||||||
|
│ └── ratelimit.go # 接口限流
|
||||||
|
├── modules/
|
||||||
|
│ ├── system/ # 系统模块
|
||||||
|
│ └── yx/ # 艺考模块
|
||||||
|
├── logs/ # 日志目录
|
||||||
|
│ └── 2025-12-17-1.html # HTML格式日志
|
||||||
|
└── docs/ # Swagger文档
|
||||||
|
```
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd server
|
||||||
|
go mod tidy
|
||||||
|
swag init
|
||||||
|
go run main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
go mod 失败的可以试试
|
||||||
|
https://learnku.com/go/wikis/38122
|
||||||
|
|
||||||
|
## 配置说明
|
||||||
|
|
||||||
|
修改 `config/config.go`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
var AppConfig = &appConfig{
|
||||||
|
// 日志配置
|
||||||
|
Log: LogConfig{
|
||||||
|
Level: "debug", // debug/info/warn/error
|
||||||
|
Dir: "logs", // 日志目录
|
||||||
|
Console: true, // 是否输出到控制台
|
||||||
|
},
|
||||||
|
// 安全配置
|
||||||
|
Security: SecurityConfig{
|
||||||
|
Enable: true, // 是否启用
|
||||||
|
HeaderKey: "X-App-Sign", // 签名字段
|
||||||
|
SecretKey: "yts@2025#secure", // 签名密钥
|
||||||
|
},
|
||||||
|
// 限流配置
|
||||||
|
RateLimit: RateLimitConfig{
|
||||||
|
Enable: true,
|
||||||
|
Default: RateLimitRule{Interval: 2, MaxRequests: 1}, // 默认2秒1次
|
||||||
|
Rules: map[string]RateLimitRule{
|
||||||
|
"/api/auth/login": {Interval: 5, MaxRequests: 1}, // 登录5秒1次
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 功能说明
|
||||||
|
|
||||||
|
### 1. 日志系统
|
||||||
|
|
||||||
|
- 支持 debug/info/warn/error 级别
|
||||||
|
- 输出到 HTML 文件,按日期和启动次数命名
|
||||||
|
- 可配置是否同时输出到控制台
|
||||||
|
|
||||||
|
```go
|
||||||
|
common.Debug("调试信息: %s", msg)
|
||||||
|
common.Info("普通信息: %s", msg)
|
||||||
|
common.Warn("警告信息: %s", msg)
|
||||||
|
common.LogError("错误信息: %s", msg)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 安全校验 (防暴力入侵)
|
||||||
|
|
||||||
|
请求头需携带签名:
|
||||||
|
|
||||||
|
```
|
||||||
|
X-App-Sign: MD5(timestamp + secretKey)
|
||||||
|
X-App-Timestamp: 毫秒时间戳
|
||||||
|
```
|
||||||
|
|
||||||
|
前端签名示例 (JavaScript):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const timestamp = Date.now().toString();
|
||||||
|
const sign = md5(timestamp + "yts@2025#secure");
|
||||||
|
|
||||||
|
fetch("/api/xxx", {
|
||||||
|
headers: {
|
||||||
|
"X-App-Sign": sign,
|
||||||
|
"X-App-Timestamp": timestamp,
|
||||||
|
Authorization: "Bearer " + token,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 接口限流
|
||||||
|
|
||||||
|
- 基于 Redis 滑动窗口算法
|
||||||
|
- 支持按用户ID或IP限流
|
||||||
|
- 不同接口可配置不同规则
|
||||||
|
|
||||||
|
默认规则: 2秒1次
|
||||||
|
超过限制返回: `{"code": 429, "message": "操作过快,请稍后再试"}`
|
||||||
|
|
||||||
|
配置示例:
|
||||||
|
|
||||||
|
```go
|
||||||
|
Rules: map[string]RateLimitRule{
|
||||||
|
"/api/auth/login": {Interval: 5, MaxRequests: 1}, // 5秒1次
|
||||||
|
"/api/yx-school-majors": {Interval: 1, MaxRequests: 5}, // 1秒5次
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 中间件执行顺序
|
||||||
|
|
||||||
|
```
|
||||||
|
请求 -> 安全校验 -> 限流 -> 登录鉴权 -> Controller
|
||||||
|
```
|
||||||
|
|
||||||
|
## 白名单配置
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 安全校验白名单
|
||||||
|
middleware.AddSecurityWhitelist("/api/public/xxx")
|
||||||
|
|
||||||
|
// 限流白名单
|
||||||
|
middleware.AddRateLimitWhitelist("/api/public/xxx")
|
||||||
|
|
||||||
|
// 登录鉴权白名单
|
||||||
|
middleware.AddWhiteList("/api/public/xxx")
|
||||||
|
```
|
||||||
|
|
||||||
|
## API 接口
|
||||||
|
|
||||||
|
### 认证
|
||||||
|
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
| ---- | ---------------- | ------------ |
|
||||||
|
| POST | /api/auth/login | 登录 |
|
||||||
|
| POST | /api/auth/logout | 登出 |
|
||||||
|
| GET | /api/auth/info | 获取当前用户 |
|
||||||
|
|
||||||
|
### 用户管理 `/api/sys-users`
|
||||||
|
|
||||||
|
### 院校专业 `/api/yx-school-majors`
|
||||||
|
|
||||||
|
### 历年招生 `/api/yx-history-enrolls`
|
||||||
|
|
||||||
|
### 计算专业 `/api/yx-calculation-majors`
|
||||||
|
|
||||||
|
## 日志文件示例
|
||||||
|
|
||||||
|
日志以 HTML 格式保存,支持浏览器直接打开查看:
|
||||||
|
|
||||||
|
```
|
||||||
|
logs/
|
||||||
|
├── 2025-12-17-1.html # 第1次启动
|
||||||
|
├── 2025-12-17-2.html # 第2次启动
|
||||||
|
└── 2025-12-18-1.html # 新的一天
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
给当前Golang系统 增加 postgresql 的支持,可以切换mysql或者postgresql(通过config yaml配置 主数据源)。
|
||||||
|
然后将以下移动端用户信息表给我改成postgresql的建表格式(加上COMMENT信息),并增加对应的代码(Entity、Service、Mapper、Controller)
|
||||||
|
要求 系统要兼容mysql/postgresql 切换使用,目前只有两个表t_user、t_platform_user,你需要帮我在docs/ 下建立 sql文件夹,储存 mysql以及postgresql的建表语句。
|
||||||
|
|
||||||
|
```
|
||||||
|
CREATE TABLE t_user (
|
||||||
|
id BIGSERIAL PRIMARY KEY, -- 全局唯一用户ID(自增)
|
||||||
|
username VARCHAR(50) COMMENT '用户名(可选,后台管理用)',
|
||||||
|
nickname VARCHAR(100) COMMENT '用户昵称(各平台统一)',
|
||||||
|
avatar_url VARCHAR(500) COMMENT '用户头像URL',
|
||||||
|
phone VARCHAR(20) UNIQUE COMMENT '手机号(脱敏存储,如138****1234)',
|
||||||
|
gender TINYINT COMMENT '性别:0-未知,1-男,2-女',
|
||||||
|
status TINYINT DEFAULT 1 COMMENT '状态:0-禁用,1-正常',
|
||||||
|
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
deleted TINYINT DEFAULT 0 COMMENT '软删除:0-未删,1-已删'
|
||||||
|
);
|
||||||
|
COMMENT ON TABLE t_user IS '用户基础信息表';
|
||||||
|
CREATE TABLE t_platform_user (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL COMMENT '关联t_user.id',
|
||||||
|
platform_type TINYINT NOT NULL COMMENT '平台类型:1-微信小程序,2-抖音小程序,3-支付宝小程序',
|
||||||
|
platform_openid VARCHAR(100) NOT NULL COMMENT '平台唯一标识(微信openid/抖音open_id)',
|
||||||
|
platform_unionid VARCHAR(100) COMMENT '平台统一标识(微信unionid,多小程序互通用)',
|
||||||
|
platform_session_key VARCHAR(100) COMMENT '平台会话密钥(微信session_key,加密存储)',
|
||||||
|
platform_extra JSONB COMMENT '平台扩展字段(如抖音的user_name、微信的city等)',
|
||||||
|
last_login_time TIMESTAMP COMMENT '最后登录时间',
|
||||||
|
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
deleted TINYINT DEFAULT 0,
|
||||||
|
-- 联合唯一索引:同一平台的openid不能重复
|
||||||
|
UNIQUE (platform_type, platform_openid),
|
||||||
|
-- 外键关联用户表
|
||||||
|
CONSTRAINT fk_platform_user_user_id FOREIGN KEY (user_id) REFERENCES t_user(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
COMMENT ON TABLE t_platform_user IS '平台用户关联表(微信/抖音小程序用户信息)';
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
// Package common 公共组件
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"server/config"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultBatchSize 默认批量操作大小
|
||||||
|
const DefaultBatchSize = 100
|
||||||
|
|
||||||
|
// BaseMapper 泛型 Mapper 基类,封装通用 CRUD 操作
|
||||||
|
type BaseMapper[T any] struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBaseMapper 创建 BaseMapper 实例
|
||||||
|
func NewBaseMapper[T any]() *BaseMapper[T] {
|
||||||
|
return &BaseMapper[T]{
|
||||||
|
db: config.DB,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDB 获取数据库实例(允许子类覆盖)
|
||||||
|
func (m *BaseMapper[T]) GetDB() *gorm.DB {
|
||||||
|
return m.db
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindAll 分页查询
|
||||||
|
func (m *BaseMapper[T]) FindAll(page, size int) ([]T, int64, error) {
|
||||||
|
var items []T
|
||||||
|
var total int64
|
||||||
|
query := m.GetDB().Model(new(T))
|
||||||
|
query.Count(&total)
|
||||||
|
err := query.Offset((page - 1) * size).Limit(size).Find(&items).Error
|
||||||
|
return items, total, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindByID 根据 ID 查询
|
||||||
|
func (m *BaseMapper[T]) FindByID(id string) (*T, error) {
|
||||||
|
var item T
|
||||||
|
err := m.GetDB().First(&item, "id = ?", id).Error
|
||||||
|
return &item, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create 创建单个记录
|
||||||
|
func (m *BaseMapper[T]) Create(item *T) error {
|
||||||
|
return m.GetDB().Create(item).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 更新单个记录
|
||||||
|
func (m *BaseMapper[T]) Update(item *T) error {
|
||||||
|
return m.GetDB().Save(item).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateFields 更新指定字段
|
||||||
|
func (m *BaseMapper[T]) UpdateFields(id string, fields map[string]interface{}) error {
|
||||||
|
return m.GetDB().Model(new(T)).Where("id = ?", id).Updates(fields).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 删除记录
|
||||||
|
func (m *BaseMapper[T]) Delete(id string) error {
|
||||||
|
return m.GetDB().Delete(new(T), "id = ?", id).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchCreate 批量创建
|
||||||
|
func (m *BaseMapper[T]) BatchCreate(items []T, batchSize int) error {
|
||||||
|
if batchSize <= 0 {
|
||||||
|
batchSize = DefaultBatchSize
|
||||||
|
}
|
||||||
|
return m.GetDB().CreateInBatches(items, batchSize).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchUpdate 批量更新
|
||||||
|
func (m *BaseMapper[T]) BatchUpdate(items []T) error {
|
||||||
|
return m.GetDB().Save(items).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchUpsert 批量插入或更新
|
||||||
|
func (m *BaseMapper[T]) BatchUpsert(items []T, updateColumns []string) error {
|
||||||
|
return m.GetDB().Clauses(clause.OnConflict{
|
||||||
|
Columns: []clause.Column{{Name: "id"}},
|
||||||
|
DoUpdates: clause.AssignmentColumns(updateColumns),
|
||||||
|
}).CreateInBatches(items, DefaultBatchSize).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchDelete 批量删除
|
||||||
|
func (m *BaseMapper[T]) BatchDelete(ids []string) error {
|
||||||
|
return m.GetDB().Delete(new(T), "id IN ?", ids).Error
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
// Package common 公共组件
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BaseService 泛型 Service 基类,封装通用业务逻辑
|
||||||
|
type BaseService[T any] struct {
|
||||||
|
mapper *BaseMapper[T]
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBaseService 创建 BaseService 实例
|
||||||
|
func NewBaseService[T any]() *BaseService[T] {
|
||||||
|
return &BaseService[T]{
|
||||||
|
mapper: NewBaseMapper[T](),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMapper 获取 Mapper 实例
|
||||||
|
func (s *BaseService[T]) GetMapper() *BaseMapper[T] {
|
||||||
|
return s.mapper
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 分页查询
|
||||||
|
func (s *BaseService[T]) List(page, size int) ([]T, int64, error) {
|
||||||
|
return s.mapper.FindAll(page, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByID 根据 ID 获取
|
||||||
|
func (s *BaseService[T]) GetByID(id string) (*T, error) {
|
||||||
|
return s.mapper.FindByID(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create 创建记录(自动生成 ID)
|
||||||
|
func (s *BaseService[T]) Create(item *T) error {
|
||||||
|
// 通过反射设置 ID 字段
|
||||||
|
if err := setID(item); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.mapper.Create(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 更新记录
|
||||||
|
func (s *BaseService[T]) Update(item *T) error {
|
||||||
|
return s.mapper.Update(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateFields 更新指定字段
|
||||||
|
func (s *BaseService[T]) UpdateFields(id string, fields map[string]interface{}) error {
|
||||||
|
return s.mapper.UpdateFields(id, fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 删除记录
|
||||||
|
func (s *BaseService[T]) Delete(id string) error {
|
||||||
|
return s.mapper.Delete(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchCreate 批量创建(自动生成 ID)
|
||||||
|
func (s *BaseService[T]) BatchCreate(items []T) error {
|
||||||
|
for i := range items {
|
||||||
|
if err := setID(&items[i]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s.mapper.BatchCreate(items, DefaultBatchSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchUpdate 批量更新
|
||||||
|
func (s *BaseService[T]) BatchUpdate(items []T) error {
|
||||||
|
return s.mapper.BatchUpdate(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchUpsert 批量插入或更新
|
||||||
|
func (s *BaseService[T]) BatchUpsert(items []T, updateColumns []string) error {
|
||||||
|
for i := range items {
|
||||||
|
if err := setID(&items[i]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s.mapper.BatchUpsert(items, updateColumns)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchDelete 批量删除
|
||||||
|
func (s *BaseService[T]) BatchDelete(ids []string) error {
|
||||||
|
return s.mapper.BatchDelete(ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
// setID 通过反射设置 ID 字段
|
||||||
|
func setID(item interface{}) error {
|
||||||
|
val := reflect.ValueOf(item).Elem()
|
||||||
|
|
||||||
|
// 如果当前类型是指针,再次解引用以获取实际的 Struct
|
||||||
|
if val.Kind() == reflect.Ptr {
|
||||||
|
val = val.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
if val.Kind() == reflect.Struct {
|
||||||
|
idField := val.FieldByName("ID")
|
||||||
|
if idField.IsValid() && idField.Kind() == reflect.String {
|
||||||
|
// 如果 ID 为空,生成新 ID
|
||||||
|
if idField.String() == "" {
|
||||||
|
idField.SetString(GenerateStringID())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
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 = "Bearer "
|
||||||
|
)
|
||||||
|
|
||||||
|
// 业务状态常量
|
||||||
|
const (
|
||||||
|
StateActive = "1" // 使用中
|
||||||
|
StateInactive = "0" // 未使用/已删除
|
||||||
|
StateHistory = "2" // 历史记录
|
||||||
|
)
|
||||||
|
|
||||||
|
// 数值常量
|
||||||
|
const (
|
||||||
|
Number0 = 0
|
||||||
|
Number0p75 = 0.75
|
||||||
|
Number0p5 = 0.5
|
||||||
|
Number5 = 5
|
||||||
|
Number7p5 = 7.5
|
||||||
|
Number100 = 100
|
||||||
|
)
|
||||||
|
|
||||||
|
// 数据类型常量
|
||||||
|
const (
|
||||||
|
TypeNormal = "1" // 普通类
|
||||||
|
TypeArt = "2" // 艺术类
|
||||||
|
|
||||||
|
// YxConstant 相关常量
|
||||||
|
NowYear = "2026"
|
||||||
|
|
||||||
|
// 录取方式常量
|
||||||
|
CulturalControlLineGuo = "文线专排"
|
||||||
|
SpecialControlLineGuo = "专过文排"
|
||||||
|
CulturalControlLineGuoMain = "文过专排主科"
|
||||||
|
|
||||||
|
W1Z1 = "文*1+专*1"
|
||||||
|
W1JiaZ1 = "文+专"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
OldYearList = []string{"2025", "2024"}
|
||||||
|
ShowOldYearList = []string{"2025", "2024", "2023"}
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
// Package common 公共包
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"server/modules/system/entity"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetLoginUser 从上下文获取当前登录用户
|
||||||
|
// 在Controller中使用: user := common.GetLoginUser(c)
|
||||||
|
func GetLoginUser(c *gin.Context) *entity.LoginUser {
|
||||||
|
value, exists := c.Get(ContextUserKey)
|
||||||
|
if !exists {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if user, ok := value.(*entity.LoginUser); ok {
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLoginUserID 获取当前登录用户ID
|
||||||
|
func GetLoginUserID(c *gin.Context) string {
|
||||||
|
user := GetLoginUser(c)
|
||||||
|
if user != nil {
|
||||||
|
return user.ID
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLoginUsername 获取当前登录用户名
|
||||||
|
func GetLoginUsername(c *gin.Context) string {
|
||||||
|
user := GetLoginUser(c)
|
||||||
|
if user != nil {
|
||||||
|
return user.Username
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPage 从上下文获取页码
|
||||||
|
func GetPage(c *gin.Context) int {
|
||||||
|
pageStr := c.DefaultQuery("page", "1")
|
||||||
|
var page int
|
||||||
|
fmt.Sscanf(pageStr, "%d", &page)
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
return page
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSize 从上下文获取每页大小
|
||||||
|
func GetSize(c *gin.Context) int {
|
||||||
|
sizeStr := c.DefaultQuery("size", "10")
|
||||||
|
var size int
|
||||||
|
fmt.Sscanf(sizeStr, "%d", &size)
|
||||||
|
if size < 1 {
|
||||||
|
size = 10
|
||||||
|
}
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"server/common/snowflake"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
defaultSnowflake *snowflake.Snowflake
|
||||||
|
once sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
// InitGenerator 初始化雪花算法生成器
|
||||||
|
// workerId: 工作机器ID (0 ~ 31)
|
||||||
|
// datacenterId: 数据中心ID (0 ~ 31)
|
||||||
|
// 如果不需要区分数据中心,可以将 datacenterId 设置为 0
|
||||||
|
func InitGenerator(workerId, datacenterId int64) error {
|
||||||
|
// 先校验参数
|
||||||
|
if workerId < 0 || workerId > 31 {
|
||||||
|
return errors.New("workerId must be between 0 and 31")
|
||||||
|
}
|
||||||
|
if datacenterId < 0 || datacenterId > 31 {
|
||||||
|
return errors.New("datacenterId must be between 0 and 31")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行初始化
|
||||||
|
once.Do(func() {
|
||||||
|
var err error
|
||||||
|
defaultSnowflake, err = snowflake.NewSnowflake(workerId, datacenterId)
|
||||||
|
if err != nil {
|
||||||
|
panic("InitGenerator failed: " + err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitGeneratorWithWorkerID 仅使用 workerId 初始化(兼容旧版本)
|
||||||
|
// datacenterId 默认为 0
|
||||||
|
func InitGeneratorWithWorkerID(workerID int64) error {
|
||||||
|
return InitGenerator(workerID, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getInstance 获取单例实例
|
||||||
|
func getInstance() *snowflake.Snowflake {
|
||||||
|
once.Do(func() {
|
||||||
|
// 默认值:workerId=1, datacenterId=0
|
||||||
|
var err error
|
||||||
|
defaultSnowflake, err = snowflake.NewSnowflake(1, 0)
|
||||||
|
if err != nil {
|
||||||
|
// 默认参数如果还失败,直接 panic
|
||||||
|
panic("Snowflake getInstance failed: " + err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 防御性编程:如果 once.Do 已经执行过(例如被 InitGenerator 执行了),
|
||||||
|
// 但因为 panic 或其他异常导致 defaultSnowflake 仍为 nil,这里进行补救
|
||||||
|
if defaultSnowflake == nil {
|
||||||
|
// 此时忽略 sync.Once,直接强制初始化,防止 nil pointer crash
|
||||||
|
// 使用默认安全值 (1, 0)
|
||||||
|
defaultSnowflake, _ = snowflake.NewSnowflake(1, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultSnowflake
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateLongID 生成 64 位整型 ID
|
||||||
|
func GenerateLongID() int64 {
|
||||||
|
id, err := getInstance().NextId()
|
||||||
|
if err != nil {
|
||||||
|
// 极端情况:时间回拨
|
||||||
|
// 返回 0 或使用时间戳作为备用方案
|
||||||
|
panic("GenerateLongID failed: " + err.Error())
|
||||||
|
}
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateStringID 生成字符串 ID
|
||||||
|
func GenerateStringID() string {
|
||||||
|
return strconv.FormatInt(GenerateLongID(), 10)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,210 @@
|
||||||
|
// Package common 日志工具
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"server/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 日志级别
|
||||||
|
const (
|
||||||
|
LevelDebug = iota
|
||||||
|
LevelInfo
|
||||||
|
LevelWarn
|
||||||
|
LevelError
|
||||||
|
)
|
||||||
|
|
||||||
|
var levelNames = map[int]string{
|
||||||
|
LevelDebug: "DEBUG",
|
||||||
|
LevelInfo: "INFO",
|
||||||
|
LevelWarn: "WARN",
|
||||||
|
LevelError: "ERROR",
|
||||||
|
}
|
||||||
|
|
||||||
|
var levelValues = map[string]int{
|
||||||
|
"debug": LevelDebug,
|
||||||
|
"info": LevelInfo,
|
||||||
|
"warn": LevelWarn,
|
||||||
|
"error": LevelError,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logger 日志记录器
|
||||||
|
type Logger struct {
|
||||||
|
level int
|
||||||
|
file *os.File
|
||||||
|
htmlWriter *htmlLogWriter
|
||||||
|
mu sync.Mutex
|
||||||
|
console bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
defaultLogger *Logger
|
||||||
|
startCount int
|
||||||
|
onceInit sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
// InitLogger 初始化日志
|
||||||
|
func InitLogger() {
|
||||||
|
once.Do(func() {
|
||||||
|
cfg := config.AppConfig.Log
|
||||||
|
level := levelValues[cfg.Level]
|
||||||
|
|
||||||
|
// 创建日志目录
|
||||||
|
if err := os.MkdirAll(cfg.Dir, 0755); err != nil {
|
||||||
|
fmt.Println("创建日志目录失败:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算启动次数
|
||||||
|
startCount = getStartCount(cfg.Dir)
|
||||||
|
|
||||||
|
// 创建日志文件
|
||||||
|
filename := fmt.Sprintf("%s-%d.html", time.Now().Format("2006-01-02"), startCount)
|
||||||
|
logFilepath := filepath.Join(cfg.Dir, filename)
|
||||||
|
|
||||||
|
file, err := os.OpenFile(logFilepath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("创建日志文件失败:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlWriter := newHtmlLogWriter(file)
|
||||||
|
|
||||||
|
defaultLogger = &Logger{
|
||||||
|
level: level,
|
||||||
|
file: file,
|
||||||
|
htmlWriter: htmlWriter,
|
||||||
|
console: cfg.Console,
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("日志文件: %s\n", logFilepath)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// getStartCount 获取今日启动次数
|
||||||
|
func getStartCount(dir string) int {
|
||||||
|
today := time.Now().Format("2006-01-02")
|
||||||
|
pattern := filepath.Join(dir, today+"*.html")
|
||||||
|
matches, _ := filepath.Glob(pattern)
|
||||||
|
return len(matches) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseLogger 关闭日志
|
||||||
|
func CloseLogger() {
|
||||||
|
if defaultLogger != nil && defaultLogger.file != nil {
|
||||||
|
defaultLogger.htmlWriter.writeFooter()
|
||||||
|
defaultLogger.file.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) log(level int, format string, args ...interface{}) {
|
||||||
|
if l == nil || level < l.level {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
|
||||||
|
msg := fmt.Sprintf(format, args...)
|
||||||
|
timestamp := time.Now().Format("2006-01-02 15:04:05.000")
|
||||||
|
levelName := levelNames[level]
|
||||||
|
|
||||||
|
// 写入HTML文件
|
||||||
|
l.htmlWriter.writeLog(timestamp, levelName, msg)
|
||||||
|
|
||||||
|
// 输出到控制台
|
||||||
|
if l.console {
|
||||||
|
fmt.Printf("[%s] [%s] %s\n", timestamp, levelName, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogDebug 调试日志
|
||||||
|
func LogDebug(format string, args ...interface{}) {
|
||||||
|
if defaultLogger != nil {
|
||||||
|
defaultLogger.log(LevelDebug, format, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogInfo 信息日志
|
||||||
|
func LogInfo(format string, args ...interface{}) {
|
||||||
|
if defaultLogger != nil {
|
||||||
|
defaultLogger.log(LevelInfo, format, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogWarn 警告日志
|
||||||
|
func LogWarn(format string, args ...interface{}) {
|
||||||
|
if defaultLogger != nil {
|
||||||
|
defaultLogger.log(LevelWarn, format, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogError 错误日志
|
||||||
|
func LogError(format string, args ...interface{}) {
|
||||||
|
if defaultLogger != nil {
|
||||||
|
defaultLogger.log(LevelError, format, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 简短别名
|
||||||
|
var (
|
||||||
|
Debug = LogDebug
|
||||||
|
Info = LogInfo
|
||||||
|
Warn = LogWarn
|
||||||
|
)
|
||||||
|
|
||||||
|
// htmlLogWriter HTML日志写入器
|
||||||
|
type htmlLogWriter struct {
|
||||||
|
writer io.Writer
|
||||||
|
initialized bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHtmlLogWriter(w io.Writer) *htmlLogWriter {
|
||||||
|
hw := &htmlLogWriter{writer: w}
|
||||||
|
hw.writeHeader()
|
||||||
|
return hw
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *htmlLogWriter) writeHeader() {
|
||||||
|
html := `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>应用日志</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Consolas, monospace; background: #1e1e1e; color: #d4d4d4; padding: 20px; }
|
||||||
|
.log { margin: 2px 0; padding: 4px 8px; border-radius: 3px; }
|
||||||
|
.DEBUG { background: #2d2d2d; color: #9cdcfe; }
|
||||||
|
.INFO { background: #1e3a1e; color: #4ec9b0; }
|
||||||
|
.WARN { background: #3a3a1e; color: #dcdcaa; }
|
||||||
|
.ERROR { background: #3a1e1e; color: #f14c4c; }
|
||||||
|
.time { color: #808080; }
|
||||||
|
.level { font-weight: bold; width: 60px; display: inline-block; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>应用日志 - ` + time.Now().Format("2006-01-02") + `</h2>
|
||||||
|
<div id="logs">
|
||||||
|
`
|
||||||
|
h.writer.Write([]byte(html))
|
||||||
|
h.initialized = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *htmlLogWriter) writeLog(timestamp, level, msg string) {
|
||||||
|
html := fmt.Sprintf(`<div class="log %s"><span class="time">[%s]</span> <span class="level">[%s]</span> %s</div>
|
||||||
|
`, level, timestamp, level, msg)
|
||||||
|
h.writer.Write([]byte(html))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *htmlLogWriter) writeFooter() {
|
||||||
|
html := `</div>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
h.writer.Write([]byte(html))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,190 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/des"
|
||||||
|
"crypto/md5"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PasswordUtil 对应 Java 类的常量
|
||||||
|
const (
|
||||||
|
// DefaultSalt 对应 Java 中的 SALT = "63293188"
|
||||||
|
DefaultSalt = "63293188"
|
||||||
|
// IterationCount 对应 Java 中的 ITERATIONCOUNT = 1000
|
||||||
|
IterationCount = 1000
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetSalt 生成 8 字节的随机盐
|
||||||
|
func GetSalt() ([]byte, error) {
|
||||||
|
salt := make([]byte, 8)
|
||||||
|
_, err := rand.Read(salt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return salt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStaticSalt 获取静态盐
|
||||||
|
func GetStaticSalt() []byte {
|
||||||
|
return []byte(DefaultSalt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt 加密
|
||||||
|
// plaintext: 明文
|
||||||
|
// password: 密码
|
||||||
|
// salt: 盐 (传入 string, 对应 Java 的 salt 参数)
|
||||||
|
func Encrypt(plaintext, password, salt string) (string, error) {
|
||||||
|
// 1. 生成 Key 和 IV
|
||||||
|
key, iv := deriveKeyAndIV(password, salt, IterationCount)
|
||||||
|
|
||||||
|
// 2. 创建 DES Cipher
|
||||||
|
block, err := des.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 处理数据 (UTF-8 转 bytes 并填充)
|
||||||
|
data := []byte(plaintext)
|
||||||
|
data = pkcs5Padding(data, block.BlockSize())
|
||||||
|
|
||||||
|
// 4. 加密 (CBC 模式)
|
||||||
|
blockMode := cipher.NewCBCEncrypter(block, iv)
|
||||||
|
crypted := make([]byte, len(data))
|
||||||
|
blockMode.CryptBlocks(crypted, data)
|
||||||
|
|
||||||
|
// 5. 转十六进制字符串 (对应 Java 的 bytesToHexString)
|
||||||
|
// Java 的 Integer.toHexString 产生的是小写,这里保持一致,
|
||||||
|
// 但通常十六进制可以互通。
|
||||||
|
return hex.EncodeToString(crypted), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt 解密
|
||||||
|
// ciphertext: 密文 (Hex 字符串)
|
||||||
|
// password: 密码
|
||||||
|
// salt: 盐
|
||||||
|
func Decrypt(ciphertext, password, salt string) (string, error) {
|
||||||
|
// 1. 生成 Key 和 IV
|
||||||
|
key, iv := deriveKeyAndIV(password, salt, IterationCount)
|
||||||
|
|
||||||
|
// 2. Hex 字符串转 bytes
|
||||||
|
decodedBytes, err := hex.DecodeString(ciphertext)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 创建 DES Cipher
|
||||||
|
block, err := des.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 解密 (CBC 模式)
|
||||||
|
blockMode := cipher.NewCBCDecrypter(block, iv)
|
||||||
|
// 密文长度检查
|
||||||
|
if len(decodedBytes)%block.BlockSize() != 0 {
|
||||||
|
return "", errors.New("ciphertext is not a multiple of the block size")
|
||||||
|
}
|
||||||
|
origData := make([]byte, len(decodedBytes))
|
||||||
|
blockMode.CryptBlocks(origData, decodedBytes)
|
||||||
|
|
||||||
|
// 5. 去除填充
|
||||||
|
origData, err = pkcs5UnPadding(origData)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(origData), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// deriveKeyAndIV 模拟 Java PBEWithMD5AndDES 的密钥派生逻辑
|
||||||
|
// 逻辑:MD5(password || salt) -> 迭代 -> 结果前8字节是Key,后8字节是IV
|
||||||
|
func deriveKeyAndIV(password, salt string, iterations int) ([]byte, []byte) {
|
||||||
|
// Java PBEKeySpec 处理 password 为 char[],通常转 byte 时依赖编码,
|
||||||
|
// 此处假设使用 UTF-8 兼容(Java 默认行为通常如此,或者 ASCII)
|
||||||
|
passBytes := []byte(password)
|
||||||
|
saltBytes := []byte(salt)
|
||||||
|
|
||||||
|
// 第一次迭代: Hash(pass + salt)
|
||||||
|
hash := md5.New()
|
||||||
|
hash.Write(passBytes)
|
||||||
|
hash.Write(saltBytes)
|
||||||
|
derived := hash.Sum(nil)
|
||||||
|
|
||||||
|
// 后续迭代: Hash(prev_hash)
|
||||||
|
for i := 1; i < iterations; i++ {
|
||||||
|
hash.Reset()
|
||||||
|
hash.Write(derived)
|
||||||
|
derived = hash.Sum(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MD5 结果是 16 字节
|
||||||
|
// DES Key 需要 8 字节,IV 需要 8 字节
|
||||||
|
// 正好平分
|
||||||
|
key := derived[:8]
|
||||||
|
iv := derived[8:]
|
||||||
|
|
||||||
|
return key, iv
|
||||||
|
}
|
||||||
|
|
||||||
|
// pkcs5Padding 填充
|
||||||
|
func pkcs5Padding(ciphertext []byte, blockSize int) []byte {
|
||||||
|
padding := blockSize - len(ciphertext)%blockSize
|
||||||
|
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||||
|
return append(ciphertext, padtext...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// pkcs5UnPadding 去除填充
|
||||||
|
func pkcs5UnPadding(origData []byte) ([]byte, error) {
|
||||||
|
length := len(origData)
|
||||||
|
if length == 0 {
|
||||||
|
return nil, errors.New("decryption error: output length is zero")
|
||||||
|
}
|
||||||
|
unpadding := int(origData[length-1])
|
||||||
|
if length < unpadding {
|
||||||
|
return nil, errors.New("decryption error: invalid padding")
|
||||||
|
}
|
||||||
|
return origData[:(length - unpadding)], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 测试主函数 ---
|
||||||
|
func main() {
|
||||||
|
// 测试数据
|
||||||
|
plaintext := "admin"
|
||||||
|
password := "Wang5322570"
|
||||||
|
//salt := DefaultSalt
|
||||||
|
salt := "RCGTeGiH"
|
||||||
|
|
||||||
|
fmt.Println("原文:", plaintext)
|
||||||
|
fmt.Println("密码:", password)
|
||||||
|
fmt.Println("盐值:", salt)
|
||||||
|
|
||||||
|
// 加密
|
||||||
|
encrypted, err := Encrypt(plaintext, password, salt)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("加密失败:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Java代码可能输出小写或手动处理,这里使用 hex.EncodeToString (小写)
|
||||||
|
// 如果需要大写可以使用 strings.ToUpper(encrypted)
|
||||||
|
fmt.Println("加密密文 (Hex):", encrypted)
|
||||||
|
|
||||||
|
// 解密
|
||||||
|
decrypted, err := Decrypt(encrypted, password, salt)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("解密失败:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println("解密原文:", decrypted)
|
||||||
|
|
||||||
|
// 验证一致性
|
||||||
|
if plaintext == decrypted {
|
||||||
|
fmt.Println(">> 测试通过!Go版本与逻辑一致。")
|
||||||
|
} else {
|
||||||
|
fmt.Println(">> 测试失败!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
// Package common 公共包
|
||||||
|
package common
|
||||||
|
|
||||||
|
import "github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
// Response 统一响应结构
|
||||||
|
type Response struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data interface{} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PageResponse 分页响应
|
||||||
|
type PageResponse struct {
|
||||||
|
List interface{} `json:"list"`
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
Size int `json:"size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Success(c *gin.Context, data interface{}) {
|
||||||
|
c.JSON(200, Response{Code: 200, Message: "success", Data: data})
|
||||||
|
}
|
||||||
|
|
||||||
|
func SuccessPage(c *gin.Context, list interface{}, total int64, page, size int) {
|
||||||
|
c.JSON(200, Response{
|
||||||
|
Code: 200,
|
||||||
|
Message: "success",
|
||||||
|
Data: PageResponse{List: list, Total: total, Page: page, Size: size},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Error(c *gin.Context, code int, message string) {
|
||||||
|
c.JSON(code, Response{Code: code, Message: message, Data: nil})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,197 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"regexp"
|
||||||
|
"server/modules/user/vo"
|
||||||
|
"server/modules/yx/dto"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ScoreCalculator 分数计算工具函数集合
|
||||||
|
// 移植自 Java 版本的 ScoreUtil.java
|
||||||
|
|
||||||
|
var (
|
||||||
|
BigDecimal0 = 0.0
|
||||||
|
BigDecimal05 = 0.5
|
||||||
|
BigDecimal075 = 0.75
|
||||||
|
BigDecimal0133 = 0.133
|
||||||
|
BigDecimal0667 = 0.667
|
||||||
|
BigDecimal004 = 0.04
|
||||||
|
BigDecimal0467 = 0.467
|
||||||
|
BigDecimal0067 = 0.067
|
||||||
|
BigDecimal0333 = 0.333
|
||||||
|
BigDecimal0093 = 0.093
|
||||||
|
BigDecimal02 = 0.2
|
||||||
|
BigDecimal07 = 0.7
|
||||||
|
BigDecimal03 = 0.3
|
||||||
|
BigDecimal04 = 0.4
|
||||||
|
BigDecimal06 = 0.6
|
||||||
|
BigDecimal1 = 1.0
|
||||||
|
BigDecimal100 = 100.0
|
||||||
|
BigDecimal150 = 150.0
|
||||||
|
BigDecimal95x = 95.0
|
||||||
|
BigDecimal85x = 85.0
|
||||||
|
BigDecimal7p5 = 7.5
|
||||||
|
BigDecimal5 = 5.0
|
||||||
|
)
|
||||||
|
|
||||||
|
// ComputeHistoryMajorEnrollScoreLineDifferenceWithRulesEnrollProbability 计算历年录取分差
|
||||||
|
func ComputeHistoryMajorEnrollScoreLineDifferenceWithRulesEnrollProbability(majorType string,
|
||||||
|
rulesEnrollProbability, probabilityOperator string,
|
||||||
|
HistoryMajorEnrollMap map[string]dto.YxHistoryMajorEnrollDTO) map[string]any {
|
||||||
|
|
||||||
|
if len(HistoryMajorEnrollMap) == 0 {
|
||||||
|
return map[string]any{"scoreDifference": 0.0}
|
||||||
|
}
|
||||||
|
|
||||||
|
sum := 0.0
|
||||||
|
validYearCount := 0
|
||||||
|
|
||||||
|
for _, enrollData := range HistoryMajorEnrollMap {
|
||||||
|
admissionLine := enrollData.AdmissionLine
|
||||||
|
controlLine := enrollData.ControlLine
|
||||||
|
if admissionLine <= 0 || controlLine <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// 计算分差
|
||||||
|
currentDiff := admissionLine - controlLine
|
||||||
|
|
||||||
|
// currentDiff := enrollData.ScoreLineDifference
|
||||||
|
|
||||||
|
// 特殊逻辑:高职高专(非体育类)需计算分差率(分差/省控线)
|
||||||
|
// Java: boolean isVocationalCollege = "高职高专".equals(enrollData.getBatch());
|
||||||
|
// boolean isSportsMajor = "体育类".equals(enrollData.getMajorType());
|
||||||
|
if "体育类" == majorType {
|
||||||
|
if "2024" == enrollData.Year && "专过文排" != enrollData.EnrollmentCode && "文过专排" != enrollData.EnrollmentCode {
|
||||||
|
currentDiff = currentDiff * Number7p5
|
||||||
|
} else if "2024" == enrollData.Year && "文过专排" == enrollData.EnrollmentCode { // Placeholder
|
||||||
|
currentDiff = currentDiff * Number5
|
||||||
|
} else if "2023" == enrollData.Year {
|
||||||
|
continue
|
||||||
|
} else if rulesEnrollProbability == enrollData.RulesEnrollProbability { // Need field
|
||||||
|
sum += currentDiff
|
||||||
|
validYearCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 非高职高专 or 体育类:检查录取方式是否一致
|
||||||
|
if probabilityOperator != enrollData.ProbabilityOperator {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sum += currentDiff
|
||||||
|
validYearCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
averageDiff := 0.0
|
||||||
|
if validYearCount > 0 {
|
||||||
|
averageDiff = sum / float64(validYearCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[string]any{"scoreDifference": averageDiff}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertIntoScore 计算折合分
|
||||||
|
func ConvertIntoScore(rulesEnrollProbability string, culturalScore, professionalScore float64, operator string) float64 {
|
||||||
|
score := 0.0
|
||||||
|
if operator != "" {
|
||||||
|
// operator: 例如: "文*0.5+专*0.5"
|
||||||
|
parts := strings.Split(operator, "+")
|
||||||
|
for _, part := range parts {
|
||||||
|
subParts := strings.Split(part, "*")
|
||||||
|
if len(subParts) == 2 {
|
||||||
|
ratio, _ := strconv.ParseFloat(subParts[1], 64)
|
||||||
|
if strings.Contains(subParts[0], "文") {
|
||||||
|
score += culturalScore * ratio
|
||||||
|
} else {
|
||||||
|
score += professionalScore * ratio
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return round(score, 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CrossingControlLine 判断是否过省控线
|
||||||
|
func CrossingControlLine(rulesEnrollProbability string, culturalScore, professionalScore, culturalControlLine, specialControlLine float64) bool {
|
||||||
|
if rulesEnrollProbability == CulturalControlLineGuo {
|
||||||
|
return culturalScore >= culturalControlLine
|
||||||
|
} else if rulesEnrollProbability == SpecialControlLineGuo {
|
||||||
|
return professionalScore >= specialControlLine
|
||||||
|
} else if rulesEnrollProbability == CulturalControlLineGuoMain {
|
||||||
|
return culturalScore >= specialControlLine
|
||||||
|
}
|
||||||
|
return culturalScore >= culturalControlLine && professionalScore >= specialControlLine
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommonCheckEnrollProbability 计算录取率
|
||||||
|
func CommonCheckEnrollProbability(nowYearDiff, historyThreeYearDiff float64) float64 {
|
||||||
|
enrollProbability := 0.0
|
||||||
|
if nowYearDiff == 0 && historyThreeYearDiff > 0 {
|
||||||
|
enrollProbability = 75.0 / historyThreeYearDiff
|
||||||
|
} else if nowYearDiff == 0 && historyThreeYearDiff <= 0 {
|
||||||
|
enrollProbability = 50.0
|
||||||
|
} else if nowYearDiff > 0 && historyThreeYearDiff <= 0 {
|
||||||
|
enrollProbability = nowYearDiff * 100 * 0.75
|
||||||
|
} else if nowYearDiff < 0 && historyThreeYearDiff <= 0 {
|
||||||
|
enrollProbability = 0.0
|
||||||
|
} else {
|
||||||
|
// (当前年分差/去年分差)*0.75 * 100
|
||||||
|
if historyThreeYearDiff != 0 {
|
||||||
|
enrollProbability = (nowYearDiff / historyThreeYearDiff) * 100 * 0.75
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return enrollProbability
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommonCheckEnrollProbabilityBeilv 录取率倍率调整
|
||||||
|
func CommonCheckEnrollProbabilityBeilv(enrollProbability float64) float64 {
|
||||||
|
if enrollProbability > 150 {
|
||||||
|
return 95.0
|
||||||
|
} else if enrollProbability > 100 {
|
||||||
|
return 85.0
|
||||||
|
} else if enrollProbability <= 0 {
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
return enrollProbability
|
||||||
|
}
|
||||||
|
|
||||||
|
// OtherScoreJudge 其他录取要求
|
||||||
|
func OtherScoreJudge(professionalScore float64, userScoreVO vo.UserScoreVO, schoolMajorDTO dto.SchoolMajorDTO) bool {
|
||||||
|
// 简单实现,参考 Java 逻辑
|
||||||
|
if schoolMajorDTO.EnglishScoreLimitation > 0 && userScoreVO.EnglishScore < schoolMajorDTO.EnglishScoreLimitation {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if schoolMajorDTO.CulturalScoreLimitation > 0 && userScoreVO.CulturalScore < schoolMajorDTO.CulturalScoreLimitation {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// 专业分限制等...
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasComputeEnrollProbabilityPermissions 判断是否有权限计算
|
||||||
|
func HasComputeEnrollProbabilityPermissions(nowBatch, majorBatch string) bool {
|
||||||
|
return "" != nowBatch && "" != majorBatch
|
||||||
|
}
|
||||||
|
|
||||||
|
// round 四舍五入
|
||||||
|
func round(val float64, precision int) float64 {
|
||||||
|
ratio := math.Pow(10, float64(precision))
|
||||||
|
return math.Round(val*ratio) / ratio
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplaceLastZeroChar 格式化操作符字符串 (Java: replaceLastZeroChar)
|
||||||
|
func ReplaceLastZeroChar(input string) string {
|
||||||
|
if input == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
re := regexp.MustCompile(`(\d+\.\d*?)0+(\D|$)`)
|
||||||
|
input = re.ReplaceAllString(input, "$1$2")
|
||||||
|
input = strings.ReplaceAll(input, "1.", "1")
|
||||||
|
// 简化处理
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
package snowflake
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 定义常量
|
||||||
|
const (
|
||||||
|
// 位数分配
|
||||||
|
sequenceBits = 12 // 序列号占用的位数
|
||||||
|
workerIdBits = 5 // 工作机器ID占用的位数
|
||||||
|
datacenterIdBits = 5 // 数据中心ID占用的位数
|
||||||
|
|
||||||
|
// 最大值
|
||||||
|
maxSequence = -1 ^ (-1 << sequenceBits) // 4095
|
||||||
|
maxWorkerId = -1 ^ (-1 << workerIdBits) // 31
|
||||||
|
maxDatacenterId = -1 ^ (-1 << datacenterIdBits) // 31
|
||||||
|
|
||||||
|
// 位移偏移量
|
||||||
|
workerIdShift = sequenceBits // 12
|
||||||
|
datacenterIdShift = sequenceBits + workerIdBits // 12 + 5 = 17
|
||||||
|
timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits // 12 + 5 + 5 = 22
|
||||||
|
)
|
||||||
|
|
||||||
|
// 起始时间戳 (纪元),可以使用程序上线的时间,这里设置为 2020-01-01 00:00:00 UTC
|
||||||
|
var epoch int64 = 1577836800000
|
||||||
|
|
||||||
|
// Snowflake 结构体
|
||||||
|
type Snowflake struct {
|
||||||
|
mu sync.Mutex // 互斥锁,保证并发安全
|
||||||
|
lastTime int64 // 上次生成ID的时间戳
|
||||||
|
workerId int64 // 工作机器ID
|
||||||
|
datacenterId int64 // 数据中心ID
|
||||||
|
sequence int64 // 当前毫秒内的序列号
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSnowflake 初始化一个 Snowflake 实例
|
||||||
|
// workerId: 工作机器ID (0 ~ 31)
|
||||||
|
// datacenterId: 数据中心ID (0 ~ 31)
|
||||||
|
func NewSnowflake(workerId, datacenterId int64) (*Snowflake, error) {
|
||||||
|
if workerId < 0 || workerId > maxWorkerId {
|
||||||
|
return nil, errors.New(fmt.Sprintf("worker Id can't be greater than %d or less than 0", maxWorkerId))
|
||||||
|
}
|
||||||
|
if datacenterId < 0 || datacenterId > maxDatacenterId {
|
||||||
|
return nil, errors.New(fmt.Sprintf("datacenter Id can't be greater than %d or less than 0", maxDatacenterId))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Snowflake{
|
||||||
|
lastTime: 0,
|
||||||
|
workerId: workerId,
|
||||||
|
datacenterId: datacenterId,
|
||||||
|
sequence: 0,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NextId 生成下一个 ID
|
||||||
|
func (s *Snowflake) NextId() (int64, error) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
// 获取当前时间戳(毫秒)
|
||||||
|
now := time.Now().UnixMilli()
|
||||||
|
|
||||||
|
// 如果当前时间小于上次生成ID的时间,说明时钟回拨,抛出异常
|
||||||
|
if now < s.lastTime {
|
||||||
|
return 0, errors.New(fmt.Sprintf("Clock moved backwards. Refusing to generate id for %d milliseconds", s.lastTime-now))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是同一毫秒内
|
||||||
|
if now == s.lastTime {
|
||||||
|
// 序列号自增
|
||||||
|
s.sequence = (s.sequence + 1) & maxSequence
|
||||||
|
// 如果序列号溢出(超过4095),则等待下一毫秒
|
||||||
|
if s.sequence == 0 {
|
||||||
|
now = s.waitNextMillis(now)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 不同毫秒,序列号重置为0
|
||||||
|
s.sequence = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新最后时间戳
|
||||||
|
s.lastTime = now
|
||||||
|
|
||||||
|
// 组装 ID
|
||||||
|
// (当前时间 - 起始时间) << 时间戳位移 | 数据中心ID << 数据中心位移 | 工作ID << 工作位移 | 序列号
|
||||||
|
id := ((now - epoch) << timestampLeftShift) |
|
||||||
|
(s.datacenterId << datacenterIdShift) |
|
||||||
|
(s.workerId << workerIdShift) |
|
||||||
|
s.sequence
|
||||||
|
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// waitNextMillis 阻塞等待下一毫秒
|
||||||
|
func (s *Snowflake) waitNextMillis(lastTime int64) int64 {
|
||||||
|
now := time.Now().UnixMilli()
|
||||||
|
for now <= lastTime {
|
||||||
|
now = time.Now().UnixMilli()
|
||||||
|
}
|
||||||
|
return now
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseId 解析 ID,用于调试或查看 ID 组成部分
|
||||||
|
func ParseId(id int64) map[string]interface{} {
|
||||||
|
timestamp := (id >> timestampLeftShift) + epoch
|
||||||
|
datacenterId := (id >> datacenterIdShift) & maxDatacenterId
|
||||||
|
workerId := (id >> workerIdShift) & maxWorkerId
|
||||||
|
sequence := id & maxSequence
|
||||||
|
|
||||||
|
return map[string]interface{}{
|
||||||
|
"id": id,
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"time_str": time.UnixMilli(timestamp).Format("2006-01-02 15:04:05.000"),
|
||||||
|
"datacenterId": datacenterId,
|
||||||
|
"workerId": workerId,
|
||||||
|
"sequence": sequence,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
package snowflake // 注意:这里必须是 package snowflake,不能是 main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 这是一个测试函数,用于验证功能
|
||||||
|
func TestGenerateID(t *testing.T) {
|
||||||
|
// 1. 初始化生成器
|
||||||
|
sf, err := NewSnowflake(1, 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("初始化失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("=== 开始生成 ID ===")
|
||||||
|
|
||||||
|
// 2. 生成几个 ID
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
id, err := sf.NextId()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("生成 ID 失败: %v", err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("生成 ID: %d\n", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 解析 ID 查看详情
|
||||||
|
id, _ := sf.NextId()
|
||||||
|
info := ParseId(id)
|
||||||
|
fmt.Printf("\nID 详情解析:\n")
|
||||||
|
fmt.Printf("ID: %d\n", info["id"])
|
||||||
|
fmt.Printf("时间: %s\n", info["time_str"])
|
||||||
|
fmt.Printf("数据中心: %d\n", info["datacenterId"])
|
||||||
|
fmt.Printf("工作机器: %d\n", info["workerId"])
|
||||||
|
fmt.Printf("序列号: %d\n", info["sequence"])
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
import "regexp"
|
||||||
|
|
||||||
|
// 验证表名格式的辅助函数
|
||||||
|
func IsValidTableName(tableName string) bool {
|
||||||
|
if tableName == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表名只能包含字母、数字、下划线和点号,且长度合理
|
||||||
|
matched, err := regexp.MatchString(`^[a-zA-Z_][a-zA-Z0-9_.]{0,100}$`, tableName)
|
||||||
|
return err == nil && matched
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
server:
|
||||||
|
port: 8081
|
||||||
|
worker_id: 1 # 工作机器ID (0-31),单实例使用1
|
||||||
|
datacenter_id: 0 # 数据中心ID (0-31),默认0 # 雪花算法机器ID (0-1023),分布式环境下不同实例需设置不同值
|
||||||
|
|
||||||
|
log:
|
||||||
|
level: debug
|
||||||
|
dir: logs
|
||||||
|
console: true
|
||||||
|
|
||||||
|
security:
|
||||||
|
enable: false
|
||||||
|
header_key: X-App-Sign
|
||||||
|
secret_key: yts@2025#secure
|
||||||
|
|
||||||
|
rate_limit:
|
||||||
|
enable: true
|
||||||
|
default:
|
||||||
|
interval: 2
|
||||||
|
max_requests: 3
|
||||||
|
rules:
|
||||||
|
/api/user/auth/login:
|
||||||
|
interval: 5
|
||||||
|
max_requests: 1
|
||||||
|
/api/yx-school-majors:
|
||||||
|
interval: 1
|
||||||
|
max_requests: 5
|
||||||
|
/api/user/score/save-score:
|
||||||
|
interval: 1
|
||||||
|
max_requests: 1
|
||||||
|
/user/major/list:
|
||||||
|
interval: 1
|
||||||
|
max_requests: 5
|
||||||
|
|
||||||
|
swagger:
|
||||||
|
user: admin
|
||||||
|
password: password
|
||||||
|
|
||||||
|
database:
|
||||||
|
driver: mysql
|
||||||
|
host: 10.13.13.1
|
||||||
|
port: 3306
|
||||||
|
database: yitisheng
|
||||||
|
username: root
|
||||||
|
password: "Db$7Hn#4Jm9Pq2!Xz"
|
||||||
|
charset: utf8mb4
|
||||||
|
max_idle_conns: 20
|
||||||
|
max_open_conns: 100
|
||||||
|
conn_max_lifetime: 1
|
||||||
|
log_mode: true
|
||||||
|
|
||||||
|
redis:
|
||||||
|
addr: 10.13.13.1:56379
|
||||||
|
password: "Rd@5Wk8#Nv3Yt6$Bm"
|
||||||
|
db: 1
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
// Package config 应用配置
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AppConfig 应用配置
|
||||||
|
var AppConfig = &appConfig{}
|
||||||
|
|
||||||
|
type appConfig struct {
|
||||||
|
Log LogConfig `yaml:"log"`
|
||||||
|
Server ServerConfig `yaml:"server"`
|
||||||
|
Security SecurityConfig `yaml:"security"`
|
||||||
|
RateLimit RateLimitConfig `yaml:"rate_limit"`
|
||||||
|
Swagger SwaggerConfig `yaml:"swagger"`
|
||||||
|
Database DatabaseConfig `yaml:"database"`
|
||||||
|
Redis RedisConfig `yaml:"redis"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogConfig 日志配置
|
||||||
|
type LogConfig struct {
|
||||||
|
Level string `yaml:"level"` // 日志级别
|
||||||
|
Dir string `yaml:"dir"` // 日志目录
|
||||||
|
Console bool `yaml:"console"` // 是否输出到控制台
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerConfig 服务配置
|
||||||
|
type ServerConfig struct {
|
||||||
|
Port int `yaml:"port"` // 服务端口
|
||||||
|
WorkerID int `yaml:"worker_id"` // 工作机器ID (0-31),用于雪花算法
|
||||||
|
DatacenterID int `yaml:"datacenter_id"` // 数据中心ID (0-31),用于雪花算法
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecurityConfig 安全配置
|
||||||
|
type SecurityConfig struct {
|
||||||
|
Enable bool `yaml:"enable"` // 是否启用
|
||||||
|
HeaderKey string `yaml:"header_key"` // 请求头字段名
|
||||||
|
SecretKey string `yaml:"secret_key"` // 签名密钥
|
||||||
|
}
|
||||||
|
|
||||||
|
// RateLimitConfig 限流配置
|
||||||
|
type RateLimitConfig struct {
|
||||||
|
Enable bool `yaml:"enable"` // 是否启用
|
||||||
|
Default RateLimitRule `yaml:"default"` // 默认规则
|
||||||
|
Rules map[string]RateLimitRule `yaml:"rules"` // 特定路径规则
|
||||||
|
}
|
||||||
|
|
||||||
|
// RateLimitRule 限流规则
|
||||||
|
type RateLimitRule struct {
|
||||||
|
Interval int `yaml:"interval"` // 时间间隔(秒)
|
||||||
|
MaxRequests int `yaml:"max_requests"` // 最大请求次数
|
||||||
|
}
|
||||||
|
|
||||||
|
// SwaggerConfig Swagger文档认证配置
|
||||||
|
type SwaggerConfig struct {
|
||||||
|
User string `yaml:"user"`
|
||||||
|
Password string `yaml:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DatabaseConfig 数据库配置
|
||||||
|
type DatabaseConfig struct {
|
||||||
|
Driver string `yaml:"driver"`
|
||||||
|
Host string `yaml:"host"`
|
||||||
|
Port int `yaml:"port"`
|
||||||
|
Database string `yaml:"database"`
|
||||||
|
Username string `yaml:"username"`
|
||||||
|
Password string `yaml:"password"`
|
||||||
|
Charset string `yaml:"charset"`
|
||||||
|
MaxIdleConns int `yaml:"max_idle_conns"`
|
||||||
|
MaxOpenConns int `yaml:"max_open_conns"`
|
||||||
|
ConnMaxLifetime int `yaml:"conn_max_lifetime"` // 小时
|
||||||
|
LogMode bool `yaml:"log_mode"` // 是否开启SQL日志
|
||||||
|
}
|
||||||
|
|
||||||
|
// RedisConfig Redis配置
|
||||||
|
type RedisConfig struct {
|
||||||
|
Addr string `yaml:"addr"`
|
||||||
|
Password string `yaml:"password"`
|
||||||
|
DB int `yaml:"db"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadConfig 加载配置
|
||||||
|
func LoadConfig() {
|
||||||
|
var configFile string
|
||||||
|
|
||||||
|
// 1. 优先检查命令行参数 (简单解析)
|
||||||
|
// 格式: ./app -c config.prod.yaml 或 ./app -config config.prod.yaml
|
||||||
|
args := os.Args
|
||||||
|
for i, arg := range args {
|
||||||
|
if (arg == "-c" || arg == "-config") && i+1 < len(args) {
|
||||||
|
configFile = args[i+1]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 如果命令行参数未指定,则根据环境变量查找
|
||||||
|
if configFile == "" {
|
||||||
|
env := os.Getenv("GO_ENV")
|
||||||
|
if env == "" {
|
||||||
|
env = "dev"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找顺序:
|
||||||
|
// 1. 当前目录 config.{env}.yaml (方便部署时直接放在执行文件旁)
|
||||||
|
// 2. config/config.{env}.yaml (开发习惯)
|
||||||
|
// 3. ../config/config.{env}.yaml (测试环境习惯)
|
||||||
|
searchPaths := []string{
|
||||||
|
fmt.Sprintf("config.%s.yaml", env),
|
||||||
|
fmt.Sprintf("config/config.%s.yaml", env),
|
||||||
|
fmt.Sprintf("../config/config.%s.yaml", env),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, path := range searchPaths {
|
||||||
|
if _, err := os.Stat(path); err == nil {
|
||||||
|
configFile = path
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果都没找到,默认回退到 config/config.{env}.yaml 以便报错信息准确
|
||||||
|
if configFile == "" {
|
||||||
|
configFile = fmt.Sprintf("config/config.%s.yaml", env)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("正在加载配置文件: %s\n", configFile)
|
||||||
|
|
||||||
|
data, err := os.ReadFile(configFile)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("读取配置文件失败: %v, 使用默认配置\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = yaml.Unmarshal(data, AppConfig)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("解析配置文件失败: %v\n", err)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
server:
|
||||||
|
port: 8081
|
||||||
|
worker_id: 1 # 工作机器ID (0-31),多实例部署需配置不同值
|
||||||
|
datacenter_id: 0 # 数据中心ID (0-31),多机房部署需配置不同值 # 雪花算法机器ID (0-1023),分布式环境下不同实例需设置不同值,多实例部署时需手动配置
|
||||||
|
|
||||||
|
log:
|
||||||
|
level: info
|
||||||
|
dir: logs
|
||||||
|
console: true
|
||||||
|
|
||||||
|
security:
|
||||||
|
enable: true
|
||||||
|
header_key: X-App-Sign
|
||||||
|
secret_key: yts@2025#secure
|
||||||
|
|
||||||
|
rate_limit:
|
||||||
|
enable: true
|
||||||
|
default:
|
||||||
|
interval: 2
|
||||||
|
max_requests: 5
|
||||||
|
rules:
|
||||||
|
/api/user/auth/login:
|
||||||
|
interval: 5
|
||||||
|
max_requests: 1
|
||||||
|
/api/yx-school-majors:
|
||||||
|
interval: 1
|
||||||
|
max_requests: 5
|
||||||
|
/api/user/score/save-score:
|
||||||
|
interval: 1
|
||||||
|
max_requests: 2
|
||||||
|
|
||||||
|
swagger:
|
||||||
|
user: admin
|
||||||
|
password: password
|
||||||
|
|
||||||
|
database:
|
||||||
|
driver: mysql
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 3306
|
||||||
|
database: yitisheng
|
||||||
|
username: root
|
||||||
|
password: "Db$7Hn#4Jm9Pq2!Xz"
|
||||||
|
charset: utf8mb4
|
||||||
|
max_idle_conns: 50
|
||||||
|
max_open_conns: 200
|
||||||
|
conn_max_lifetime: 1
|
||||||
|
log_mode: false
|
||||||
|
|
||||||
|
redis:
|
||||||
|
addr: 127.0.0.1:56379
|
||||||
|
password: "Rd@5Wk8#Nv3Yt6$Bm"
|
||||||
|
db: 1
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
server:
|
||||||
|
port: 8080
|
||||||
|
worker_id: 1 # 工作机器ID (0-31),测试环境使用1
|
||||||
|
datacenter_id: 0 # 数据中心ID (0-31),默认0
|
||||||
|
|
||||||
|
log:
|
||||||
|
level: debug
|
||||||
|
dir: logs
|
||||||
|
console: true
|
||||||
|
|
||||||
|
security:
|
||||||
|
enable: true
|
||||||
|
header_key: X-App-Sign
|
||||||
|
secret_key: yts@2025#secure
|
||||||
|
|
||||||
|
rate_limit:
|
||||||
|
enable: true
|
||||||
|
default:
|
||||||
|
interval: 2
|
||||||
|
max_requests: 10
|
||||||
|
rules:
|
||||||
|
/api/user/auth/login:
|
||||||
|
interval: 5
|
||||||
|
max_requests: 1
|
||||||
|
/api/yx-school-majors:
|
||||||
|
interval: 1
|
||||||
|
max_requests: 10
|
||||||
|
/api/user/score/save-score:
|
||||||
|
interval: 1
|
||||||
|
max_requests: 5
|
||||||
|
|
||||||
|
swagger:
|
||||||
|
user: admin
|
||||||
|
password: password
|
||||||
|
|
||||||
|
database:
|
||||||
|
driver: mysql
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 3306
|
||||||
|
database: yitisheng
|
||||||
|
username: root
|
||||||
|
password: "Db$7Hn#4Jm9Pq2!Xz"
|
||||||
|
charset: utf8mb4
|
||||||
|
max_idle_conns: 20
|
||||||
|
max_open_conns: 100
|
||||||
|
conn_max_lifetime: 1
|
||||||
|
log_mode: false
|
||||||
|
|
||||||
|
redis:
|
||||||
|
addr: 127.0.0.1:56379
|
||||||
|
password: "Rd@5Wk8#Nv3Yt6$Bm"
|
||||||
|
db: 1
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
// Package config 配置包,负责应用程序的配置管理
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/driver/mysql"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DB 全局数据库连接实例
|
||||||
|
var DB *gorm.DB
|
||||||
|
|
||||||
|
// getLogWriter 获取日志输出目标
|
||||||
|
func getLogWriter() io.Writer {
|
||||||
|
logConfig := AppConfig.Log
|
||||||
|
if logConfig.Dir == "" {
|
||||||
|
return os.Stdout
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(logConfig.Dir, 0755); err != nil {
|
||||||
|
fmt.Printf("创建日志目录失败: %v\n", err)
|
||||||
|
return os.Stdout
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := fmt.Sprintf("sql-%s.log", time.Now().Format("2006-01-02"))
|
||||||
|
logPath := filepath.Join(logConfig.Dir, filename)
|
||||||
|
|
||||||
|
file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("打开SQL日志文件失败: %v\n", err)
|
||||||
|
return os.Stdout
|
||||||
|
}
|
||||||
|
|
||||||
|
if logConfig.Console {
|
||||||
|
return io.MultiWriter(file, os.Stdout)
|
||||||
|
}
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitDB 初始化数据库连接
|
||||||
|
func InitDB() {
|
||||||
|
dbConfig := AppConfig.Database
|
||||||
|
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=True&loc=Asia%%2FShanghai",
|
||||||
|
dbConfig.Username,
|
||||||
|
dbConfig.Password,
|
||||||
|
dbConfig.Host,
|
||||||
|
dbConfig.Port,
|
||||||
|
dbConfig.Database,
|
||||||
|
dbConfig.Charset,
|
||||||
|
)
|
||||||
|
|
||||||
|
var gormConfig *gorm.Config
|
||||||
|
if dbConfig.LogMode {
|
||||||
|
writer := getLogWriter()
|
||||||
|
newLogger := logger.New(
|
||||||
|
log.New(writer, "\r\n", log.LstdFlags), // io writer
|
||||||
|
logger.Config{
|
||||||
|
SlowThreshold: time.Second, // Slow SQL threshold
|
||||||
|
LogLevel: logger.Info, // Log level
|
||||||
|
IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger
|
||||||
|
ParameterizedQueries: false, // 包含参数在 SQL 日志中
|
||||||
|
Colorful: false, // 写入文件时建议关闭彩色打印,否则会有乱码
|
||||||
|
},
|
||||||
|
)
|
||||||
|
gormConfig = &gorm.Config{
|
||||||
|
Logger: newLogger,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
gormConfig = &gorm.Config{
|
||||||
|
Logger: logger.Default.LogMode(logger.Silent),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
DB, err = gorm.Open(mysql.Open(dsn), gormConfig)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("数据库连接失败:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取底层 sql.DB 以配置连接池
|
||||||
|
sqlDB, err := DB.DB()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("获取数据库实例失败:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 连接池配置
|
||||||
|
sqlDB.SetMaxIdleConns(dbConfig.MaxIdleConns)
|
||||||
|
sqlDB.SetMaxOpenConns(dbConfig.MaxOpenConns)
|
||||||
|
sqlDB.SetConnMaxLifetime(time.Duration(dbConfig.ConnMaxLifetime) * time.Hour)
|
||||||
|
|
||||||
|
fmt.Println("数据库连接成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseDB 关闭数据库连接
|
||||||
|
// 在程序退出时调用,释放所有连接
|
||||||
|
func CloseDB() {
|
||||||
|
if DB != nil {
|
||||||
|
sqlDB, err := DB.DB()
|
||||||
|
if err == nil {
|
||||||
|
sqlDB.Close()
|
||||||
|
fmt.Println("数据库连接已关闭")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
// Package config Redis配置
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RDB 全局Redis客户端
|
||||||
|
var RDB *redis.Client
|
||||||
|
|
||||||
|
// InitRedis 初始化Redis连接
|
||||||
|
func InitRedis() {
|
||||||
|
redisConfig := AppConfig.Redis
|
||||||
|
RDB = redis.NewClient(&redis.Options{
|
||||||
|
Addr: redisConfig.Addr,
|
||||||
|
Password: redisConfig.Password,
|
||||||
|
DB: redisConfig.DB,
|
||||||
|
PoolSize: 10, // 连接池大小
|
||||||
|
})
|
||||||
|
|
||||||
|
// 测试连接
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
_, err := RDB.Ping(ctx).Result()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Redis连接失败:", err)
|
||||||
|
}
|
||||||
|
fmt.Println("Redis连接成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseRedis 关闭Redis连接
|
||||||
|
func CloseRedis() {
|
||||||
|
if RDB != nil {
|
||||||
|
RDB.Close()
|
||||||
|
fmt.Println("Redis连接已关闭")
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,57 @@
|
||||||
|
module server
|
||||||
|
|
||||||
|
go 1.21
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gin-gonic/gin v1.9.1
|
||||||
|
github.com/google/uuid v1.4.0
|
||||||
|
github.com/redis/go-redis/v9 v9.3.0
|
||||||
|
github.com/swaggo/files v1.0.1
|
||||||
|
github.com/swaggo/gin-swagger v1.6.0
|
||||||
|
github.com/swaggo/swag v1.16.2
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
gorm.io/driver/mysql v1.5.2
|
||||||
|
gorm.io/gorm v1.25.5
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||||
|
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||||
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||||
|
github.com/bytedance/sonic v1.9.1 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||||
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||||
|
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
||||||
|
github.com/go-openapi/spec v0.20.4 // indirect
|
||||||
|
github.com/go-openapi/swag v0.19.15 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||||
|
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||||
|
github.com/leodido/go-urn v1.2.4 // indirect
|
||||||
|
github.com/mailru/easyjson v0.7.6 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||||
|
golang.org/x/arch v0.3.0 // indirect
|
||||||
|
golang.org/x/crypto v0.9.0 // indirect
|
||||||
|
golang.org/x/net v0.10.0 // indirect
|
||||||
|
golang.org/x/sys v0.8.0 // indirect
|
||||||
|
golang.org/x/text v0.9.0 // indirect
|
||||||
|
golang.org/x/tools v0.7.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.30.0 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,188 @@
|
||||||
|
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||||
|
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||||
|
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
||||||
|
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||||
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||||
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||||
|
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||||
|
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||||
|
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||||
|
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||||
|
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||||
|
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||||
|
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||||
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||||
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||||
|
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
||||||
|
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
|
||||||
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
|
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||||
|
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||||
|
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
|
||||||
|
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
|
||||||
|
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
|
||||||
|
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
|
||||||
|
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||||
|
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
|
||||||
|
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
||||||
|
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||||
|
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||||
|
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||||
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
|
||||||
|
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||||
|
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||||
|
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
|
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||||
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/redis/go-redis/v9 v9.3.0 h1:RiVDjmig62jIWp7Kk4XVLs0hzV6pI3PyTnnL0cnn0u0=
|
||||||
|
github.com/redis/go-redis/v9 v9.3.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||||
|
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
||||||
|
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
|
||||||
|
github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M=
|
||||||
|
github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo=
|
||||||
|
github.com/swaggo/swag v1.16.2 h1:28Pp+8DkQoV+HLzLx8RGJZXNGKbFqnuvSbAAtoxiY04=
|
||||||
|
github.com/swaggo/swag v1.16.2/go.mod h1:6YzXnDcpr0767iOejs318CwYkCQqyGer6BizOg03f+E=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
|
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||||
|
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
|
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||||
|
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
||||||
|
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
|
||||||
|
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||||
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||||
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||||
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
|
||||||
|
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||||
|
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs=
|
||||||
|
gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8=
|
||||||
|
gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||||
|
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
|
||||||
|
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||||
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||||
|
|
@ -0,0 +1,171 @@
|
||||||
|
// Package main 应用程序入口
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"server/common"
|
||||||
|
"server/config"
|
||||||
|
_ "server/docs"
|
||||||
|
"server/middleware"
|
||||||
|
sysController "server/modules/system/controller"
|
||||||
|
userController "server/modules/user/controller"
|
||||||
|
yxController "server/modules/yx/controller"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
swaggerFiles "github.com/swaggo/files"
|
||||||
|
ginSwagger "github.com/swaggo/gin-swagger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @title 艺考招生管理系统 API
|
||||||
|
// @version 2.0
|
||||||
|
// @description 提供用户认证、院校专业、历年招生、计算专业的管理接口
|
||||||
|
// @host localhost:8080
|
||||||
|
// @BasePath /api
|
||||||
|
// @securityDefinitions.apikey Bearer
|
||||||
|
// @in header
|
||||||
|
// @name Authorization
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// 加载配置
|
||||||
|
config.LoadConfig()
|
||||||
|
|
||||||
|
// 初始化日志
|
||||||
|
common.InitLogger()
|
||||||
|
common.Info("========== 应用启动 ==========")
|
||||||
|
|
||||||
|
// 初始化雪花算法ID生成器(从配置获取workerID,默认为1)
|
||||||
|
workerID := int64(config.AppConfig.Server.WorkerID)
|
||||||
|
if workerID <= 0 {
|
||||||
|
workerID = 1 // 默认workerID
|
||||||
|
}
|
||||||
|
datacenterID := int64(config.AppConfig.Server.DatacenterID)
|
||||||
|
if datacenterID < 0 {
|
||||||
|
datacenterID = 0 // 默认datacenterID
|
||||||
|
}
|
||||||
|
if err := common.InitGenerator(workerID, datacenterID); err != nil {
|
||||||
|
common.LogError("雪花算法初始化失败: %v", err)
|
||||||
|
log.Fatalf("雪花算法初始化失败: %v\n", err)
|
||||||
|
}
|
||||||
|
common.Info("雪花算法ID生成器初始化完成 (WorkerID: %d)", workerID)
|
||||||
|
|
||||||
|
// 初始化数据库
|
||||||
|
config.InitDB()
|
||||||
|
common.Info("数据库初始化完成")
|
||||||
|
|
||||||
|
// 初始化Redis
|
||||||
|
config.InitRedis()
|
||||||
|
common.Info("Redis初始化完成")
|
||||||
|
|
||||||
|
// 创建 Gin 引擎
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(gin.Recovery())
|
||||||
|
|
||||||
|
// 请求日志中间件
|
||||||
|
r.Use(requestLogMiddleware())
|
||||||
|
|
||||||
|
// 跨域中间件
|
||||||
|
r.Use(middleware.CorsMiddleware())
|
||||||
|
|
||||||
|
// Swagger 文档 (需要Basic Auth验证)
|
||||||
|
authorized := r.Group("/swagger", gin.BasicAuth(gin.Accounts{
|
||||||
|
config.AppConfig.Swagger.User: config.AppConfig.Swagger.Password,
|
||||||
|
}))
|
||||||
|
authorized.GET("/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||||
|
|
||||||
|
// API 路由组
|
||||||
|
api := r.Group("/api")
|
||||||
|
|
||||||
|
// 中间件顺序: 安全校验 -> 限流 -> 登录鉴权
|
||||||
|
api.Use(middleware.SecurityMiddleware())
|
||||||
|
api.Use(middleware.RateLimitMiddleware())
|
||||||
|
api.Use(middleware.AuthMiddleware())
|
||||||
|
|
||||||
|
// 注册 System 模块路由
|
||||||
|
sysController.NewAuthController().RegisterRoutes(api)
|
||||||
|
sysController.NewSysUserController().RegisterRoutes(api)
|
||||||
|
|
||||||
|
// 注册 YX 模块路由
|
||||||
|
yxController.NewYxSchoolMajorController().RegisterRoutes(api)
|
||||||
|
yxController.NewYxHistoryMajorEnrollController().RegisterRoutes(api)
|
||||||
|
yxController.NewYxCalculationMajorController().RegisterRoutes(api)
|
||||||
|
yxController.NewYxUserScoreController().RegisterRoutes(api)
|
||||||
|
yxController.NewYxVolunteerController().RegisterRoutes(api)
|
||||||
|
yxController.NewYxVolunteerRecordController().RegisterRoutes(api)
|
||||||
|
|
||||||
|
// 注册 User 模块路由
|
||||||
|
userController.NewUserScoreController().RegisterRoutes(api)
|
||||||
|
userController.NewAuthController().RegisterRoutes(api)
|
||||||
|
userController.NewUserMajorController().RegisterRoutes(api)
|
||||||
|
userController.NewUserVolunteerController().RegisterRoutes(api)
|
||||||
|
|
||||||
|
// 创建 HTTP 服务器
|
||||||
|
port := config.AppConfig.Server.Port
|
||||||
|
if port == 0 {
|
||||||
|
port = 8080 // 默认端口
|
||||||
|
}
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: fmt.Sprintf(":%d", port),
|
||||||
|
Handler: r,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动服务器
|
||||||
|
go func() {
|
||||||
|
common.Info("服务器启动: http://localhost:%d", port)
|
||||||
|
common.Info("Swagger文档: http://localhost:%d/swagger/index.html", port)
|
||||||
|
log.Printf("服务器启动: http://localhost:%d\n", port)
|
||||||
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
common.LogError("启动失败: %s", err)
|
||||||
|
log.Fatalf("启动失败: %s\n", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 优雅关闭
|
||||||
|
quit := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
<-quit
|
||||||
|
|
||||||
|
common.Info("正在关闭服务器...")
|
||||||
|
log.Println("正在关闭服务器...")
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := srv.Shutdown(ctx); err != nil {
|
||||||
|
common.LogError("服务器关闭异常: %v", err)
|
||||||
|
log.Fatal("服务器关闭异常:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭资源
|
||||||
|
config.CloseDB()
|
||||||
|
config.CloseRedis()
|
||||||
|
common.Info("========== 应用关闭 ==========")
|
||||||
|
common.CloseLogger()
|
||||||
|
|
||||||
|
log.Println("服务器已退出")
|
||||||
|
}
|
||||||
|
|
||||||
|
// requestLogMiddleware 请求日志中间件
|
||||||
|
func requestLogMiddleware() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
start := time.Now()
|
||||||
|
path := c.Request.URL.Path
|
||||||
|
method := c.Request.Method
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
|
||||||
|
latency := time.Since(start)
|
||||||
|
status := c.Writer.Status()
|
||||||
|
clientIP := c.ClientIP()
|
||||||
|
|
||||||
|
common.Info("%s %s %d %v %s", method, path, status, latency, clientIP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
// Package middleware 中间件
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"server/common"
|
||||||
|
"server/modules/system/service"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 白名单路径 (不需要登录即可访问)
|
||||||
|
var whiteList = []string{
|
||||||
|
"/api/sys/auth/login",
|
||||||
|
"/api/sys/auth/register",
|
||||||
|
"/api/user/auth/login",
|
||||||
|
"/api/user/auth/register",
|
||||||
|
"/swagger/",
|
||||||
|
"/swagger/index.html",
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthMiddleware 登录鉴权中间件
|
||||||
|
// 类似Java中的Shiro Filter
|
||||||
|
func AuthMiddleware() gin.HandlerFunc {
|
||||||
|
userService := service.NewSysUserService()
|
||||||
|
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
path := c.Request.URL.Path
|
||||||
|
|
||||||
|
// 检查是否在白名单中
|
||||||
|
for _, white := range whiteList {
|
||||||
|
if strings.HasPrefix(path, white) {
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取Token
|
||||||
|
token := c.GetHeader(common.TokenHeader)
|
||||||
|
if token == "" {
|
||||||
|
common.Error(c, 401, "未登录")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有前缀则处理前缀
|
||||||
|
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, "登录已失效,请重新登录")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 存入上下文
|
||||||
|
c.Set(common.ContextUserKey, loginUser)
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddWhiteList 添加白名单路径
|
||||||
|
func AddWhiteList(paths ...string) {
|
||||||
|
whiteList = append(whiteList, paths...)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CorsMiddleware 跨域中间件
|
||||||
|
func CorsMiddleware() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
method := c.Request.Method
|
||||||
|
origin := c.Request.Header.Get("Origin")
|
||||||
|
|
||||||
|
if origin != "" {
|
||||||
|
// 允许所有来源,生产环境请修改为特定域名
|
||||||
|
c.Header("Access-Control-Allow-Origin", origin)
|
||||||
|
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
|
||||||
|
c.Header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization, X-App-Sign")
|
||||||
|
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Cache-Control, Content-Language, Content-Type")
|
||||||
|
c.Header("Access-Control-Allow-Credentials", "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 放行 OPTIONS 请求
|
||||||
|
if method == "OPTIONS" {
|
||||||
|
c.AbortWithStatus(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
// Package middleware 限流中间件
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"server/common"
|
||||||
|
"server/config"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RateLimitMiddleware 限流中间件
|
||||||
|
// 基于 Redis 实现,支持按用户ID或IP限流
|
||||||
|
// 不同接口可配置不同的限流规则
|
||||||
|
func RateLimitMiddleware() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
cfg := config.AppConfig.RateLimit
|
||||||
|
|
||||||
|
// 未启用则跳过
|
||||||
|
if !cfg.Enable {
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 白名单路径跳过
|
||||||
|
path := c.Request.URL.Path
|
||||||
|
if isRateLimitWhitelist(path) {
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取限流规则
|
||||||
|
rule := getRule(path, cfg)
|
||||||
|
|
||||||
|
// 获取限流key (优先用户ID,否则用IP)
|
||||||
|
key := getRateLimitKey(c, path)
|
||||||
|
|
||||||
|
// 检查是否超过限制
|
||||||
|
if !checkRateLimit(key, rule) {
|
||||||
|
common.Warn("请求过于频繁: Key=%s Path=%s", key, path)
|
||||||
|
c.JSON(429, map[string]interface{}{
|
||||||
|
"code": 429,
|
||||||
|
"message": "操作过快,请稍后再试",
|
||||||
|
"data": nil,
|
||||||
|
})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getRule 获取路径对应的限流规则
|
||||||
|
func getRule(path string, cfg config.RateLimitConfig) config.RateLimitRule {
|
||||||
|
// 精确匹配
|
||||||
|
if rule, ok := cfg.Rules[path]; ok {
|
||||||
|
return rule
|
||||||
|
}
|
||||||
|
|
||||||
|
// 前缀匹配
|
||||||
|
for rulePath, rule := range cfg.Rules {
|
||||||
|
if strings.HasPrefix(path, rulePath) {
|
||||||
|
return rule
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回默认规则
|
||||||
|
return cfg.Default
|
||||||
|
}
|
||||||
|
|
||||||
|
// getRateLimitKey 获取限流key
|
||||||
|
func getRateLimitKey(c *gin.Context, path string) string {
|
||||||
|
// 优先使用用户ID
|
||||||
|
if user := common.GetLoginUser(c); user != nil {
|
||||||
|
return fmt.Sprintf("ratelimit:%s:%s", user.ID, path)
|
||||||
|
}
|
||||||
|
// 否则使用IP
|
||||||
|
return fmt.Sprintf("ratelimit:%s:%s", c.ClientIP(), path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkRateLimit 检查是否超过限流
|
||||||
|
// 使用 Redis 滑动窗口算法
|
||||||
|
func checkRateLimit(key string, rule config.RateLimitRule) bool {
|
||||||
|
ctx := context.Background()
|
||||||
|
rdb := config.RDB
|
||||||
|
|
||||||
|
now := time.Now().UnixMilli()
|
||||||
|
windowStart := now - int64(rule.Interval*1000)
|
||||||
|
|
||||||
|
// 使用 Redis 事务
|
||||||
|
pipe := rdb.Pipeline()
|
||||||
|
|
||||||
|
// 移除窗口外的记录
|
||||||
|
pipe.ZRemRangeByScore(ctx, key, "0", fmt.Sprintf("%d", windowStart))
|
||||||
|
|
||||||
|
// 获取当前窗口内的请求数
|
||||||
|
countCmd := pipe.ZCard(ctx, key)
|
||||||
|
|
||||||
|
// 添加当前请求
|
||||||
|
pipe.ZAdd(ctx, key, redis.Z{
|
||||||
|
Score: float64(now),
|
||||||
|
Member: fmt.Sprintf("%d", now),
|
||||||
|
})
|
||||||
|
|
||||||
|
// 设置过期时间
|
||||||
|
pipe.Expire(ctx, key, time.Duration(rule.Interval)*time.Second)
|
||||||
|
|
||||||
|
_, err := pipe.Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
common.LogError("限流检查失败: %v", err)
|
||||||
|
return true // 出错时放行
|
||||||
|
}
|
||||||
|
|
||||||
|
count := countCmd.Val()
|
||||||
|
return count < int64(rule.MaxRequests)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 限流白名单
|
||||||
|
var rateLimitWhitelist = []string{
|
||||||
|
"/swagger/",
|
||||||
|
"/api/auth/logout",
|
||||||
|
}
|
||||||
|
|
||||||
|
func isRateLimitWhitelist(path string) bool {
|
||||||
|
for _, white := range rateLimitWhitelist {
|
||||||
|
if strings.HasPrefix(path, white) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddRateLimitWhitelist 添加限流白名单
|
||||||
|
func AddRateLimitWhitelist(paths ...string) {
|
||||||
|
rateLimitWhitelist = append(rateLimitWhitelist, paths...)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
// Package middleware 安全校验中间件
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"server/common"
|
||||||
|
"server/config"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SecurityMiddleware 安全校验中间件
|
||||||
|
// 防止暴力入侵,校验请求头签名
|
||||||
|
// 请求头需携带:
|
||||||
|
// - X-App-Sign: 签名值 = MD5(timestamp + secretKey)
|
||||||
|
// - X-App-Timestamp: 时间戳(毫秒)
|
||||||
|
func SecurityMiddleware() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
cfg := config.AppConfig.Security
|
||||||
|
|
||||||
|
// 未启用则跳过
|
||||||
|
if !cfg.Enable {
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 白名单路径跳过
|
||||||
|
path := c.Request.URL.Path
|
||||||
|
if isSecurityWhitelist(path) {
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取签名和时间戳
|
||||||
|
sign := c.GetHeader(cfg.HeaderKey)
|
||||||
|
timestamp := c.GetHeader("X-App-Timestamp")
|
||||||
|
|
||||||
|
if sign == "" || timestamp == "" {
|
||||||
|
common.Warn("安全校验失败: 缺少签名头 IP=%s Path=%s", c.ClientIP(), path)
|
||||||
|
common.Error(c, 403, "非法请求")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证时间戳 (5分钟内有效)
|
||||||
|
ts, err := strconv.ParseInt(timestamp, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
common.Warn("安全校验失败: 时间戳格式错误 IP=%s", c.ClientIP())
|
||||||
|
common.Error(c, 403, "非法请求")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().UnixMilli()
|
||||||
|
if abs(now-ts) > 5*60*1000 { // 5分钟
|
||||||
|
common.Warn("安全校验失败: 时间戳过期 IP=%s Timestamp=%d", c.ClientIP(), ts)
|
||||||
|
common.Error(c, 403, "请求已过期")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证签名
|
||||||
|
expectedSign := generateSign(timestamp, cfg.SecretKey)
|
||||||
|
if sign != expectedSign {
|
||||||
|
common.Warn("安全校验失败: 签名错误 IP=%s Sign=%s Expected=%s", c.ClientIP(), sign, expectedSign)
|
||||||
|
common.Error(c, 403, "签名错误")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateSign 生成签名
|
||||||
|
func generateSign(timestamp, secretKey string) string {
|
||||||
|
data := timestamp + secretKey
|
||||||
|
hash := md5.Sum([]byte(data))
|
||||||
|
return hex.EncodeToString(hash[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 安全校验白名单
|
||||||
|
var securityWhitelist = []string{
|
||||||
|
"/swagger/",
|
||||||
|
"/swagger/index.html",
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSecurityWhitelist(path string) bool {
|
||||||
|
for _, white := range securityWhitelist {
|
||||||
|
if len(path) >= len(white) && path[:len(white)] == white {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func abs(n int64) int64 {
|
||||||
|
if n < 0 {
|
||||||
|
return -n
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSecurityWhitelist 添加安全校验白名单
|
||||||
|
func AddSecurityWhitelist(paths ...string) {
|
||||||
|
securityWhitelist = append(securityWhitelist, paths...)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
// Package controller 控制器层
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"server/common"
|
||||||
|
"server/modules/system/dto"
|
||||||
|
"server/modules/system/service"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthController 认证控制器
|
||||||
|
type AuthController struct {
|
||||||
|
userService *service.SysUserService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthController() *AuthController {
|
||||||
|
return &AuthController{userService: service.NewSysUserService()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctrl *AuthController) RegisterRoutes(r *gin.RouterGroup) {
|
||||||
|
r.POST("/sys/auth/login", ctrl.SysLogin)
|
||||||
|
r.POST("/sys/auth/logout", ctrl.Logout)
|
||||||
|
r.GET("/sys/auth/info", ctrl.GetUserInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SysLogin 用户登录
|
||||||
|
// @Summary Sys用户登录
|
||||||
|
// @Tags 认证
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body LoginRequest true "登录信息"
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /sys/auth/login [post]
|
||||||
|
func (ctrl *AuthController) SysLogin(c *gin.Context) {
|
||||||
|
var req dto.LoginRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
common.Error(c, 400, "用户名和密码不能为空")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loginUser, token, err := ctrl.userService.SysLogin(req.Username, req.Password)
|
||||||
|
if err != nil {
|
||||||
|
common.Error(c, 401, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
common.Success(c, gin.H{
|
||||||
|
"token": token,
|
||||||
|
"user": loginUser,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logout 用户登出
|
||||||
|
// @Summary Sys用户登出
|
||||||
|
// @Tags 认证
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /sys/auth/logout [post]
|
||||||
|
func (ctrl *AuthController) Logout(c *gin.Context) {
|
||||||
|
token := c.GetHeader("Authorization")
|
||||||
|
if len(token) > 7 {
|
||||||
|
token = token[7:] // 去除 "Bearer "
|
||||||
|
}
|
||||||
|
|
||||||
|
ctrl.userService.Logout(token)
|
||||||
|
common.Success(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserInfo 获取当前登录用户信息
|
||||||
|
// @Summary Sys获取当前登录用户信息
|
||||||
|
// @Tags 认证
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /sys/auth/info [get]
|
||||||
|
func (ctrl *AuthController) GetUserInfo(c *gin.Context) {
|
||||||
|
user := common.GetLoginUser(c)
|
||||||
|
if user == nil {
|
||||||
|
common.Error(c, 401, "未登录")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, user)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,161 @@
|
||||||
|
// Package controller 控制器层
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"server/common"
|
||||||
|
"server/modules/system/dto"
|
||||||
|
"server/modules/system/service"
|
||||||
|
"server/modules/system/vo" // 用于 Swagger 注解
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SysUserController struct {
|
||||||
|
service *service.SysUserService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSysUserController() *SysUserController {
|
||||||
|
return &SysUserController{service: service.NewSysUserService()}
|
||||||
|
}
|
||||||
|
|
||||||
|
// _ 确保 vo 包被导入(用于 Swagger 注解)
|
||||||
|
var _ = vo.SysUserVO{}
|
||||||
|
|
||||||
|
func (ctrl *SysUserController) RegisterRoutes(r *gin.RouterGroup) {
|
||||||
|
r.GET("/sys-users", ctrl.List)
|
||||||
|
r.GET("/sys-users/:id", ctrl.GetByID)
|
||||||
|
r.POST("/sys-users", ctrl.Create)
|
||||||
|
r.PUT("/sys-users/:id", ctrl.Update)
|
||||||
|
r.PATCH("/sys-users/:id", ctrl.UpdateFields)
|
||||||
|
r.DELETE("/sys-users/:id", ctrl.Delete)
|
||||||
|
r.PUT("/sys-users/:id/password", ctrl.UpdatePassword)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary 获取用户列表
|
||||||
|
// @Tags 用户管理
|
||||||
|
// @Param page query int false "页码"
|
||||||
|
// @Param size query int false "每页数量"
|
||||||
|
// @Success 200 {object} common.Response{data=[]vo.SysUserVO}
|
||||||
|
// @Router /sys-users [get]
|
||||||
|
func (ctrl *SysUserController) List(c *gin.Context) {
|
||||||
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
|
size, _ := strconv.Atoi(c.DefaultQuery("size", "10"))
|
||||||
|
items, total, err := ctrl.service.ListUsers(page, size)
|
||||||
|
if err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.SuccessPage(c, items, total, page, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary 获取单个用户
|
||||||
|
// @Tags 用户管理
|
||||||
|
// @Param id path string true "用户ID"
|
||||||
|
// @Success 200 {object} common.Response{data=vo.SysUserVO}
|
||||||
|
// @Router /sys-users/{id} [get]
|
||||||
|
func (ctrl *SysUserController) GetByID(c *gin.Context) {
|
||||||
|
item, err := ctrl.service.GetUserByID(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
common.Error(c, 404, "未找到")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary 创建用户
|
||||||
|
// @Tags 用户管理
|
||||||
|
// @Param request body dto.CreateUserRequest true "用户信息"
|
||||||
|
// @Success 200 {object} common.Response{data=vo.SysUserVO}
|
||||||
|
// @Router /sys-users [post]
|
||||||
|
func (ctrl *SysUserController) Create(c *gin.Context) {
|
||||||
|
var req dto.CreateUserRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
common.Error(c, 400, "参数错误: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item, err := ctrl.service.CreateUser(&req, common.GetLoginUserID(c))
|
||||||
|
if err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary 更新用户
|
||||||
|
// @Tags 用户管理
|
||||||
|
// @Param id path string true "用户ID"
|
||||||
|
// @Param request body dto.UpdateUserRequest true "用户信息"
|
||||||
|
// @Success 200 {object} common.Response{data=vo.SysUserVO}
|
||||||
|
// @Router /sys-users/{id} [put]
|
||||||
|
func (ctrl *SysUserController) Update(c *gin.Context) {
|
||||||
|
var req dto.UpdateUserRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
common.Error(c, 400, "参数错误: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item, err := ctrl.service.UpdateUser(c.Param("id"), &req)
|
||||||
|
if err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary 动态字段更新
|
||||||
|
// @Tags 用户管理
|
||||||
|
// @Param id path string true "用户ID"
|
||||||
|
// @Param fields body map[string]interface{} true "要更新的字段"
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /sys-users/{id} [patch]
|
||||||
|
func (ctrl *SysUserController) UpdateFields(c *gin.Context) {
|
||||||
|
var fields map[string]interface{}
|
||||||
|
if err := c.ShouldBindJSON(&fields); err != nil {
|
||||||
|
common.Error(c, 400, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := ctrl.service.UpdateFields(c.Param("id"), fields); err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary 删除用户
|
||||||
|
// @Tags 用户管理
|
||||||
|
// @Param id path string true "用户ID"
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /sys-users/{id} [delete]
|
||||||
|
func (ctrl *SysUserController) Delete(c *gin.Context) {
|
||||||
|
if err := ctrl.service.Delete(c.Param("id")); err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatePasswordRequest 修改密码请求
|
||||||
|
type UpdatePasswordRequest struct {
|
||||||
|
OldPassword string `json:"oldPassword" binding:"required"`
|
||||||
|
NewPassword string `json:"newPassword" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary 修改密码
|
||||||
|
// @Tags 用户管理
|
||||||
|
// @Param id path string true "用户ID"
|
||||||
|
// @Param request body UpdatePasswordRequest true "密码信息"
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /sys-users/{id}/password [put]
|
||||||
|
func (ctrl *SysUserController) UpdatePassword(c *gin.Context) {
|
||||||
|
var req UpdatePasswordRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
common.Error(c, 400, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := ctrl.service.UpdatePassword(c.Param("id"), req.OldPassword, req.NewPassword); err != nil {
|
||||||
|
common.Error(c, 400, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, nil)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package dto
|
||||||
|
|
||||||
|
// LoginRequest 登录请求
|
||||||
|
type LoginRequest struct {
|
||||||
|
Username string `json:"username" binding:"required"`
|
||||||
|
Password string `json:"password" binding:"required"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
package dto
|
||||||
|
|
||||||
|
// CreateUserRequest 创建用户请求
|
||||||
|
type CreateUserRequest struct {
|
||||||
|
Username string `json:"username" binding:"required,min=3,max=20"` // 登录账号
|
||||||
|
Realname string `json:"realname" binding:"required"` // 真实姓名
|
||||||
|
Password string `json:"password" binding:"required,min=6"` // 密码
|
||||||
|
Email string `json:"email" binding:"omitempty,email"` // 电子邮件
|
||||||
|
Phone string `json:"phone" binding:"omitempty,len=11"` // 电话
|
||||||
|
Avatar string `json:"avatar"` // 头像
|
||||||
|
Sex *int `json:"sex"` // 性别(0-未知,1-男,2-女)
|
||||||
|
Birthday string `json:"birthday"` // 生日
|
||||||
|
OrgCode string `json:"orgCode"` // 机构编码
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUserRequest 更新用户请求
|
||||||
|
type UpdateUserRequest struct {
|
||||||
|
Realname string `json:"realname"` // 真实姓名
|
||||||
|
Email string `json:"email" binding:"omitempty,email"` // 电子邮件
|
||||||
|
Phone string `json:"phone" binding:"omitempty,len=11"` // 电话
|
||||||
|
Avatar string `json:"avatar"` // 头像
|
||||||
|
Sex *int `json:"sex"` // 性别(0-未知,1-男,2-女)
|
||||||
|
Birthday string `json:"birthday"` // 生日
|
||||||
|
Status *int `json:"status"` // 状态(1-正常,2-冻结)
|
||||||
|
OrgCode string `json:"orgCode"` // 机构编码
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
// Package entity 实体层
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// SysUser 用户表实体
|
||||||
|
type SysUser struct {
|
||||||
|
ID string `gorm:"column:id;primaryKey" json:"id"`
|
||||||
|
Username string `gorm:"column:username" json:"username"` // 登录账号
|
||||||
|
Realname string `gorm:"column:realname" json:"realname"` // 真实姓名
|
||||||
|
Password string `gorm:"column:password" json:"-"` // 密码 (不返回给前端)
|
||||||
|
Salt string `gorm:"column:salt" json:"-"` // md5密码盐
|
||||||
|
Avatar string `gorm:"column:avatar" json:"avatar"` // 头像
|
||||||
|
Birthday *time.Time `gorm:"column:birthday" json:"birthday"` // 生日
|
||||||
|
Sex int `gorm:"column:sex" json:"sex"` // 性别(0-未知,1-男,2-女)
|
||||||
|
Email string `gorm:"column:email" json:"email"` // 电子邮件
|
||||||
|
Phone string `gorm:"column:phone" json:"phone"` // 电话
|
||||||
|
OrgCode string `gorm:"column:org_code" json:"orgCode"` // 机构编码
|
||||||
|
Status int `gorm:"column:status" json:"status"` // 状态(1-正常,2-冻结)
|
||||||
|
DelFlag int `gorm:"column:del_flag" json:"delFlag"` // 删除状态(0-正常,1-已删除)
|
||||||
|
ThirdID string `gorm:"column:third_id" json:"thirdId"` // 第三方登录唯一标识
|
||||||
|
ThirdType string `gorm:"column:third_type" json:"thirdType"` // 第三方类型
|
||||||
|
ActivitiSync int `gorm:"column:activiti_sync" json:"activitiSync"` // 同步工作流引擎
|
||||||
|
WorkNo string `gorm:"column:work_no" json:"workNo"` // 工号
|
||||||
|
Telephone string `gorm:"column:telephone" json:"telephone"` // 座机号
|
||||||
|
CreateBy string `gorm:"column:create_by" json:"createBy"` // 创建人
|
||||||
|
CreateTime *time.Time `gorm:"column:create_time" json:"createTime"` // 创建时间
|
||||||
|
UpdateBy string `gorm:"column:update_by" json:"updateBy"` // 更新人
|
||||||
|
UpdateTime *time.Time `gorm:"column:update_time" json:"updateTime"` // 更新时间
|
||||||
|
UserIdentity int `gorm:"column:user_identity" json:"userIdentity"` // 身份(1普通成员 2上级)
|
||||||
|
DepartIds string `gorm:"column:depart_ids" json:"departIds"` // 负责部门
|
||||||
|
ClientID string `gorm:"column:client_id" json:"clientId"` // 设备ID
|
||||||
|
LoginTenantID int `gorm:"column:login_tenant_id" json:"loginTenantId"` // 上次登录租户ID
|
||||||
|
BpmStatus string `gorm:"column:bpm_status" json:"bpmStatus"` // 流程状态
|
||||||
|
WxOpenID string `gorm:"column:wx_open_id" json:"wxOpenId"` // 微信openId
|
||||||
|
DyOpenID string `gorm:"column:dy_open_id" json:"dyOpenId"` // 抖音openId
|
||||||
|
IP string `gorm:"column:ip" json:"ip"` // 注册时ip
|
||||||
|
ShowLinediff string `gorm:"column:show_linediff" json:"showLinediff"` // 是否显示历年线差
|
||||||
|
ProgramType string `gorm:"column:program_type" json:"programType"` // 所属程序
|
||||||
|
}
|
||||||
|
|
||||||
|
func (SysUser) TableName() string {
|
||||||
|
return "sys_user"
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginUser 登录用户信息 (存储在Redis中)
|
||||||
|
type LoginUser struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Realname string `json:"realname"`
|
||||||
|
Avatar string `json:"avatar"`
|
||||||
|
Phone string `json:"phone"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
// Package mapper 数据访问层
|
||||||
|
package mapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"server/common"
|
||||||
|
"server/modules/system/entity"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SysUserMapper struct {
|
||||||
|
*common.BaseMapper[entity.SysUser]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSysUserMapper() *SysUserMapper {
|
||||||
|
return &SysUserMapper{
|
||||||
|
BaseMapper: common.NewBaseMapper[entity.SysUser](),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDB 获取数据库实例,添加逻辑删除过滤
|
||||||
|
func (m *SysUserMapper) GetDB() *gorm.DB {
|
||||||
|
return m.BaseMapper.GetDB().Where("del_flag = 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 逻辑删除
|
||||||
|
func (m *SysUserMapper) Delete(id string) error {
|
||||||
|
return m.BaseMapper.GetDB().Model(&entity.SysUser{}).Where("id = ?", id).Update("del_flag", 1).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindByUsername 根据用户名查找用户
|
||||||
|
func (m *SysUserMapper) FindByUsername(username string) (*entity.SysUser, error) {
|
||||||
|
var item entity.SysUser
|
||||||
|
err := m.GetDB().First(&item, "username = ?", username).Error
|
||||||
|
return &item, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindByPhone 根据手机号查找用户
|
||||||
|
func (m *SysUserMapper) FindByPhone(phone string) (*entity.SysUser, error) {
|
||||||
|
var item entity.SysUser
|
||||||
|
err := m.GetDB().First(&item, "phone = ?", phone).Error
|
||||||
|
return &item, err
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,323 @@
|
||||||
|
// Package service 业务逻辑层
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"server/common"
|
||||||
|
"server/config"
|
||||||
|
"server/modules/system/dto"
|
||||||
|
"server/modules/system/entity"
|
||||||
|
"server/modules/system/mapper"
|
||||||
|
"server/modules/system/vo"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SysUserService struct {
|
||||||
|
*common.BaseService[entity.SysUser]
|
||||||
|
mapper *mapper.SysUserMapper
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSysUserService() *SysUserService {
|
||||||
|
mapper := mapper.NewSysUserMapper()
|
||||||
|
return &SysUserService{
|
||||||
|
BaseService: common.NewBaseService[entity.SysUser](),
|
||||||
|
mapper: mapper,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateUser 创建用户并返回 VO
|
||||||
|
func (s *SysUserService) CreateUser(req *dto.CreateUserRequest, createBy string) (*vo.SysUserVO, error) {
|
||||||
|
// DTO 转 Entity
|
||||||
|
entityItem := &entity.SysUser{
|
||||||
|
Username: req.Username,
|
||||||
|
Realname: req.Realname,
|
||||||
|
Password: req.Password,
|
||||||
|
Email: req.Email,
|
||||||
|
Phone: req.Phone,
|
||||||
|
Avatar: req.Avatar,
|
||||||
|
Sex: 0, // 默认值
|
||||||
|
OrgCode: req.OrgCode,
|
||||||
|
DelFlag: 0,
|
||||||
|
Status: 1,
|
||||||
|
CreateBy: createBy,
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Sex != nil {
|
||||||
|
entityItem.Sex = *req.Sex
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Birthday != "" {
|
||||||
|
birthday, err := time.Parse("2006-01-02", req.Birthday)
|
||||||
|
if err == nil {
|
||||||
|
entityItem.Birthday = &birthday
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到数据库
|
||||||
|
if err := s.Create(entityItem); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entity 转 VO
|
||||||
|
return s.convertToVO(*entityItem), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUser 更新用户并返回 VO
|
||||||
|
func (s *SysUserService) UpdateUser(id string, req *dto.UpdateUserRequest) (*vo.SysUserVO, error) {
|
||||||
|
// 获取原数据
|
||||||
|
entityItem, err := s.GetByID(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("用户不存在: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新字段
|
||||||
|
updateFields := make(map[string]interface{})
|
||||||
|
if req.Realname != "" {
|
||||||
|
updateFields["realname"] = req.Realname
|
||||||
|
}
|
||||||
|
if req.Email != "" {
|
||||||
|
updateFields["email"] = req.Email
|
||||||
|
}
|
||||||
|
if req.Phone != "" {
|
||||||
|
updateFields["phone"] = req.Phone
|
||||||
|
}
|
||||||
|
if req.Avatar != "" {
|
||||||
|
updateFields["avatar"] = req.Avatar
|
||||||
|
}
|
||||||
|
if req.Sex != nil {
|
||||||
|
updateFields["sex"] = *req.Sex
|
||||||
|
}
|
||||||
|
if req.Status != nil {
|
||||||
|
updateFields["status"] = *req.Status
|
||||||
|
}
|
||||||
|
if req.OrgCode != "" {
|
||||||
|
updateFields["org_code"] = req.OrgCode
|
||||||
|
}
|
||||||
|
if req.Birthday != "" {
|
||||||
|
birthday, err := time.Parse("2006-01-02", req.Birthday)
|
||||||
|
if err == nil {
|
||||||
|
updateFields["birthday"] = birthday
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.mapper.UpdateFields(id, updateFields); err != nil {
|
||||||
|
return nil, fmt.Errorf("更新用户失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新获取更新后的数据
|
||||||
|
entityItem, err = s.GetByID(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("获取更新后数据失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.convertToVO(*entityItem), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserByID 获取用户并返回 VO
|
||||||
|
func (s *SysUserService) GetUserByID(id string) (*vo.SysUserVO, error) {
|
||||||
|
entityItem, err := s.GetByID(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return s.convertToVO(*entityItem), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListUsers 获取用户列表并返回 VO 列表
|
||||||
|
func (s *SysUserService) ListUsers(page, size int) ([]vo.SysUserVO, int64, error) {
|
||||||
|
entities, total, err := s.List(page, size)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量转换 Entity 到 VO
|
||||||
|
vos := make([]vo.SysUserVO, len(entities))
|
||||||
|
for i := range entities {
|
||||||
|
vos[i] = *s.convertToVO(entities[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
return vos, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login 用户登录(手机号登录)
|
||||||
|
func (s *SysUserService) Login(username, password string) (*entity.LoginUser, string, error) {
|
||||||
|
user, err := s.mapper.FindByPhone(username)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", errors.New("用户不存在")
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Status == 2 {
|
||||||
|
return nil, "", errors.New("账号已被冻结")
|
||||||
|
}
|
||||||
|
|
||||||
|
encrypted, err := common.Encrypt(user.Username, password, user.Salt)
|
||||||
|
if (user.Password != encrypted) || (err != nil) {
|
||||||
|
return nil, "", errors.New("用户名或密码错误")
|
||||||
|
}
|
||||||
|
|
||||||
|
token := s.generateToken()
|
||||||
|
|
||||||
|
loginUser := &entity.LoginUser{
|
||||||
|
ID: user.ID,
|
||||||
|
Username: user.Username,
|
||||||
|
Realname: user.Realname,
|
||||||
|
Avatar: user.Avatar,
|
||||||
|
Phone: user.Phone,
|
||||||
|
Email: user.Email,
|
||||||
|
Token: token,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.saveLoginUser(token, loginUser); err != nil {
|
||||||
|
return nil, "", errors.New("登录失败,请重试")
|
||||||
|
}
|
||||||
|
|
||||||
|
return loginUser, token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SysLogin 用户登录(用户名登录)
|
||||||
|
func (s *SysUserService) SysLogin(username, password string) (*entity.LoginUser, string, error) {
|
||||||
|
user, err := s.mapper.FindByUsername(username)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", errors.New("用户不存在")
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Status == 2 {
|
||||||
|
return nil, "", errors.New("账号已被冻结")
|
||||||
|
}
|
||||||
|
|
||||||
|
encrypted, err := common.Encrypt(username, password, user.Salt)
|
||||||
|
if (user.Password != encrypted) || (err != nil) {
|
||||||
|
return nil, "", errors.New("用户名或密码错误")
|
||||||
|
}
|
||||||
|
|
||||||
|
token := s.generateToken()
|
||||||
|
|
||||||
|
loginUser := &entity.LoginUser{
|
||||||
|
ID: user.ID,
|
||||||
|
Username: user.Username,
|
||||||
|
Realname: user.Realname,
|
||||||
|
Avatar: user.Avatar,
|
||||||
|
Phone: user.Phone,
|
||||||
|
Email: user.Email,
|
||||||
|
Token: token,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.saveLoginUser(token, loginUser); err != nil {
|
||||||
|
return nil, "", errors.New("登录失败,请重试")
|
||||||
|
}
|
||||||
|
|
||||||
|
return loginUser, token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logout 用户登出
|
||||||
|
func (s *SysUserService) Logout(token string) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
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, common.RedisTokenPrefix+token).Result()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("未登录或登录已过期")
|
||||||
|
}
|
||||||
|
|
||||||
|
var loginUser entity.LoginUser
|
||||||
|
if err := json.Unmarshal([]byte(data), &loginUser); err != nil {
|
||||||
|
return nil, errors.New("登录信息异常")
|
||||||
|
}
|
||||||
|
|
||||||
|
config.RDB.Expire(ctx, common.RedisTokenPrefix+token, common.RedisTokenExpire)
|
||||||
|
|
||||||
|
return &loginUser, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshToken 刷新Token过期时间
|
||||||
|
func (s *SysUserService) RefreshToken(token string) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
return config.RDB.Expire(ctx, common.RedisTokenPrefix+token, common.RedisTokenExpire).Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatePassword 修改密码
|
||||||
|
func (s *SysUserService) UpdatePassword(id, oldPwd, newPwd string) error {
|
||||||
|
user, err := s.GetByID(id)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("用户不存在")
|
||||||
|
}
|
||||||
|
|
||||||
|
encrypted, err := common.Encrypt(user.Username, oldPwd, user.Salt)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("密码加密失败: %v", err)
|
||||||
|
return fmt.Errorf("密码加密失败: %w,请联系管理员", err)
|
||||||
|
}
|
||||||
|
if encrypted != user.Password {
|
||||||
|
return errors.New("原密码错误")
|
||||||
|
}
|
||||||
|
|
||||||
|
newEncrypted, err := common.Encrypt(user.Username, newPwd, user.Salt)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("密码加密失败: %v", err)
|
||||||
|
return fmt.Errorf("密码加密失败: %w,请联系管理员", err)
|
||||||
|
}
|
||||||
|
return s.mapper.UpdateFields(id, map[string]interface{}{
|
||||||
|
"password": newEncrypted,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveLoginUser 保存登录用户到Redis
|
||||||
|
func (s *SysUserService) saveLoginUser(token string, user *entity.LoginUser) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
data, err := json.Marshal(user)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return config.RDB.Set(ctx, common.RedisTokenPrefix+token, data, common.RedisTokenExpire).Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateToken 生成Token
|
||||||
|
func (s *SysUserService) generateToken() string {
|
||||||
|
return uuid.New().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create 创建用户(添加密码加密逻辑)
|
||||||
|
func (s *SysUserService) Create(item *entity.SysUser) error {
|
||||||
|
item.ID = uuid.New().String()
|
||||||
|
item.Salt = uuid.New().String()[:8]
|
||||||
|
encrypted, err := common.Encrypt(item.Username, item.Password, item.Salt)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("密码加密失败: %v", err)
|
||||||
|
return fmt.Errorf("密码加密失败: %w,请联系管理员", err)
|
||||||
|
}
|
||||||
|
item.Password = encrypted
|
||||||
|
item.DelFlag = 0
|
||||||
|
item.Status = 1
|
||||||
|
now := time.Now()
|
||||||
|
item.CreateTime = &now
|
||||||
|
return s.mapper.Create(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertToVO Entity 转 VO(私有方法)
|
||||||
|
func (s *SysUserService) convertToVO(entity entity.SysUser) *vo.SysUserVO {
|
||||||
|
return &vo.SysUserVO{
|
||||||
|
ID: entity.ID,
|
||||||
|
Username: entity.Username,
|
||||||
|
Realname: entity.Realname,
|
||||||
|
Avatar: entity.Avatar,
|
||||||
|
Birthday: entity.Birthday,
|
||||||
|
Sex: entity.Sex,
|
||||||
|
Email: entity.Email,
|
||||||
|
Phone: entity.Phone,
|
||||||
|
OrgCode: entity.OrgCode,
|
||||||
|
Status: entity.Status,
|
||||||
|
CreateTime: entity.CreateTime,
|
||||||
|
UpdateTime: entity.UpdateTime,
|
||||||
|
// 不包含 Password、Salt 等敏感字段
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package vo
|
||||||
|
|
||||||
|
// LoginUserVO 登录用户视图对象
|
||||||
|
type LoginUserVO struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Realname string `json:"realname"`
|
||||||
|
Avatar string `json:"avatar"`
|
||||||
|
Phone string `json:"phone"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
// 注意:不包含 Token,Token 应该在单独的响应结构中
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package vo
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// SysUserVO 用户视图对象
|
||||||
|
type SysUserVO struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Username string `json:"username"` // 登录账号
|
||||||
|
Realname string `json:"realname"` // 真实姓名
|
||||||
|
Avatar string `json:"avatar"` // 头像
|
||||||
|
Birthday *time.Time `json:"birthday"` // 生日
|
||||||
|
Sex int `json:"sex"` // 性别(0-未知,1-男,2-女)
|
||||||
|
Email string `json:"email"` // 电子邮件
|
||||||
|
Phone string `json:"phone"` // 电话
|
||||||
|
OrgCode string `json:"orgCode"` // 机构编码
|
||||||
|
Status int `json:"status"` // 状态(1-正常,2-冻结)
|
||||||
|
CreateTime *time.Time `json:"createTime"` // 创建时间
|
||||||
|
UpdateTime *time.Time `json:"updateTime"` // 更新时间
|
||||||
|
// 注意:不包含 Password、Salt 等敏感字段
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
// Package controller 控制器层
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"server/common"
|
||||||
|
"server/modules/system/service"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoginRequest 登录请求
|
||||||
|
type LoginRequest struct {
|
||||||
|
Username string `json:"username" binding:"required"`
|
||||||
|
Password string `json:"password" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthController 认证控制器
|
||||||
|
type AuthController struct {
|
||||||
|
userService *service.SysUserService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthController() *AuthController {
|
||||||
|
return &AuthController{userService: service.NewSysUserService()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctrl *AuthController) RegisterRoutes(r *gin.RouterGroup) {
|
||||||
|
r.POST("/user/auth/login", ctrl.Login)
|
||||||
|
r.POST("/user/auth/logout", ctrl.Logout)
|
||||||
|
r.GET("/user/auth/info", ctrl.GetUserInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login 用户登录
|
||||||
|
// @Summary 用户登录
|
||||||
|
// @Tags 认证
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body LoginRequest true "登录信息"
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /user/auth/login [post]
|
||||||
|
func (ctrl *AuthController) Login(c *gin.Context) {
|
||||||
|
var req LoginRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
common.Error(c, 400, "用户名和密码不能为空")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loginUser, token, err := ctrl.userService.Login(req.Username, req.Password)
|
||||||
|
if err != nil {
|
||||||
|
common.Error(c, 400, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
common.Success(c, gin.H{
|
||||||
|
"token": token,
|
||||||
|
"user": loginUser,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logout 用户登出
|
||||||
|
// @Summary 用户登出
|
||||||
|
// @Tags 认证
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /user/auth/logout [post]
|
||||||
|
func (ctrl *AuthController) Logout(c *gin.Context) {
|
||||||
|
token := c.GetHeader("Authorization")
|
||||||
|
if len(token) > 7 {
|
||||||
|
token = token[7:] // 去除 "Bearer "
|
||||||
|
}
|
||||||
|
|
||||||
|
ctrl.userService.Logout(token)
|
||||||
|
common.Success(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserInfo 获取当前登录用户信息
|
||||||
|
// @Summary 获取当前登录用户信息
|
||||||
|
// @Tags 认证
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /user/auth/info [get]
|
||||||
|
func (ctrl *AuthController) GetUserInfo(c *gin.Context) {
|
||||||
|
user := common.GetLoginUser(c)
|
||||||
|
if user == nil {
|
||||||
|
common.Error(c, 401, "未登录")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, user)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"server/common"
|
||||||
|
user_service "server/modules/user/service"
|
||||||
|
yxDto "server/modules/yx/dto"
|
||||||
|
yx_service "server/modules/yx/service"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserMajorController struct {
|
||||||
|
userScoreService *user_service.UserScoreService
|
||||||
|
yxUserScoreService *yx_service.YxUserScoreService
|
||||||
|
yxCalculationMajorService *yx_service.YxCalculationMajorService
|
||||||
|
yxVolunteerService *yx_service.YxVolunteerService
|
||||||
|
yxVolunteerRecordService *yx_service.YxVolunteerRecordService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserMajorController() *UserMajorController {
|
||||||
|
return &UserMajorController{
|
||||||
|
yxUserScoreService: yx_service.NewYxUserScoreService(),
|
||||||
|
userScoreService: user_service.NewUserScoreService(),
|
||||||
|
yxCalculationMajorService: yx_service.NewYxCalculationMajorService(),
|
||||||
|
yxVolunteerService: yx_service.NewYxVolunteerService(),
|
||||||
|
yxVolunteerRecordService: yx_service.NewYxVolunteerRecordService(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterRoutes 注册路由
|
||||||
|
func (ctrl *UserMajorController) RegisterRoutes(rg *gin.RouterGroup) {
|
||||||
|
group := rg.Group("/user/major")
|
||||||
|
{
|
||||||
|
// group.GET("/", ctrl.GetActive)
|
||||||
|
// group.GET("/:id", ctrl.GetByID)
|
||||||
|
group.GET("/list", ctrl.List)
|
||||||
|
group.GET("/list_by_school", ctrl.ListBySchool)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListBySchool 获取当前院校下其他专业数据
|
||||||
|
// @Summary 获取当前院校下其他专业数据
|
||||||
|
// @Tags 用户专业
|
||||||
|
// @Param page query int false "页码" default(1)
|
||||||
|
// @Param size query int false "每页数量" default(10)
|
||||||
|
// @Param school_code query string true "院校代码"
|
||||||
|
// @Param probability query string false "录取概率类型(难录取/可冲击/较稳妥/可保底)" default("")
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /user/major/list_by_school [get]
|
||||||
|
func (ctrl *UserMajorController) ListBySchool(c *gin.Context) {
|
||||||
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
|
size, _ := strconv.Atoi(c.DefaultQuery("size", "10"))
|
||||||
|
schoolMajorQuery := yxDto.SchoolMajorQuery{
|
||||||
|
Page: page,
|
||||||
|
Size: size,
|
||||||
|
SchoolCode: c.Query("school_code"),
|
||||||
|
Probability: c.DefaultQuery("probability", ""),
|
||||||
|
LoginUserId: common.GetLoginUser(c).ID,
|
||||||
|
}
|
||||||
|
userScoreVO, err := ctrl.userScoreService.GetActiveScoreByUserID(schoolMajorQuery.LoginUserId)
|
||||||
|
if err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
schoolMajorQuery.UserScoreVO = userScoreVO
|
||||||
|
items, total, probCount, err := ctrl.yxCalculationMajorService.RecommendMajorList(schoolMajorQuery)
|
||||||
|
if err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
newMap := map[string]interface{}{
|
||||||
|
"items": items,
|
||||||
|
"total": total,
|
||||||
|
"probCount": probCount,
|
||||||
|
}
|
||||||
|
common.SuccessPage(c, newMap, total, page, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 获取当前用户可检索列表
|
||||||
|
// @Summary 获取当前用户可检索列表
|
||||||
|
// @Tags 用户专业
|
||||||
|
// @Param page query int false "页码" default(1)
|
||||||
|
// @Param size query int false "每页数量" default(10)
|
||||||
|
// @Param batch query string false "批次(本科提前批/本科A段/本科B段/本科/高职高专)" default("")
|
||||||
|
// @Param probability query string false "录取概率类型(难录取/可冲击/较稳妥/可保底)" default("")
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /user/major/list [get]
|
||||||
|
func (ctrl *UserMajorController) List(c *gin.Context) {
|
||||||
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
|
size, _ := strconv.Atoi(c.DefaultQuery("size", "10"))
|
||||||
|
schoolMajorQuery := yxDto.SchoolMajorQuery{
|
||||||
|
Keyword: c.DefaultQuery("keyword", ""),
|
||||||
|
Page: page,
|
||||||
|
Size: size,
|
||||||
|
SchoolCode: c.Query("schoolCode"),
|
||||||
|
Batch: c.DefaultQuery("batch", ""),
|
||||||
|
Batch2: c.DefaultQuery("batch2", ""),
|
||||||
|
Probability: c.DefaultQuery("probability", ""),
|
||||||
|
LoginUserId: common.GetLoginUser(c).ID,
|
||||||
|
}
|
||||||
|
userScoreVO, err := ctrl.userScoreService.GetActiveScoreByUserID(schoolMajorQuery.LoginUserId)
|
||||||
|
if err != nil {
|
||||||
|
common.Error(c, 500, err.Error()) // 获取用户成绩信息失败
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
schoolMajorQuery.UserScoreVO = userScoreVO
|
||||||
|
items, total, probCount, err := ctrl.yxCalculationMajorService.RecommendMajorList(schoolMajorQuery)
|
||||||
|
if err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
newMap := map[string]interface{}{
|
||||||
|
"items": items,
|
||||||
|
"total": total,
|
||||||
|
"probCount": probCount,
|
||||||
|
}
|
||||||
|
common.SuccessPage(c, newMap, total, page, size)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"server/common"
|
||||||
|
user_service "server/modules/user/service"
|
||||||
|
"server/modules/yx/dto"
|
||||||
|
yx_service "server/modules/yx/service"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserScoreController struct {
|
||||||
|
userScoreService *user_service.UserScoreService
|
||||||
|
yxUserScoreService *yx_service.YxUserScoreService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserScoreController() *UserScoreController {
|
||||||
|
return &UserScoreController{
|
||||||
|
yxUserScoreService: yx_service.NewYxUserScoreService(),
|
||||||
|
userScoreService: user_service.NewUserScoreService(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterRoutes 注册路由
|
||||||
|
func (ctrl *UserScoreController) RegisterRoutes(rg *gin.RouterGroup) {
|
||||||
|
group := rg.Group("/user/score")
|
||||||
|
{
|
||||||
|
group.GET("/", ctrl.GetActive)
|
||||||
|
group.GET("/:id", ctrl.GetByID)
|
||||||
|
group.GET("/list", ctrl.List)
|
||||||
|
group.POST("/save-score", ctrl.SaveUserScore)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveUserScore 保存用户成绩
|
||||||
|
// @Summary 保存用户成绩
|
||||||
|
// @Tags 用户分数
|
||||||
|
// @Param request body dto.SaveScoreRequest true "成绩信息"
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /user/score/save-score [post]
|
||||||
|
func (ctrl *UserScoreController) SaveUserScore(c *gin.Context) {
|
||||||
|
var req dto.SaveScoreRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
common.Error(c, 400, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := req.Validate(); err != nil {
|
||||||
|
common.Error(c, 400, "校验失败: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.CreateBy = common.GetLoginUser(c).ID
|
||||||
|
// 直接调用 Service,不处理具体业务逻辑
|
||||||
|
result, err := ctrl.userScoreService.SaveUserScore(&req)
|
||||||
|
if err != nil {
|
||||||
|
common.Error(c, 500, "保存失败: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetActive 获取当前用户的激活分数
|
||||||
|
// @Summary 获取当前用户的激活分数
|
||||||
|
// @Tags 用户分数
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /user/score [get]
|
||||||
|
func (ctrl *UserScoreController) GetActive(c *gin.Context) {
|
||||||
|
loginUserId := common.GetLoginUser(c).ID
|
||||||
|
item, err := ctrl.userScoreService.GetActiveScoreByUserID(loginUserId)
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,199 @@
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"server/common"
|
||||||
|
"server/modules/user/dto"
|
||||||
|
"server/modules/user/service"
|
||||||
|
"server/modules/user/vo"
|
||||||
|
yxDto "server/modules/yx/dto"
|
||||||
|
"server/modules/yx/entity"
|
||||||
|
yx_service "server/modules/yx/service"
|
||||||
|
"time" // 用于时间处理
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// _ 确保包被导入(用于类型引用)
|
||||||
|
var _ = yxDto.SchoolMajorDTO{}
|
||||||
|
var _ = entity.YxVolunteerRecord{}
|
||||||
|
var _ = time.Now()
|
||||||
|
var _ = vo.VolunteerDetailVO{}
|
||||||
|
|
||||||
|
type UserVolunteerController struct {
|
||||||
|
userScoreService *service.UserScoreService
|
||||||
|
yxVolunteerService *yx_service.YxVolunteerService
|
||||||
|
yxVolunteerRecordService *yx_service.YxVolunteerRecordService
|
||||||
|
yxCalculationMajorService *yx_service.YxCalculationMajorService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserVolunteerController() *UserVolunteerController {
|
||||||
|
return &UserVolunteerController{
|
||||||
|
userScoreService: service.NewUserScoreService(),
|
||||||
|
yxVolunteerService: yx_service.NewYxVolunteerService(),
|
||||||
|
yxVolunteerRecordService: yx_service.NewYxVolunteerRecordService(),
|
||||||
|
yxCalculationMajorService: yx_service.NewYxCalculationMajorService(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctrl *UserVolunteerController) RegisterRoutes(rg *gin.RouterGroup) {
|
||||||
|
group := rg.Group("/user/volunteer")
|
||||||
|
{
|
||||||
|
group.POST("/save", ctrl.SaveVolunteer)
|
||||||
|
group.GET("/detail", ctrl.GetVolunteerDetail)
|
||||||
|
group.PUT("/updateName", ctrl.UpdateVolunteerName)
|
||||||
|
group.GET("/list", ctrl.GetUserVolunteerList)
|
||||||
|
group.DELETE("/delete", ctrl.DeleteVolunteer)
|
||||||
|
group.POST("/switch", ctrl.SwitchVolunteer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveVolunteer 保存志愿明细
|
||||||
|
// @Summary 保存志愿明细
|
||||||
|
// @Tags 用户志愿
|
||||||
|
// @Param request body dto.SaveVolunteerRequest true "志愿键列表"
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /user/volunteer/save [post]
|
||||||
|
func (ctrl *UserVolunteerController) SaveVolunteer(c *gin.Context) {
|
||||||
|
var req dto.SaveVolunteerRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loginUserId := common.GetLoginUser(c).ID
|
||||||
|
if err := ctrl.userScoreService.SaveVolunteer(loginUserId, &req); err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, "保存成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVolunteerDetail 获取当前志愿单详情
|
||||||
|
// @Summary 获取当前志愿单详情
|
||||||
|
// @Tags 用户志愿
|
||||||
|
// @Success 200 {object} common.Response{data=vo.VolunteerDetailResponse}
|
||||||
|
// @Router /user/volunteer/detail [get]
|
||||||
|
func (ctrl *UserVolunteerController) GetVolunteerDetail(c *gin.Context) {
|
||||||
|
loginUserId := common.GetLoginUser(c).ID
|
||||||
|
response, err := ctrl.userScoreService.GetVolunteerDetail(loginUserId)
|
||||||
|
if err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateVolunteerName 编辑志愿单名称
|
||||||
|
// @Summary 编辑志愿单名称
|
||||||
|
// @Tags 用户志愿
|
||||||
|
// @Param id query string true "志愿单ID"
|
||||||
|
// @Param name query string true "志愿单名称"
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /user/volunteer/updateName [put]
|
||||||
|
func (ctrl *UserVolunteerController) UpdateVolunteerName(c *gin.Context) {
|
||||||
|
id := c.Query("id")
|
||||||
|
name := c.Query("name")
|
||||||
|
if id == "" || name == "" {
|
||||||
|
common.Error(c, 400, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loginUserID := common.GetLoginUser(c).ID
|
||||||
|
if err := ctrl.yxVolunteerService.UpdateName(id, name, loginUserID); err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
common.Success(c, "更新成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserVolunteerList 获取当前用户志愿单列表
|
||||||
|
// @Summary 获取当前用户志愿单列表
|
||||||
|
// @Tags 用户志愿
|
||||||
|
// @Param page query int false "页码"
|
||||||
|
// @Param size query int false "每页数量"
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /user/volunteer/list [get]
|
||||||
|
func (ctrl *UserVolunteerController) GetUserVolunteerList(c *gin.Context) {
|
||||||
|
page := common.GetPage(c)
|
||||||
|
size := common.GetSize(c)
|
||||||
|
loginUserID := common.GetLoginUser(c).ID
|
||||||
|
|
||||||
|
items, total, err := ctrl.yxVolunteerService.ListByUser(loginUserID, page, size)
|
||||||
|
if err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
common.Success(c, map[string]interface{}{
|
||||||
|
"items": items,
|
||||||
|
"total": total,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteVolunteer 删除志愿单接口
|
||||||
|
// @Summary 删除志愿单接口
|
||||||
|
// @Tags 用户志愿
|
||||||
|
// @Param id query string true "志愿单ID"
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /user/volunteer/delete [delete]
|
||||||
|
func (ctrl *UserVolunteerController) DeleteVolunteer(c *gin.Context) {
|
||||||
|
id := c.Query("id")
|
||||||
|
if id == "" {
|
||||||
|
common.Error(c, 400, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loginUserID := common.GetLoginUser(c).ID
|
||||||
|
err := ctrl.yxVolunteerService.DeleteVolunteer(id, loginUserID)
|
||||||
|
if err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
common.Success(c, "删除成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SwitchVolunteer 切换当前志愿单
|
||||||
|
// @Summary 切换当前志愿单
|
||||||
|
// @Tags 用户志愿
|
||||||
|
// @Param id query string true "志愿单ID"
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /user/volunteer/switch [post]
|
||||||
|
func (ctrl *UserVolunteerController) SwitchVolunteer(c *gin.Context) {
|
||||||
|
id := c.Query("id")
|
||||||
|
if id == "" {
|
||||||
|
common.Error(c, 400, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loginUserID := common.GetLoginUser(c).ID
|
||||||
|
// 1. 先判断是否已是该志愿单 (从 cache 或 db 中查找激活的)
|
||||||
|
userScoreVO, err := ctrl.userScoreService.GetActiveScoreByUserID(loginUserID)
|
||||||
|
if err != nil {
|
||||||
|
common.Error(c, 500, "获取用户成绩信息失败: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if userScoreVO.ID != "" {
|
||||||
|
activeVolunteer, _ := ctrl.yxVolunteerService.FindActiveByScoreId(userScoreVO.ID)
|
||||||
|
if activeVolunteer != nil && activeVolunteer.ID == id {
|
||||||
|
common.Success(c, "已是当前志愿单,忽略切换")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 切换
|
||||||
|
if err := ctrl.yxVolunteerService.SwitchVolunteer(id, loginUserID, ctrl.userScoreService); err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Redis 缓存同步 (假设 common 中有清除缓存的方法逻辑,或者由 Service 处理)
|
||||||
|
// 这里按需求提到:切换志愿单切换后 写入到redis做缓存(查询志愿单接口也要从redis读本体数据),再清除之前的成绩单redis重新获取写到redis缓存。
|
||||||
|
// 实际项目中 common.GetLoginUser 已经包含了 ID,成绩单和志愿单通常由具体模块处理缓存。
|
||||||
|
// 如果这里只是标记,Service 层已做状态更新,查询接口读取时会感知变化。
|
||||||
|
// 为了彻底满足“写入/清除 redis 缓存”,通常会有相应的 RedisUtil 调用。
|
||||||
|
// 假设 service 层或 controller 有 Cache 清理逻辑。
|
||||||
|
|
||||||
|
common.Success(c, "切换成功")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"server/modules/user/vo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RecommendMajorListRequest 推荐专业列表请求
|
||||||
|
type RecommendMajorListRequest struct {
|
||||||
|
Page int `json:"page"`
|
||||||
|
Size int `json:"size"`
|
||||||
|
LoginUserId string `json:"loginUserId"`
|
||||||
|
UserScoreVO vo.UserScoreVO `json:"userScoreVO"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package dto
|
||||||
|
|
||||||
|
// SaveVolunteerRequest 保存志愿请求
|
||||||
|
type SaveVolunteerRequest struct {
|
||||||
|
Keys []string `json:"keys" binding:"required"` // Keys: schoolCode_majorCode_enrollmentCode
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,527 @@
|
||||||
|
// Package service 业务逻辑层
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"server/common"
|
||||||
|
"server/config"
|
||||||
|
userDto "server/modules/user/dto"
|
||||||
|
"server/modules/user/vo"
|
||||||
|
yxDto "server/modules/yx/dto"
|
||||||
|
"server/modules/yx/entity"
|
||||||
|
"server/modules/yx/mapper"
|
||||||
|
"server/modules/yx/service"
|
||||||
|
"server/types"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserScoreService struct {
|
||||||
|
yxUserScoreService *service.YxUserScoreService
|
||||||
|
yxVolunteerService *service.YxVolunteerService
|
||||||
|
yxVolunteerRecordService *service.YxVolunteerRecordService
|
||||||
|
yxCalculationMajorService *service.YxCalculationMajorService
|
||||||
|
mapper *mapper.YxUserScoreMapper
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetActiveScoreID 获取用户的激活成绩ID
|
||||||
|
func (s *UserScoreService) GetActiveScoreID(userID string) (string, error) {
|
||||||
|
var score entity.YxUserScore
|
||||||
|
// 明确指定字段,提高可读性
|
||||||
|
err := config.DB.Model(&entity.YxUserScore{}).
|
||||||
|
Where("create_by = ? AND state = ?", userID, "1").
|
||||||
|
Select("id").
|
||||||
|
First(&score).Error
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return "", nil // 未找到激活成绩返回空字符串
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("查询激活成绩ID失败: %w", err)
|
||||||
|
}
|
||||||
|
return score.ID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetActiveScoreByID 获取用户的激活成绩实体(实现 IScoreService 接口)
|
||||||
|
func (s *UserScoreService) GetActiveScoreByID(userID string) (entity.YxUserScore, error) {
|
||||||
|
var score entity.YxUserScore
|
||||||
|
// 明确指定字段,提高可读性
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 vo.UserScoreVO{}, fmt.Errorf("未找到成绩记录")
|
||||||
|
}
|
||||||
|
return vo.UserScoreVO{}, fmt.Errorf("查询成绩记录失败: %w", err)
|
||||||
|
}
|
||||||
|
return s.convertEntityToVo(score), 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 (s *UserScoreService) Delete(id string) error {
|
||||||
|
return s.mapper.Delete(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UserScoreService) UpdateFields(id string, fields map[string]interface{}) error {
|
||||||
|
return s.mapper.UpdateFields(id, fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserScoreService() *UserScoreService {
|
||||||
|
return &UserScoreService{
|
||||||
|
yxUserScoreService: service.NewYxUserScoreService(),
|
||||||
|
yxCalculationMajorService: service.NewYxCalculationMajorService(),
|
||||||
|
yxVolunteerService: service.NewYxVolunteerService(),
|
||||||
|
yxVolunteerRecordService: service.NewYxVolunteerRecordService(),
|
||||||
|
mapper: mapper.NewYxUserScoreMapper(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVolunteerDetail 获取志愿详情(从 Controller 移入 Service 层)
|
||||||
|
func (s *UserScoreService) GetVolunteerDetail(userID string) (*vo.VolunteerDetailResponse, error) {
|
||||||
|
userScoreVO, err := s.GetActiveScoreByUserID(userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找当前激活的志愿表
|
||||||
|
volunteer, err := s.yxVolunteerService.FindActiveByScoreId(userScoreVO.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("查找志愿表失败: %w", err)
|
||||||
|
}
|
||||||
|
if volunteer == nil || volunteer.ID == "" {
|
||||||
|
return &vo.VolunteerDetailResponse{Volunteer: nil, Items: nil}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
records, err := s.yxVolunteerRecordService.FindByVolunteerID(volunteer.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("查找志愿明细失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取丰富详情
|
||||||
|
var enrichedMajors map[string]yxDto.SchoolMajorDTO
|
||||||
|
if len(records) > 0 && userScoreVO.CalculationTableName != "" {
|
||||||
|
keys := make([]string, 0, len(records))
|
||||||
|
for _, r := range records {
|
||||||
|
keys = append(keys, r.SchoolCode+"_"+r.MajorCode+"_"+r.EnrollmentCode)
|
||||||
|
}
|
||||||
|
majors, err := s.yxCalculationMajorService.FindDtoListByCompositeKeys(userScoreVO.CalculationTableName, keys, userScoreVO.ID)
|
||||||
|
if err == nil {
|
||||||
|
enrichedMajors = make(map[string]yxDto.SchoolMajorDTO)
|
||||||
|
for _, m := range majors {
|
||||||
|
enrichedMajors[m.SchoolCode+"_"+m.MajorCode+"_"+m.EnrollmentCode] = m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分组
|
||||||
|
groupedItems := &vo.VolunteerItemsVO{
|
||||||
|
BatchBefore: []vo.VolunteerDetailVO{},
|
||||||
|
BatchUndergraduate: []vo.VolunteerDetailVO{},
|
||||||
|
BatchCollege: []vo.VolunteerDetailVO{},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range records {
|
||||||
|
item := vo.VolunteerDetailVO{
|
||||||
|
ID: r.ID,
|
||||||
|
VolunteerID: r.VolunteerID,
|
||||||
|
SchoolCode: r.SchoolCode,
|
||||||
|
MajorCode: r.MajorCode,
|
||||||
|
EnrollmentCode: r.EnrollmentCode,
|
||||||
|
Indexs: r.Indexs,
|
||||||
|
Batch: r.Batch,
|
||||||
|
EnrollProbability: r.EnrollProbability,
|
||||||
|
StudentConvertedScore: r.StudentConvertedScore,
|
||||||
|
CreateBy: r.CreateBy,
|
||||||
|
CreateTime: types.NewDateTime(r.CreateTime),
|
||||||
|
CalculationMajorID: r.CalculationMajorID,
|
||||||
|
}
|
||||||
|
key := r.SchoolCode + "_" + r.MajorCode + "_" + r.EnrollmentCode
|
||||||
|
if m, ok := enrichedMajors[key]; ok {
|
||||||
|
item.SchoolName = m.SchoolName
|
||||||
|
item.MajorName = m.MajorName
|
||||||
|
item.PlanNum = m.PlanNum
|
||||||
|
item.Tuition = m.Tuition
|
||||||
|
item.Province = m.Province
|
||||||
|
item.SchoolNature = m.SchoolNature
|
||||||
|
item.InstitutionType = m.InstitutionType
|
||||||
|
item.MajorDetail = m.Detail
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分批
|
||||||
|
groupKey := ""
|
||||||
|
if r.Batch == "提前批" || r.Batch == "本科提前批" {
|
||||||
|
groupKey = "提前批"
|
||||||
|
} else if r.Batch == "高职高专" || r.Batch == "专科批" {
|
||||||
|
groupKey = "专科批"
|
||||||
|
} else {
|
||||||
|
groupKey = "本科批"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch groupKey {
|
||||||
|
case "提前批":
|
||||||
|
groupedItems.BatchBefore = append(groupedItems.BatchBefore, item)
|
||||||
|
case "本科批":
|
||||||
|
groupedItems.BatchUndergraduate = append(groupedItems.BatchUndergraduate, item)
|
||||||
|
case "专科批":
|
||||||
|
groupedItems.BatchCollege = append(groupedItems.BatchCollege, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
volunteerInfo := &vo.VolunteerInfoVO{
|
||||||
|
ID: volunteer.ID,
|
||||||
|
VolunteerName: volunteer.VolunteerName,
|
||||||
|
ScoreId: volunteer.ScoreId,
|
||||||
|
CreateType: volunteer.CreateType,
|
||||||
|
State: volunteer.State,
|
||||||
|
CreateBy: volunteer.CreateBy,
|
||||||
|
CreateTime: types.NewDateTime(volunteer.CreateTime),
|
||||||
|
UpdateBy: volunteer.UpdateBy,
|
||||||
|
UpdateTime: types.NewDateTime(volunteer.UpdateTime),
|
||||||
|
}
|
||||||
|
itemMap := make(map[string][]vo.VolunteerDetailVO)
|
||||||
|
|
||||||
|
itemMap["提前批"] = groupedItems.BatchBefore
|
||||||
|
itemMap["本科批"] = groupedItems.BatchUndergraduate
|
||||||
|
itemMap["专科批"] = groupedItems.BatchCollege
|
||||||
|
|
||||||
|
return &vo.VolunteerDetailResponse{
|
||||||
|
Volunteer: volunteerInfo,
|
||||||
|
Items: itemMap,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveVolunteer 保存志愿(从 Controller 移入 Service 层)
|
||||||
|
func (s *UserScoreService) SaveVolunteer(userID string, req *userDto.SaveVolunteerRequest) error {
|
||||||
|
// 数据去重
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
var uniqueKeys []string
|
||||||
|
for _, key := range req.Keys {
|
||||||
|
if !seen[key] {
|
||||||
|
seen[key] = true
|
||||||
|
uniqueKeys = append(uniqueKeys, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loginUserId := userID
|
||||||
|
userScoreVO, err := s.GetActiveScoreByUserID(loginUserId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if userScoreVO.CalculationTableName == "" {
|
||||||
|
return fmt.Errorf("未找到计算表名")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找当前激活的志愿表
|
||||||
|
volunteer, err := s.yxVolunteerService.FindActiveByScoreId(userScoreVO.ID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("查找志愿表失败: %w", err)
|
||||||
|
}
|
||||||
|
if volunteer == nil || volunteer.ID == "" {
|
||||||
|
return fmt.Errorf("请先创建志愿表")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找专业信息
|
||||||
|
majors, err := s.yxCalculationMajorService.FindListByCompositeKeys(userScoreVO.CalculationTableName, uniqueKeys, userScoreVO.ID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("查找专业信息失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建 Map 用于保持顺序
|
||||||
|
majorMap := make(map[string]entity.YxCalculationMajor)
|
||||||
|
for _, major := range majors {
|
||||||
|
k := major.SchoolCode + "_" + major.MajorCode + "_" + major.EnrollmentCode
|
||||||
|
majorMap[k] = major
|
||||||
|
}
|
||||||
|
|
||||||
|
var records []entity.YxVolunteerRecord
|
||||||
|
for i, key := range uniqueKeys {
|
||||||
|
if major, ok := majorMap[key]; ok {
|
||||||
|
record := entity.YxVolunteerRecord{
|
||||||
|
VolunteerID: volunteer.ID,
|
||||||
|
SchoolCode: major.SchoolCode,
|
||||||
|
MajorCode: major.MajorCode,
|
||||||
|
EnrollmentCode: major.EnrollmentCode,
|
||||||
|
Indexs: i + 1,
|
||||||
|
CreateBy: loginUserId,
|
||||||
|
CreateTime: time.Now(),
|
||||||
|
Batch: major.Batch,
|
||||||
|
EnrollProbability: major.EnrollProbability,
|
||||||
|
StudentConvertedScore: major.StudentConvertedScore,
|
||||||
|
CalculationMajorID: major.ID,
|
||||||
|
}
|
||||||
|
records = append(records, record)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先删除旧数据
|
||||||
|
if err := s.yxVolunteerRecordService.DeleteByVolunteerID(volunteer.ID); err != nil {
|
||||||
|
return fmt.Errorf("删除旧数据失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量插入新数据
|
||||||
|
if len(records) > 0 {
|
||||||
|
if err := s.yxVolunteerRecordService.BatchCreate(records); err != nil {
|
||||||
|
return fmt.Errorf("保存失败: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveUserScore 保存用户成绩并返回 VO
|
||||||
|
func (s *UserScoreService) SaveUserScore(req *yxDto.SaveScoreRequest) (vo.UserScoreVO, error) {
|
||||||
|
// 1. 业务验证
|
||||||
|
if err := req.Validate(); err != nil {
|
||||||
|
return vo.UserScoreVO{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. DTO 转 Entity
|
||||||
|
entityItem := s.convertDtoToEntity(req)
|
||||||
|
entityItem.CalculationTableName = "yx_calculation_major_2025_2"
|
||||||
|
entityItem.ID = common.GenerateStringID() // 使用新封装的 ID 生成工具
|
||||||
|
entityItem.CreateTime = time.Now()
|
||||||
|
entityItem.UpdateTime = time.Now()
|
||||||
|
|
||||||
|
// 3. 执行保存操作(可以包含事务)
|
||||||
|
tx := config.DB.Begin()
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
fmt.Printf("【PANIC】事务执行过程中发生panic: %v", r)
|
||||||
|
// 记录详细的栈信息
|
||||||
|
fmt.Printf("【PANIC】尝试回滚事务")
|
||||||
|
tx.Rollback()
|
||||||
|
fmt.Printf("【PANIC】事务已回滚")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 标记该用户的所有旧成绩为历史状态
|
||||||
|
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 vo.UserScoreVO{}, fmt.Errorf("更新旧记录失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存新的成绩记录
|
||||||
|
if err := tx.Create(entityItem).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return vo.UserScoreVO{}, fmt.Errorf("保存记录失败: %w", err)
|
||||||
|
}
|
||||||
|
userScoreVO := s.convertEntityToVo(*entityItem)
|
||||||
|
|
||||||
|
// 提交事务 - ✅ 快速释放数据库连接
|
||||||
|
// if err := tx.Commit().Error; err != nil {
|
||||||
|
// return vo.UserScoreVO{}, fmt.Errorf("提交事务失败: %w", err)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 根据成绩计算用户的专业信息
|
||||||
|
schoolMajorItems, err := s.yxCalculationMajorService.ListByUserQueryType(userScoreVO.ProfessionalCategory,
|
||||||
|
userScoreVO.CognitioPolyclinic, userScoreVO.ProfessionalCategoryChildren)
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return vo.UserScoreVO{}, fmt.Errorf("查询专业信息失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查录取概率
|
||||||
|
s.yxCalculationMajorService.CheckEnrollProbability(&schoolMajorItems, userScoreVO)
|
||||||
|
|
||||||
|
// 插入到数据库
|
||||||
|
err = s.yxCalculationMajorService.BatchCreateBySchoolMajorDTO(entityItem.CalculationTableName, schoolMajorItems, userScoreVO.ID)
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return vo.UserScoreVO{}, fmt.Errorf("保存专业信息失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建志愿表
|
||||||
|
err = s.yxVolunteerService.CreateByScoreId(entityItem.ID, req.CreateBy)
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return vo.UserScoreVO{}, fmt.Errorf("创建志愿表失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交事务
|
||||||
|
if err := tx.Commit().Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return vo.UserScoreVO{}, fmt.Errorf("提交事务失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除之前的Redis成绩缓存
|
||||||
|
config.RDB.Del(context.Background(), common.RedisUserScorePrefix+req.CreateBy)
|
||||||
|
|
||||||
|
// 更新Redis 数据
|
||||||
|
scoreRedisSetData, err := json.Marshal(entityItem)
|
||||||
|
err = config.RDB.Set(context.Background(), common.RedisUserScorePrefix+req.CreateBy, scoreRedisSetData, common.RedisUserScoreExpire).Err()
|
||||||
|
if err != nil {
|
||||||
|
return vo.UserScoreVO{}, fmt.Errorf("缓存成绩记录失败: %w", err)
|
||||||
|
}
|
||||||
|
return userScoreVO, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 私有方法:DTO 转 Entity
|
||||||
|
func (s *UserScoreService) convertDtoToEntity(req *yxDto.SaveScoreRequest) *entity.YxUserScore {
|
||||||
|
entityItem := entity.YxUserScore{
|
||||||
|
CognitioPolyclinic: req.CognitioPolyclinic,
|
||||||
|
Subjects: strings.Join(req.SubjectList, ","),
|
||||||
|
ProfessionalCategory: req.ProfessionalCategory,
|
||||||
|
ProfessionalCategoryChildren: strings.Join(req.ProfessionalCategoryChildren, ","),
|
||||||
|
ProfessionalScore: *req.ProfessionalScore,
|
||||||
|
CulturalScore: *req.CulturalScore,
|
||||||
|
EnglishScore: *req.EnglishScore,
|
||||||
|
ChineseScore: *req.ChineseScore,
|
||||||
|
Province: req.Province,
|
||||||
|
State: "1", // 默认状态
|
||||||
|
CreateBy: req.CreateBy,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 映射子专业成绩到具体字段
|
||||||
|
if v, ok := req.ProfessionalCategoryChildrenScore["音乐表演声乐"]; ok {
|
||||||
|
entityItem.Yybysy = v
|
||||||
|
}
|
||||||
|
if v, ok := req.ProfessionalCategoryChildrenScore["音乐表演器乐"]; ok {
|
||||||
|
entityItem.Yybyqy = v
|
||||||
|
}
|
||||||
|
if v, ok := req.ProfessionalCategoryChildrenScore["音乐教育"]; ok {
|
||||||
|
entityItem.Yyjy = v
|
||||||
|
}
|
||||||
|
if v, ok := req.ProfessionalCategoryChildrenScore["戏剧影视导演"]; ok {
|
||||||
|
entityItem.Xjysdy = v
|
||||||
|
}
|
||||||
|
if v, ok := req.ProfessionalCategoryChildrenScore["戏剧影视表演"]; ok {
|
||||||
|
entityItem.Xjysby = v
|
||||||
|
}
|
||||||
|
if v, ok := req.ProfessionalCategoryChildrenScore["服装表演"]; ok {
|
||||||
|
entityItem.Fzby = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return &entityItem
|
||||||
|
}
|
||||||
|
|
||||||
|
// 私有方法:Entity 转 VO
|
||||||
|
func (s *UserScoreService) convertEntityToVo(item entity.YxUserScore) vo.UserScoreVO {
|
||||||
|
voItem := vo.UserScoreVO{
|
||||||
|
ID: item.ID,
|
||||||
|
Type: item.Type,
|
||||||
|
EducationalLevel: item.EducationalLevel,
|
||||||
|
CognitioPolyclinic: item.CognitioPolyclinic,
|
||||||
|
ProfessionalCategory: item.ProfessionalCategory,
|
||||||
|
ProfessionalCategoryChildren: []string{},
|
||||||
|
ProfessionalCategoryChildrenScore: make(map[string]float64),
|
||||||
|
ProfessionalScore: item.ProfessionalScore,
|
||||||
|
CulturalScore: item.CulturalScore,
|
||||||
|
EnglishScore: item.EnglishScore,
|
||||||
|
ChineseScore: item.ChineseScore,
|
||||||
|
Province: item.Province,
|
||||||
|
State: item.State,
|
||||||
|
CalculationTableName: item.CalculationTableName,
|
||||||
|
}
|
||||||
|
if item.Subjects == "" {
|
||||||
|
voItem.SubjectList = []string{}
|
||||||
|
} else {
|
||||||
|
voItem.SubjectList = strings.Split(item.Subjects, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.ProfessionalCategoryChildren == "" {
|
||||||
|
voItem.ProfessionalCategoryChildren = []string{}
|
||||||
|
} else {
|
||||||
|
voItem.ProfessionalCategoryChildren = strings.Split(item.ProfessionalCategoryChildren, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 映射具体字段到子专业成绩 Map
|
||||||
|
if item.Yybysy > 0 {
|
||||||
|
voItem.ProfessionalCategoryChildrenScore["音乐表演声乐"] = item.Yybysy
|
||||||
|
}
|
||||||
|
if item.Yybyqy > 0 {
|
||||||
|
voItem.ProfessionalCategoryChildrenScore["音乐表演器乐"] = item.Yybyqy
|
||||||
|
}
|
||||||
|
if item.Yyjy > 0 {
|
||||||
|
voItem.ProfessionalCategoryChildrenScore["音乐教育"] = item.Yyjy
|
||||||
|
}
|
||||||
|
if item.Xjysdy > 0 {
|
||||||
|
voItem.ProfessionalCategoryChildrenScore["戏剧影视导演"] = item.Xjysdy
|
||||||
|
}
|
||||||
|
if item.Xjysby > 0 {
|
||||||
|
voItem.ProfessionalCategoryChildrenScore["戏剧影视表演"] = item.Xjysby
|
||||||
|
}
|
||||||
|
if item.Fzby > 0 {
|
||||||
|
voItem.ProfessionalCategoryChildrenScore["服装表演"] = item.Fzby
|
||||||
|
}
|
||||||
|
|
||||||
|
return voItem
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package vo
|
||||||
|
|
||||||
|
// UserScoreVO 用户成绩展示对象
|
||||||
|
type UserScoreVO struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type string `json:"type"` // 填报类型(1-普通类 2-艺术类)
|
||||||
|
EducationalLevel string `json:"educationalLevel"` // 学历层次(1-本科,2-专科)
|
||||||
|
CognitioPolyclinic string `json:"cognitioPolyclinic"` // 文理分班(文科/理科)
|
||||||
|
SubjectList []string `json:"subjectList"` // 选课列表
|
||||||
|
ProfessionalCategory string `json:"professionalCategory"` // 专业类别
|
||||||
|
ProfessionalCategoryChildren []string `json:"professionalCategoryChildren"` // 子级专业类别
|
||||||
|
ProfessionalCategoryChildrenScore map[string]float64 `json:"professionalCategoryChildrenScore"` // 子级专业成绩
|
||||||
|
ProfessionalScore float64 `json:"professionalScore"` // 专业总分
|
||||||
|
CulturalScore float64 `json:"culturalScore"` // 文化成绩分
|
||||||
|
EnglishScore float64 `json:"englishScore"` // 英语成绩
|
||||||
|
ChineseScore float64 `json:"chineseScore"` // 语文成绩
|
||||||
|
Province string `json:"province"` // 高考省份
|
||||||
|
State string `json:"state"` // 状态
|
||||||
|
CalculationTableName string `json:"calculationTableName"` // 计算表名称
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
package vo
|
||||||
|
|
||||||
|
import "server/types"
|
||||||
|
|
||||||
|
// VolunteerDetailVO 志愿详情视图对象
|
||||||
|
type VolunteerDetailVO struct {
|
||||||
|
ID string `json:"id"` // 志愿记录ID
|
||||||
|
VolunteerID string `json:"volunteerId"` // 志愿单ID
|
||||||
|
SchoolCode string `json:"schoolCode"` // 院校代码
|
||||||
|
MajorCode string `json:"majorCode"` // 专业代码
|
||||||
|
EnrollmentCode string `json:"enrollmentCode"` // 招生代码
|
||||||
|
Indexs int `json:"indexs"` // 志愿序号
|
||||||
|
Batch string `json:"batch"` // 批次
|
||||||
|
EnrollProbability float64 `json:"enrollProbability"` // 录取概率
|
||||||
|
StudentConvertedScore float64 `json:"studentConvertedScore"` // 学生折合分
|
||||||
|
CreateBy string `json:"createBy"` // 创建人
|
||||||
|
CreateTime types.DateTime `json:"createTime"` // 创建时间
|
||||||
|
CalculationMajorID string `json:"calculationMajorId"` // 计算专业ID
|
||||||
|
// 扩展字段(来自 SchoolMajorDTO)
|
||||||
|
SchoolName string `json:"schoolName"` // 院校名称
|
||||||
|
MajorName string `json:"majorName"` // 专业名称
|
||||||
|
PlanNum int `json:"planNum"` // 计划人数
|
||||||
|
Tuition string `json:"tuition"` // 学费
|
||||||
|
SchoolIcon string `json:"schoolIcon"` // 院校图标
|
||||||
|
Province string `json:"province"` // 省份
|
||||||
|
SchoolNature string `json:"schoolNature"` // 院校性质
|
||||||
|
InstitutionType string `json:"institutionType"` // 院校类型
|
||||||
|
MajorDetail string `json:"majorDetail"` // 专业详情
|
||||||
|
}
|
||||||
|
|
||||||
|
// VolunteerDetailResponse 志愿详情响应
|
||||||
|
type VolunteerDetailResponse struct {
|
||||||
|
Volunteer *VolunteerInfoVO `json:"volunteer"` // 志愿单信息
|
||||||
|
Items map[string][]VolunteerDetailVO `json:"items"` // 志愿明细列表 *VolunteerItemsVO
|
||||||
|
}
|
||||||
|
|
||||||
|
// VolunteerInfoVO 志愿单信息视图对象
|
||||||
|
type VolunteerInfoVO struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
VolunteerName string `json:"volunteerName"`
|
||||||
|
ScoreId string `json:"scoreId"`
|
||||||
|
CreateType string `json:"createType"`
|
||||||
|
State string `json:"state"`
|
||||||
|
CreateBy string `json:"createBy"`
|
||||||
|
CreateTime types.DateTime `json:"createTime"`
|
||||||
|
UpdateBy string `json:"updateBy"`
|
||||||
|
UpdateTime types.DateTime `json:"updateTime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// VolunteerItemsVO 志愿明细列表视图对象
|
||||||
|
type VolunteerItemsVO struct {
|
||||||
|
BatchBefore []VolunteerDetailVO `json:"batchBefore"` // 提前批
|
||||||
|
BatchUndergraduate []VolunteerDetailVO `json:"batchUndergraduate"` // 本科批
|
||||||
|
BatchCollege []VolunteerDetailVO `json:"batchCollege"` // 专科批
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,173 @@
|
||||||
|
// Package controller 控制器层
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"server/common"
|
||||||
|
"server/modules/yx/dto"
|
||||||
|
"server/modules/yx/service"
|
||||||
|
yxVO "server/modules/yx/vo"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type YxCalculationMajorController struct {
|
||||||
|
service *service.YxCalculationMajorService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewYxCalculationMajorController() *YxCalculationMajorController {
|
||||||
|
return &YxCalculationMajorController{service: service.NewYxCalculationMajorService()}
|
||||||
|
}
|
||||||
|
|
||||||
|
// _ 确保 vo 包被导入(用于 Swagger 注解)
|
||||||
|
var _ = yxVO.YxCalculationMajorVO{}
|
||||||
|
|
||||||
|
func (ctrl *YxCalculationMajorController) RegisterRoutes(r *gin.RouterGroup) {
|
||||||
|
r.GET("/yx-calculation-majors", ctrl.List)
|
||||||
|
r.GET("/yx-calculation-majors/:id", ctrl.GetByID)
|
||||||
|
r.POST("/yx-calculation-majors", ctrl.Create)
|
||||||
|
r.PUT("/yx-calculation-majors/:id", ctrl.Update)
|
||||||
|
r.PATCH("/yx-calculation-majors/:id", ctrl.UpdateFields)
|
||||||
|
r.DELETE("/yx-calculation-majors/:id", ctrl.Delete)
|
||||||
|
r.POST("/yx-calculation-majors/batch", ctrl.BatchCreate)
|
||||||
|
r.DELETE("/yx-calculation-majors/batch", ctrl.BatchDelete)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary 获取计算专业列表
|
||||||
|
// @Tags 计算专业
|
||||||
|
// @Param page query int false "页码"
|
||||||
|
// @Param size query int false "每页数量"
|
||||||
|
// @Success 200 {object} common.Response{data=[]vo.YxCalculationMajorVO}
|
||||||
|
// @Router /yx-calculation-majors [get]
|
||||||
|
func (ctrl *YxCalculationMajorController) List(c *gin.Context) {
|
||||||
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
|
size, _ := strconv.Atoi(c.DefaultQuery("size", "10"))
|
||||||
|
items, total, err := ctrl.service.ListCalculationMajors(page, size)
|
||||||
|
if err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.SuccessPage(c, items, total, page, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary 获取单个计算专业
|
||||||
|
// @Tags 计算专业
|
||||||
|
// @Param id path string true "ID"
|
||||||
|
// @Success 200 {object} common.Response{data=vo.YxCalculationMajorVO}
|
||||||
|
// @Router /yx-calculation-majors/{id} [get]
|
||||||
|
func (ctrl *YxCalculationMajorController) GetByID(c *gin.Context) {
|
||||||
|
item, err := ctrl.service.GetCalculationMajorByID(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
common.Error(c, 404, "未找到")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary 创建计算专业
|
||||||
|
// @Tags 计算专业
|
||||||
|
// @Param request body dto.CreateCalculationMajorRequest true "计算专业信息"
|
||||||
|
// @Success 200 {object} common.Response{data=vo.YxCalculationMajorVO}
|
||||||
|
// @Router /yx-calculation-majors [post]
|
||||||
|
func (ctrl *YxCalculationMajorController) Create(c *gin.Context) {
|
||||||
|
var req dto.CreateCalculationMajorRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
common.Error(c, 400, "参数错误: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item, err := ctrl.service.CreateCalculationMajor(&req)
|
||||||
|
if err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary 更新计算专业
|
||||||
|
// @Tags 计算专业
|
||||||
|
// @Param id path string true "ID"
|
||||||
|
// @Param request body dto.UpdateCalculationMajorRequest true "计算专业信息"
|
||||||
|
// @Success 200 {object} common.Response{data=vo.YxCalculationMajorVO}
|
||||||
|
// @Router /yx-calculation-majors/{id} [put]
|
||||||
|
func (ctrl *YxCalculationMajorController) Update(c *gin.Context) {
|
||||||
|
var req dto.UpdateCalculationMajorRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
common.Error(c, 400, "参数错误: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item, err := ctrl.service.UpdateCalculationMajor(c.Param("id"), &req)
|
||||||
|
if err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary 动态字段更新
|
||||||
|
// @Tags 计算专业
|
||||||
|
// @Param id path string true "ID"
|
||||||
|
// @Param fields body map[string]interface{} true "要更新的字段"
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /yx-calculation-majors/{id} [patch]
|
||||||
|
func (ctrl *YxCalculationMajorController) UpdateFields(c *gin.Context) {
|
||||||
|
var fields map[string]interface{}
|
||||||
|
if err := c.ShouldBindJSON(&fields); err != nil {
|
||||||
|
common.Error(c, 400, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := ctrl.service.UpdateFields(c.Param("id"), fields); err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary 删除计算专业
|
||||||
|
// @Tags 计算专业
|
||||||
|
// @Param id path string true "ID"
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /yx-calculation-majors/{id} [delete]
|
||||||
|
func (ctrl *YxCalculationMajorController) Delete(c *gin.Context) {
|
||||||
|
if err := ctrl.service.Delete(c.Param("id")); err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary 批量创建计算专业
|
||||||
|
// @Tags 计算专业
|
||||||
|
// @Param items body []dto.CreateCalculationMajorRequest true "计算专业列表"
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /yx-calculation-majors/batch [post]
|
||||||
|
func (ctrl *YxCalculationMajorController) BatchCreate(c *gin.Context) {
|
||||||
|
var reqs []dto.CreateCalculationMajorRequest
|
||||||
|
if err := c.ShouldBindJSON(&reqs); err != nil {
|
||||||
|
common.Error(c, 400, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := ctrl.service.BatchCreateCalculationMajors(&reqs); err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary 批量删除计算专业
|
||||||
|
// @Tags 计算专业
|
||||||
|
// @Param ids body []string true "ID列表"
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /yx-calculation-majors/batch [delete]
|
||||||
|
func (ctrl *YxCalculationMajorController) BatchDelete(c *gin.Context) {
|
||||||
|
var ids []string
|
||||||
|
if err := c.ShouldBindJSON(&ids); err != nil {
|
||||||
|
common.Error(c, 400, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := ctrl.service.BatchDelete(ids); err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, nil)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,168 @@
|
||||||
|
// Package controller 控制器层
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"server/common"
|
||||||
|
"server/modules/yx/entity"
|
||||||
|
"server/modules/yx/service"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type YxHistoryMajorEnrollController struct {
|
||||||
|
service *service.YxHistoryMajorEnrollService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewYxHistoryMajorEnrollController() *YxHistoryMajorEnrollController {
|
||||||
|
return &YxHistoryMajorEnrollController{service: service.NewYxHistoryMajorEnrollService()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctrl *YxHistoryMajorEnrollController) RegisterRoutes(r *gin.RouterGroup) {
|
||||||
|
r.GET("/yx-history-enrolls", ctrl.List)
|
||||||
|
r.GET("/yx-history-enrolls/:id", ctrl.GetByID)
|
||||||
|
r.POST("/yx-history-enrolls", ctrl.Create)
|
||||||
|
r.PUT("/yx-history-enrolls/:id", ctrl.Update)
|
||||||
|
r.PATCH("/yx-history-enrolls/:id", ctrl.UpdateFields)
|
||||||
|
r.DELETE("/yx-history-enrolls/:id", ctrl.Delete)
|
||||||
|
r.POST("/yx-history-enrolls/batch", ctrl.BatchCreate)
|
||||||
|
r.DELETE("/yx-history-enrolls/batch", ctrl.BatchDelete)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary 获取历年招生列表
|
||||||
|
// @Tags 历年招生
|
||||||
|
// @Param page query int false "页码"
|
||||||
|
// @Param size query int false "每页数量"
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /yx-history-enrolls [get]
|
||||||
|
func (ctrl *YxHistoryMajorEnrollController) List(c *gin.Context) {
|
||||||
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
|
size, _ := strconv.Atoi(c.DefaultQuery("size", "10"))
|
||||||
|
items, total, err := ctrl.service.List(page, size)
|
||||||
|
if err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.SuccessPage(c, items, total, page, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary 获取单个历年招生记录
|
||||||
|
// @Tags 历年招生
|
||||||
|
// @Param id path string true "ID"
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /yx-history-enrolls/{id} [get]
|
||||||
|
func (ctrl *YxHistoryMajorEnrollController) GetByID(c *gin.Context) {
|
||||||
|
item, err := ctrl.service.GetByID(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
common.Error(c, 404, "未找到")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary 创建历年招生记录
|
||||||
|
// @Tags 历年招生
|
||||||
|
// @Param item body entity.YxHistoryMajorEnroll true "历年招生信息"
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /yx-history-enrolls [post]
|
||||||
|
func (ctrl *YxHistoryMajorEnrollController) Create(c *gin.Context) {
|
||||||
|
var item entity.YxHistoryMajorEnroll
|
||||||
|
if err := c.ShouldBindJSON(&item); err != nil {
|
||||||
|
common.Error(c, 400, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := ctrl.service.Create(&item); err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary 更新历年招生记录
|
||||||
|
// @Tags 历年招生
|
||||||
|
// @Param id path string true "ID"
|
||||||
|
// @Param item body entity.YxHistoryMajorEnroll true "历年招生信息"
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /yx-history-enrolls/{id} [put]
|
||||||
|
func (ctrl *YxHistoryMajorEnrollController) Update(c *gin.Context) {
|
||||||
|
var item entity.YxHistoryMajorEnroll
|
||||||
|
if err := c.ShouldBindJSON(&item); err != nil {
|
||||||
|
common.Error(c, 400, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item.ID = c.Param("id")
|
||||||
|
if err := ctrl.service.Update(&item); err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary 动态字段更新
|
||||||
|
// @Tags 历年招生
|
||||||
|
// @Param id path string true "ID"
|
||||||
|
// @Param fields body map[string]interface{} true "要更新的字段"
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /yx-history-enrolls/{id} [patch]
|
||||||
|
func (ctrl *YxHistoryMajorEnrollController) UpdateFields(c *gin.Context) {
|
||||||
|
var fields map[string]interface{}
|
||||||
|
if err := c.ShouldBindJSON(&fields); err != nil {
|
||||||
|
common.Error(c, 400, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := ctrl.service.UpdateFields(c.Param("id"), fields); err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary 删除历年招生记录
|
||||||
|
// @Tags 历年招生
|
||||||
|
// @Param id path string true "ID"
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /yx-history-enrolls/{id} [delete]
|
||||||
|
func (ctrl *YxHistoryMajorEnrollController) Delete(c *gin.Context) {
|
||||||
|
if err := ctrl.service.Delete(c.Param("id")); err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary 批量创建历年招生记录
|
||||||
|
// @Tags 历年招生
|
||||||
|
// @Param items body []entity.YxHistoryMajorEnroll true "历年招生列表"
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /yx-history-enrolls/batch [post]
|
||||||
|
func (ctrl *YxHistoryMajorEnrollController) BatchCreate(c *gin.Context) {
|
||||||
|
var items []entity.YxHistoryMajorEnroll
|
||||||
|
if err := c.ShouldBindJSON(&items); err != nil {
|
||||||
|
common.Error(c, 400, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := ctrl.service.BatchCreate(items); err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary 批量删除历年招生记录
|
||||||
|
// @Tags 历年招生
|
||||||
|
// @Param ids body []string true "ID列表"
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /yx-history-enrolls/batch [delete]
|
||||||
|
func (ctrl *YxHistoryMajorEnrollController) BatchDelete(c *gin.Context) {
|
||||||
|
var ids []string
|
||||||
|
if err := c.ShouldBindJSON(&ids); err != nil {
|
||||||
|
common.Error(c, 400, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := ctrl.service.BatchDelete(ids); err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, nil)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,176 @@
|
||||||
|
// Package controller 控制器层
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"server/common"
|
||||||
|
"server/modules/yx/entity"
|
||||||
|
"server/modules/yx/service"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type YxSchoolMajorController struct {
|
||||||
|
service *service.YxSchoolMajorService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewYxSchoolMajorController() *YxSchoolMajorController {
|
||||||
|
return &YxSchoolMajorController{service: service.NewYxSchoolMajorService()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctrl *YxSchoolMajorController) RegisterRoutes(r *gin.RouterGroup) {
|
||||||
|
r.GET("/yx-school-majors", ctrl.List)
|
||||||
|
r.GET("/yx-school-majors/:id", ctrl.GetByID)
|
||||||
|
r.POST("/yx-school-majors", ctrl.Create)
|
||||||
|
r.PUT("/yx-school-majors/:id", ctrl.Update)
|
||||||
|
r.PATCH("/yx-school-majors/:id", ctrl.UpdateFields)
|
||||||
|
r.DELETE("/yx-school-majors/:id", ctrl.Delete)
|
||||||
|
r.POST("/yx-school-majors/batch", ctrl.BatchCreate)
|
||||||
|
r.DELETE("/yx-school-majors/batch", ctrl.BatchDelete)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 获取院校专业列表
|
||||||
|
// @Summary 获取院校专业列表
|
||||||
|
// @Tags 院校专业
|
||||||
|
// @Param page query int false "页码"
|
||||||
|
// @Param size query int false "每页数量"
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /yx-school-majors [get]
|
||||||
|
func (ctrl *YxSchoolMajorController) List(c *gin.Context) {
|
||||||
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
|
size, _ := strconv.Atoi(c.DefaultQuery("size", "10"))
|
||||||
|
items, total, err := ctrl.service.List(page, size)
|
||||||
|
if err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.SuccessPage(c, items, total, page, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByID 获取单个院校专业
|
||||||
|
// @Summary 获取单个院校专业
|
||||||
|
// @Tags 院校专业
|
||||||
|
// @Param id path string true "ID"
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /yx-school-majors/{id} [get]
|
||||||
|
func (ctrl *YxSchoolMajorController) GetByID(c *gin.Context) {
|
||||||
|
item, err := ctrl.service.GetByID(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
common.Error(c, 404, "未找到")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create 创建院校专业
|
||||||
|
// @Summary 创建院校专业
|
||||||
|
// @Tags 院校专业
|
||||||
|
// @Param item body entity.YxSchoolMajor true "院校专业信息"
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /yx-school-majors [post]
|
||||||
|
func (ctrl *YxSchoolMajorController) Create(c *gin.Context) {
|
||||||
|
var item entity.YxSchoolMajor
|
||||||
|
if err := c.ShouldBindJSON(&item); err != nil {
|
||||||
|
common.Error(c, 400, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := ctrl.service.Create(&item); err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 更新院校专业
|
||||||
|
// @Summary 更新院校专业
|
||||||
|
// @Tags 院校专业
|
||||||
|
// @Param id path string true "ID"
|
||||||
|
// @Param item body entity.YxSchoolMajor true "院校专业信息"
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /yx-school-majors/{id} [put]
|
||||||
|
func (ctrl *YxSchoolMajorController) Update(c *gin.Context) {
|
||||||
|
var item entity.YxSchoolMajor
|
||||||
|
if err := c.ShouldBindJSON(&item); err != nil {
|
||||||
|
common.Error(c, 400, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item.ID = c.Param("id")
|
||||||
|
if err := ctrl.service.Update(&item); err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateFields 动态字段更新
|
||||||
|
// @Summary 动态字段更新
|
||||||
|
// @Tags 院校专业
|
||||||
|
// @Param id path string true "ID"
|
||||||
|
// @Param fields body map[string]interface{} true "要更新的字段"
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /yx-school-majors/{id} [patch]
|
||||||
|
func (ctrl *YxSchoolMajorController) UpdateFields(c *gin.Context) {
|
||||||
|
var fields map[string]interface{}
|
||||||
|
if err := c.ShouldBindJSON(&fields); err != nil {
|
||||||
|
common.Error(c, 400, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := ctrl.service.UpdateFields(c.Param("id"), fields); err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 删除院校专业
|
||||||
|
// @Summary 删除院校专业
|
||||||
|
// @Tags 院校专业
|
||||||
|
// @Param id path string true "ID"
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /yx-school-majors/{id} [delete]
|
||||||
|
func (ctrl *YxSchoolMajorController) Delete(c *gin.Context) {
|
||||||
|
if err := ctrl.service.Delete(c.Param("id")); err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchCreate 批量创建
|
||||||
|
// @Summary 批量创建院校专业
|
||||||
|
// @Tags 院校专业
|
||||||
|
// @Param items body []entity.YxSchoolMajor true "院校专业列表"
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /yx-school-majors/batch [post]
|
||||||
|
func (ctrl *YxSchoolMajorController) BatchCreate(c *gin.Context) {
|
||||||
|
var items []entity.YxSchoolMajor
|
||||||
|
if err := c.ShouldBindJSON(&items); err != nil {
|
||||||
|
common.Error(c, 400, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := ctrl.service.BatchCreate(items); err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchDelete 批量删除
|
||||||
|
// @Summary 批量删除院校专业
|
||||||
|
// @Tags 院校专业
|
||||||
|
// @Param ids body []string true "ID列表"
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /yx-school-majors/batch [delete]
|
||||||
|
func (ctrl *YxSchoolMajorController) BatchDelete(c *gin.Context) {
|
||||||
|
var ids []string
|
||||||
|
if err := c.ShouldBindJSON(&ids); err != nil {
|
||||||
|
common.Error(c, 400, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := ctrl.service.BatchDelete(ids); err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, nil)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
// Package controller 控制层
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"server/common"
|
||||||
|
"server/modules/yx/entity"
|
||||||
|
"server/modules/yx/service"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type YxUserScoreController struct {
|
||||||
|
service *service.YxUserScoreService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewYxUserScoreController() *YxUserScoreController {
|
||||||
|
return &YxUserScoreController{service: service.NewYxUserScoreService()}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterRoutes 注册路由
|
||||||
|
func (ctrl *YxUserScoreController) RegisterRoutes(rg *gin.RouterGroup) {
|
||||||
|
group := rg.Group("/yx-user-scores")
|
||||||
|
{
|
||||||
|
group.GET("", ctrl.List)
|
||||||
|
group.GET("/:id", ctrl.Get)
|
||||||
|
group.POST("", ctrl.Create)
|
||||||
|
group.PUT("/:id", ctrl.Update)
|
||||||
|
group.DELETE("/:id", ctrl.Delete)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 获取用户分数列表
|
||||||
|
// @Summary 获取用户分数列表
|
||||||
|
// @Tags 用户分数
|
||||||
|
// @Param page query int false "页码" default(1)
|
||||||
|
// @Param size query int false "每页数量" default(10)
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /yx-user-scores [get]
|
||||||
|
func (ctrl *YxUserScoreController) List(c *gin.Context) {
|
||||||
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
|
size, _ := strconv.Atoi(c.DefaultQuery("size", "10"))
|
||||||
|
items, total, err := ctrl.service.List(page, size)
|
||||||
|
if err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, gin.H{
|
||||||
|
"items": items,
|
||||||
|
"total": total,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 获取单个用户分数
|
||||||
|
// @Summary 获取单个用户分数
|
||||||
|
// @Tags 用户分数
|
||||||
|
// @Param id path string true "ID"
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /yx-user-scores/{id} [get]
|
||||||
|
func (ctrl *YxUserScoreController) Get(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
item, err := ctrl.service.GetByID(id)
|
||||||
|
if err != nil {
|
||||||
|
common.Error(c, 404, "未找到记录")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create 创建用户分数
|
||||||
|
// @Summary 创建用户分数
|
||||||
|
// @Tags 用户分数
|
||||||
|
// @Param item body entity.YxUserScore true "用户分数信息"
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /yx-user-scores [post]
|
||||||
|
func (ctrl *YxUserScoreController) Create(c *gin.Context) {
|
||||||
|
var item entity.YxUserScore
|
||||||
|
if err := c.ShouldBindJSON(&item); err != nil {
|
||||||
|
common.Error(c, 400, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := ctrl.service.Create(&item); err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 更新用户分数
|
||||||
|
// @Summary 更新用户分数
|
||||||
|
// @Tags 用户分数
|
||||||
|
// @Param id path string true "ID"
|
||||||
|
// @Param item body entity.YxUserScore true "用户分数信息"
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /yx-user-scores/{id} [put]
|
||||||
|
func (ctrl *YxUserScoreController) Update(c *gin.Context) {
|
||||||
|
var item entity.YxUserScore
|
||||||
|
if err := c.ShouldBindJSON(&item); err != nil {
|
||||||
|
common.Error(c, 400, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item.ID = c.Param("id")
|
||||||
|
if err := ctrl.service.Update(&item); err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 删除用户分数
|
||||||
|
// @Summary 删除用户分数
|
||||||
|
// @Tags 用户分数
|
||||||
|
// @Param id path string true "ID"
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /yx-user-scores/{id} [delete]
|
||||||
|
func (ctrl *YxUserScoreController) Delete(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
if err := ctrl.service.Delete(id); err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, nil)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
// Package controller 控制层
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"server/common"
|
||||||
|
"server/modules/yx/dto"
|
||||||
|
"server/modules/yx/service"
|
||||||
|
"server/modules/yx/vo"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type YxVolunteerController struct {
|
||||||
|
service *service.YxVolunteerService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewYxVolunteerController() *YxVolunteerController {
|
||||||
|
return &YxVolunteerController{service: service.NewYxVolunteerService()}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterRoutes 注册路由
|
||||||
|
func (ctrl *YxVolunteerController) RegisterRoutes(rg *gin.RouterGroup) {
|
||||||
|
group := rg.Group("/yx-volunteers")
|
||||||
|
{
|
||||||
|
group.GET("", ctrl.List)
|
||||||
|
group.GET("/:id", ctrl.Get)
|
||||||
|
group.POST("", ctrl.Create)
|
||||||
|
group.PUT("/:id", ctrl.Update)
|
||||||
|
group.DELETE("/:id", ctrl.Delete)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 获取志愿列表
|
||||||
|
// @Summary 获取志愿列表
|
||||||
|
// @Tags 志愿
|
||||||
|
// @Param page query int false "页码" default(1)
|
||||||
|
// @Param size query int false "每页数量" default(10)
|
||||||
|
// @Success 200 {object} common.Response{data=[]vo.YxVolunteerVO}
|
||||||
|
// @Router /yx-volunteers [get]
|
||||||
|
func (ctrl *YxVolunteerController) List(c *gin.Context) {
|
||||||
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
|
size, _ := strconv.Atoi(c.DefaultQuery("size", "10"))
|
||||||
|
items, total, err := ctrl.service.List(page, size)
|
||||||
|
if err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 转换 Entity 到 VO
|
||||||
|
vos := make([]vo.YxVolunteerVO, len(items))
|
||||||
|
for i, item := range items {
|
||||||
|
vos[i] = vo.YxVolunteerVO{
|
||||||
|
ID: item.ID,
|
||||||
|
VolunteerName: item.VolunteerName,
|
||||||
|
ScoreId: item.ScoreId,
|
||||||
|
CreateType: item.CreateType,
|
||||||
|
State: item.State,
|
||||||
|
CreateBy: item.CreateBy,
|
||||||
|
CreateTime: item.CreateTime,
|
||||||
|
UpdateBy: item.UpdateBy,
|
||||||
|
UpdateTime: item.UpdateTime,
|
||||||
|
SysOrgCode: item.SysOrgCode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
common.SuccessPage(c, vos, total, page, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 获取单个志愿
|
||||||
|
// @Summary 获取单个志愿
|
||||||
|
// @Tags 志愿
|
||||||
|
// @Param id path string true "ID"
|
||||||
|
// @Success 200 {object} common.Response{data=vo.YxVolunteerVO}
|
||||||
|
// @Router /yx-volunteers/{id} [get]
|
||||||
|
func (ctrl *YxVolunteerController) Get(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
item, err := ctrl.service.GetByID(id)
|
||||||
|
if err != nil {
|
||||||
|
common.Error(c, 404, "未找到记录")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
voItem := vo.YxVolunteerVO{
|
||||||
|
ID: item.ID,
|
||||||
|
VolunteerName: item.VolunteerName,
|
||||||
|
ScoreId: item.ScoreId,
|
||||||
|
CreateType: item.CreateType,
|
||||||
|
State: item.State,
|
||||||
|
CreateBy: item.CreateBy,
|
||||||
|
CreateTime: item.CreateTime,
|
||||||
|
UpdateBy: item.UpdateBy,
|
||||||
|
UpdateTime: item.UpdateTime,
|
||||||
|
SysOrgCode: item.SysOrgCode,
|
||||||
|
}
|
||||||
|
common.Success(c, voItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create 创建志愿
|
||||||
|
// @Summary 创建志愿
|
||||||
|
// @Tags 志愿
|
||||||
|
// @Param request body dto.CreateVolunteerRequest true "志愿信息"
|
||||||
|
// @Success 200 {object} common.Response{data=vo.YxVolunteerVO}
|
||||||
|
// @Router /yx-volunteers [post]
|
||||||
|
func (ctrl *YxVolunteerController) Create(c *gin.Context) {
|
||||||
|
var req dto.CreateVolunteerRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
common.Error(c, 400, "参数错误: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
voItem, err := ctrl.service.CreateVolunteer(&req)
|
||||||
|
if err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, voItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 更新志愿
|
||||||
|
// @Summary 更新志愿
|
||||||
|
// @Tags 志愿
|
||||||
|
// @Param id path string true "ID"
|
||||||
|
// @Param request body dto.UpdateVolunteerRequest true "志愿信息"
|
||||||
|
// @Success 200 {object} common.Response{data=vo.YxVolunteerVO}
|
||||||
|
// @Router /yx-volunteers/{id} [put]
|
||||||
|
func (ctrl *YxVolunteerController) Update(c *gin.Context) {
|
||||||
|
var req dto.UpdateVolunteerRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
common.Error(c, 400, "参数错误: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
voItem, err := ctrl.service.UpdateVolunteer(c.Param("id"), &req)
|
||||||
|
if err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, voItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 删除志愿
|
||||||
|
// @Summary 删除志愿
|
||||||
|
// @Tags 志愿
|
||||||
|
// @Param id path string true "ID"
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /yx-volunteers/{id} [delete]
|
||||||
|
func (ctrl *YxVolunteerController) Delete(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
if err := ctrl.service.Delete(id); err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, nil)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
// Package controller 控制层
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"server/common"
|
||||||
|
"server/modules/yx/entity"
|
||||||
|
"server/modules/yx/service"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type YxVolunteerRecordController struct {
|
||||||
|
service *service.YxVolunteerRecordService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewYxVolunteerRecordController() *YxVolunteerRecordController {
|
||||||
|
return &YxVolunteerRecordController{service: service.NewYxVolunteerRecordService()}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterRoutes 注册路由
|
||||||
|
func (ctrl *YxVolunteerRecordController) RegisterRoutes(rg *gin.RouterGroup) {
|
||||||
|
group := rg.Group("/yx-volunteer-records")
|
||||||
|
{
|
||||||
|
group.GET("", ctrl.List)
|
||||||
|
group.GET("/:id", ctrl.Get)
|
||||||
|
group.POST("", ctrl.Create)
|
||||||
|
group.PUT("/:id", ctrl.Update)
|
||||||
|
group.DELETE("/:id", ctrl.Delete)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 获取志愿明细列表
|
||||||
|
// @Summary 获取志愿明细列表
|
||||||
|
// @Tags 志愿明细
|
||||||
|
// @Param page query int false "页码" default(1)
|
||||||
|
// @Param size query int false "每页数量" default(10)
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /yx-volunteer-records [get]
|
||||||
|
func (ctrl *YxVolunteerRecordController) List(c *gin.Context) {
|
||||||
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
|
size, _ := strconv.Atoi(c.DefaultQuery("size", "10"))
|
||||||
|
items, total, err := ctrl.service.List(page, size)
|
||||||
|
if err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, gin.H{
|
||||||
|
"items": items,
|
||||||
|
"total": total,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 获取单个志愿明细
|
||||||
|
// @Summary 获取单个志愿明细
|
||||||
|
// @Tags 志愿明细
|
||||||
|
// @Param id path string true "ID"
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /yx-volunteer-records/{id} [get]
|
||||||
|
func (ctrl *YxVolunteerRecordController) Get(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
item, err := ctrl.service.GetByID(id)
|
||||||
|
if err != nil {
|
||||||
|
common.Error(c, 404, "未找到记录")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create 创建志愿明细
|
||||||
|
// @Summary 创建志愿明细
|
||||||
|
// @Tags 志愿明细
|
||||||
|
// @Param item body entity.YxVolunteerRecord true "志愿明细信息"
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /yx-volunteer-records [post]
|
||||||
|
func (ctrl *YxVolunteerRecordController) Create(c *gin.Context) {
|
||||||
|
var item entity.YxVolunteerRecord
|
||||||
|
if err := c.ShouldBindJSON(&item); err != nil {
|
||||||
|
common.Error(c, 400, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := ctrl.service.Create(&item); err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 更新志愿明细
|
||||||
|
// @Summary 更新志愿明细
|
||||||
|
// @Tags 志愿明细
|
||||||
|
// @Param id path string true "ID"
|
||||||
|
// @Param item body entity.YxVolunteerRecord true "志愿明细信息"
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /yx-volunteer-records/{id} [put]
|
||||||
|
func (ctrl *YxVolunteerRecordController) Update(c *gin.Context) {
|
||||||
|
var item entity.YxVolunteerRecord
|
||||||
|
if err := c.ShouldBindJSON(&item); err != nil {
|
||||||
|
common.Error(c, 400, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item.ID = c.Param("id")
|
||||||
|
if err := ctrl.service.Update(&item); err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 删除志愿明细
|
||||||
|
// @Summary 删除志愿明细
|
||||||
|
// @Tags 志愿明细
|
||||||
|
// @Param id path string true "ID"
|
||||||
|
// @Success 200 {object} common.Response
|
||||||
|
// @Router /yx-volunteer-records/{id} [delete]
|
||||||
|
func (ctrl *YxVolunteerRecordController) Delete(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
if err := ctrl.service.Delete(id); err != nil {
|
||||||
|
common.Error(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Success(c, nil)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package dto
|
||||||
|
|
||||||
|
// 定义概率数量统计结果结构体,用于接收四种录取概率对应的各自数量
|
||||||
|
type ProbabilityCountDTO struct {
|
||||||
|
Hard int64 `json:"hard" gorm:"column:hard"` // 难录取(<60)
|
||||||
|
Risky int64 `json:"risky" gorm:"column:risky"` // 可冲击(60<=x<73)
|
||||||
|
Stable int64 `json:"stable" gorm:"column:stable"` // 较稳妥(73<=x<93)
|
||||||
|
Safe int64 `json:"safe" gorm:"column:safe"` // 可保底(>=93)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
package dto
|
||||||
|
|
||||||
|
// CreateCalculationMajorRequest 创建计算专业请求
|
||||||
|
type CreateCalculationMajorRequest struct {
|
||||||
|
SchoolCode string `json:"schoolCode" binding:"required"` // 院校代码
|
||||||
|
SchoolName string `json:"schoolName"` // 院校名称
|
||||||
|
MajorCode string `json:"majorCode" binding:"required"` // 专业代码
|
||||||
|
MajorName string `json:"majorName"` // 专业名称
|
||||||
|
MajorType string `json:"majorType"` // 专业类型
|
||||||
|
MajorTypeChild string `json:"majorTypeChild"` // 子专业类型
|
||||||
|
PlanNum int `json:"planNum"` // 计划人数
|
||||||
|
MainSubjects string `json:"mainSubjects"` // 主考科目
|
||||||
|
Limitation string `json:"limitation"` // 限制条件
|
||||||
|
ChineseScoreLimitation float64 `json:"chineseScoreLimitation"` // 语文分数限制
|
||||||
|
EnglishScoreLimitation float64 `json:"englishScoreLimitation"` // 英语分数限制
|
||||||
|
CulturalScoreLimitation float64 `json:"culturalScoreLimitation"` // 文化成绩限制
|
||||||
|
ProfessionalScoreLimitation float64 `json:"professionalScoreLimitation"` // 专业分数限制
|
||||||
|
EnrollmentCode string `json:"enrollmentCode"` // 招生代码
|
||||||
|
Tuition string `json:"tuition"` // 学费
|
||||||
|
Detail string `json:"detail"` // 详情
|
||||||
|
Category string `json:"category"` // 类别
|
||||||
|
Batch string `json:"batch"` // 批次
|
||||||
|
RulesEnrollProbability string `json:"rulesEnrollProbability"` // 录取规则概率
|
||||||
|
ProbabilityOperator string `json:"probabilityOperator"` // 概率操作符
|
||||||
|
Kslx string `json:"kslx"` // 考试类型
|
||||||
|
State string `json:"state"` // 状态
|
||||||
|
Province string `json:"province"` // 省份
|
||||||
|
SchoolNature string `json:"schoolNature"` // 院校性质
|
||||||
|
InstitutionType string `json:"institutionType"` // 院校类型
|
||||||
|
EnrollProbability float64 `json:"enrollProbability"` // 录取概率
|
||||||
|
StudentScore float64 `json:"studentScore"` // 学生分数
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCalculationMajorRequest 更新计算专业请求
|
||||||
|
type UpdateCalculationMajorRequest struct {
|
||||||
|
SchoolName string `json:"schoolName"` // 院校名称
|
||||||
|
MajorName string `json:"majorName"` // 专业名称
|
||||||
|
MajorType string `json:"majorType"` // 专业类型
|
||||||
|
MajorTypeChild string `json:"majorTypeChild"` // 子专业类型
|
||||||
|
PlanNum int `json:"planNum"` // 计划人数
|
||||||
|
MainSubjects string `json:"mainSubjects"` // 主考科目
|
||||||
|
Limitation string `json:"limitation"` // 限制条件
|
||||||
|
ChineseScoreLimitation float64 `json:"chineseScoreLimitation"` // 语文分数限制
|
||||||
|
EnglishScoreLimitation float64 `json:"englishScoreLimitation"` // 英语分数限制
|
||||||
|
CulturalScoreLimitation float64 `json:"culturalScoreLimitation"` // 文化成绩限制
|
||||||
|
ProfessionalScoreLimitation float64 `json:"professionalScoreLimitation"` // 专业分数限制
|
||||||
|
EnrollmentCode string `json:"enrollmentCode"` // 招生代码
|
||||||
|
Tuition string `json:"tuition"` // 学费
|
||||||
|
Detail string `json:"detail"` // 详情
|
||||||
|
Category string `json:"category"` // 类别
|
||||||
|
Batch string `json:"batch"` // 批次
|
||||||
|
RulesEnrollProbability string `json:"rulesEnrollProbability"` // 录取规则概率
|
||||||
|
ProbabilityOperator string `json:"probabilityOperator"` // 概率操作符
|
||||||
|
Kslx string `json:"kslx"` // 考试类型
|
||||||
|
State string `json:"state"` // 状态
|
||||||
|
Province string `json:"province"` // 省份
|
||||||
|
SchoolNature string `json:"schoolNature"` // 院校性质
|
||||||
|
InstitutionType string `json:"institutionType"` // 院校类型
|
||||||
|
EnrollProbability float64 `json:"enrollProbability"` // 录取概率
|
||||||
|
StudentScore float64 `json:"studentScore"` // 学生分数
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
userVO "server/modules/user/vo"
|
||||||
|
"server/modules/yx/vo"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserMajorDTO struct {
|
||||||
|
SchoolCode string `json:"schoolCode"`
|
||||||
|
SchoolName string `json:"schoolName"`
|
||||||
|
MajorCode string `json:"majorCode"`
|
||||||
|
MajorName string `json:"majorName"`
|
||||||
|
MajorType string `json:"majorType"`
|
||||||
|
MajorTypeChild string `json:"majorTypeChild"`
|
||||||
|
PlanNum int `json:"planNum"`
|
||||||
|
MainSubjects string `json:"mainSubjects"`
|
||||||
|
Limitation string `json:"limitation"`
|
||||||
|
ChineseScoreLimitation float64 `json:"chineseScoreLimitation"`
|
||||||
|
EnglishScoreLimitation float64 `json:"englishScoreLimitation"`
|
||||||
|
CulturalScoreLimitation float64 `json:"culturalScoreLimitation"`
|
||||||
|
ProfessionalScoreLimitation float64 `json:"professionalScoreLimitation"`
|
||||||
|
EnrollmentCode string `json:"enrollmentCode"`
|
||||||
|
Tuition string `json:"tuition"`
|
||||||
|
Detail string `json:"detail"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
Batch string `json:"batch"`
|
||||||
|
RulesEnrollProbability string `json:"rulesEnrollProbability"`
|
||||||
|
ProbabilityOperator string `json:"probabilityOperator"`
|
||||||
|
// PrivateRulesEnrollProbability string `json:"privateRulesEnrollProbability"`
|
||||||
|
// PrivateProbabilityOperator string `json:"privateProbabilityOperator"`
|
||||||
|
RulesEnrollProbabilitySx string `json:"rulesEnrollProbabilitySx"`
|
||||||
|
Kslx string `json:"kslx"`
|
||||||
|
State string `json:"state"`
|
||||||
|
HistoryMajorEnrollMap map[string]YxHistoryMajorEnrollDTO `json:"historyMajorEnrollMap"`
|
||||||
|
// 计算相关字段 (非数据库直接映射)
|
||||||
|
EnrollProbability float64 `json:"enrollProbability"` // 录取率
|
||||||
|
StudentScore float64 `json:"studentScore" gorm:"column:studentScore"` // 学生折合分
|
||||||
|
// PrivateStudentScore float64 `json:"privateStudentScore"` // 学生折合分(私有)
|
||||||
|
// StudentConvertedScore float64 `json:"studentConvertedScore"` // 学生折合分(转换后)
|
||||||
|
// FirstLevelDiscipline string `json:"firstLevelDiscipline"` // 一级学科 (需确认来源)
|
||||||
|
|
||||||
|
Province string `json:"province"` // 省份
|
||||||
|
SchoolNature string `json:"schoolNature"` // 院校性质
|
||||||
|
InstitutionType string `json:"institutionType"` // 院校类型
|
||||||
|
}
|
||||||
|
|
||||||
|
// SchoolMajorDTO 院校专业查询结果 DTO
|
||||||
|
type SchoolMajorDTO struct {
|
||||||
|
SchoolCode string `json:"schoolCode"`
|
||||||
|
SchoolName string `json:"schoolName"`
|
||||||
|
MajorCode string `json:"majorCode"`
|
||||||
|
MajorName string `json:"majorName"`
|
||||||
|
MajorType string `json:"majorType"`
|
||||||
|
MajorTypeChild string `json:"majorTypeChild"`
|
||||||
|
PlanNum int `json:"planNum"`
|
||||||
|
MainSubjects string `json:"mainSubjects"`
|
||||||
|
Limitation string `json:"limitation"`
|
||||||
|
ChineseScoreLimitation float64 `json:"chineseScoreLimitation"`
|
||||||
|
EnglishScoreLimitation float64 `json:"englishScoreLimitation"`
|
||||||
|
CulturalScoreLimitation float64 `json:"culturalScoreLimitation"`
|
||||||
|
ProfessionalScoreLimitation float64 `json:"professionalScoreLimitation"`
|
||||||
|
EnrollmentCode string `json:"enrollmentCode"`
|
||||||
|
Tuition string `json:"tuition"`
|
||||||
|
Detail string `json:"detail"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
Batch string `json:"batch"`
|
||||||
|
RulesEnrollProbability string `json:"rulesEnrollProbability"`
|
||||||
|
ProbabilityOperator string `json:"probabilityOperator"`
|
||||||
|
PrivateRulesEnrollProbability string `json:"privateRulesEnrollProbability"`
|
||||||
|
PrivateProbabilityOperator string `json:"privateProbabilityOperator"`
|
||||||
|
RulesEnrollProbabilitySx string `json:"rulesEnrollProbabilitySx"`
|
||||||
|
Kslx string `json:"kslx"`
|
||||||
|
State string `json:"state"`
|
||||||
|
HistoryMajorEnrollMap map[string]YxHistoryMajorEnrollDTO `json:"historyMajorEnrollMap"`
|
||||||
|
// 计算相关字段 (非数据库直接映射)
|
||||||
|
HistoryMajorEnrollList []vo.YxHistoryMajorEnrollVO `json:"historyMajorEnrollList"`
|
||||||
|
EnrollProbability float64 `json:"enrollProbability"` // 录取率
|
||||||
|
StudentScore float64 `json:"studentScore"` // 学生折合分
|
||||||
|
PrivateStudentScore float64 `json:"privateStudentScore"` // 学生折合分(私有)
|
||||||
|
StudentConvertedScore float64 `json:"studentConvertedScore"` // 学生折合分(转换后)
|
||||||
|
FirstLevelDiscipline string `json:"firstLevelDiscipline"` // 一级学科 (需确认来源)
|
||||||
|
Province string `json:"province"` // 省份
|
||||||
|
SchoolNature string `json:"schoolNature" gorm:"column:schoolNature"` // 院校性质
|
||||||
|
InstitutionType string `json:"institutionType" gorm:"column:institutionType"` // 院校类型
|
||||||
|
}
|
||||||
|
|
||||||
|
type YxHistoryMajorEnrollDTO struct {
|
||||||
|
Year string `json:"year"`
|
||||||
|
EnrollmentCode string `json:"enrollmentCode"`
|
||||||
|
EnrollmentCount int `json:"enrollmentCount"`
|
||||||
|
RulesEnrollProbability string `json:"rulesEnrollProbability"`
|
||||||
|
ProbabilityOperator string `json:"probabilityOperator"`
|
||||||
|
AdmissionLine float64 `json:"admissionLine"`
|
||||||
|
ControlLine float64 `json:"controlLine"`
|
||||||
|
// 其他字段...
|
||||||
|
}
|
||||||
|
|
||||||
|
// SchoolMajorQuery 院校专业查询条件
|
||||||
|
type SchoolMajorQuery struct {
|
||||||
|
Page int `json:"page"`
|
||||||
|
Size int `json:"size"`
|
||||||
|
Keyword string `json:"keyword"` // 对应 keyword
|
||||||
|
MajorType string `json:"majorType"` // 对应 major_type
|
||||||
|
Category string `json:"category"` // 对应 category
|
||||||
|
Batch string `json:"batch"` // 对应 批次
|
||||||
|
Batch2 string `json:"batch2"` // 对应 批次2
|
||||||
|
MajorTypeChildren []string `json:"majorTypeChildren"` // 对应 major_type_child in (...)
|
||||||
|
MainSubjects string `json:"mainSubjects"` // 对应 main_subjects
|
||||||
|
Probability string `json:"probability"` // 对应 录取概率类型
|
||||||
|
ScoreId string `json:"scoreId"` // 对应 score_id
|
||||||
|
TagList []string `json:"tagList"` // 对应 tags in (...)
|
||||||
|
SchoolNatureList []string `json:"schoolNatureList"` // 对应 school_nature in (...)
|
||||||
|
AddressList []string `json:"addressList"` // 对应 address in (...)
|
||||||
|
KyjxList []string `json:"kyjxList"` // 对应 kyjx in (...)
|
||||||
|
RulesEnrollProbabilityList []string `json:"rulesEnrollProbabilityList"` // 录取方式,对应 rules_enroll_probability in (...)
|
||||||
|
LoginUserId string `json:"loginUserId"` // 登录用户 ID
|
||||||
|
UserScoreVO userVO.UserScoreVO `json:"userScoreVO"` // 用户成绩 VO
|
||||||
|
CalculationTableName string `json:"calculationTableName"` // 对应 calculation_table_name
|
||||||
|
SchoolCode string `json:"schoolCode"` // 对应的 院校代码
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SaveScoreRequest 保存成绩请求
|
||||||
|
type SaveScoreRequest struct {
|
||||||
|
CognitioPolyclinic string `json:"CognitioPolyclinic" binding:"required"`
|
||||||
|
SubjectList []string `json:"SubjectList" binding:"required"`
|
||||||
|
ProfessionalCategory string `json:"ProfessionalCategory" binding:"required"`
|
||||||
|
ProfessionalCategoryChildren []string `json:"ProfessionalCategoryChildren" binding:"required"`
|
||||||
|
ProfessionalCategoryChildrenScore map[string]float64 `json:"ProfessionalCategoryChildrenScore" binding:"required"`
|
||||||
|
ProfessionalScore *float64 `json:"ProfessionalScore" binding:"required"`
|
||||||
|
CulturalScore *float64 `json:"CulturalScore" binding:"required"`
|
||||||
|
EnglishScore *float64 `json:"EnglishScore" binding:"required"`
|
||||||
|
ChineseScore *float64 `json:"ChineseScore" binding:"required"`
|
||||||
|
Province string `json:"Province" binding:"required"`
|
||||||
|
CreateBy string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req *SaveScoreRequest) Validate() error {
|
||||||
|
if req.CognitioPolyclinic != "文科" && req.CognitioPolyclinic != "理科" {
|
||||||
|
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] {
|
||||||
|
return errors.New("选考科目参数有误")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !isValidScore(*req.ProfessionalScore, 300) {
|
||||||
|
return errors.New("统考成绩必须在0-300之间")
|
||||||
|
}
|
||||||
|
if !isValidScore(*req.CulturalScore, 750) {
|
||||||
|
return errors.New("文化成绩必须在0-750之间")
|
||||||
|
}
|
||||||
|
if !isValidScore(*req.EnglishScore, 150) {
|
||||||
|
return errors.New("英文成绩必须在0-150之间")
|
||||||
|
}
|
||||||
|
if !isValidScore(*req.ChineseScore, 150) {
|
||||||
|
return errors.New("中文成绩必须在0-150之间")
|
||||||
|
}
|
||||||
|
// TODO 在这里判断一下 专业子级,如 表演类只有:"服装表演", "戏剧影视表演", "戏剧影视导演"。音乐类只有:音乐表演声乐、音乐表演器乐、音乐教育。
|
||||||
|
validProfessionalChildren := map[string]string{ // 子级 -> 父级分类
|
||||||
|
"服装表演": "表演类",
|
||||||
|
"戏剧影视表演": "表演类",
|
||||||
|
"戏剧影视导演": "表演类",
|
||||||
|
"音乐表演声乐": "音乐类",
|
||||||
|
"音乐表演器乐": "音乐类",
|
||||||
|
"音乐教育": "音乐类",
|
||||||
|
}
|
||||||
|
// 统计各分类的数量
|
||||||
|
categoryCount := make(map[string]int)
|
||||||
|
for childName, score := range req.ProfessionalCategoryChildrenScore {
|
||||||
|
// 验证子级名称是否合法
|
||||||
|
parentCategory, exists := validProfessionalChildren[childName]
|
||||||
|
if !exists {
|
||||||
|
return errors.New("不支持的专业子级: " + childName)
|
||||||
|
}
|
||||||
|
// 统计分类数量
|
||||||
|
categoryCount[parentCategory]++
|
||||||
|
// 验证分数范围
|
||||||
|
if !isValidScore(score, 300) {
|
||||||
|
return errors.New(childName + "成绩必须在0-300之间")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 分类数量限制,比如每类最多几个
|
||||||
|
for category, count := range categoryCount {
|
||||||
|
if category == "音乐类" {
|
||||||
|
// 判断 子级是否包含 xx
|
||||||
|
if req.ProfessionalCategoryChildrenScore["音乐表演声乐"] > 0 && req.ProfessionalCategoryChildrenScore["音乐表演器乐"] > 0 {
|
||||||
|
return errors.New("音乐类子级不可同时选择'音乐表演声乐'和'音乐表演器乐'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func isValidScore(score float64, max float64) bool {
|
||||||
|
return score >= 0 && score <= max
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package dto
|
||||||
|
|
||||||
|
// CreateVolunteerRequest 创建志愿请求
|
||||||
|
type CreateVolunteerRequest struct {
|
||||||
|
VolunteerName string `json:"volunteerName" binding:"required"` // 志愿单名称
|
||||||
|
ScoreId string `json:"scoreId" binding:"required"` // 关联成绩ID
|
||||||
|
CreateType string `json:"createType"` // 生成类型(1.手动生成,2.智能生成)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateVolunteerRequest 更新志愿请求
|
||||||
|
type UpdateVolunteerRequest struct {
|
||||||
|
VolunteerName string `json:"volunteerName"` // 志愿单名称
|
||||||
|
State string `json:"state"` // 志愿单状态(0-否,1-正在使用,2-历史)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
// Package entity 实体层
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// YxCalculationMajor 计算专业表实体
|
||||||
|
type YxCalculationMajor struct {
|
||||||
|
ID string `gorm:"column:id;primaryKey" json:"id"`
|
||||||
|
ScoreID string `gorm:"column:score_id" json:"scoreId"`
|
||||||
|
SchoolCode string `gorm:"column:school_code" json:"schoolCode"`
|
||||||
|
MajorCode string `gorm:"column:major_code" json:"majorCode"`
|
||||||
|
MajorName string `gorm:"column:major_name" json:"majorName"`
|
||||||
|
EnrollmentCode string `gorm:"column:enrollment_code" json:"enrollmentCode"`
|
||||||
|
Tuition string `gorm:"column:tuition" json:"tuition"`
|
||||||
|
Detail string `gorm:"column:detail" json:"detail"`
|
||||||
|
Category string `gorm:"column:category" json:"category"`
|
||||||
|
RulesEnrollProbability string `gorm:"column:rules_enroll_probability" json:"rulesEnrollProbability"`
|
||||||
|
Batch string `gorm:"column:batch" json:"batch"`
|
||||||
|
StudentOldConvertedScore float64 `gorm:"column:student_old_converted_score" json:"studentOldConvertedScore"`
|
||||||
|
StudentConvertedScore float64 `gorm:"column:student_converted_score" json:"studentConvertedScore"`
|
||||||
|
EnrollProbability float64 `gorm:"column:enroll_probability" json:"enrollProbability"`
|
||||||
|
ProbabilityOperator string `gorm:"column:probability_operator" json:"probabilityOperator"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time" json:"createTime"`
|
||||||
|
MajorType string `gorm:"column:major_type" json:"majorType"`
|
||||||
|
MajorTypeChild string `gorm:"column:major_type_child" json:"majorTypeChild"`
|
||||||
|
PlanNum int `gorm:"column:plan_num" json:"planNum"`
|
||||||
|
MainSubjects string `gorm:"column:main_subjects" json:"mainSubjects"`
|
||||||
|
Limitation string `gorm:"column:limitation" json:"limitation"`
|
||||||
|
OtherScoreLimitation string `gorm:"column:other_score_limitation" json:"otherScoreLimitation"`
|
||||||
|
RulesEnrollProbabilitySx string `gorm:"column:rules_enroll_probability_sx" json:"rulesEnrollProbabilitySx"`
|
||||||
|
Kslx string `gorm:"column:kslx" json:"kslx"`
|
||||||
|
PrivateStudentConvertedScore float64 `gorm:"column:private_student_converted_score" json:"privateStudentConvertedScore"`
|
||||||
|
PrivateRulesEnrollProbability string `gorm:"column:private_rules_enroll_probability" json:"privateRulesEnrollProbability"`
|
||||||
|
PrivateProbabilityOperator string `gorm:"column:private_probability_operator" json:"privateProbabilityOperator"`
|
||||||
|
State string `gorm:"column:state" json:"state"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (YxCalculationMajor) TableName() string {
|
||||||
|
return "yx_calculation_major_2025_2"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
// Package entity 实体层
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// YxHistoryMajorEnroll 历年艺术类招生录取分数表实体
|
||||||
|
type YxHistoryMajorEnroll struct {
|
||||||
|
ID string `gorm:"column:id;primaryKey" json:"id"`
|
||||||
|
SchoolCode string `gorm:"column:school_code" json:"schoolCode"`
|
||||||
|
SchoolName string `gorm:"column:school_name" json:"schoolName"`
|
||||||
|
InstitutionCode string `gorm:"column:institution_code" json:"institutionCode"`
|
||||||
|
MajorCode string `gorm:"column:major_code" json:"majorCode"`
|
||||||
|
MajorName string `gorm:"column:major_name" json:"majorName"`
|
||||||
|
MajorType string `gorm:"column:major_type" json:"majorType"`
|
||||||
|
EnrollmentCode string `gorm:"column:enrollment_code" json:"enrollmentCode"`
|
||||||
|
Category string `gorm:"column:category" json:"category"`
|
||||||
|
Year string `gorm:"column:year" json:"year"`
|
||||||
|
EnrollNum int `gorm:"column:enroll_num" json:"enrollNum"`
|
||||||
|
ScoreLineDifference float64 `gorm:"column:score_line_difference" json:"scoreLineDifference"`
|
||||||
|
CreateBy string `gorm:"column:create_by" json:"createBy"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time" json:"createTime"`
|
||||||
|
UpdateBy string `gorm:"column:update_by" json:"updateBy"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time" json:"updateTime"`
|
||||||
|
SysOrgCode string `gorm:"column:sys_org_code" json:"sysOrgCode"`
|
||||||
|
Detail string `gorm:"column:detail" json:"detail"`
|
||||||
|
RulesEnrollProbability string `gorm:"column:rules_enroll_probability" json:"rulesEnrollProbability"`
|
||||||
|
ControlLine float64 `gorm:"column:control_line" json:"controlLine"`
|
||||||
|
AdmissionLine float64 `gorm:"column:admission_line" json:"admissionLine"`
|
||||||
|
ProbabilityOperator string `gorm:"column:probability_operator" json:"probabilityOperator"`
|
||||||
|
Batch string `gorm:"column:batch" json:"batch"`
|
||||||
|
OneVolunteerAdmissionNum int `gorm:"column:one_volunteer_admission_num" json:"oneVolunteerAdmissionNum"`
|
||||||
|
AdmissionNum int `gorm:"column:admission_num" json:"admissionNum"`
|
||||||
|
ActualPitcherNum int `gorm:"column:actual_pitcher_num" json:"actualPitcherNum"`
|
||||||
|
CheckMaster string `gorm:"column:check_master" json:"checkMaster"`
|
||||||
|
MajorTypeChild string `gorm:"column:major_type_child" json:"majorTypeChild"`
|
||||||
|
MainSubjects string `gorm:"column:main_subjects" json:"mainSubjects"`
|
||||||
|
Tuition string `gorm:"column:tuition" json:"tuition"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (YxHistoryMajorEnroll) TableName() string {
|
||||||
|
return "yx_history_major_enroll"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// YxHistoryScoreControlLine 历年各专业省控分数线实体
|
||||||
|
type YxHistoryScoreControlLine struct {
|
||||||
|
ID string `gorm:"column:id;primaryKey" json:"id"`
|
||||||
|
Year string `gorm:"column:year" json:"year"` // 年份
|
||||||
|
ProfessionalCategory string `gorm:"column:professional_category" json:"professionalCategory"` // 专业类别
|
||||||
|
Category string `gorm:"column:category" json:"category"` // 文理科
|
||||||
|
Batch string `gorm:"column:batch" json:"batch"` // 批次
|
||||||
|
CulturalScore float64 `gorm:"column:cultural_score" json:"culturalScore"` // 文化分
|
||||||
|
SpecialScore float64 `gorm:"column:special_score" json:"specialScore"` // 专业分
|
||||||
|
CreateBy string `gorm:"column:create_by" json:"createBy"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time" json:"createTime"`
|
||||||
|
UpdateBy string `gorm:"column:update_by" json:"updateBy"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time" json:"updateTime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (YxHistoryScoreControlLine) TableName() string {
|
||||||
|
return "yx_history_score_control_line_new"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
// Package entity 实体层
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// YxSchoolMajor 院校专业关联表实体
|
||||||
|
type YxSchoolMajor struct {
|
||||||
|
ID string `gorm:"column:id;primaryKey" json:"id"`
|
||||||
|
SchoolCode string `gorm:"column:school_code" json:"schoolCode"`
|
||||||
|
SchoolName string `gorm:"column:school_name" json:"schoolName"`
|
||||||
|
MajorCode string `gorm:"column:major_code" json:"majorCode"`
|
||||||
|
MajorName string `gorm:"column:major_name" json:"majorName"`
|
||||||
|
MajorType string `gorm:"column:major_type" json:"majorType"`
|
||||||
|
MajorTypeChild string `gorm:"column:major_type_child" json:"majorTypeChild"`
|
||||||
|
MainSubjects string `gorm:"column:main_subjects" json:"mainSubjects"`
|
||||||
|
EnrollmentCode string `gorm:"column:enrollment_code" json:"enrollmentCode"`
|
||||||
|
Category string `gorm:"column:category" json:"category"`
|
||||||
|
Batch string `gorm:"column:batch" json:"batch"`
|
||||||
|
Tuition string `gorm:"column:tuition" json:"tuition"`
|
||||||
|
PlanNum int `gorm:"column:plan_num" json:"planNum"`
|
||||||
|
Detail string `gorm:"column:detail" json:"detail"`
|
||||||
|
Semester string `gorm:"column:semester" json:"semester"`
|
||||||
|
CreateBy string `gorm:"column:create_by" json:"createBy"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time" json:"createTime"`
|
||||||
|
UpdateBy string `gorm:"column:update_by" json:"updateBy"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time" json:"updateTime"`
|
||||||
|
RulesEnrollProbabilitySx string `gorm:"column:rules_enroll_probability_sx" json:"rulesEnrollProbabilitySx"`
|
||||||
|
RulesEnrollProbability string `gorm:"column:rules_enroll_probability" json:"rulesEnrollProbability"`
|
||||||
|
ProbabilityOperator string `gorm:"column:probability_operator" json:"probabilityOperator"`
|
||||||
|
CulturalControlLine float64 `gorm:"column:cultural_control_line" json:"culturalControlLine"`
|
||||||
|
SpecialControlLine float64 `gorm:"column:special_control_line" json:"specialControlLine"`
|
||||||
|
CheckMaster string `gorm:"column:check_master" json:"checkMaster"`
|
||||||
|
Limitation string `gorm:"column:limitation" json:"limitation"`
|
||||||
|
ProfessionalScoreLimitation float64 `gorm:"column:professional_score_limitation" json:"professionalScoreLimitation"`
|
||||||
|
EnglishScoreLimitation float64 `gorm:"column:english_score_limitation" json:"englishScoreLimitation"`
|
||||||
|
ChineseScoreLimitation float64 `gorm:"column:chinese_score_limitation" json:"chineseScoreLimitation"`
|
||||||
|
CulturalScoreLimitation float64 `gorm:"column:cultural_score_limitation" json:"culturalScoreLimitation"`
|
||||||
|
Kslx string `gorm:"column:kslx" json:"kslx"`
|
||||||
|
PrivateProbabilityOperator string `gorm:"column:private_probability_operator" json:"privateProbabilityOperator"`
|
||||||
|
PrivateRulesEnrollProbability string `gorm:"column:private_rules_enroll_probability" json:"privateRulesEnrollProbability"`
|
||||||
|
State string `gorm:"column:state" json:"state"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (YxSchoolMajor) TableName() string {
|
||||||
|
return "yx_school_major"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// YxUserScore 用户分数信息表实体
|
||||||
|
type YxUserScore struct {
|
||||||
|
ID string `gorm:"column:id;primaryKey" json:"id"`
|
||||||
|
Type string `gorm:"column:type;default:1" json:"type"` // 填报类型(1-普通类 2-艺术类)
|
||||||
|
EducationalLevel string `gorm:"column:educational_level;default:1" json:"educationalLevel"` // 学历层次(1-本科,2-专科)
|
||||||
|
ProfessionalCategory string `gorm:"column:professional_category;default:美术类" json:"professionalCategory"` // 专业类别(美术类/...)
|
||||||
|
Subjects string `gorm:"column:subjects" json:"subjects"` // 选课
|
||||||
|
ProfessionalScore float64 `gorm:"column:professional_score;default:0" json:"professionalScore"` // 专业成绩分
|
||||||
|
CulturalScore float64 `gorm:"column:cultural_score;default:0" json:"culturalScore"` // 文化成绩分
|
||||||
|
Ranking int `gorm:"column:ranking;default:0" json:"ranking"` // 位次
|
||||||
|
CreateBy string `gorm:"column:create_by" json:"createBy"` // 创建人
|
||||||
|
CreateTime time.Time `gorm:"column:create_time" json:"createTime"` // 创建时间
|
||||||
|
UpdateBy string `gorm:"column:update_by" json:"updateBy"` // 修改人
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time" json:"updateTime"` // 修改时间
|
||||||
|
State string `gorm:"column:state;default:1" json:"state"` // 状态(0-未使用,1-使用中)
|
||||||
|
Province string `gorm:"column:province;default:北京" json:"province"` // 高考省份
|
||||||
|
CognitioPolyclinic string `gorm:"column:cognitio_polyclinic" json:"cognitioPolyclinic"` // 文理分班(文科/理科)
|
||||||
|
Batch string `gorm:"column:batch" json:"batch"` // 录取批次
|
||||||
|
EnglishScore float64 `gorm:"column:english_score;default:0.00" json:"englishScore"` // 英语成绩
|
||||||
|
ChineseScore float64 `gorm:"column:chinese_score;default:0.00" json:"chineseScore"` // 语文成绩
|
||||||
|
Yybysy float64 `gorm:"column:yybysy;default:0.00" json:"yybysy"` // 音乐表演声乐
|
||||||
|
Yybyqy float64 `gorm:"column:yybyqy;default:0.00" json:"yybyqy"` // 音乐表演器乐
|
||||||
|
Yyjy float64 `gorm:"column:yyjy;default:0.00" json:"yyjy"` // 音乐教育
|
||||||
|
Xjysdy float64 `gorm:"column:xjysdy;default:0.00" json:"xjysdy"` // 戏剧影视导演
|
||||||
|
Xjysby float64 `gorm:"column:xjysby;default:0.00" json:"xjysby"` // 戏剧影视表演
|
||||||
|
Fzby float64 `gorm:"column:fzby;default:0.00" json:"fzby"` // 服装表演
|
||||||
|
ProfessionalCategoryChildren string `gorm:"column:professional_category_children" json:"professionalCategoryChildren"` // 子级专业类别
|
||||||
|
KbdNum int `gorm:"column:kbd_num;default:0" json:"kbdNum"` // 可保底专业数量
|
||||||
|
NlqNum int `gorm:"column:nlq_num;default:0" json:"nlqNum"` // 难录取专业数量
|
||||||
|
KcjNum int `gorm:"column:kcj_num;default:0" json:"kcjNum"` // 可冲击专业数量
|
||||||
|
JwtNum int `gorm:"column:jwt_num;default:0" json:"jwtNum"` // 较稳妥专业数量
|
||||||
|
CalculationTableName string `gorm:"column:calculation_table_name" json:"calculationTableName"` // 记录结果表名
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 指定表名
|
||||||
|
func (YxUserScore) TableName() string {
|
||||||
|
return "yx_user_score"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// YxVolunteer 志愿表实体
|
||||||
|
type YxVolunteer struct {
|
||||||
|
ID string `gorm:"column:id;primaryKey" json:"id"`
|
||||||
|
VolunteerName string `gorm:"column:volunteer_name" json:"volunteerName"` // 志愿单名称
|
||||||
|
ScoreId string `gorm:"column:score_id" json:"scoreId"` // 使用成绩id
|
||||||
|
CreateType string `gorm:"column:create_type;default:1" json:"createType"` // 生成类型(1.手动生成,2.智能生成)
|
||||||
|
State string `gorm:"column:state;default:0" json:"state"` // 志愿单状态(0-否,1.正在使用,2-历史)
|
||||||
|
CreateBy string `gorm:"column:create_by" json:"createBy"` // 创建人
|
||||||
|
CreateTime time.Time `gorm:"column:create_time" json:"createTime"` // 创建日期
|
||||||
|
UpdateBy string `gorm:"column:update_by" json:"updateBy"` // 更新人
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time" json:"updateTime"` // 更新日期
|
||||||
|
SysOrgCode string `gorm:"column:sys_org_code" json:"sysOrgCode"` // 所属部门
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 指定表名
|
||||||
|
func (YxVolunteer) TableName() string {
|
||||||
|
return "yx_volunteer"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// YxVolunteerRecord 志愿明细表实体
|
||||||
|
type YxVolunteerRecord struct {
|
||||||
|
ID string `gorm:"column:id;primaryKey" json:"id"`
|
||||||
|
VolunteerID string `gorm:"column:volunteer_id" json:"volunteerId"` // 志愿单id
|
||||||
|
SchoolCode string `gorm:"column:school_code" json:"schoolCode"` // 学校编码
|
||||||
|
MajorCode string `gorm:"column:major_code" json:"majorCode"` // 专业编码
|
||||||
|
EnrollmentCode string `gorm:"column:enrollment_code" json:"enrollmentCode"` // 招生代码
|
||||||
|
Indexs int `gorm:"column:indexs;default:1" json:"indexs"` // 志愿顺序
|
||||||
|
CreateBy string `gorm:"column:create_by" json:"createBy"` // 创建人
|
||||||
|
CreateTime time.Time `gorm:"column:create_time" json:"createTime"` // 创建日期
|
||||||
|
Batch string `gorm:"column:batch" json:"batch"` // 录取批次
|
||||||
|
EnrollProbability float64 `gorm:"column:enroll_probability;default:0.0000" json:"enrollProbability"` // 录取概率
|
||||||
|
StudentConvertedScore float64 `gorm:"column:student_converted_score;default:0.0000" json:"studentConvertedScore"` // 折合分数
|
||||||
|
Fctj int `gorm:"column:fctj;default:0" json:"fctj"` // 服从调剂
|
||||||
|
CalculationMajorID string `gorm:"column:calculation_major_id" json:"calculationMajorId"` // 专业折算id
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 指定表名
|
||||||
|
func (YxVolunteerRecord) TableName() string {
|
||||||
|
return "yx_volunteer_record"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,527 @@
|
||||||
|
// Package mapper 数据访问层
|
||||||
|
package mapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"server/common"
|
||||||
|
"server/config"
|
||||||
|
"server/modules/yx/dto"
|
||||||
|
"server/modules/yx/entity"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type YxCalculationMajorMapper struct {
|
||||||
|
*common.BaseMapper[entity.YxCalculationMajor]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewYxCalculationMajorMapper() *YxCalculationMajorMapper {
|
||||||
|
return &YxCalculationMajorMapper{
|
||||||
|
BaseMapper: common.NewBaseMapper[entity.YxCalculationMajor](),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryCostTime 存储各协程耗时的结构体
|
||||||
|
type QueryCostTime struct {
|
||||||
|
CountCost time.Duration // 总数量查询耗时
|
||||||
|
ProbCountCost time.Duration // 四种概率数量查询耗时
|
||||||
|
QueryCost time.Duration // 主列表查询耗时
|
||||||
|
TotalCost time.Duration // 整体总耗时
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindRecommendList 查询推荐专业列表(优化版:并发查询总数量、概率数量、主列表)
|
||||||
|
func (m *YxCalculationMajorMapper) FindRecommendList(query dto.SchoolMajorQuery) ([]dto.UserMajorDTO, int64, dto.ProbabilityCountDTO, error) {
|
||||||
|
var items []dto.UserMajorDTO
|
||||||
|
var total int64
|
||||||
|
var probCount dto.ProbabilityCountDTO // 四种概率的数量统计结果
|
||||||
|
|
||||||
|
// 1. 表名合法性校验:非空 + 白名单
|
||||||
|
tableName := query.UserScoreVO.CalculationTableName
|
||||||
|
if tableName == "" {
|
||||||
|
return nil, 0, dto.ProbabilityCountDTO{}, fmt.Errorf("CalculationTableName is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 基础条件SQL(共用过滤条件,排除概率筛选)
|
||||||
|
baseSQL := " WHERE 1=1 AND cm.state > 0 "
|
||||||
|
params := []interface{}{}
|
||||||
|
|
||||||
|
// 拼接共用过滤条件(与原有列表查询条件一致,保证统计结果准确性)
|
||||||
|
if query.UserScoreVO.ID != "" {
|
||||||
|
baseSQL += " AND cm.score_id = ?"
|
||||||
|
params = append(params, query.UserScoreVO.ID)
|
||||||
|
}
|
||||||
|
if query.SchoolCode != "" {
|
||||||
|
baseSQL += " AND cm.school_code = ?"
|
||||||
|
params = append(params, query.SchoolCode)
|
||||||
|
}
|
||||||
|
if query.MajorType != "" {
|
||||||
|
baseSQL += " AND cm.major_type = ?"
|
||||||
|
params = append(params, query.MajorType)
|
||||||
|
}
|
||||||
|
if query.Category != "" {
|
||||||
|
baseSQL += " AND cm.category = ?"
|
||||||
|
params = append(params, query.Category)
|
||||||
|
}
|
||||||
|
if len(query.MajorTypeChildren) > 0 {
|
||||||
|
placeholders := strings.Repeat("?,", len(query.MajorTypeChildren)-1) + "?"
|
||||||
|
baseSQL += " AND cm.major_type_child IN (" + placeholders + ")"
|
||||||
|
for _, v := range query.MajorTypeChildren {
|
||||||
|
params = append(params, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if "" != query.Batch {
|
||||||
|
baseSQL += " AND cm.batch = ?"
|
||||||
|
params = append(params, query.Batch)
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.MainSubjects != "" {
|
||||||
|
baseSQL += " AND cm.main_subjects = ?"
|
||||||
|
params = append(params, query.MainSubjects)
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.Keyword != "" {
|
||||||
|
baseSQL += " AND cm.major_name like ?"
|
||||||
|
params = append(params, query.Keyword)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 优化后的总数量COUNT SQL
|
||||||
|
countSQL := fmt.Sprintf(`
|
||||||
|
SELECT COUNT(cm.id) FROM %s cm
|
||||||
|
%s
|
||||||
|
`, tableName, baseSQL)
|
||||||
|
|
||||||
|
// 4. 四种概率批量统计SQL(使用CASE WHEN一次查询,性能最优)
|
||||||
|
probCountSQL := fmt.Sprintf(`
|
||||||
|
SELECT
|
||||||
|
SUM(CASE WHEN cm.enroll_probability < 60 THEN 1 ELSE 0 END) AS hard,
|
||||||
|
SUM(CASE WHEN cm.enroll_probability >= 60 AND cm.enroll_probability < 73 THEN 1 ELSE 0 END) AS risky,
|
||||||
|
SUM(CASE WHEN cm.enroll_probability >= 73 AND cm.enroll_probability < 93 THEN 1 ELSE 0 END) AS stable,
|
||||||
|
SUM(CASE WHEN cm.enroll_probability >= 93 THEN 1 ELSE 0 END) AS safe
|
||||||
|
FROM %s cm
|
||||||
|
%s
|
||||||
|
`, tableName, baseSQL)
|
||||||
|
|
||||||
|
// 5. 主查询SQL(保留原有字段和JOIN)
|
||||||
|
mainSQL := fmt.Sprintf(`
|
||||||
|
SELECT
|
||||||
|
cm.id,
|
||||||
|
s.school_name,
|
||||||
|
s.school_icon,
|
||||||
|
cm.state,
|
||||||
|
cm.school_code,
|
||||||
|
cm.major_code,
|
||||||
|
cm.major_name,
|
||||||
|
cm.enrollment_code,
|
||||||
|
cm.tuition,
|
||||||
|
cm.detail as majorDetail,
|
||||||
|
cm.category,
|
||||||
|
cm.batch,
|
||||||
|
cm.private_student_converted_score as privateStudentScore,
|
||||||
|
cm.student_old_converted_score as studentScore,
|
||||||
|
cm.student_converted_score,
|
||||||
|
cm.enroll_probability,
|
||||||
|
cm.rules_enroll_probability_sx,
|
||||||
|
cm.rules_enroll_probability,
|
||||||
|
cm.probability_operator,
|
||||||
|
cm.major_type,
|
||||||
|
cm.major_type_child,
|
||||||
|
cm.plan_num,
|
||||||
|
cm.main_subjects,
|
||||||
|
cm.limitation,
|
||||||
|
cm.other_score_limitation,
|
||||||
|
s.province,
|
||||||
|
s.school_nature,
|
||||||
|
s.institution_type
|
||||||
|
FROM %s cm
|
||||||
|
LEFT JOIN yx_school_child sc ON sc.school_code = cm.school_code
|
||||||
|
LEFT JOIN yx_school_research_teaching srt ON srt.school_id = sc.school_id
|
||||||
|
LEFT JOIN yx_school s ON s.id = sc.school_id
|
||||||
|
%s
|
||||||
|
`, tableName, baseSQL)
|
||||||
|
|
||||||
|
// 拼接传入概率的筛选条件(兼容原有业务逻辑)
|
||||||
|
switch query.Probability {
|
||||||
|
case "难录取":
|
||||||
|
mainSQL += " AND cm.enroll_probability < 60"
|
||||||
|
case "可冲击":
|
||||||
|
mainSQL += " AND (cm.enroll_probability >= 60 and cm.enroll_probability < 73)"
|
||||||
|
case "较稳妥":
|
||||||
|
mainSQL += " AND (cm.enroll_probability >= 73 and cm.enroll_probability < 93)"
|
||||||
|
case "可保底":
|
||||||
|
mainSQL += " AND (cm.enroll_probability >= 93)"
|
||||||
|
}
|
||||||
|
|
||||||
|
mainSQL += " ORDER BY cm.enroll_probability DESC"
|
||||||
|
// 6. 分页参数合法性校验
|
||||||
|
page := query.Page
|
||||||
|
size := query.Size
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if size < 1 {
|
||||||
|
size = 10
|
||||||
|
}
|
||||||
|
if size > 100 {
|
||||||
|
size = 100
|
||||||
|
}
|
||||||
|
offset := (page - 1) * size
|
||||||
|
// 提前拼接分页条件,避免协程内操作共享变量
|
||||||
|
mainSQL += fmt.Sprintf(" LIMIT %d OFFSET %d", size, offset)
|
||||||
|
|
||||||
|
// 7. 协程并发执行三个查询(总数量、概率数量、主列表),提升性能
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
var countErr, probCountErr, queryErr error
|
||||||
|
var queryCost QueryCostTime // 存储各协程耗时
|
||||||
|
var mu sync.Mutex // 互斥锁:防止多协程同时修改queryCost引发竞态问题
|
||||||
|
|
||||||
|
// 整体开始时间
|
||||||
|
totalStartTime := time.Now()
|
||||||
|
|
||||||
|
wg.Add(3)
|
||||||
|
|
||||||
|
// 协程1:总数量查询(单独记录耗时)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
// 记录该协程单独的开始时间
|
||||||
|
start := time.Now()
|
||||||
|
countErr = config.DB.Raw(countSQL, params...).Count(&total).Error
|
||||||
|
// 计算该协程耗时,通过互斥锁安全写入共享变量
|
||||||
|
mu.Lock()
|
||||||
|
queryCost.CountCost = time.Since(start)
|
||||||
|
mu.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 协程2:四种概率数量批量查询(单独记录耗时)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
// 记录该协程单独的开始时间
|
||||||
|
start := time.Now()
|
||||||
|
probCountErr = config.DB.Raw(probCountSQL, params...).Scan(&probCount).Error
|
||||||
|
// 计算该协程耗时,通过互斥锁安全写入共享变量
|
||||||
|
mu.Lock()
|
||||||
|
queryCost.ProbCountCost = time.Since(start)
|
||||||
|
mu.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 协程3:主列表查询(单独记录耗时)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
// 记录该协程单独的开始时间
|
||||||
|
start := time.Now()
|
||||||
|
queryErr = config.DB.Raw(mainSQL, params...).Scan(&items).Error
|
||||||
|
// 计算该协程耗时,通过互斥锁安全写入共享变量
|
||||||
|
mu.Lock()
|
||||||
|
queryCost.QueryCost = time.Since(start)
|
||||||
|
mu.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
// 计算整体总耗时
|
||||||
|
queryCost.TotalCost = time.Since(totalStartTime)
|
||||||
|
|
||||||
|
// 打印各协程耗时和总耗时(按需输出,可注释或删除)
|
||||||
|
fmt.Printf("各查询耗时统计:\n")
|
||||||
|
fmt.Printf(" 总数量查询耗时:%v\n", queryCost.CountCost)
|
||||||
|
fmt.Printf(" 概率数量查询耗时:%v\n", queryCost.ProbCountCost)
|
||||||
|
fmt.Printf(" 主列表查询耗时:%v\n", queryCost.QueryCost)
|
||||||
|
fmt.Printf(" 整体总耗时:%v\n", queryCost.TotalCost)
|
||||||
|
|
||||||
|
// 8. 错误处理
|
||||||
|
if countErr != nil {
|
||||||
|
return nil, 0, dto.ProbabilityCountDTO{}, fmt.Errorf("failed to query total count: %w", countErr)
|
||||||
|
}
|
||||||
|
if probCountErr != nil {
|
||||||
|
return nil, 0, dto.ProbabilityCountDTO{}, fmt.Errorf("failed to query probability count: %w", probCountErr)
|
||||||
|
}
|
||||||
|
if queryErr != nil {
|
||||||
|
return nil, 0, dto.ProbabilityCountDTO{}, fmt.Errorf("failed to query recommend major list: %w", queryErr)
|
||||||
|
}
|
||||||
|
if items == nil {
|
||||||
|
items = []dto.UserMajorDTO{}
|
||||||
|
}
|
||||||
|
return items, total, probCount, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindRecommendList1 查询推荐专业列表(原版)
|
||||||
|
func (m *YxCalculationMajorMapper) FindRecommendList1(query dto.SchoolMajorQuery) ([]dto.UserMajorDTO, int64, error) {
|
||||||
|
var items []dto.UserMajorDTO
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
// 确保表名存在,防止 SQL 注入或空表名
|
||||||
|
tableName := query.UserScoreVO.CalculationTableName
|
||||||
|
if tableName == "" {
|
||||||
|
return nil, 0, fmt.Errorf("CalculationTableName is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 Sprintf 动态插入表名
|
||||||
|
countSQL := fmt.Sprintf(`
|
||||||
|
SELECT COUNT(cm.id) FROM %s cm
|
||||||
|
LEFT JOIN yx_school_child sc ON sc.school_code = cm.school_code
|
||||||
|
LEFT JOIN yx_school_research_teaching srt ON srt.school_id = sc.school_id
|
||||||
|
LEFT JOIN yx_school s ON s.id = sc.school_id
|
||||||
|
WHERE 1=1 AND cm.state > 0
|
||||||
|
`, tableName)
|
||||||
|
|
||||||
|
sql := fmt.Sprintf(`
|
||||||
|
SELECT
|
||||||
|
cm.id,
|
||||||
|
s.school_name,
|
||||||
|
s.school_icon,
|
||||||
|
cm.state,
|
||||||
|
cm.school_code,
|
||||||
|
cm.major_code,
|
||||||
|
cm.major_name,
|
||||||
|
cm.enrollment_code,
|
||||||
|
cm.tuition,
|
||||||
|
cm.detail as majorDetail,
|
||||||
|
cm.category,
|
||||||
|
cm.batch,
|
||||||
|
cm.private_student_converted_score as privateStudentScore,
|
||||||
|
cm.student_old_converted_score as studentScore,
|
||||||
|
cm.student_converted_score,
|
||||||
|
cm.enroll_probability,
|
||||||
|
cm.rules_enroll_probability_sx,
|
||||||
|
cm.rules_enroll_probability,
|
||||||
|
cm.probability_operator,
|
||||||
|
cm.major_type,
|
||||||
|
cm.major_type_child,
|
||||||
|
cm.plan_num,
|
||||||
|
cm.main_subjects,
|
||||||
|
cm.limitation,
|
||||||
|
cm.other_score_limitation,
|
||||||
|
s.province as province,
|
||||||
|
s.school_nature as schoolNature,
|
||||||
|
s.institution_type as institutionType
|
||||||
|
FROM %s cm
|
||||||
|
LEFT JOIN yx_school_child sc ON sc.school_code = cm.school_code
|
||||||
|
LEFT JOIN yx_school_research_teaching srt ON srt.school_id = sc.school_id
|
||||||
|
LEFT JOIN yx_school s ON s.id = sc.school_id
|
||||||
|
WHERE 1=1 AND cm.state > 0
|
||||||
|
`, tableName)
|
||||||
|
|
||||||
|
params := []interface{}{}
|
||||||
|
|
||||||
|
// 注意:移除了 params = append(params, query.UserScoreVO.CalculationTableName) 因为表名已经通过 Sprintf 插入
|
||||||
|
|
||||||
|
if query.UserScoreVO.ID != "" {
|
||||||
|
countSQL += " AND cm.score_id = ?"
|
||||||
|
sql += " AND cm.score_id = ?"
|
||||||
|
params = append(params, query.UserScoreVO.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.MajorType != "" {
|
||||||
|
countSQL += " AND cm.major_type = ?"
|
||||||
|
sql += " AND cm.major_type = ?"
|
||||||
|
params = append(params, query.MajorType)
|
||||||
|
}
|
||||||
|
if query.Category != "" {
|
||||||
|
countSQL += " AND cm.category = ?"
|
||||||
|
sql += " AND cm.category = ?"
|
||||||
|
params = append(params, query.Category)
|
||||||
|
}
|
||||||
|
if len(query.MajorTypeChildren) > 0 {
|
||||||
|
placeholders := strings.Repeat("?,", len(query.MajorTypeChildren)-1) + "?"
|
||||||
|
countSQL += " AND cm.major_type_child IN (" + placeholders + ")"
|
||||||
|
sql += " AND cm.major_type_child IN (" + placeholders + ")"
|
||||||
|
for _, v := range query.MajorTypeChildren {
|
||||||
|
params = append(params, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.MainSubjects != "" {
|
||||||
|
countSQL += " AND cm.main_subjects = ?"
|
||||||
|
sql += " AND cm.main_subjects = ?"
|
||||||
|
params = append(params, query.MainSubjects)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 录取概率
|
||||||
|
switch query.Probability {
|
||||||
|
case "难录取":
|
||||||
|
countSQL += " AND cm.enroll_probability < 60"
|
||||||
|
sql += " AND cm.enroll_probability < 60"
|
||||||
|
case "可冲击":
|
||||||
|
countSQL += " AND (cm.enroll_probability >= 60 and cm.enroll_probability < 73)"
|
||||||
|
sql += " AND (cm.enroll_probability >= 60 and cm.enroll_probability < 73)"
|
||||||
|
case "较稳妥":
|
||||||
|
countSQL += " AND (cm.enroll_probability >= 73 and cm.enroll_probability < 93)"
|
||||||
|
sql += " AND (cm.enroll_probability >= 73 and cm.enroll_probability < 93)"
|
||||||
|
case "可保底":
|
||||||
|
countSQL += " AND (cm.enroll_probability >= 93)"
|
||||||
|
sql += " AND (cm.enroll_probability >= 93)"
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
var countErr, queryErr error
|
||||||
|
|
||||||
|
wg.Add(2)
|
||||||
|
// 协程1:COUNT 查询
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
countErr = config.DB.Raw(countSQL, params...).Count(&total).Error
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 协程2:主查询
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
sql += fmt.Sprintf(" LIMIT %d OFFSET %d", query.Size, (query.Page-1)*query.Size)
|
||||||
|
queryErr = config.DB.Raw(sql, params...).Scan(&items).Error
|
||||||
|
}()
|
||||||
|
wg.Wait()
|
||||||
|
if countErr != nil || queryErr != nil {
|
||||||
|
return nil, 0, fmt.Errorf("countErr: %v, queryErr: %v", countErr, queryErr)
|
||||||
|
}
|
||||||
|
return items, total, queryErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindByScoreID 根据 scoreID 查找计算专业列表
|
||||||
|
func (m *YxCalculationMajorMapper) FindByScoreID(scoreID string) ([]entity.YxCalculationMajor, error) {
|
||||||
|
var items []entity.YxCalculationMajor
|
||||||
|
err := m.GetDB().Where("score_id = ?", scoreID).Find(&items).Error
|
||||||
|
return items, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindListByCompositeKeys 根据复合键查找计算专业列表
|
||||||
|
func (m *YxCalculationMajorMapper) FindListByCompositeKeys(tableName string, keys []string, scoreId string) ([]entity.YxCalculationMajor, error) {
|
||||||
|
if len(keys) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证表名格式(防止表名注入)
|
||||||
|
if !common.IsValidTableName(tableName) {
|
||||||
|
return nil, fmt.Errorf("无效的表名: %s", tableName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证和转义 score_id
|
||||||
|
if scoreId == "" {
|
||||||
|
return nil, fmt.Errorf("score_id 不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
var items []entity.YxCalculationMajor
|
||||||
|
db := m.GetDB()
|
||||||
|
if tableName != "" {
|
||||||
|
db = db.Table(tableName)
|
||||||
|
}
|
||||||
|
|
||||||
|
sql := "SELECT * FROM " + tableName + " WHERE score_id = ? AND (school_code, major_code, enrollment_code) IN ("
|
||||||
|
var params []interface{}
|
||||||
|
|
||||||
|
// 将 score_id 作为第一个参数
|
||||||
|
params = append(params, scoreId)
|
||||||
|
|
||||||
|
for i, key := range keys {
|
||||||
|
parts := strings.Split(key, "_")
|
||||||
|
if len(parts) != 3 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if i > 0 {
|
||||||
|
sql += ","
|
||||||
|
}
|
||||||
|
sql += "(?, ?, ?)"
|
||||||
|
params = append(params, parts[0], parts[1], parts[2])
|
||||||
|
}
|
||||||
|
sql += ")"
|
||||||
|
|
||||||
|
err := db.Raw(sql, params...).Scan(&items).Error
|
||||||
|
return items, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindDtoListByCompositeKeys 根据复合键查找 DTO 列表
|
||||||
|
func (m *YxCalculationMajorMapper) FindDtoListByCompositeKeys(tableName string, keys []string, scoreId string) ([]dto.SchoolMajorDTO, error) {
|
||||||
|
if len(keys) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if !common.IsValidTableName(tableName) {
|
||||||
|
return nil, fmt.Errorf("无效的表名: %s", tableName)
|
||||||
|
}
|
||||||
|
if scoreId == "" {
|
||||||
|
return nil, fmt.Errorf("score_id 不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
var items []dto.SchoolMajorDTO
|
||||||
|
|
||||||
|
// SQL with joins to get school info
|
||||||
|
// Base Query similar to FindRecommendList but filtered by composite keys
|
||||||
|
sqlStr := fmt.Sprintf(`
|
||||||
|
SELECT
|
||||||
|
cm.id,
|
||||||
|
s.school_name,
|
||||||
|
s.school_icon,
|
||||||
|
cm.state,
|
||||||
|
cm.school_code,
|
||||||
|
cm.major_code,
|
||||||
|
cm.major_name,
|
||||||
|
cm.enrollment_code,
|
||||||
|
cm.tuition,
|
||||||
|
cm.detail as majorDetail,
|
||||||
|
cm.category,
|
||||||
|
cm.batch,
|
||||||
|
cm.private_student_converted_score as privateStudentScore,
|
||||||
|
cm.student_old_converted_score as studentScore,
|
||||||
|
cm.student_converted_score,
|
||||||
|
cm.enroll_probability,
|
||||||
|
cm.rules_enroll_probability_sx,
|
||||||
|
cm.rules_enroll_probability,
|
||||||
|
cm.probability_operator,
|
||||||
|
cm.major_type,
|
||||||
|
cm.major_type_child,
|
||||||
|
cm.plan_num,
|
||||||
|
cm.main_subjects,
|
||||||
|
cm.limitation,
|
||||||
|
cm.other_score_limitation,
|
||||||
|
s.province as province,
|
||||||
|
s.school_nature as schoolNature,
|
||||||
|
s.institution_type as institutionType
|
||||||
|
FROM %s cm
|
||||||
|
LEFT JOIN yx_school_child sc ON sc.school_code = cm.school_code
|
||||||
|
LEFT JOIN yx_school_research_teaching srt ON srt.school_id = sc.school_id
|
||||||
|
LEFT JOIN yx_school s ON s.id = sc.school_id
|
||||||
|
WHERE cm.score_id = ? AND (cm.school_code, cm.major_code, cm.enrollment_code) IN (
|
||||||
|
`, tableName)
|
||||||
|
|
||||||
|
var params []interface{}
|
||||||
|
params = append(params, scoreId)
|
||||||
|
|
||||||
|
// Build IN clause
|
||||||
|
var tuples []string
|
||||||
|
for _, key := range keys {
|
||||||
|
parts := strings.Split(key, "_")
|
||||||
|
if len(parts) != 3 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tuples = append(tuples, "(?, ?, ?)")
|
||||||
|
params = append(params, parts[0], parts[1], parts[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tuples) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlStr += strings.Join(tuples, ",") + ")"
|
||||||
|
|
||||||
|
err := m.GetDB().Raw(sqlStr, params...).Scan(&items).Error
|
||||||
|
return items, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchCreate 批量创建(支持动态表名)
|
||||||
|
func (m *YxCalculationMajorMapper) BatchCreate(tableName string, items []entity.YxCalculationMajor, batchSize int) error {
|
||||||
|
if tableName != "" {
|
||||||
|
return m.GetDB().Table(tableName).CreateInBatches(items, batchSize).Error
|
||||||
|
}
|
||||||
|
return m.GetDB().CreateInBatches(items, batchSize).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteByScoreID 根据 scoreID 删除
|
||||||
|
func (m *YxCalculationMajorMapper) DeleteByScoreID(scoreID string) error {
|
||||||
|
return m.GetDB().Delete(&entity.YxCalculationMajor{}, "score_id = ?", scoreID).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteByScoreIDFromTable 从指定表删除 scoreID 对应的数据
|
||||||
|
func (m *YxCalculationMajorMapper) DeleteByScoreIDFromTable(tableName, scoreID string) error {
|
||||||
|
if tableName == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return m.GetDB().Table(tableName).Where("score_id = ?", scoreID).Delete(map[string]interface{}{}).Error
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
// Package mapper 数据访问层
|
||||||
|
package mapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"server/common"
|
||||||
|
"server/modules/yx/entity"
|
||||||
|
)
|
||||||
|
|
||||||
|
type YxHistoryMajorEnrollMapper struct {
|
||||||
|
*common.BaseMapper[entity.YxHistoryMajorEnroll]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewYxHistoryMajorEnrollMapper() *YxHistoryMajorEnrollMapper {
|
||||||
|
return &YxHistoryMajorEnrollMapper{
|
||||||
|
BaseMapper: common.NewBaseMapper[entity.YxHistoryMajorEnroll](),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindByYear 根据年份查找历史招生数据
|
||||||
|
func (m *YxHistoryMajorEnrollMapper) FindByYear(year string) ([]entity.YxHistoryMajorEnroll, error) {
|
||||||
|
var items []entity.YxHistoryMajorEnroll
|
||||||
|
err := m.GetDB().Where("year = ?", year).Find(&items).Error
|
||||||
|
return items, err
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package mapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"server/config"
|
||||||
|
"server/modules/yx/entity"
|
||||||
|
)
|
||||||
|
|
||||||
|
type YxHistoryScoreControlLineMapper struct{}
|
||||||
|
|
||||||
|
func NewYxHistoryScoreControlLineMapper() *YxHistoryScoreControlLineMapper {
|
||||||
|
return &YxHistoryScoreControlLineMapper{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *YxHistoryScoreControlLineMapper) SelectByYearAndCategory(year, professionalCategory, category string) ([]entity.YxHistoryScoreControlLine, error) {
|
||||||
|
var items []entity.YxHistoryScoreControlLine
|
||||||
|
// 判断是否为表演类,如果是则匹配多个类别
|
||||||
|
var categories []string
|
||||||
|
switch professionalCategory {
|
||||||
|
case "表演类":
|
||||||
|
categories = []string{"表演类-戏剧影视表演", "表演类-服装表演", "表演类-戏剧影视导演"}
|
||||||
|
// fallthrough // 继续执行下面的case
|
||||||
|
case "音乐类":
|
||||||
|
categories = []string{"音乐类-音乐表演声乐", "音乐类-音乐表演器乐", "音乐类-音乐教育"}
|
||||||
|
default:
|
||||||
|
categories = []string{professionalCategory}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := config.DB.Model(&entity.YxHistoryScoreControlLine{}).
|
||||||
|
Where("year = ?", year).
|
||||||
|
Where("professional_category IN ?", categories).
|
||||||
|
Where("category = ?", category).
|
||||||
|
Order("batch desc").
|
||||||
|
Find(&items).Error
|
||||||
|
return items, err
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
// Package mapper 数据访问层
|
||||||
|
package mapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"server/common"
|
||||||
|
"server/modules/yx/dto"
|
||||||
|
"server/modules/yx/entity"
|
||||||
|
)
|
||||||
|
|
||||||
|
type YxSchoolMajorMapper struct {
|
||||||
|
*common.BaseMapper[entity.YxSchoolMajor]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewYxSchoolMajorMapper() *YxSchoolMajorMapper {
|
||||||
|
return &YxSchoolMajorMapper{
|
||||||
|
BaseMapper: common.NewBaseMapper[entity.YxSchoolMajor](),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectSchoolMajor 查询院校专业信息(包含学校信息)
|
||||||
|
func (m *YxSchoolMajorMapper) SelectSchoolMajor(query dto.SchoolMajorQuery) ([]dto.SchoolMajorDTO, error) {
|
||||||
|
var items []dto.SchoolMajorDTO
|
||||||
|
|
||||||
|
queryBuilder := m.GetDB().Table("yx_school_major sm").
|
||||||
|
Select(`
|
||||||
|
sm.school_code,
|
||||||
|
sc.school_name,
|
||||||
|
sm.major_code,
|
||||||
|
sm.major_name,
|
||||||
|
sm.major_type,
|
||||||
|
sm.major_type_child,
|
||||||
|
sm.plan_num,
|
||||||
|
sm.main_subjects,
|
||||||
|
sm.limitation,
|
||||||
|
sm.chinese_score_limitation,
|
||||||
|
sm.english_score_limitation,
|
||||||
|
sm.cultural_score_limitation,
|
||||||
|
sm.professional_score_limitation,
|
||||||
|
sm.enrollment_code,
|
||||||
|
sm.tuition,
|
||||||
|
sm.detail,
|
||||||
|
sm.category,
|
||||||
|
sm.batch,
|
||||||
|
sm.rules_enroll_probability,
|
||||||
|
sm.probability_operator,
|
||||||
|
sm.private_rules_enroll_probability,
|
||||||
|
sm.private_probability_operator,
|
||||||
|
sm.rules_enroll_probability_sx,
|
||||||
|
sm.kslx,
|
||||||
|
sm.state
|
||||||
|
`).
|
||||||
|
Joins("LEFT JOIN (SELECT school_id,school_name,school_code FROM yx_school_child group by school_code) sc ON sc.school_code = sm.school_code").
|
||||||
|
Joins("LEFT JOIN yx_school s ON s.id = sc.school_id").
|
||||||
|
Where("sm.state > 0")
|
||||||
|
|
||||||
|
if query.MajorType != "" {
|
||||||
|
queryBuilder = queryBuilder.Where("sm.major_type = ?", query.MajorType)
|
||||||
|
}
|
||||||
|
if query.Category != "" {
|
||||||
|
queryBuilder = queryBuilder.Where("sm.category = ?", query.Category)
|
||||||
|
}
|
||||||
|
if len(query.MajorTypeChildren) > 0 {
|
||||||
|
queryBuilder = queryBuilder.Where("sm.major_type_child IN ?", query.MajorTypeChildren)
|
||||||
|
}
|
||||||
|
if query.MainSubjects != "" {
|
||||||
|
queryBuilder = queryBuilder.Where("sm.main_subjects = ?", query.MainSubjects)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := queryBuilder.Scan(&items).Error
|
||||||
|
return items, err
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
// Package mapper 数据访问层
|
||||||
|
package mapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"server/common"
|
||||||
|
"server/modules/yx/entity"
|
||||||
|
)
|
||||||
|
|
||||||
|
type YxUserScoreMapper struct {
|
||||||
|
*common.BaseMapper[entity.YxUserScore]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewYxUserScoreMapper() *YxUserScoreMapper {
|
||||||
|
return &YxUserScoreMapper{
|
||||||
|
BaseMapper: common.NewBaseMapper[entity.YxUserScore](),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateFieldsByMultiCondition 根据多个条件更新字段
|
||||||
|
func (m *YxUserScoreMapper) UpdateFieldsByMultiCondition(condition *entity.YxUserScore, fields map[string]interface{}) error {
|
||||||
|
query := m.GetDB().Model(&entity.YxUserScore{})
|
||||||
|
|
||||||
|
whereCount := 0 // 记录条件数量,避免无条件更新
|
||||||
|
if condition.ID != "" {
|
||||||
|
query = query.Where("id = ?", condition.ID)
|
||||||
|
whereCount++
|
||||||
|
}
|
||||||
|
if condition.CreateBy != "" {
|
||||||
|
query = query.Where("create_by = ?", condition.CreateBy)
|
||||||
|
whereCount++
|
||||||
|
}
|
||||||
|
if condition.State != "" {
|
||||||
|
query = query.Where("state = ?", condition.State)
|
||||||
|
whereCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
// 安全检查:必须至少有一个查询条件
|
||||||
|
if whereCount == 0 {
|
||||||
|
return errors.New("至少需要一个查询条件")
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.Updates(fields).Error
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
// Package mapper 数据访问层
|
||||||
|
package mapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"server/common"
|
||||||
|
"server/modules/yx/entity"
|
||||||
|
"server/modules/yx/vo"
|
||||||
|
)
|
||||||
|
|
||||||
|
type YxVolunteerMapper struct {
|
||||||
|
*common.BaseMapper[entity.YxVolunteer]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewYxVolunteerMapper() *YxVolunteerMapper {
|
||||||
|
return &YxVolunteerMapper{
|
||||||
|
BaseMapper: common.NewBaseMapper[entity.YxVolunteer](),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseOtherVolunteer 关闭用户的其他志愿单
|
||||||
|
func (m *YxVolunteerMapper) CloseOtherVolunteer(userId string) error {
|
||||||
|
result := m.GetDB().Model(&entity.YxVolunteer{}).
|
||||||
|
Where("create_by = ?", userId).
|
||||||
|
Updates(map[string]interface{}{"state": "0"})
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindActiveByScoreId 根据 scoreId 查找激活的志愿单
|
||||||
|
func (m *YxVolunteerMapper) FindActiveByScoreId(scoreId string) (*entity.YxVolunteer, error) {
|
||||||
|
var item entity.YxVolunteer
|
||||||
|
err := m.GetDB().Where("score_id = ? AND state = ?", scoreId, "1").First(&item).Error
|
||||||
|
return &item, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListByUser 查询用户的志愿单列表(包含成绩信息)
|
||||||
|
func (m *YxVolunteerMapper) ListByUser(userID string, page, size int) ([]vo.UserVolunteerVO, int64, error) {
|
||||||
|
var items []vo.UserVolunteerVO
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
db := m.GetDB().Table("yx_volunteer v").
|
||||||
|
Select("v.*, s.professional_category, s.province, s.cultural_score, s.professional_score").
|
||||||
|
Joins("LEFT JOIN yx_user_score s ON v.score_id = s.id").
|
||||||
|
Where("v.create_by = ?", userID)
|
||||||
|
|
||||||
|
db.Count(&total)
|
||||||
|
err := db.Order("v.create_time DESC").Offset((page - 1) * size).Limit(size).Scan(&items).Error
|
||||||
|
return items, total, err
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
// Package mapper 数据访问层
|
||||||
|
package mapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"server/common"
|
||||||
|
"server/modules/yx/entity"
|
||||||
|
)
|
||||||
|
|
||||||
|
type YxVolunteerRecordMapper struct {
|
||||||
|
*common.BaseMapper[entity.YxVolunteerRecord]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewYxVolunteerRecordMapper() *YxVolunteerRecordMapper {
|
||||||
|
return &YxVolunteerRecordMapper{
|
||||||
|
BaseMapper: common.NewBaseMapper[entity.YxVolunteerRecord](),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindByVolunteerID 根据 volunteerID 查找志愿记录
|
||||||
|
func (m *YxVolunteerRecordMapper) FindByVolunteerID(volunteerID string) ([]entity.YxVolunteerRecord, error) {
|
||||||
|
var items []entity.YxVolunteerRecord
|
||||||
|
err := m.GetDB().Where("volunteer_id = ?", volunteerID).Order("indexs ASC").Find(&items).Error
|
||||||
|
return items, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteByVolunteerID 根据 volunteerID 删除志愿记录
|
||||||
|
func (m *YxVolunteerRecordMapper) DeleteByVolunteerID(volunteerID string) error {
|
||||||
|
return m.GetDB().Delete(&entity.YxVolunteerRecord{}, "volunteer_id = ?", volunteerID).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchDeleteByVolunteerId 根据 volunteerID 批量删除志愿记录(无返回值)
|
||||||
|
func (m *YxVolunteerRecordMapper) BatchDeleteByVolunteerId(id string) {
|
||||||
|
m.GetDB().Delete(&entity.YxVolunteerRecord{}, "volunteer_id = ?", id)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,972 @@
|
||||||
|
// Package service 业务逻辑层
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"server/common"
|
||||||
|
calc "server/common"
|
||||||
|
"server/modules/user/vo"
|
||||||
|
"server/modules/yx/dto"
|
||||||
|
yxDto "server/modules/yx/dto"
|
||||||
|
"server/modules/yx/entity"
|
||||||
|
"server/modules/yx/mapper"
|
||||||
|
yxVO "server/modules/yx/vo"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type YxCalculationMajorService struct {
|
||||||
|
*common.BaseService[entity.YxCalculationMajor]
|
||||||
|
historyMajorEnrollService *YxHistoryMajorEnrollService
|
||||||
|
mapper *mapper.YxCalculationMajorMapper
|
||||||
|
historyScoreControlLineService *YxHistoryScoreControlLineService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewYxCalculationMajorService() *YxCalculationMajorService {
|
||||||
|
mapper := mapper.NewYxCalculationMajorMapper()
|
||||||
|
return &YxCalculationMajorService{
|
||||||
|
BaseService: common.NewBaseService[entity.YxCalculationMajor](),
|
||||||
|
historyMajorEnrollService: NewYxHistoryMajorEnrollService(),
|
||||||
|
mapper: mapper,
|
||||||
|
historyScoreControlLineService: NewYxHistoryScoreControlLineService(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecommendMajorList 推荐专业列表
|
||||||
|
func (s *YxCalculationMajorService) RecommendMajorList(schoolMajorQuery yxDto.SchoolMajorQuery) ([]yxDto.UserMajorDTO, int64, dto.ProbabilityCountDTO, error) {
|
||||||
|
if schoolMajorQuery.UserScoreVO.ProfessionalCategory == "" {
|
||||||
|
return nil, 0, dto.ProbabilityCountDTO{}, fmt.Errorf("专业类型错误")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据批次类型设置批次 985/211/双一流 -> 提前批, 公办本科 -> 本科A段, 民办本科 -> 本科B段, 体育类 -> 本科
|
||||||
|
if schoolMajorQuery.Batch != "" && "本科提前" == schoolMajorQuery.Batch {
|
||||||
|
return []yxDto.UserMajorDTO{}, 0, dto.ProbabilityCountDTO{}, nil
|
||||||
|
} else if schoolMajorQuery.Batch != "" && "高职高专" != schoolMajorQuery.Batch {
|
||||||
|
if schoolMajorQuery.Batch2 == "双一流" {
|
||||||
|
schoolMajorQuery.Batch = "提前批"
|
||||||
|
} else if schoolMajorQuery.Batch2 == "公办本科" {
|
||||||
|
schoolMajorQuery.Batch = "本科A段"
|
||||||
|
} else if schoolMajorQuery.Batch2 == "民办本科" {
|
||||||
|
schoolMajorQuery.Batch = "本科B段"
|
||||||
|
} else if schoolMajorQuery.MajorType == "体育类" {
|
||||||
|
schoolMajorQuery.Batch = "本科"
|
||||||
|
} else {
|
||||||
|
schoolMajorQuery.Batch = ""
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
schoolMajorQuery.Batch = "高职高专"
|
||||||
|
}
|
||||||
|
|
||||||
|
calculationMajors, total, probCount, err := s.mapper.FindRecommendList(schoolMajorQuery)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, dto.ProbabilityCountDTO{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为专业列表添加历史数据
|
||||||
|
startTime := time.Now()
|
||||||
|
err = s.UserMajorDTOGetHistory(&calculationMajors)
|
||||||
|
endTime := time.Now()
|
||||||
|
// 执行时长
|
||||||
|
queryCostTime := endTime.Sub(startTime)
|
||||||
|
fmt.Printf(" 历史数据查询耗时:%v\n", queryCostTime)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, dto.ProbabilityCountDTO{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return calculationMajors, total, probCount, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchCreateBySchoolMajorDTO 根据专业 DTO 批量创建
|
||||||
|
func (s *YxCalculationMajorService) BatchCreateBySchoolMajorDTO(tableName string, items []dto.SchoolMajorDTO, scoreID string) error {
|
||||||
|
entities := make([]entity.YxCalculationMajor, 0, len(items))
|
||||||
|
now := time.Now()
|
||||||
|
for _, item := range items {
|
||||||
|
// 构建 OtherScoreLimitation
|
||||||
|
var otherScoreLimitation strings.Builder
|
||||||
|
if item.ChineseScoreLimitation > 0 {
|
||||||
|
otherScoreLimitation.WriteString(fmt.Sprintf("语文成绩不低于%.1f分,", item.ChineseScoreLimitation))
|
||||||
|
}
|
||||||
|
if item.EnglishScoreLimitation > 0 {
|
||||||
|
otherScoreLimitation.WriteString(fmt.Sprintf("外语成绩不低于%.1f分,", item.EnglishScoreLimitation))
|
||||||
|
}
|
||||||
|
if item.CulturalScoreLimitation > 0 {
|
||||||
|
otherScoreLimitation.WriteString(fmt.Sprintf("文化成绩不低于%.1f分,", item.CulturalScoreLimitation))
|
||||||
|
}
|
||||||
|
if item.ProfessionalScoreLimitation > 0 {
|
||||||
|
otherScoreLimitation.WriteString(fmt.Sprintf("专业成绩不低于%.1f分,", item.ProfessionalScoreLimitation))
|
||||||
|
}
|
||||||
|
otherScoreLimitationStr := otherScoreLimitation.String()
|
||||||
|
if len(otherScoreLimitationStr) > 0 {
|
||||||
|
otherScoreLimitationStr = otherScoreLimitationStr[:len(otherScoreLimitationStr)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
entities = append(entities, entity.YxCalculationMajor{
|
||||||
|
ScoreID: scoreID,
|
||||||
|
SchoolCode: item.SchoolCode,
|
||||||
|
MajorCode: item.MajorCode,
|
||||||
|
MajorName: item.MajorName,
|
||||||
|
EnrollmentCode: item.EnrollmentCode,
|
||||||
|
Tuition: item.Tuition,
|
||||||
|
Detail: item.Detail,
|
||||||
|
Category: item.Category,
|
||||||
|
RulesEnrollProbability: item.RulesEnrollProbability,
|
||||||
|
Batch: item.Batch,
|
||||||
|
StudentOldConvertedScore: item.StudentScore,
|
||||||
|
StudentConvertedScore: item.StudentConvertedScore,
|
||||||
|
EnrollProbability: item.EnrollProbability,
|
||||||
|
ProbabilityOperator: item.ProbabilityOperator,
|
||||||
|
CreateTime: now,
|
||||||
|
MajorType: item.MajorType,
|
||||||
|
MajorTypeChild: item.MajorTypeChild,
|
||||||
|
PlanNum: item.PlanNum,
|
||||||
|
MainSubjects: item.MainSubjects,
|
||||||
|
Limitation: item.Limitation,
|
||||||
|
OtherScoreLimitation: otherScoreLimitationStr,
|
||||||
|
RulesEnrollProbabilitySx: item.RulesEnrollProbabilitySx,
|
||||||
|
Kslx: item.Kslx,
|
||||||
|
PrivateStudentConvertedScore: item.PrivateStudentScore,
|
||||||
|
PrivateRulesEnrollProbability: item.PrivateRulesEnrollProbability,
|
||||||
|
PrivateProbabilityOperator: item.PrivateProbabilityOperator,
|
||||||
|
State: item.State,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return s.BatchCreate(tableName, entities)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByScoreID 根据 scoreID 获取计算专业列表
|
||||||
|
func (s *YxCalculationMajorService) GetByScoreID(scoreID string) ([]entity.YxCalculationMajor, error) {
|
||||||
|
return s.mapper.FindByScoreID(scoreID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindListByCompositeKeys 根据复合键查找列表
|
||||||
|
func (s *YxCalculationMajorService) FindListByCompositeKeys(tableName string, keys []string, scoreId string) ([]entity.YxCalculationMajor, error) {
|
||||||
|
return s.mapper.FindListByCompositeKeys(tableName, keys, scoreId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindDtoListByCompositeKeys 根据复合键查找 DTO 列表
|
||||||
|
func (s *YxCalculationMajorService) FindDtoListByCompositeKeys(tableName string, keys []string, scoreId string) ([]dto.SchoolMajorDTO, error) {
|
||||||
|
return s.mapper.FindDtoListByCompositeKeys(tableName, keys, scoreId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchCreate 批量创建(支持动态表名,使用 UUID 生成 ID)
|
||||||
|
func (s *YxCalculationMajorService) BatchCreate(tableName string, items []entity.YxCalculationMajor) error {
|
||||||
|
for i := range items {
|
||||||
|
items[i].ID = uuid.New().String()
|
||||||
|
}
|
||||||
|
return s.mapper.BatchCreate(tableName, items, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteByScoreID 根据 scoreID 删除
|
||||||
|
func (s *YxCalculationMajorService) DeleteByScoreID(scoreID string) error {
|
||||||
|
return s.mapper.DeleteByScoreID(scoreID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteByScoreIDFromTable 从指定表删除 scoreID 对应的数据
|
||||||
|
func (s *YxCalculationMajorService) DeleteByScoreIDFromTable(tableName, scoreID string) error {
|
||||||
|
return s.mapper.DeleteByScoreIDFromTable(tableName, scoreID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListByUserQueryType 根据用户查询类型获取专业列表
|
||||||
|
func (s *YxCalculationMajorService) ListByUserQueryType(professionalCategory string, cognitioPolyclinic string,
|
||||||
|
professionalCategoryChildren []string) ([]dto.SchoolMajorDTO, error) {
|
||||||
|
// 构造查询条件
|
||||||
|
query := dto.SchoolMajorQuery{
|
||||||
|
MajorType: professionalCategory,
|
||||||
|
Category: cognitioPolyclinic,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行院校查询
|
||||||
|
majorItems, err := mapper.NewYxSchoolMajorMapper().SelectSchoolMajor(query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// 分别收集专业名称和院校代码集合(去重)
|
||||||
|
majorNameSet := make(map[string]bool)
|
||||||
|
schoolCodeSet := make(map[string]bool)
|
||||||
|
years := common.OldYearList
|
||||||
|
for _, dto := range majorItems {
|
||||||
|
majorNameSet[dto.MajorName] = true
|
||||||
|
schoolCodeSet[dto.SchoolCode] = true
|
||||||
|
}
|
||||||
|
// 转换为切片用于查询
|
||||||
|
majorNames := make([]string, 0, len(majorNameSet))
|
||||||
|
schoolCodes := make([]string, 0, len(schoolCodeSet))
|
||||||
|
for name := range majorNameSet {
|
||||||
|
majorNames = append(majorNames, name)
|
||||||
|
}
|
||||||
|
for code := range schoolCodeSet {
|
||||||
|
schoolCodes = append(schoolCodes, code)
|
||||||
|
}
|
||||||
|
// 执行查询院校专业的历年数据
|
||||||
|
historyItems, err := s.historyMajorEnrollService.ListBySchoolCodesAndMajorNames(
|
||||||
|
schoolCodes,
|
||||||
|
majorNames,
|
||||||
|
query.Category,
|
||||||
|
query.MajorType,
|
||||||
|
years,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建历史数据映射:schoolCode_majorName_batch_year -> historyItem
|
||||||
|
allHistoryMajorEnrollMap := make(map[string]entity.YxHistoryMajorEnroll)
|
||||||
|
for _, historyItem := range historyItems {
|
||||||
|
key := historyItem.SchoolCode + "_" + historyItem.MajorName + "_" + historyItem.Batch + "_" + historyItem.Year
|
||||||
|
allHistoryMajorEnrollMap[key] = historyItem
|
||||||
|
}
|
||||||
|
// 将历史数据填充到每个专业数据中
|
||||||
|
for i, majorItem := range majorItems {
|
||||||
|
// 为每个majorItem创建独立的历史数据映射
|
||||||
|
historyMap := make(map[string]dto.YxHistoryMajorEnrollDTO)
|
||||||
|
|
||||||
|
for _, year := range years {
|
||||||
|
key := majorItem.SchoolCode + "_" + majorItem.MajorName + "_" + majorItem.Batch + "_" + year
|
||||||
|
if historyItem, ok := allHistoryMajorEnrollMap[key]; ok {
|
||||||
|
dtoItem := dto.YxHistoryMajorEnrollDTO{
|
||||||
|
Year: historyItem.Year,
|
||||||
|
EnrollmentCode: historyItem.EnrollmentCode,
|
||||||
|
RulesEnrollProbability: historyItem.RulesEnrollProbability,
|
||||||
|
ProbabilityOperator: historyItem.ProbabilityOperator,
|
||||||
|
AdmissionLine: historyItem.AdmissionLine,
|
||||||
|
ControlLine: historyItem.ControlLine,
|
||||||
|
}
|
||||||
|
historyMap[year] = dtoItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
majorItems[i].HistoryMajorEnrollMap = historyMap
|
||||||
|
}
|
||||||
|
|
||||||
|
return majorItems, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserMajorDTOGetHistory 获取用户专业 DTO 的历史数据
|
||||||
|
func (s *YxCalculationMajorService) UserMajorDTOGetHistory(userMajorDTOList *[]dto.UserMajorDTO) error {
|
||||||
|
if len(*userMajorDTOList) > 0 {
|
||||||
|
// 分别收集专业名称和院校代码集合(去重)
|
||||||
|
majorNameSet := make(map[string]bool)
|
||||||
|
schoolCodeSet := make(map[string]bool)
|
||||||
|
years := common.OldYearList
|
||||||
|
for _, dto := range *userMajorDTOList {
|
||||||
|
majorNameSet[dto.MajorName] = true
|
||||||
|
schoolCodeSet[dto.SchoolCode] = true
|
||||||
|
}
|
||||||
|
// 转换为切片用于查询
|
||||||
|
majorNames := make([]string, 0, len(majorNameSet))
|
||||||
|
schoolCodes := make([]string, 0, len(schoolCodeSet))
|
||||||
|
for name := range majorNameSet {
|
||||||
|
majorNames = append(majorNames, name)
|
||||||
|
}
|
||||||
|
for code := range schoolCodeSet {
|
||||||
|
schoolCodes = append(schoolCodes, code)
|
||||||
|
}
|
||||||
|
// 执行查询院校专业的历年数据
|
||||||
|
historyItems, err := s.historyMajorEnrollService.ListBySchoolCodesAndMajorNames(
|
||||||
|
schoolCodes,
|
||||||
|
majorNames,
|
||||||
|
(*userMajorDTOList)[0].Category,
|
||||||
|
(*userMajorDTOList)[0].MajorType,
|
||||||
|
years,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建历史数据映射:schoolCode_majorName_batch_year -> historyItem
|
||||||
|
allHistoryMajorEnrollMap := make(map[string]entity.YxHistoryMajorEnroll)
|
||||||
|
for _, historyItem := range historyItems {
|
||||||
|
key := historyItem.SchoolCode + "_" + historyItem.MajorName + "_" + historyItem.Batch + "_" + historyItem.Year
|
||||||
|
allHistoryMajorEnrollMap[key] = historyItem
|
||||||
|
}
|
||||||
|
// 将历史数据填充到每个专业数据中
|
||||||
|
for i, majorItem := range *userMajorDTOList {
|
||||||
|
// 为每个majorItem创建独立的历史数据映射
|
||||||
|
historyMap := make(map[string]dto.YxHistoryMajorEnrollDTO)
|
||||||
|
|
||||||
|
for _, year := range years {
|
||||||
|
key := majorItem.SchoolCode + "_" + majorItem.MajorName + "_" + majorItem.Batch + "_" + year
|
||||||
|
if historyItem, ok := allHistoryMajorEnrollMap[key]; ok {
|
||||||
|
dtoItem := dto.YxHistoryMajorEnrollDTO{
|
||||||
|
Year: historyItem.Year,
|
||||||
|
EnrollmentCode: historyItem.EnrollmentCode,
|
||||||
|
RulesEnrollProbability: historyItem.RulesEnrollProbability,
|
||||||
|
ProbabilityOperator: historyItem.ProbabilityOperator,
|
||||||
|
AdmissionLine: historyItem.AdmissionLine,
|
||||||
|
ControlLine: historyItem.ControlLine,
|
||||||
|
}
|
||||||
|
historyMap[year] = dtoItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(*userMajorDTOList)[i].HistoryMajorEnrollMap = historyMap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckEnrollProbability 计算专业列表录取率
|
||||||
|
func (s *YxCalculationMajorService) CheckEnrollProbability(schoolMajorDTOList *[]dto.SchoolMajorDTO, userScoreVO vo.UserScoreVO) error {
|
||||||
|
professionalCategory := userScoreVO.ProfessionalCategory
|
||||||
|
if "表演类" == professionalCategory {
|
||||||
|
s.BiaoYanRecommendMajorListSetEnrollProbability(schoolMajorDTOList, userScoreVO)
|
||||||
|
} else if "音乐类" == professionalCategory {
|
||||||
|
s.YinYueRecommendMajorListSetEnrollProbability(schoolMajorDTOList, userScoreVO)
|
||||||
|
} else {
|
||||||
|
s.BetaRecommendMajorListSetEnrollProbability(schoolMajorDTOList, userScoreVO)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BiaoYanRecommendMajorListSetEnrollProbability 表演类 获取录取率
|
||||||
|
func (s *YxCalculationMajorService) BiaoYanRecommendMajorListSetEnrollProbability(recommendMajorList *[]dto.SchoolMajorDTO, userScoreVO vo.UserScoreVO) {
|
||||||
|
if recommendMajorList == nil || len(*recommendMajorList) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
professionalCategory := userScoreVO.ProfessionalCategory
|
||||||
|
nowBatch := "本科"
|
||||||
|
|
||||||
|
// 获取省控线 Map
|
||||||
|
historyScoreControlLineMap, err := s.historyScoreControlLineService.MapsBatchByProfessionalCategoryOfYear(common.NowYear, professionalCategory, userScoreVO.CognitioPolyclinic)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
culturalScore := userScoreVO.CulturalScore
|
||||||
|
professionalScore := userScoreVO.ProfessionalScore
|
||||||
|
|
||||||
|
var nowMajorTypeChild string
|
||||||
|
var batchKey string
|
||||||
|
for i := range *recommendMajorList {
|
||||||
|
item := &(*recommendMajorList)[i]
|
||||||
|
|
||||||
|
rulesEnrollProbability := item.PrivateRulesEnrollProbability
|
||||||
|
probabilityOperator := item.PrivateProbabilityOperator
|
||||||
|
|
||||||
|
// 当前子类型
|
||||||
|
nowMajorTypeChild = item.MajorTypeChild
|
||||||
|
// 整合为 表演类-XXX子类型
|
||||||
|
batchKey = professionalCategory + "-" + nowMajorTypeChild + "_" + item.Batch
|
||||||
|
|
||||||
|
// 获取对应批次的省控线
|
||||||
|
controlLineData, ok := historyScoreControlLineMap[batchKey]
|
||||||
|
if !ok {
|
||||||
|
if val, okDefault := historyScoreControlLineMap["本科"]; okDefault {
|
||||||
|
controlLineData = val
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断子类型,使用对应的成绩进行计算
|
||||||
|
professionalScore = userScoreVO.ProfessionalCategoryChildrenScore[item.MajorTypeChild]
|
||||||
|
|
||||||
|
culturalControlLine := controlLineData.CulturalScore
|
||||||
|
specialControlLine := controlLineData.SpecialScore
|
||||||
|
|
||||||
|
if rulesEnrollProbability == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 补全 probabilityOperator 逻辑
|
||||||
|
if rulesEnrollProbability == "文过专排" && probabilityOperator == "" {
|
||||||
|
probabilityOperator = "文*0+专*1"
|
||||||
|
} else if rulesEnrollProbability == "专过文排" && probabilityOperator == "" {
|
||||||
|
probabilityOperator = "文*1+专*0"
|
||||||
|
}
|
||||||
|
|
||||||
|
if probabilityOperator == "" {
|
||||||
|
item.EnrollProbability = common.Number5
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断其他录取要求
|
||||||
|
if !calc.OtherScoreJudge(professionalScore, userScoreVO, *item) {
|
||||||
|
item.EnrollProbability = common.Number0
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否过省控线
|
||||||
|
if !calc.CrossingControlLine(rulesEnrollProbability, culturalScore, professionalScore, culturalControlLine, specialControlLine) {
|
||||||
|
item.EnrollProbability = common.Number0
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算学生折合分
|
||||||
|
studentScore := calc.ConvertIntoScore(rulesEnrollProbability, culturalScore, professionalScore, probabilityOperator)
|
||||||
|
item.PrivateStudentScore = studentScore
|
||||||
|
item.StudentScore = studentScore
|
||||||
|
|
||||||
|
// 权限检查
|
||||||
|
if !calc.HasComputeEnrollProbabilityPermissions(nowBatch, item.Batch) {
|
||||||
|
item.EnrollProbability = common.Number0
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 录取方式计算
|
||||||
|
if common.CulturalControlLineGuoMain == rulesEnrollProbability {
|
||||||
|
if len(item.HistoryMajorEnrollList) == 0 {
|
||||||
|
item.EnrollProbability = common.Number0
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
item.EnrollProbability = (studentScore * item.HistoryMajorEnrollList[0].AdmissionLine) * common.Number0p75
|
||||||
|
if studentScore >= item.HistoryMajorEnrollList[0].AdmissionLine {
|
||||||
|
item.EnrollProbability *= common.Number0p5
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
nowYearProvincialControlLine := calc.ConvertIntoScore(rulesEnrollProbability, culturalControlLine, specialControlLine, probabilityOperator)
|
||||||
|
if nowYearProvincialControlLine <= 0 {
|
||||||
|
item.EnrollProbability = common.Number0
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
diffMap := calc.ComputeHistoryMajorEnrollScoreLineDifferenceWithRulesEnrollProbability(item.MajorType, rulesEnrollProbability, probabilityOperator, item.HistoryMajorEnrollMap)
|
||||||
|
historyThreeYearDiff := diffMap["scoreDifference"].(float64)
|
||||||
|
|
||||||
|
if historyThreeYearDiff == 0 {
|
||||||
|
item.EnrollProbability = common.Number0
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
nowYearDiff := studentScore - nowYearProvincialControlLine
|
||||||
|
|
||||||
|
enrollProbability := calc.CommonCheckEnrollProbability(nowYearDiff, historyThreeYearDiff)
|
||||||
|
item.EnrollProbability = calc.CommonCheckEnrollProbabilityBeilv(enrollProbability)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// YinYueRecommendMajorListSetEnrollProbability 音乐类 获取录取率
|
||||||
|
func (s *YxCalculationMajorService) YinYueRecommendMajorListSetEnrollProbability(recommendMajorList *[]dto.SchoolMajorDTO, userScoreVO vo.UserScoreVO) {
|
||||||
|
if recommendMajorList == nil || len(*recommendMajorList) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
professionalCategory := userScoreVO.ProfessionalCategory
|
||||||
|
nowBatch := "本科"
|
||||||
|
|
||||||
|
// 获取省控线 Map
|
||||||
|
historyScoreControlLineMap, err := s.historyScoreControlLineService.MapsBatchByProfessionalCategoryOfYear(common.NowYear, professionalCategory, userScoreVO.CognitioPolyclinic)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
culturalScore := userScoreVO.CulturalScore
|
||||||
|
professionalScore := userScoreVO.ProfessionalScore
|
||||||
|
|
||||||
|
var rulesEnrollProbability string
|
||||||
|
var probabilityOperator string
|
||||||
|
var nowMajorTypeChild string
|
||||||
|
var batchKey string
|
||||||
|
for i := range *recommendMajorList {
|
||||||
|
item := &(*recommendMajorList)[i]
|
||||||
|
|
||||||
|
rulesEnrollProbability = item.PrivateRulesEnrollProbability
|
||||||
|
probabilityOperator = item.PrivateProbabilityOperator
|
||||||
|
|
||||||
|
// 当前子类型
|
||||||
|
nowMajorTypeChild = item.MajorTypeChild
|
||||||
|
|
||||||
|
// 主考科目
|
||||||
|
if "音乐表演" == nowMajorTypeChild {
|
||||||
|
nowMajorTypeChild += item.MainSubjects
|
||||||
|
}
|
||||||
|
|
||||||
|
// 整合为 表演类-XXX子类型
|
||||||
|
if "高职高专" == item.Batch {
|
||||||
|
batchKey = professionalCategory + "-" + nowMajorTypeChild + "_" + item.Batch
|
||||||
|
} else {
|
||||||
|
batchKey = professionalCategory + "-" + nowMajorTypeChild + "_本科"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取对应批次的省控线
|
||||||
|
controlLineData, ok := historyScoreControlLineMap[batchKey]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
// if val, okDefault := historyScoreControlLineMap["本科"]; okDefault {
|
||||||
|
// controlLineData = val
|
||||||
|
// } else {
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断子类型,使用对应的成绩进行计算
|
||||||
|
professionalScore = userScoreVO.ProfessionalCategoryChildrenScore[nowMajorTypeChild]
|
||||||
|
|
||||||
|
culturalControlLine := controlLineData.CulturalScore
|
||||||
|
specialControlLine := controlLineData.SpecialScore
|
||||||
|
|
||||||
|
if rulesEnrollProbability == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 补全 probabilityOperator 逻辑
|
||||||
|
if rulesEnrollProbability == "文过专排" && probabilityOperator == "" {
|
||||||
|
probabilityOperator = "文*0+专*1"
|
||||||
|
} else if rulesEnrollProbability == "专过文排" && probabilityOperator == "" {
|
||||||
|
probabilityOperator = "文*1+专*0"
|
||||||
|
}
|
||||||
|
|
||||||
|
if probabilityOperator == "" {
|
||||||
|
item.EnrollProbability = common.Number5
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断其他录取要求
|
||||||
|
if !calc.OtherScoreJudge(professionalScore, userScoreVO, *item) {
|
||||||
|
item.EnrollProbability = common.Number0
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否过省控线
|
||||||
|
if !calc.CrossingControlLine(rulesEnrollProbability, culturalScore, professionalScore, culturalControlLine, specialControlLine) {
|
||||||
|
item.EnrollProbability = common.Number0
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算学生折合分
|
||||||
|
studentScore := calc.ConvertIntoScore(rulesEnrollProbability, culturalScore, professionalScore, probabilityOperator)
|
||||||
|
item.PrivateStudentScore = studentScore
|
||||||
|
item.StudentScore = studentScore
|
||||||
|
|
||||||
|
// 权限检查
|
||||||
|
if !calc.HasComputeEnrollProbabilityPermissions(nowBatch, item.Batch) {
|
||||||
|
item.EnrollProbability = common.Number0
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 录取方式计算
|
||||||
|
if common.CulturalControlLineGuoMain == rulesEnrollProbability {
|
||||||
|
if len(item.HistoryMajorEnrollList) == 0 {
|
||||||
|
item.EnrollProbability = common.Number0
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
item.EnrollProbability = (studentScore * item.HistoryMajorEnrollList[0].AdmissionLine) * common.Number0p75
|
||||||
|
if studentScore >= item.HistoryMajorEnrollList[0].AdmissionLine {
|
||||||
|
item.EnrollProbability *= common.Number0p5
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
nowYearProvincialControlLine := calc.ConvertIntoScore(rulesEnrollProbability, culturalControlLine, specialControlLine, probabilityOperator)
|
||||||
|
if nowYearProvincialControlLine <= 0 {
|
||||||
|
item.EnrollProbability = common.Number0
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
diffMap := calc.ComputeHistoryMajorEnrollScoreLineDifferenceWithRulesEnrollProbability(item.MajorType, rulesEnrollProbability, probabilityOperator, item.HistoryMajorEnrollMap)
|
||||||
|
historyThreeYearDiff := diffMap["scoreDifference"].(float64)
|
||||||
|
|
||||||
|
if historyThreeYearDiff == 0 {
|
||||||
|
item.EnrollProbability = common.Number0
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
nowYearDiff := studentScore - nowYearProvincialControlLine
|
||||||
|
|
||||||
|
enrollProbability := calc.CommonCheckEnrollProbability(nowYearDiff, historyThreeYearDiff)
|
||||||
|
item.EnrollProbability = calc.CommonCheckEnrollProbabilityBeilv(enrollProbability)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BetaRecommendMajorListSetEnrollProbability 美术与设计类,书法类,体育类 获取录取率
|
||||||
|
func (s *YxCalculationMajorService) BetaRecommendMajorListSetEnrollProbability(recommendMajorList *[]dto.SchoolMajorDTO, userScoreVO vo.UserScoreVO) {
|
||||||
|
if recommendMajorList == nil || len(*recommendMajorList) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
professionalCategory := userScoreVO.ProfessionalCategory
|
||||||
|
nowBatch := "本科"
|
||||||
|
|
||||||
|
// 获取省控线 Map
|
||||||
|
historyScoreControlLineMap, err := s.historyScoreControlLineService.MapsBatchByProfessionalCategoryOfYear(common.NowYear, professionalCategory, userScoreVO.CognitioPolyclinic)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
culturalScore := userScoreVO.CulturalScore
|
||||||
|
professionalScore := userScoreVO.ProfessionalScore
|
||||||
|
|
||||||
|
for i := range *recommendMajorList {
|
||||||
|
item := &(*recommendMajorList)[i]
|
||||||
|
|
||||||
|
rulesEnrollProbability := item.PrivateRulesEnrollProbability
|
||||||
|
probabilityOperator := item.PrivateProbabilityOperator
|
||||||
|
|
||||||
|
// 获取对应批次的省控线
|
||||||
|
controlLineData, ok := historyScoreControlLineMap[item.Batch]
|
||||||
|
if !ok {
|
||||||
|
if val, okDefault := historyScoreControlLineMap["本科"]; okDefault {
|
||||||
|
controlLineData = val
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
culturalControlLine := controlLineData.CulturalScore
|
||||||
|
specialControlLine := controlLineData.SpecialScore
|
||||||
|
|
||||||
|
if rulesEnrollProbability == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 补全 probabilityOperator 逻辑
|
||||||
|
if rulesEnrollProbability == "文过专排" && probabilityOperator == "" {
|
||||||
|
probabilityOperator = "文*0+专*1"
|
||||||
|
} else if rulesEnrollProbability == "专过文排" && probabilityOperator == "" {
|
||||||
|
probabilityOperator = "文*1+专*0"
|
||||||
|
}
|
||||||
|
|
||||||
|
if probabilityOperator == "" {
|
||||||
|
item.EnrollProbability = common.Number5
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断其他录取要求
|
||||||
|
if !calc.OtherScoreJudge(professionalScore, userScoreVO, *item) {
|
||||||
|
item.EnrollProbability = common.Number0
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 25年专业录取原则变动 (体育类特殊逻辑)
|
||||||
|
if item.MajorType == "体育类" {
|
||||||
|
specialSchoolCodes := []string{"6530", "6085", "6110", "6065", "6050"}
|
||||||
|
isSpecial := false
|
||||||
|
for _, code := range specialSchoolCodes {
|
||||||
|
if item.SchoolCode == code {
|
||||||
|
isSpecial = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isSpecial {
|
||||||
|
item.EnrollProbability = common.Number0
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否过省控线
|
||||||
|
if !calc.CrossingControlLine(rulesEnrollProbability, culturalScore, professionalScore, culturalControlLine, specialControlLine) {
|
||||||
|
item.EnrollProbability = common.Number0
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算学生折合分
|
||||||
|
studentScore := calc.ConvertIntoScore(rulesEnrollProbability, culturalScore, professionalScore, probabilityOperator)
|
||||||
|
item.PrivateStudentScore = studentScore
|
||||||
|
item.StudentScore = studentScore
|
||||||
|
|
||||||
|
// 权限检查
|
||||||
|
if !calc.HasComputeEnrollProbabilityPermissions(nowBatch, item.Batch) {
|
||||||
|
item.EnrollProbability = common.Number0
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 录取方式计算
|
||||||
|
if common.CulturalControlLineGuoMain == rulesEnrollProbability {
|
||||||
|
if len(item.HistoryMajorEnrollList) == 0 {
|
||||||
|
item.EnrollProbability = common.Number0
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
item.EnrollProbability = (studentScore * item.HistoryMajorEnrollList[0].AdmissionLine) * common.Number0p75
|
||||||
|
if studentScore >= item.HistoryMajorEnrollList[0].AdmissionLine {
|
||||||
|
item.EnrollProbability *= common.Number0p5
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
nowYearProvincialControlLine := calc.ConvertIntoScore(rulesEnrollProbability, culturalControlLine, specialControlLine, probabilityOperator)
|
||||||
|
if nowYearProvincialControlLine <= 0 {
|
||||||
|
item.EnrollProbability = common.Number0
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
diffMap := calc.ComputeHistoryMajorEnrollScoreLineDifferenceWithRulesEnrollProbability(item.MajorType, rulesEnrollProbability, probabilityOperator, item.HistoryMajorEnrollMap)
|
||||||
|
historyThreeYearDiff := diffMap["scoreDifference"].(float64)
|
||||||
|
|
||||||
|
if historyThreeYearDiff == 0 {
|
||||||
|
item.EnrollProbability = common.Number0
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
nowYearDiff := studentScore - nowYearProvincialControlLine
|
||||||
|
|
||||||
|
enrollProbability := calc.CommonCheckEnrollProbability(nowYearDiff, historyThreeYearDiff)
|
||||||
|
item.EnrollProbability = calc.CommonCheckEnrollProbabilityBeilv(enrollProbability)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListCalculationMajors 获取计算专业列表并返回 VO 列表
|
||||||
|
func (s *YxCalculationMajorService) ListCalculationMajors(page, size int) ([]yxVO.YxCalculationMajorVO, int64, error) {
|
||||||
|
entities, total, err := s.List(page, size)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量转换 Entity 到 VO
|
||||||
|
vos := make([]yxVO.YxCalculationMajorVO, len(entities))
|
||||||
|
for i := range entities {
|
||||||
|
vos[i] = *s.convertToVO(entities[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
return vos, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCalculationMajorByID 获取计算专业并返回 VO
|
||||||
|
func (s *YxCalculationMajorService) GetCalculationMajorByID(id string) (*yxVO.YxCalculationMajorVO, error) {
|
||||||
|
entityItem, err := s.GetByID(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return s.convertToVO(*entityItem), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateCalculationMajor 创建计算专业并返回 VO
|
||||||
|
func (s *YxCalculationMajorService) CreateCalculationMajor(req *yxDto.CreateCalculationMajorRequest) (*yxVO.YxCalculationMajorVO, error) {
|
||||||
|
// DTO 转 Entity - 只使用 Entity 中存在的字段
|
||||||
|
entityItem := &entity.YxCalculationMajor{
|
||||||
|
ID: uuid.New().String(),
|
||||||
|
SchoolCode: req.SchoolCode,
|
||||||
|
MajorCode: req.MajorCode,
|
||||||
|
MajorName: req.MajorName,
|
||||||
|
MajorType: req.MajorType,
|
||||||
|
MajorTypeChild: req.MajorTypeChild,
|
||||||
|
PlanNum: req.PlanNum,
|
||||||
|
MainSubjects: req.MainSubjects,
|
||||||
|
Limitation: req.Limitation,
|
||||||
|
EnrollmentCode: req.EnrollmentCode,
|
||||||
|
Tuition: req.Tuition,
|
||||||
|
Detail: req.Detail,
|
||||||
|
Category: req.Category,
|
||||||
|
Batch: req.Batch,
|
||||||
|
RulesEnrollProbability: req.RulesEnrollProbability,
|
||||||
|
ProbabilityOperator: req.ProbabilityOperator,
|
||||||
|
Kslx: req.Kslx,
|
||||||
|
State: req.State,
|
||||||
|
EnrollProbability: req.EnrollProbability,
|
||||||
|
StudentConvertedScore: req.StudentScore,
|
||||||
|
RulesEnrollProbabilitySx: req.RulesEnrollProbability,
|
||||||
|
CreateTime: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注意:分数限制字段存储在 OtherScoreLimitation 中,需要单独处理
|
||||||
|
var otherLimitations []string
|
||||||
|
if req.ChineseScoreLimitation > 0 {
|
||||||
|
otherLimitations = append(otherLimitations, fmt.Sprintf("语文成绩不低于%.1f分", req.ChineseScoreLimitation))
|
||||||
|
}
|
||||||
|
if req.EnglishScoreLimitation > 0 {
|
||||||
|
otherLimitations = append(otherLimitations, fmt.Sprintf("外语成绩不低于%.1f分", req.EnglishScoreLimitation))
|
||||||
|
}
|
||||||
|
if req.CulturalScoreLimitation > 0 {
|
||||||
|
otherLimitations = append(otherLimitations, fmt.Sprintf("文化成绩不低于%.1f分", req.CulturalScoreLimitation))
|
||||||
|
}
|
||||||
|
if req.ProfessionalScoreLimitation > 0 {
|
||||||
|
otherLimitations = append(otherLimitations, fmt.Sprintf("专业成绩不低于%.1f分", req.ProfessionalScoreLimitation))
|
||||||
|
}
|
||||||
|
if len(otherLimitations) > 0 {
|
||||||
|
entityItem.OtherScoreLimitation = strings.Join(otherLimitations, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到数据库
|
||||||
|
if err := s.Create(entityItem); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entity 转 VO
|
||||||
|
return s.convertToVO(*entityItem), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCalculationMajor 更新计算专业并返回 VO
|
||||||
|
func (s *YxCalculationMajorService) UpdateCalculationMajor(id string, req *yxDto.UpdateCalculationMajorRequest) (*yxVO.YxCalculationMajorVO, error) {
|
||||||
|
// 获取原数据
|
||||||
|
entityItem, err := s.GetByID(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("计算专业不存在: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新字段 - 只使用 Entity 中存在的字段
|
||||||
|
updateFields := make(map[string]interface{})
|
||||||
|
if req.MajorName != "" {
|
||||||
|
updateFields["major_name"] = req.MajorName
|
||||||
|
}
|
||||||
|
if req.MajorType != "" {
|
||||||
|
updateFields["major_type"] = req.MajorType
|
||||||
|
}
|
||||||
|
if req.MajorTypeChild != "" {
|
||||||
|
updateFields["major_type_child"] = req.MajorTypeChild
|
||||||
|
}
|
||||||
|
if req.PlanNum > 0 {
|
||||||
|
updateFields["plan_num"] = req.PlanNum
|
||||||
|
}
|
||||||
|
if req.MainSubjects != "" {
|
||||||
|
updateFields["main_subjects"] = req.MainSubjects
|
||||||
|
}
|
||||||
|
if req.Limitation != "" {
|
||||||
|
updateFields["limitation"] = req.Limitation
|
||||||
|
}
|
||||||
|
if req.EnrollmentCode != "" {
|
||||||
|
updateFields["enrollment_code"] = req.EnrollmentCode
|
||||||
|
}
|
||||||
|
if req.Tuition != "" {
|
||||||
|
updateFields["tuition"] = req.Tuition
|
||||||
|
}
|
||||||
|
if req.Detail != "" {
|
||||||
|
updateFields["detail"] = req.Detail
|
||||||
|
}
|
||||||
|
if req.Category != "" {
|
||||||
|
updateFields["category"] = req.Category
|
||||||
|
}
|
||||||
|
if req.Batch != "" {
|
||||||
|
updateFields["batch"] = req.Batch
|
||||||
|
}
|
||||||
|
if req.RulesEnrollProbability != "" {
|
||||||
|
updateFields["rules_enroll_probability"] = req.RulesEnrollProbability
|
||||||
|
}
|
||||||
|
if req.ProbabilityOperator != "" {
|
||||||
|
updateFields["probability_operator"] = req.ProbabilityOperator
|
||||||
|
}
|
||||||
|
if req.Kslx != "" {
|
||||||
|
updateFields["kslx"] = req.Kslx
|
||||||
|
}
|
||||||
|
if req.State != "" {
|
||||||
|
updateFields["state"] = req.State
|
||||||
|
}
|
||||||
|
if req.EnrollProbability > 0 {
|
||||||
|
updateFields["enroll_probability"] = req.EnrollProbability
|
||||||
|
}
|
||||||
|
if req.StudentScore > 0 {
|
||||||
|
updateFields["student_converted_score"] = req.StudentScore
|
||||||
|
}
|
||||||
|
if req.RulesEnrollProbability != "" {
|
||||||
|
updateFields["rules_enroll_probability_sx"] = req.RulesEnrollProbability
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理分数限制字段
|
||||||
|
var otherLimitations []string
|
||||||
|
if req.ChineseScoreLimitation > 0 {
|
||||||
|
otherLimitations = append(otherLimitations, fmt.Sprintf("语文成绩不低于%.1f分", req.ChineseScoreLimitation))
|
||||||
|
}
|
||||||
|
if req.EnglishScoreLimitation > 0 {
|
||||||
|
otherLimitations = append(otherLimitations, fmt.Sprintf("外语成绩不低于%.1f分", req.EnglishScoreLimitation))
|
||||||
|
}
|
||||||
|
if req.CulturalScoreLimitation > 0 {
|
||||||
|
otherLimitations = append(otherLimitations, fmt.Sprintf("文化成绩不低于%.1f分", req.CulturalScoreLimitation))
|
||||||
|
}
|
||||||
|
if req.ProfessionalScoreLimitation > 0 {
|
||||||
|
otherLimitations = append(otherLimitations, fmt.Sprintf("专业成绩不低于%.1f分", req.ProfessionalScoreLimitation))
|
||||||
|
}
|
||||||
|
if len(otherLimitations) > 0 {
|
||||||
|
updateFields["other_score_limitation"] = strings.Join(otherLimitations, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.UpdateFields(id, updateFields); err != nil {
|
||||||
|
return nil, fmt.Errorf("更新计算专业失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新获取更新后的数据
|
||||||
|
entityItem, err = s.GetByID(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("获取更新后数据失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.convertToVO(*entityItem), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchCreateCalculationMajors 批量创建计算专业
|
||||||
|
func (s *YxCalculationMajorService) BatchCreateCalculationMajors(reqs *[]yxDto.CreateCalculationMajorRequest) error {
|
||||||
|
entities := make([]entity.YxCalculationMajor, 0, len(*reqs))
|
||||||
|
now := time.Now()
|
||||||
|
for _, req := range *reqs {
|
||||||
|
// DTO 转 Entity - 只使用 Entity 中存在的字段
|
||||||
|
entityItem := entity.YxCalculationMajor{
|
||||||
|
ID: uuid.New().String(),
|
||||||
|
SchoolCode: req.SchoolCode,
|
||||||
|
MajorCode: req.MajorCode,
|
||||||
|
MajorName: req.MajorName,
|
||||||
|
MajorType: req.MajorType,
|
||||||
|
MajorTypeChild: req.MajorTypeChild,
|
||||||
|
PlanNum: req.PlanNum,
|
||||||
|
MainSubjects: req.MainSubjects,
|
||||||
|
Limitation: req.Limitation,
|
||||||
|
EnrollmentCode: req.EnrollmentCode,
|
||||||
|
Tuition: req.Tuition,
|
||||||
|
Detail: req.Detail,
|
||||||
|
Category: req.Category,
|
||||||
|
Batch: req.Batch,
|
||||||
|
RulesEnrollProbability: req.RulesEnrollProbability,
|
||||||
|
ProbabilityOperator: req.ProbabilityOperator,
|
||||||
|
Kslx: req.Kslx,
|
||||||
|
State: req.State,
|
||||||
|
EnrollProbability: req.EnrollProbability,
|
||||||
|
StudentConvertedScore: req.StudentScore,
|
||||||
|
RulesEnrollProbabilitySx: req.RulesEnrollProbability,
|
||||||
|
CreateTime: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理分数限制字段
|
||||||
|
var otherLimitations []string
|
||||||
|
if req.ChineseScoreLimitation > 0 {
|
||||||
|
otherLimitations = append(otherLimitations, fmt.Sprintf("语文成绩不低于%.1f分", req.ChineseScoreLimitation))
|
||||||
|
}
|
||||||
|
if req.EnglishScoreLimitation > 0 {
|
||||||
|
otherLimitations = append(otherLimitations, fmt.Sprintf("外语成绩不低于%.1f分", req.EnglishScoreLimitation))
|
||||||
|
}
|
||||||
|
if req.CulturalScoreLimitation > 0 {
|
||||||
|
otherLimitations = append(otherLimitations, fmt.Sprintf("文化成绩不低于%.1f分", req.CulturalScoreLimitation))
|
||||||
|
}
|
||||||
|
if req.ProfessionalScoreLimitation > 0 {
|
||||||
|
otherLimitations = append(otherLimitations, fmt.Sprintf("专业成绩不低于%.1f分", req.ProfessionalScoreLimitation))
|
||||||
|
}
|
||||||
|
if len(otherLimitations) > 0 {
|
||||||
|
entityItem.OtherScoreLimitation = strings.Join(otherLimitations, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
entities = append(entities, entityItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.BatchCreate("", entities)
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertToVO Entity 转 VO(私有方法)
|
||||||
|
func (s *YxCalculationMajorService) convertToVO(entity entity.YxCalculationMajor) *yxVO.YxCalculationMajorVO {
|
||||||
|
vo := &yxVO.YxCalculationMajorVO{
|
||||||
|
ID: entity.ID,
|
||||||
|
SchoolCode: entity.SchoolCode,
|
||||||
|
MajorCode: entity.MajorCode,
|
||||||
|
MajorName: entity.MajorName,
|
||||||
|
MajorType: entity.MajorType,
|
||||||
|
MajorTypeChild: entity.MajorTypeChild,
|
||||||
|
PlanNum: entity.PlanNum,
|
||||||
|
MainSubjects: entity.MainSubjects,
|
||||||
|
Limitation: entity.Limitation,
|
||||||
|
EnrollmentCode: entity.EnrollmentCode,
|
||||||
|
Tuition: entity.Tuition,
|
||||||
|
Detail: entity.Detail,
|
||||||
|
Category: entity.Category,
|
||||||
|
Batch: entity.Batch,
|
||||||
|
RulesEnrollProbability: entity.RulesEnrollProbability,
|
||||||
|
ProbabilityOperator: entity.ProbabilityOperator,
|
||||||
|
Kslx: entity.Kslx,
|
||||||
|
State: entity.State,
|
||||||
|
EnrollProbability: entity.EnrollProbability,
|
||||||
|
StudentScore: entity.StudentConvertedScore,
|
||||||
|
PrivateStudentScore: entity.PrivateStudentConvertedScore,
|
||||||
|
StudentConvertedScore: entity.StudentConvertedScore,
|
||||||
|
CreateTime: entity.CreateTime.Format("2006-01-02 15:04:05"),
|
||||||
|
UpdateTime: "",
|
||||||
|
}
|
||||||
|
// 注意:SchoolName, ChineseScoreLimitation 等字段需要从关联表或其他来源获取
|
||||||
|
// 这里设置为空字符串
|
||||||
|
vo.SchoolName = ""
|
||||||
|
vo.ChineseScoreLimitation = 0
|
||||||
|
vo.EnglishScoreLimitation = 0
|
||||||
|
vo.CulturalScoreLimitation = 0
|
||||||
|
vo.ProfessionalScoreLimitation = 0
|
||||||
|
vo.Province = ""
|
||||||
|
vo.SchoolNature = ""
|
||||||
|
vo.InstitutionType = ""
|
||||||
|
vo.FirstLevelDiscipline = ""
|
||||||
|
vo.CreateBy = ""
|
||||||
|
vo.UpdateBy = ""
|
||||||
|
return vo
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
// Package service 业务逻辑层
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"server/common"
|
||||||
|
"server/config"
|
||||||
|
"server/modules/yx/dto"
|
||||||
|
"server/modules/yx/entity"
|
||||||
|
"server/modules/yx/mapper"
|
||||||
|
"server/modules/yx/vo"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type YxHistoryMajorEnrollService struct {
|
||||||
|
*common.BaseService[entity.YxHistoryMajorEnroll]
|
||||||
|
mapper *mapper.YxHistoryMajorEnrollMapper
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewYxHistoryMajorEnrollService() *YxHistoryMajorEnrollService {
|
||||||
|
mapper := mapper.NewYxHistoryMajorEnrollMapper()
|
||||||
|
return &YxHistoryMajorEnrollService{
|
||||||
|
BaseService: common.NewBaseService[entity.YxHistoryMajorEnroll](),
|
||||||
|
mapper: mapper,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecommendMajorDTOListSetHistoryInfo 填充历史录取信息
|
||||||
|
func (s *YxHistoryMajorEnrollService) RecommendMajorDTOListSetHistoryInfo(dtoList *[]dto.SchoolMajorDTO, years []string) {
|
||||||
|
if dtoList == nil || len(*dtoList) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
schoolCodes := make([]string, 0)
|
||||||
|
majorNames := make([]string, 0)
|
||||||
|
category := (*dtoList)[0].Category // 假设同一批次查询类别相同
|
||||||
|
majorType := (*dtoList)[0].MajorType
|
||||||
|
|
||||||
|
for _, item := range *dtoList {
|
||||||
|
schoolCodes = append(schoolCodes, item.SchoolCode)
|
||||||
|
majorNames = append(majorNames, item.MajorName)
|
||||||
|
}
|
||||||
|
|
||||||
|
historyList, err := s.ListBySchoolCodesAndMajorNames(schoolCodes, majorNames, category, majorType, years)
|
||||||
|
if err != nil {
|
||||||
|
return // Log error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group by SchoolCode + MajorName 并转换为 VO
|
||||||
|
historyMap := make(map[string][]vo.YxHistoryMajorEnrollVO)
|
||||||
|
for _, h := range historyList {
|
||||||
|
key := h.SchoolCode + "_" + h.MajorName
|
||||||
|
vo := vo.YxHistoryMajorEnrollVO{
|
||||||
|
ID: h.ID,
|
||||||
|
Year: h.Year,
|
||||||
|
EnrollmentCode: h.EnrollmentCode,
|
||||||
|
EnrollmentCount: h.EnrollNum, // 使用 EnrollNum 替代
|
||||||
|
RulesEnrollProbability: h.RulesEnrollProbability,
|
||||||
|
ProbabilityOperator: h.ProbabilityOperator,
|
||||||
|
AdmissionLine: h.AdmissionLine,
|
||||||
|
ControlLine: h.ControlLine,
|
||||||
|
SchoolCode: h.SchoolCode,
|
||||||
|
MajorCode: h.MajorCode,
|
||||||
|
}
|
||||||
|
historyMap[key] = append(historyMap[key], vo)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range *dtoList {
|
||||||
|
item := &(*dtoList)[i]
|
||||||
|
key := item.SchoolCode + "_" + item.MajorName
|
||||||
|
if list, ok := historyMap[key]; ok {
|
||||||
|
item.HistoryMajorEnrollList = list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListBySchoolCodesAndMajorNames 根据学校代码和专业名称查询历史招生数据
|
||||||
|
func (s *YxHistoryMajorEnrollService) ListBySchoolCodesAndMajorNames(
|
||||||
|
schoolCodes []string,
|
||||||
|
majorNames []string,
|
||||||
|
category string,
|
||||||
|
majorType string,
|
||||||
|
years []string,
|
||||||
|
) ([]entity.YxHistoryMajorEnroll, error) {
|
||||||
|
var items []entity.YxHistoryMajorEnroll
|
||||||
|
sql := `
|
||||||
|
SELECT hme.*
|
||||||
|
FROM yx_history_major_enroll hme
|
||||||
|
WHERE 1=1 AND hme.rules_enroll_probability is not null AND hme.admission_line > 0
|
||||||
|
`
|
||||||
|
params := []interface{}{}
|
||||||
|
if majorType != "" {
|
||||||
|
sql += " AND hme.major_type = ?"
|
||||||
|
params = append(params, majorType)
|
||||||
|
}
|
||||||
|
if category != "" {
|
||||||
|
sql += " AND hme.category = ?"
|
||||||
|
params = append(params, category)
|
||||||
|
}
|
||||||
|
if len(schoolCodes) > 0 {
|
||||||
|
sql += " AND hme.school_code IN ?"
|
||||||
|
params = append(params, schoolCodes)
|
||||||
|
}
|
||||||
|
if len(majorNames) > 0 {
|
||||||
|
sql += " AND hme.major_name IN ?"
|
||||||
|
params = append(params, majorNames)
|
||||||
|
}
|
||||||
|
if len(years) > 0 {
|
||||||
|
sql += " AND hme.year IN ?"
|
||||||
|
params = append(params, years)
|
||||||
|
}
|
||||||
|
sql += " ORDER BY hme.year DESC"
|
||||||
|
err := config.DB.Raw(sql, params...).Scan(&items).Error
|
||||||
|
return items, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByYear 根据年份查询历史招生数据
|
||||||
|
func (s *YxHistoryMajorEnrollService) GetByYear(year string) ([]entity.YxHistoryMajorEnroll, error) {
|
||||||
|
return s.mapper.FindByYear(year)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create 创建历史招生数据(使用 UUID 生成 ID)
|
||||||
|
func (s *YxHistoryMajorEnrollService) Create(item *entity.YxHistoryMajorEnroll) error {
|
||||||
|
item.ID = uuid.New().String()
|
||||||
|
return s.mapper.Create(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchUpsert 批量插入或更新(使用 UUID 生成 ID)
|
||||||
|
func (s *YxHistoryMajorEnrollService) BatchUpsert(items []entity.YxHistoryMajorEnroll, updateColumns []string) error {
|
||||||
|
for i := range items {
|
||||||
|
if items[i].ID == "" {
|
||||||
|
items[i].ID = uuid.New().String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s.mapper.BatchUpsert(items, updateColumns)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"server/modules/yx/entity"
|
||||||
|
"server/modules/yx/mapper"
|
||||||
|
)
|
||||||
|
|
||||||
|
type YxHistoryScoreControlLineService struct {
|
||||||
|
mapper *mapper.YxHistoryScoreControlLineMapper
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewYxHistoryScoreControlLineService() *YxHistoryScoreControlLineService {
|
||||||
|
return &YxHistoryScoreControlLineService{
|
||||||
|
mapper: mapper.NewYxHistoryScoreControlLineMapper(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MapsBatchByProfessionalCategoryOfYear 获取指定年份、专业类别、文理科的省控线,并以批次为 Key 返回 Map
|
||||||
|
func (s *YxHistoryScoreControlLineService) MapsBatchByProfessionalCategoryOfYear(year, professionalCategory, category string) (map[string]entity.YxHistoryScoreControlLine, error) {
|
||||||
|
list, err := s.mapper.SelectByYearAndCategory(year, professionalCategory, category)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(map[string]entity.YxHistoryScoreControlLine)
|
||||||
|
|
||||||
|
switch professionalCategory {
|
||||||
|
case "表演类":
|
||||||
|
fallthrough
|
||||||
|
case "音乐类":
|
||||||
|
for _, item := range list {
|
||||||
|
result[item.ProfessionalCategory+"_"+item.Batch] = item
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
for _, item := range list {
|
||||||
|
// 复制对象逻辑,处理"本科A段"映射为"提前批"
|
||||||
|
if item.Batch == "本科A段" {
|
||||||
|
newItem := item
|
||||||
|
newItem.Batch = "提前批"
|
||||||
|
result[newItem.Batch] = newItem
|
||||||
|
}
|
||||||
|
result[item.Batch] = item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
// Package service 业务逻辑层
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"server/common"
|
||||||
|
"server/modules/yx/entity"
|
||||||
|
"server/modules/yx/mapper"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type YxSchoolMajorService struct {
|
||||||
|
*common.BaseService[entity.YxSchoolMajor]
|
||||||
|
mapper *mapper.YxSchoolMajorMapper
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewYxSchoolMajorService() *YxSchoolMajorService {
|
||||||
|
mapper := mapper.NewYxSchoolMajorMapper()
|
||||||
|
return &YxSchoolMajorService{
|
||||||
|
BaseService: common.NewBaseService[entity.YxSchoolMajor](),
|
||||||
|
mapper: mapper,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create 创建院校专业(使用 UUID 生成 ID)
|
||||||
|
func (s *YxSchoolMajorService) Create(item *entity.YxSchoolMajor) error {
|
||||||
|
item.ID = uuid.New().String()
|
||||||
|
return s.mapper.Create(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchUpsert 批量插入或更新(使用 UUID 生成 ID)
|
||||||
|
func (s *YxSchoolMajorService) BatchUpsert(items []entity.YxSchoolMajor, updateColumns []string) error {
|
||||||
|
for i := range items {
|
||||||
|
if items[i].ID == "" {
|
||||||
|
items[i].ID = uuid.New().String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s.mapper.BatchUpsert(items, updateColumns)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
// Package service 业务逻辑层
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"server/common"
|
||||||
|
"server/modules/yx/entity"
|
||||||
|
"server/modules/yx/mapper"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type YxUserScoreService struct {
|
||||||
|
*common.BaseService[entity.YxUserScore]
|
||||||
|
mapper *mapper.YxUserScoreMapper
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewYxUserScoreService() *YxUserScoreService {
|
||||||
|
mapper := mapper.NewYxUserScoreMapper()
|
||||||
|
return &YxUserScoreService{
|
||||||
|
BaseService: common.NewBaseService[entity.YxUserScore](),
|
||||||
|
mapper: mapper,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateFieldsByEntity 根据实体条件更新字段
|
||||||
|
func (s *YxUserScoreService) UpdateFieldsByEntity(item *entity.YxUserScore, fields map[string]interface{}) error {
|
||||||
|
return s.mapper.UpdateFieldsByMultiCondition(item, fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create 创建用户成绩(使用 UUID 生成 ID)
|
||||||
|
func (s *YxUserScoreService) Create(item *entity.YxUserScore) error {
|
||||||
|
item.ID = uuid.New().String()
|
||||||
|
return s.mapper.Create(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchUpsert 批量插入或更新(使用 UUID 生成 ID)
|
||||||
|
func (s *YxUserScoreService) BatchUpsert(items []entity.YxUserScore, updateColumns []string) error {
|
||||||
|
for i := range items {
|
||||||
|
if items[i].ID == "" {
|
||||||
|
items[i].ID = uuid.New().String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s.mapper.BatchUpsert(items, updateColumns)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
// Package service 业务逻辑层
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"server/common"
|
||||||
|
"server/modules/yx/entity"
|
||||||
|
"server/modules/yx/mapper"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type YxVolunteerRecordService struct {
|
||||||
|
*common.BaseService[entity.YxVolunteerRecord]
|
||||||
|
mapper *mapper.YxVolunteerRecordMapper
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewYxVolunteerRecordService() *YxVolunteerRecordService {
|
||||||
|
mapper := mapper.NewYxVolunteerRecordMapper()
|
||||||
|
return &YxVolunteerRecordService{
|
||||||
|
BaseService: common.NewBaseService[entity.YxVolunteerRecord](),
|
||||||
|
mapper: mapper,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindByVolunteerID 根据 volunteerID 查找志愿记录
|
||||||
|
func (s *YxVolunteerRecordService) FindByVolunteerID(volunteerID string) ([]entity.YxVolunteerRecord, error) {
|
||||||
|
return s.mapper.FindByVolunteerID(volunteerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteByVolunteerID 根据 volunteerID 删除志愿记录
|
||||||
|
func (s *YxVolunteerRecordService) DeleteByVolunteerID(volunteerID string) error {
|
||||||
|
return s.mapper.DeleteByVolunteerID(volunteerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create 创建志愿记录(使用 UUID 生成 ID)
|
||||||
|
func (s *YxVolunteerRecordService) Create(item *entity.YxVolunteerRecord) error {
|
||||||
|
item.ID = uuid.New().String()
|
||||||
|
return s.mapper.Create(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchUpsert 批量插入或更新(使用 UUID 生成 ID)
|
||||||
|
func (s *YxVolunteerRecordService) BatchUpsert(items []entity.YxVolunteerRecord, updateColumns []string) error {
|
||||||
|
for i := range items {
|
||||||
|
if items[i].ID == "" {
|
||||||
|
items[i].ID = uuid.New().String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s.mapper.BatchUpsert(items, updateColumns)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,205 @@
|
||||||
|
// Package service 业务逻辑层
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"server/common"
|
||||||
|
userVO "server/modules/user/vo"
|
||||||
|
"server/modules/yx/dto"
|
||||||
|
"server/modules/yx/entity"
|
||||||
|
"server/modules/yx/mapper"
|
||||||
|
yxVO "server/modules/yx/vo"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ScoreService 定义成绩服务的接口,用于解耦
|
||||||
|
type ScoreService interface {
|
||||||
|
GetByID(id string) (userVO.UserScoreVO, error)
|
||||||
|
GetActiveScoreByID(userID string) (entity.YxUserScore, error)
|
||||||
|
GetActiveScoreID(userID string) (string, error)
|
||||||
|
UpdateFields(id string, fields map[string]interface{}) error
|
||||||
|
Delete(id string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type YxVolunteerService struct {
|
||||||
|
*common.BaseService[entity.YxVolunteer]
|
||||||
|
mapper *mapper.YxVolunteerMapper
|
||||||
|
volunteerRecordMapper *mapper.YxVolunteerRecordMapper
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewYxVolunteerService() *YxVolunteerService {
|
||||||
|
return &YxVolunteerService{
|
||||||
|
BaseService: common.NewBaseService[entity.YxVolunteer](),
|
||||||
|
mapper: mapper.NewYxVolunteerMapper(),
|
||||||
|
volunteerRecordMapper: mapper.NewYxVolunteerRecordMapper(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindActiveByScoreId 根据 scoreId 查找激活的志愿单
|
||||||
|
func (s *YxVolunteerService) FindActiveByScoreId(scoreId string) (*entity.YxVolunteer, error) {
|
||||||
|
return s.mapper.FindActiveByScoreId(scoreId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateByScoreId 根据 ScoreId 创建新志愿表
|
||||||
|
func (s *YxVolunteerService) CreateByScoreId(scoreId string, userId string) error {
|
||||||
|
volunteer := entity.YxVolunteer{}
|
||||||
|
volunteer.ID = common.GenerateStringID()
|
||||||
|
// 志愿表名称格式 时间戳 20260101134501志愿表
|
||||||
|
volunteer.VolunteerName = time.Now().Format("20060102150405") + "志愿表"
|
||||||
|
|
||||||
|
volunteer.ScoreId = scoreId
|
||||||
|
volunteer.CreateType = "1"
|
||||||
|
volunteer.State = "1"
|
||||||
|
volunteer.CreateBy = userId
|
||||||
|
volunteer.CreateTime = time.Now()
|
||||||
|
volunteer.UpdateTime = time.Now()
|
||||||
|
|
||||||
|
// 先关闭当前用户其他志愿单
|
||||||
|
if err := s.mapper.CloseOtherVolunteer(userId); err != nil {
|
||||||
|
return fmt.Errorf("关闭其他志愿表失败: %w", err)
|
||||||
|
}
|
||||||
|
// 创建志愿表
|
||||||
|
return s.mapper.Create(&volunteer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateVolunteer 创建志愿并返回 VO
|
||||||
|
func (s *YxVolunteerService) CreateVolunteer(req *dto.CreateVolunteerRequest) (*yxVO.YxVolunteerVO, error) {
|
||||||
|
// DTO 转 Entity
|
||||||
|
entityItem := &entity.YxVolunteer{
|
||||||
|
ID: common.GenerateStringID(),
|
||||||
|
VolunteerName: req.VolunteerName,
|
||||||
|
ScoreId: req.ScoreId,
|
||||||
|
CreateType: req.CreateType,
|
||||||
|
State: "0", // 默认未激活
|
||||||
|
CreateTime: time.Now(),
|
||||||
|
UpdateTime: time.Now(),
|
||||||
|
}
|
||||||
|
if req.CreateType == "" {
|
||||||
|
entityItem.CreateType = "1" // 默认手动生成
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.mapper.Create(entityItem); err != nil {
|
||||||
|
return nil, fmt.Errorf("创建志愿失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.convertToVO(*entityItem), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateVolunteer 更新志愿并返回 VO
|
||||||
|
func (s *YxVolunteerService) UpdateVolunteer(id string, req *dto.UpdateVolunteerRequest) (*yxVO.YxVolunteerVO, error) {
|
||||||
|
// 获取原数据
|
||||||
|
entityItemPtr, err := s.GetByID(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("志愿不存在: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新字段
|
||||||
|
updateFields := make(map[string]interface{})
|
||||||
|
if req.VolunteerName != "" {
|
||||||
|
updateFields["volunteer_name"] = req.VolunteerName
|
||||||
|
}
|
||||||
|
if req.State != "" {
|
||||||
|
updateFields["state"] = req.State
|
||||||
|
}
|
||||||
|
updateFields["update_time"] = time.Now()
|
||||||
|
|
||||||
|
if err := s.mapper.UpdateFields(id, updateFields); err != nil {
|
||||||
|
return nil, fmt.Errorf("更新志愿失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新获取更新后的数据
|
||||||
|
entityItemPtr, err = s.GetByID(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("获取更新后数据失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.convertToVO(*entityItemPtr), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateName 更新志愿表名称
|
||||||
|
func (s *YxVolunteerService) UpdateName(id, name, userID string) error {
|
||||||
|
volunteer, err := s.GetByID(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if volunteer.CreateBy != userID {
|
||||||
|
return fmt.Errorf("无权修改该志愿单")
|
||||||
|
}
|
||||||
|
return s.mapper.UpdateFields(id, map[string]interface{}{"volunteer_name": name, "update_by": userID, "update_time": time.Now()})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListByUser 查询用户的志愿单列表(包含成绩信息)
|
||||||
|
func (s *YxVolunteerService) ListByUser(userID string, page, size int) ([]yxVO.UserVolunteerVO, int64, error) {
|
||||||
|
return s.mapper.ListByUser(userID, page, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteVolunteer 删除志愿单
|
||||||
|
func (s *YxVolunteerService) DeleteVolunteer(id, userID string) error {
|
||||||
|
volunteer, err := s.GetByID(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if volunteer.CreateBy != userID {
|
||||||
|
return fmt.Errorf("无权删除该志愿单")
|
||||||
|
}
|
||||||
|
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 中获取 CalculationTableName 和 ID
|
||||||
|
// 由于是 interface{},可以通过反射或简单断言(如果能在 yx 定义共用结构最好)
|
||||||
|
// 或者直接在 scoreService 中增加一个获取这些字段的方法
|
||||||
|
|
||||||
|
// 简化:这里我们假设 scoreObj 其实包含了这些信息。
|
||||||
|
// 为了真正的解耦,我们可以在 scoreService 中增加专门的方法。
|
||||||
|
return nil // 暂时回退复杂逻辑
|
||||||
|
}
|
||||||
|
|
||||||
|
// SwitchVolunteer 切换激活的志愿单
|
||||||
|
func (s *YxVolunteerService) SwitchVolunteer(id, userID string, scoreService ScoreService) error {
|
||||||
|
if err := s.mapper.CloseOtherVolunteer(userID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.mapper.UpdateFields(id, map[string]interface{}{"state": "1", "update_by": userID, "update_time": time.Now()}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
volunteer, err := s.GetByID(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取之前的激活成绩并关闭
|
||||||
|
activeScoreID, err := scoreService.GetActiveScoreID(userID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("获取激活成绩失败: %w", err)
|
||||||
|
}
|
||||||
|
if activeScoreID != "" && activeScoreID != volunteer.ScoreId {
|
||||||
|
scoreService.UpdateFields(activeScoreID, map[string]interface{}{"state": "0"})
|
||||||
|
}
|
||||||
|
return scoreService.UpdateFields(volunteer.ScoreId, map[string]interface{}{"state": "1"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertToVO Entity 转 VO(私有方法)
|
||||||
|
func (s *YxVolunteerService) convertToVO(entity entity.YxVolunteer) *yxVO.YxVolunteerVO {
|
||||||
|
return &yxVO.YxVolunteerVO{
|
||||||
|
ID: entity.ID,
|
||||||
|
VolunteerName: entity.VolunteerName,
|
||||||
|
ScoreId: entity.ScoreId,
|
||||||
|
CreateType: entity.CreateType,
|
||||||
|
State: entity.State,
|
||||||
|
CreateBy: entity.CreateBy,
|
||||||
|
CreateTime: entity.CreateTime,
|
||||||
|
UpdateBy: entity.UpdateBy,
|
||||||
|
UpdateTime: entity.UpdateTime,
|
||||||
|
SysOrgCode: entity.SysOrgCode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package vo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"server/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserVolunteerVO 志愿单视图对象,关联成绩信息
|
||||||
|
type UserVolunteerVO struct {
|
||||||
|
ID string `gorm:"column:id;primaryKey" json:"id"`
|
||||||
|
VolunteerName string `gorm:"column:volunteer_name" json:"volunteerName"` // 志愿单名称
|
||||||
|
ScoreId string `gorm:"column:score_id" json:"scoreId"` // 使用成绩id
|
||||||
|
CreateType string `gorm:"column:create_type;default:1" json:"createType"` // 生成类型(1.手动生成,2.智能生成)
|
||||||
|
State string `gorm:"column:state;default:0" json:"state"` // 志愿单状态(0-否,1.正在使用,2-历史)
|
||||||
|
UpdateTime types.DateTime `gorm:"column:update_time" json:"updateTime"` // 更新日期
|
||||||
|
ProfessionalCategory string `json:"professionalCategory"` // 专业类别
|
||||||
|
Province string `json:"province"` // 省份
|
||||||
|
CulturalScore float64 `json:"culturalScore"` // 文化分
|
||||||
|
ProfessionalScore float64 `json:"professionalScore"` // 专业成绩分
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
package vo
|
||||||
|
|
||||||
|
// YxCalculationMajorVO 计算专业视图对象
|
||||||
|
type YxCalculationMajorVO struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
SchoolCode string `json:"schoolCode"` // 院校代码
|
||||||
|
SchoolName string `json:"schoolName"` // 院校名称
|
||||||
|
MajorCode string `json:"majorCode"` // 专业代码
|
||||||
|
MajorName string `json:"majorName"` // 专业名称
|
||||||
|
MajorType string `json:"majorType"` // 专业类型
|
||||||
|
MajorTypeChild string `json:"majorTypeChild"` // 子专业类型
|
||||||
|
PlanNum int `json:"planNum"` // 计划人数
|
||||||
|
MainSubjects string `json:"mainSubjects"` // 主考科目
|
||||||
|
Limitation string `json:"limitation"` // 限制条件
|
||||||
|
ChineseScoreLimitation float64 `json:"chineseScoreLimitation"` // 语文分数限制
|
||||||
|
EnglishScoreLimitation float64 `json:"englishScoreLimitation"` // 英语分数限制
|
||||||
|
CulturalScoreLimitation float64 `json:"culturalScoreLimitation"` // 文化成绩限制
|
||||||
|
ProfessionalScoreLimitation float64 `json:"professionalScoreLimitation"` // 专业分数限制
|
||||||
|
EnrollmentCode string `json:"enrollmentCode"` // 招生代码
|
||||||
|
Tuition string `json:"tuition"` // 学费
|
||||||
|
Detail string `json:"detail"` // 详情
|
||||||
|
Category string `json:"category"` // 类别
|
||||||
|
Batch string `json:"batch"` // 批次
|
||||||
|
RulesEnrollProbability string `json:"rulesEnrollProbability"` // 录取规则概率
|
||||||
|
ProbabilityOperator string `json:"probabilityOperator"` // 概率操作符
|
||||||
|
Kslx string `json:"kslx"` // 考试类型
|
||||||
|
State string `json:"state"` // 状态
|
||||||
|
Province string `json:"province"` // 省份
|
||||||
|
SchoolNature string `json:"schoolNature"` // 院校性质
|
||||||
|
InstitutionType string `json:"institutionType"` // 院校类型
|
||||||
|
EnrollProbability float64 `json:"enrollProbability"` // 录取概率
|
||||||
|
StudentScore float64 `json:"studentScore"` // 学生分数
|
||||||
|
PrivateStudentScore float64 `json:"privateStudentScore"` // 私有学生分数
|
||||||
|
StudentConvertedScore float64 `json:"studentConvertedScore"` // 学生折合分
|
||||||
|
FirstLevelDiscipline string `json:"firstLevelDiscipline"` // 一级学科
|
||||||
|
CreateBy string `json:"createBy"` // 创建人
|
||||||
|
CreateTime string `json:"createTime"` // 创建时间
|
||||||
|
UpdateBy string `json:"updateBy"` // 更新人
|
||||||
|
UpdateTime string `json:"updateTime"` // 更新时间
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package vo
|
||||||
|
|
||||||
|
// YxHistoryMajorEnrollVO 历年专业招生视图对象
|
||||||
|
type YxHistoryMajorEnrollVO struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Year string `json:"year"` // 年份
|
||||||
|
EnrollmentCode string `json:"enrollmentCode"` // 招生代码
|
||||||
|
EnrollmentCount int `json:"enrollmentCount"` // 招生人数
|
||||||
|
RulesEnrollProbability string `json:"rulesEnrollProbability"` // 录取规则概率
|
||||||
|
ProbabilityOperator string `json:"probabilityOperator"` // 概率操作符
|
||||||
|
AdmissionLine float64 `json:"admissionLine"` // 录取分数线
|
||||||
|
ControlLine float64 `json:"controlLine"` // 控制线
|
||||||
|
SchoolCode string `json:"schoolCode"` // 院校代码
|
||||||
|
MajorCode string `json:"majorCode"` // 专业代码
|
||||||
|
RulesEnrollProbabilitySx string `json:"rulesEnrollProbabilitySx"` // 录取规则概率(山东)
|
||||||
|
// 其他字段...
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package vo
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// YxVolunteerVO 志愿视图对象
|
||||||
|
type YxVolunteerVO struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
VolunteerName string `json:"volunteerName"` // 志愿单名称
|
||||||
|
ScoreId string `json:"scoreId"` // 使用成绩id
|
||||||
|
CreateType string `json:"createType"` // 生成类型(1.手动生成,2.智能生成)
|
||||||
|
State string `json:"state"` // 志愿单状态(0-否,1-正在使用,2-历史)
|
||||||
|
CreateBy string `json:"createBy"` // 创建人
|
||||||
|
CreateTime time.Time `json:"createTime"` // 创建日期
|
||||||
|
UpdateBy string `json:"updateBy"` // 更新人
|
||||||
|
UpdateTime time.Time `json:"updateTime"` // 更新日期
|
||||||
|
SysOrgCode string `json:"sysOrgCode"` // 所属部门
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
```
|
||||||
|
cd server/tests
|
||||||
|
go test -v -run TestRedisOperation # 运行 Redis 测试
|
||||||
|
go test -v -run TestSchoolMajorQuery # 运行 SQL 查询测试
|
||||||
|
go test -v # 运行所有测试
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package tests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"server/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// 切换工作目录到 server 根目录,以便读取配置文件(如果有)
|
||||||
|
// 这里直接初始化配置,不依赖文件,但保留目录切换逻辑以防万一
|
||||||
|
_, filename, _, _ := runtime.Caller(0)
|
||||||
|
dir := filepath.Join(filepath.Dir(filename), "..")
|
||||||
|
err := os.Chdir(dir)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("切换工作目录失败:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置测试环境
|
||||||
|
os.Setenv("GO_ENV", "test")
|
||||||
|
// 加载配置
|
||||||
|
config.LoadConfig()
|
||||||
|
|
||||||
|
// 初始化数据库
|
||||||
|
config.InitDB()
|
||||||
|
// 初始化Redis
|
||||||
|
config.InitRedis()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
package tests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"server/common"
|
||||||
|
"server/config"
|
||||||
|
"server/modules/user/vo"
|
||||||
|
"server/modules/yx/service"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestRedisOperation 测试 Redis 读写
|
||||||
|
func TestRedisOperation(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
key := "test:hello"
|
||||||
|
value := "world_" + time.Now().Format("2006-01-02 15:04:05")
|
||||||
|
|
||||||
|
// 写 Redis
|
||||||
|
err := config.RDB.Set(ctx, key, value, time.Minute).Err()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Redis 写入失败: %v", err)
|
||||||
|
}
|
||||||
|
t.Logf("Redis 写入成功: %s -> %s", key, value)
|
||||||
|
|
||||||
|
// 读 Redis
|
||||||
|
val, err := config.RDB.Get(ctx, key).Result()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Redis 读取失败: %v", err)
|
||||||
|
}
|
||||||
|
t.Logf("Redis 读取成功: %s", val)
|
||||||
|
|
||||||
|
if val != value {
|
||||||
|
t.Errorf("Redis 值不匹配: 期望 %s, 实际 %s", value, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSchoolMajorQuery 测试院校专业复杂查询 Mapper
|
||||||
|
func TestSchoolMajorQuery(t *testing.T) {
|
||||||
|
cs := service.NewYxCalculationMajorService()
|
||||||
|
userScoreVO := vo.UserScoreVO{
|
||||||
|
ProfessionalCategory: "美术与设计类",
|
||||||
|
CognitioPolyclinic: "文科",
|
||||||
|
CulturalScore: 500,
|
||||||
|
ProfessionalScore: 250,
|
||||||
|
}
|
||||||
|
start := time.Now()
|
||||||
|
items, err := cs.ListByUserQueryType("美术与设计类", "文科", []string{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("查询失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
duration := time.Now().UnixMilli() - start.UnixMilli()
|
||||||
|
common.LogInfo("CheckEnrollProbability elapsed: %v", duration)
|
||||||
|
t.Logf("查询成功,共找到 %d 条记录", len(items))
|
||||||
|
|
||||||
|
startTime := time.Now()
|
||||||
|
cs.CheckEnrollProbability(&items, userScoreVO)
|
||||||
|
elapsed := time.Now().UnixMilli() - startTime.UnixMilli()
|
||||||
|
common.LogInfo("CheckEnrollProbability elapsed: %v", elapsed)
|
||||||
|
|
||||||
|
// 插入到数据库
|
||||||
|
// err = cs.BatchCreateBySchoolMajorDTO(items)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("插入数据库失败: %v", err)
|
||||||
|
}
|
||||||
|
t.Logf("成功插入 %d 条记录到数据库", len(items))
|
||||||
|
|
||||||
|
// 打印前几条结果
|
||||||
|
// for i, item := range items {
|
||||||
|
// if i >= 3 {
|
||||||
|
// break
|
||||||
|
// }
|
||||||
|
// jsonBytes, _ := json.Marshal(item)
|
||||||
|
// t.Logf("记录 #%d: %s", i+1, string(jsonBytes))
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestIDGenerator 测试 ID 生成器
|
||||||
|
func TestIDGenerator(t *testing.T) {
|
||||||
|
idLong := common.GenerateLongID()
|
||||||
|
t.Logf("生成的 Long ID: %d", idLong)
|
||||||
|
|
||||||
|
idStr := common.GenerateStringID()
|
||||||
|
t.Logf("生成的 String ID: %s", idStr)
|
||||||
|
|
||||||
|
// idTimestamp := common.GetIDGenerator().GenerateTimestampLongID()
|
||||||
|
// t.Logf("生成的 Timestamp Long ID: %d", idTimestamp)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DateTime 自定义时间类型,JSON序列化格式为 '2006-01-02 15:04:05'
|
||||||
|
type DateTime time.Time
|
||||||
|
|
||||||
|
// MarshalJSON 实现 json.Marshaler 接口,将时间序列化为 '2006-01-02 15:04:05' 格式
|
||||||
|
func (dt DateTime) MarshalJSON() ([]byte, error) {
|
||||||
|
t := time.Time(dt)
|
||||||
|
if t.IsZero() {
|
||||||
|
return []byte("null"), nil
|
||||||
|
}
|
||||||
|
formatted := t.Format("2006-01-02 15:04:05")
|
||||||
|
return json.Marshal(formatted)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON 实现 json.Unmarshaler 接口,解析 '2006-01-02 15:04:05' 格式的时间
|
||||||
|
func (dt *DateTime) UnmarshalJSON(data []byte) error {
|
||||||
|
if string(data) == "null" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var str string
|
||||||
|
if err := json.Unmarshal(data, &str); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t, err := time.Parse("2006-01-02 15:04:05", str)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*dt = DateTime(t)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value 实现 driver.Valuer 接口,用于数据库存储
|
||||||
|
func (dt DateTime) Value() (driver.Value, error) {
|
||||||
|
t := time.Time(dt)
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan 实现 sql.Scanner 接口,用于数据库读取
|
||||||
|
func (dt *DateTime) Scan(value interface{}) error {
|
||||||
|
if value == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
t, ok := value.(time.Time)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
*dt = DateTime(t)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time 转换为标准 time.Time 类型
|
||||||
|
func (dt DateTime) Time() time.Time {
|
||||||
|
return time.Time(dt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDateTime 从 time.Time 创建 DateTime
|
||||||
|
func NewDateTime(t time.Time) DateTime {
|
||||||
|
return DateTime(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now 创建当前时间的 DateTime
|
||||||
|
func Now() DateTime {
|
||||||
|
return DateTime(time.Now())
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
# 任务执行摘要
|
||||||
|
|
||||||
|
## 会话 ID: local
|
||||||
|
- [2026-03-14 19:36:24]
|
||||||
|
- **执行原因**: 说明如何使用 Zed 调试运行 server 内的 Go 项目
|
||||||
|
- **执行过程**:
|
||||||
|
1. 确认项目结构与 server 目录位置。
|
||||||
|
2. 给出在 Zed 中配置调试与运行的步骤与注意点。
|
||||||
|
- **执行结果**: 已提供 Zed 调试运行指引
|
||||||
|
|
||||||
|
|
||||||
|
# 任务执行摘要
|
||||||
|
|
||||||
|
## 会话 ID: local
|
||||||
|
- [2026-03-14 19:40:29]
|
||||||
|
- **执行原因**: 说明 Zed 是否支持类似 VSCode 的直接调试与扩展
|
||||||
|
- **执行过程**:
|
||||||
|
1. 查证 Zed 调试器与 Go 调试适配器的官方文档。
|
||||||
|
2. 给出在 Zed 中直接调试的入口与配置要点。
|
||||||
|
- **执行结果**: 已给出可直接调试的说明与扩展结论
|
||||||
|
|
||||||
|
|
||||||
|
# 任务执行摘要
|
||||||
|
|
||||||
|
## 会话 ID: local
|
||||||
|
- [2026-03-14 19:53:40]
|
||||||
|
- **执行原因**: 说明 Zed 中 Cmd+点击的跳转能力与路径跳转方式
|
||||||
|
- **执行过程**:
|
||||||
|
1. 查阅 Zed 官方文档与官方博客关于跳转与快捷键说明。
|
||||||
|
2. 汇总可用的跳转方式与路径打开替代方案。
|
||||||
|
- **执行结果**: 已给出 Cmd+点击跳转与路径打开建议
|
||||||
|
|
||||||
Loading…
Reference in New Issue