diff --git a/.gitignore b/.gitignore index adf8f72..4b3d3dc 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ *.so *.dylib +*__debug_bin** # Test binary, built with `go test -c` *.test @@ -21,3 +22,171 @@ # Go workspace file go.work +# ---> Java +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +# ---> Vue +# gitignore template for Vue.js projects +# +# Recommended template: Node.gitignore + +# TODO: where does this rule come from? +docs/_book + +# TODO: where does this rule come from? +test/ + +# ---> Node +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..f38bf33 --- /dev/null +++ b/AGENTS.md @@ -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) +- 生产环境务必开启安全校验 +- 需要配置合适的限流规则防止恶意请求 +- 定期清理日志文件避免磁盘空间不足 \ No newline at end of file diff --git a/Help.md b/Help.md new file mode 100644 index 0000000..af6da5c --- /dev/null +++ b/Help.md @@ -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"}) +``` \ No newline at end of file diff --git a/IFLOW.md b/IFLOW.md new file mode 100644 index 0000000..ea88afd --- /dev/null +++ b/IFLOW.md @@ -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 ` | +| 文件删除 | `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`(修复记录) \ No newline at end of file diff --git a/README.md b/README.md index 7e55529..e46710d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,184 @@ -# wz-golang-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 # 新的一天 +``` diff --git a/docs/AppConfigVersionPlan.md b/docs/AppConfigVersionPlan.md new file mode 100644 index 0000000..99a87eb --- /dev/null +++ b/docs/AppConfigVersionPlan.md @@ -0,0 +1,95 @@ +# 小程序接口与 WebView 版本一致性方案 + +## 目标 +- 小程序获取接口地址与 WebView 地址。 +- 判断版本一致性与可用性。 +- 不可用时提示用户退出小程序重新打开。 + +## 总体思路 +采用“配置中心 + 启动校验 + 缓存兜底 + 失效提示”的方案: +1. 小程序启动时请求配置中心接口获取最新配置。 +2. 校验小程序版本、API版本、WebView版本一致性。 +3. 校验失败则提示退出重启;通过则写入缓存并使用配置。 +4. 配置接口不可用时使用缓存,缓存失效则提示退出重启。 + +## 配置中心接口 +- 方法: `GET` +- 路径: `/api/open/app/config` + +### 返回示例 +```json +{ + "app": { + "minVersion": "1.2.0", + "latestVersion": "1.3.5", + "forceUpdate": true + }, + "api": { + "baseUrl": "https://api.xxx.com", + "version": "2026-03-16", + "minClientVersion": "1.2.0" + }, + "webview": { + "baseUrl": "https://m.xxx.com", + "version": "2026-03-16", + "minClientVersion": "1.2.0" + }, + "ttlSeconds": 3600, + "disabled": false, + "disableReason": "" +} +``` + +### 字段说明 +- `app.minVersion`: 最低可用小程序版本。 +- `app.latestVersion`: 最新版本号。 +- `app.forceUpdate`: 是否强制更新。 +- `api.baseUrl`: API 入口地址。 +- `api.version`: API 版本号(用于一致性校验)。 +- `api.minClientVersion`: API 允许的最小客户端版本。 +- `webview.baseUrl`: WebView 入口地址。 +- `webview.version`: WebView 版本号(用于一致性校验)。 +- `webview.minClientVersion`: WebView 允许的最小客户端版本。 +- `ttlSeconds`: 配置缓存有效期(秒)。 +- `disabled`: 服务下线开关。 +- `disableReason`: 下线原因。 + +## 小程序启动校验流程 +1. 小程序 `App.onLaunch` 请求 `/api/open/app/config`。 +2. 校验顺序: + - `disabled == true` → 提示“服务暂不可用,请退出小程序重新打开”。 + - `当前版本 < app.minVersion` → 提示强更并阻断。 + - `api.version != webview.version` → 提示“版本不一致,请退出小程序重新打开”。 + - `当前版本 < api.minClientVersion` 或 `当前版本 < webview.minClientVersion` → 提示强更。 +3. 校验通过: + - 缓存配置(本地 `storage`)。 + - 全局设置 API baseUrl 与 WebView baseUrl。 + +## 缓存与兜底策略 +- 配置接口失败: + - 如果本地缓存存在且未过期 → 使用缓存。 + - 若缓存过期或不存在 → 提示“配置获取失败,请退出小程序重新打开”。 + +## 提示文案建议 +- 版本不一致: + - “当前版本不一致,请退出小程序并重新打开以获取最新配置。” +- 服务不可用: + - “服务暂不可用,请退出小程序重新打开。” +- 配置获取失败: + - “配置获取失败,请退出小程序重新打开。” + +## WebView 入口处理 +- WebView URL 必须由配置中心下发,避免在客户端写死。 +- 进入 WebView 前再次校验缓存是否过期;过期则重新拉取。 + +## 前端需要完成的部分 +1. 在 `App.onLaunch` 拉取 `/api/open/app/config`,并写入本地缓存。 +2. 实现版本校验逻辑(`app.minVersion`、`api.version`、`webview.version`、`minClientVersion`)。 +3. 校验失败时弹窗提示,并引导退出小程序重新打开。 +4. 缓存过期或接口失败时,优先使用缓存;无缓存则提示退出。 +5. WebView 入口统一从配置下发的 `webview.baseUrl` 拼接跳转。 +6. 请求 API 时统一使用配置下发的 `api.baseUrl`。 + +## 可选扩展 +- 灰度策略:按用户或设备下发不同配置。 +- 公告字段:返回 `notice` 用于维护提示。 diff --git a/docs/Task1.md b/docs/Task1.md new file mode 100644 index 0000000..a729f40 --- /dev/null +++ b/docs/Task1.md @@ -0,0 +1,74 @@ +### 优化后的精准提示词 + +你需要为基于Golang开发的系统完成PostgreSQL适配改造,核心目标是实现MySQL/PostgreSQL双数据源可配置切换,并完成指定数据表的全链路代码开发,具体要求如下: + +#### 一、核心改造要求 + +1. 数据源适配:支持通过config.yaml配置文件指定主数据源类型(mysql/postgresql),实现系统层面的双数据源无缝切换,需保证SQL语法、数据类型、函数等的兼容性(如自增主键、时间函数、JSON类型等)。 +2. 目录规范:在项目根目录的docs/下新建sql文件夹,分别创建mysql/和postgresql/子文件夹,将t_user、t_platform_user两张表的建表语句按数据源类型分类存放,文件名统一为t_user.sql、t_platform_user.sql。 +3. 建表语句要求: + - PostgreSQL版本:基于提供的建表语句完善,保证COMMENT(字段+表)完整,数据类型符合PostgreSQL规范(如BIGSERIAL、JSONB、TIMESTAMP等),保留联合唯一索引、外键约束; + - MySQL版本:补充适配版建表语句(如自增主键用BIGINT AUTO_INCREMENT、JSON类型用JSON、时间函数适配),同样保留完整COMMENT和约束; + - 所有建表语句需包含软删除、创建/更新时间的默认值逻辑。 + +#### 二、代码开发要求 + +针对t_user、t_platform_user两张表,补充PostgreSQL适配后的全链路代码,需保证代码兼容双数据源,结构如下: + +1. Entity层:定义与数据表字段一一对应的结构体,适配PostgreSQL/MySQL的数据类型差异(如JSONB对应Golang的map[string]interface{}或自定义结构体),添加字段注释(tag包含comment); +2. Mapper层(数据访问层):实现两张表的CRUD操作,使用兼容双数据源的SQL语法(避免数据库专属函数),支持通过配置切换数据源; +3. Service层:封装业务逻辑,调用Mapper层完成数据操作,对外提供统一的业务接口; +4. Controller层:暴露HTTP接口(如用户信息新增/查询/修改/删除),接收请求参数并调用Service层,返回标准化响应结果。 + +#### 三、兼容性要求 + +1. 所有代码需通过接口或配置隔离数据库专属逻辑,避免硬编码数据源类型; +2. 重点兼容项:自增主键生成、时间字段的插入/更新逻辑、JSON类型的序列化/反序列化、唯一索引/外键约束的实现方式; +3. 保证切换数据源后,系统功能无异常,数据读写正常。 + +#### 附:参考建表语句(PostgreSQL版) + +```sql +-- t_user 建表语句(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 '用户基础信息表'; + +-- t_platform_user 建表语句(PostgreSQL) +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 '平台用户关联表(微信/抖音小程序用户信息)'; +``` + +--- + +### 优化关键点总结 + +1. **结构化拆分**:将需求拆分为数据源适配、目录规范、建表语句、代码开发、兼容性5个明确模块,避免模糊表述; +2. **细节补全**:补充了MySQL建表语句要求、代码层的具体实现规范(如Entity的tag注释、Mapper的CRUD要求),明确兼容性重点; +3. **精准性提升**:明确了文件命名、目录层级、数据类型适配等细节,避免执行过程中的歧义,同时保留了原有的核心要求和参考建表语句。 diff --git a/docs/UserPasswordLogin.md b/docs/UserPasswordLogin.md new file mode 100644 index 0000000..8c49235 --- /dev/null +++ b/docs/UserPasswordLogin.md @@ -0,0 +1,70 @@ +# 手机号+密码登录接口文档 + +## 概述 +用于移动端/前端通过手机号和密码登录,成功后返回 `token` 与用户信息,后续请求携带 `Authorization: Bearer `。 + +## 基础信息 +- 方法: `POST` +- 路径: `/api/open/user/login` +- Content-Type: `application/json` + +## 请求头 +- `Content-Type: application/json` +- `Authorization`: 不需要(已加入登录白名单) +- 安全校验(当 `security.enable: true` 时必须): + - `X-App-Timestamp`: 毫秒时间戳 + - `X-App-Sign`: MD5(`timestamp` + `secret_key`) + +## 请求参数 +| 字段 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| phone | string | 是 | 手机号 | +| password | string | 是 | 密码 | + +### 请求示例 +```json +{ + "phone": "13800000000", + "password": "your_password" +} +``` + +## 响应参数 +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| token | string | 登录令牌 | +| user | object | 登录用户信息(基础字段) | + +### 成功响应示例 +```json +{ + "code": 200, + "message": "success", + "data": { + "token": "c6f7f1e4-5a3b-4f4e-9d0b-6b3f7b8c5e3a", + "user": { + "id": "10001", + "username": "13800000000", + "realname": "张三", + "avatar": "https://oss-xxx/avatar.png", + "phone": "13800000000", + "email": "", + "token": "c6f7f1e4-5a3b-4f4e-9d0b-6b3f7b8c5e3a" + } + } +} +``` + +## 错误响应示例 +```json +{ + "code": 401, + "message": "手机号或密码错误", + "data": null +} +``` + +## 备注 +- 需要在 `t_user` 中预先设置 `password` 与 `salt`。 +- 密码加密方式与系统一致:`common.Encrypt(phone, rawPassword, salt)`。 +- token 默认 24 小时过期(Redis)。 diff --git a/docs/UserProfile.md b/docs/UserProfile.md new file mode 100644 index 0000000..0b0aaa9 --- /dev/null +++ b/docs/UserProfile.md @@ -0,0 +1,85 @@ +# 登录后获取用户信息接口文档 + +## 概述 + +登录成功后,前端携带 `token` 调用该接口获取用户基础信息与平台扩展信息(头像、昵称、性别、地区等)。 + +## 基础信息 + +- 方法: `GET` +- 路径: `/api/user/profile` +- Content-Type: `application/json` + +## 请求头 + +- `Authorization: Bearer ` + +## 请求参数(Query) + +| 字段 | 类型 | 必填 | 说明 | +| ------------ | ---- | ---- | -------------------------------- | +| platformType | int | 否 | 平台类型,默认 `1`(微信小程序) | + +### 请求示例 + +``` +GET /api/user/profile?platformType=1 +Authorization: Bearer +``` + +## 响应参数 + +| 字段 | 类型 | 说明 | +| --------------- | ------ | -------------------------------------------------------- | +| userId | int64 | 用户ID(t_user.id) | +| username | string | 用户名 | +| nickname | string | 昵称 | +| avatarUrl | string | 头像URL | +| phone | string | 手机号 | +| gender | int | 性别:0-未知,1-男,2-女 | +| region | string | 地区(优先 `platform_extra.region`,否则拼接国家/省/市) | +| platformType | int | 平台类型 | +| platformOpenid | string | 平台 openid | +| platformUnionid | string | 平台 unionid | +| platformExtra | object | 平台扩展字段(原样返回) | + +### 成功响应示例 + +```json +{ + "code": 200, + "message": "success", + "data": { + "userId": 10001, + "username": "wx_20260315121500_ab12cd34", + "nickname": "张三", + "avatarUrl": "https://oss-xxx/avatar.png", + "phone": "13800000000", + "gender": 1, + "region": "中国 广东 深圳", + "platformType": 1, + "platformOpenid": "oL1oU5gS6Q...", + "platformUnionid": "o6_bmasdasds...", + "platformExtra": { + "country": "中国", + "province": "广东", + "city": "深圳" + } + } +} +``` + +## 错误响应示例 + +```json +{ + "code": 401, + "message": "未登录", + "data": null +} +``` + +## 备注 + +- 需要先登录并获取 `token`。 +- `platformExtra` 为空时,`region` 可能为空。 diff --git a/docs/WeChatMiniLogin.md b/docs/WeChatMiniLogin.md new file mode 100644 index 0000000..3117489 --- /dev/null +++ b/docs/WeChatMiniLogin.md @@ -0,0 +1,111 @@ +# 微信小程序登录接口文档 + +## 概述 +- 接口用于微信小程序登录,前端通过 `code` 换取 `openid/session_key`,后端自动创建或更新 `t_user` 与 `t_platform_user`。 +- 成功后返回用户ID与平台用户ID,供前端后续业务使用。 + +## 基础信息 +- 方法: `POST` +- 路径: `/api/open/wechat/mini/login` +- Content-Type: `application/json` + +## 请求头 +- `Content-Type: application/json` +- `Authorization`: 不需要(已加入登录白名单) +- 安全校验(当 `security.enable: true` 时必须): + - `X-App-Timestamp`: 毫秒时间戳 + - `X-App-Sign`: MD5(`timestamp` + `secret_key`) + +## 请求参数 +| 字段 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| code | string | 是 | 微信登录 `wx.login` 返回的 `code` | +| phoneCode | string | 否 | 快速手机号登录返回的 `phoneCode`(优先使用) | +| encryptedData | string | 否 | `getPhoneNumber` 返回的加密数据 | +| iv | string | 否 | `getPhoneNumber` 返回的解密向量 | +| nickname | string | 否 | 用户昵称 | +| avatarUrl | string | 否 | 用户头像URL | +| phone | string | 否 | 手机号(不传或空字符串将写入为 NULL) | +| gender | int | 否 | 性别:0-未知,1-男,2-女 | +| platformExtra | object | 否 | 平台扩展字段(如城市、语言等) | + +### 请求示例 +```json +{ + "code": "wx_login_code", + "phoneCode": "8064f569d035bf3a9b4c7fc223bbbc589bf442c0557e0ae51039031d883668f5", + "encryptedData": "EZyKBdrHgQoAjgMDPNGXRGOjsPrB8LHcupwFCztA3IBNvbdkrSsk6iU6FqQsrn5TfpMJeTzHJ2l7lg6e+EBqqDXVgVEgQxkTWlxBS6mwUN4NgRI2FanA0wPFAWMZGmn7jKgEJhu8xKGSihcI111Y/mq+3T6gDzjZvkz32MhngL5/wBEjDQbBdBXfvY6FveyV7CinM0j17wE7pbjNIFrExw==", + "iv": "DMzzMUAx5ke1S8nFItBHSg==", + "nickname": "张三", + "avatarUrl": "https://example.com/avatar.png", + "phone": "13800000000", + "gender": 1, + "platformExtra": { + "city": "Shenzhen", + "language": "zh_CN" + } +} +``` + +## 响应参数 +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| userId | int64 | 用户ID(t_user.id) | +| platformUserId | int64 | 平台用户ID(t_platform_user.id) | +| openid | string | 微信 openid | +| unionid | string | 微信 unionid(可能为空) | +| sessionKey | string | 微信 session_key | +| phone | string | 手机号(如成功获取) | +| token | string | 登录令牌(用于鉴权) | +| isNewPlatform | bool | 是否新创建平台用户 | +| isNewUser | bool | 是否新创建用户 | + +### 成功响应示例 +```json +{ + "code": 200, + "message": "success", + "data": { + "userId": 10001, + "platformUserId": 20001, + "openid": "oL1oU5gS6Q...", + "unionid": "o6_bmasdasds...", + "sessionKey": "HKq6lZzG2u...", + "phone": "13800000000", + "token": "c6f7f1e4-5a3b-4f4e-9d0b-6b3f7b8c5e3a", + "isNewPlatform": false, + "isNewUser": false + } +} +``` + +## 错误响应示例 +```json +{ + "code": 400, + "message": "微信接口错误: 40163 code been used", + "data": null +} +``` + +## 业务说明 +- 首次登录时会创建 `t_user` 与 `t_platform_user`。 +- 之后登录会更新 `platform_session_key`、`last_login_time`,并按需更新 `nickname/avatarUrl/gender/phone`。 +- `phoneCode` 优先于 `encryptedData+iv` 用于获取手机号;两者都不提供时不更新手机号。 +- 返回 `token` 可直接用于鉴权(`Authorization: Bearer `)。 +- 若未传 `avatarUrl`,系统会从 `docs/avatar.md` 列表中随机取一张作为默认头像。 +- 新用户默认密码为 `123456`(仅在获取到手机号时设置),昵称为手机号格式 `138****0000`。 +- 若 `wechat.mini_program.app_id/app_secret` 未配置,将返回错误。 + +## 配置说明 +配置文件中新增: +```yaml +wechat: + mini_program: + app_id: "wx_your_app_id" + app_secret: "wx_your_app_secret" +``` + +## 相关配置与限制 +- 若启用安全校验(`security.enable: true`),该接口仍需携带签名头。 +- 默认未做登录态发放(如需 JWT/Token,可在接口层扩展)。 diff --git a/docs/avatar.md b/docs/avatar.md new file mode 100644 index 0000000..7cd5263 --- /dev/null +++ b/docs/avatar.md @@ -0,0 +1,15 @@ +d +https://oss-1322049369.cos.ap-beijing.myqcloud.com/images/xianxingnanxuesheng.png?imageSlim +https://oss-1322049369.cos.ap-beijing.myqcloud.com/images/nvxuesheng.png?imageSlim +https://oss-1322049369.cos.ap-beijing.myqcloud.com/images/nvxuesheng_1.png?imageSlim +https://oss-1322049369.cos.ap-beijing.myqcloud.com/images/nanxuesheng.png?imageSlim +https://oss-1322049369.cos.ap-beijing.myqcloud.com/images/nanxuesheng_1.png?imageSlim +https://oss-1322049369.cos.ap-beijing.myqcloud.com/images/a-389363773.png?imageSlim +https://oss-1322049369.cos.ap-beijing.myqcloud.com/images/a-389363772.png?imageSlim +https://oss-1322049369.cos.ap-beijing.myqcloud.com/images/a-389363771.png?imageSlim +https://oss-1322049369.cos.ap-beijing.myqcloud.com/images/a-389363770.png?imageSlim +https://oss-1322049369.cos.ap-beijing.myqcloud.com/images/a-389363769.png?imageSlim +https://oss-1322049369.cos.ap-beijing.myqcloud.com/images/a-389363768.png?imageSlim +https://oss-1322049369.cos.ap-beijing.myqcloud.com/images/a-389363767.png?imageSlim +https://oss-1322049369.cos.ap-beijing.myqcloud.com/images/a-8nvxuesheng.png?imageSlim +https://oss-1322049369.cos.ap-beijing.myqcloud.com/images/a-8nanxuesheng.png?imageSlim diff --git a/docs/sql/mysql/t_platform_user.sql b/docs/sql/mysql/t_platform_user.sql new file mode 100644 index 0000000..f6c92fe --- /dev/null +++ b/docs/sql/mysql/t_platform_user.sql @@ -0,0 +1,15 @@ +CREATE TABLE t_platform_user ( + id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '平台用户ID(自增)', + 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 JSON COMMENT '平台扩展字段(如抖音的user_name、微信的city等)', + last_login_time TIMESTAMP NULL COMMENT '最后登录时间', + 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-已删', + UNIQUE KEY uk_platform_openid (platform_type, platform_openid), + CONSTRAINT fk_platform_user_user_id FOREIGN KEY (user_id) REFERENCES t_user(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台用户关联表(微信/抖音小程序用户信息)'; diff --git a/docs/sql/mysql/t_user.sql b/docs/sql/mysql/t_user.sql new file mode 100644 index 0000000..cde5085 --- /dev/null +++ b/docs/sql/mysql/t_user.sql @@ -0,0 +1,14 @@ +CREATE TABLE t_user ( + id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '全局唯一用户ID(自增)', + username VARCHAR(50) COMMENT '用户名(可选,后台管理用)', + nickname VARCHAR(100) COMMENT '用户昵称(各平台统一)', + avatar_url VARCHAR(500) COMMENT '用户头像URL', + phone VARCHAR(20) UNIQUE COMMENT '手机号(脱敏存储,如138****1234)', + password VARCHAR(255) COMMENT '登录密码', + salt VARCHAR(32) COMMENT '密码盐值', + 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-已删' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户基础信息表'; diff --git a/docs/sql/postgresql/t_platform_user.sql b/docs/sql/postgresql/t_platform_user.sql new file mode 100644 index 0000000..a0d26f3 --- /dev/null +++ b/docs/sql/postgresql/t_platform_user.sql @@ -0,0 +1,41 @@ +CREATE TABLE t_platform_user ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL, + platform_type SMALLINT NOT NULL, + platform_openid VARCHAR(100) NOT NULL, + platform_unionid VARCHAR(100), + platform_session_key VARCHAR(100), + platform_extra JSONB, + last_login_time TIMESTAMP, + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted SMALLINT DEFAULT 0, + 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 '平台用户关联表(微信/抖音小程序用户信息)'; +COMMENT ON COLUMN t_platform_user.id IS '平台用户ID(自增)'; +COMMENT ON COLUMN t_platform_user.user_id IS '关联t_user.id'; +COMMENT ON COLUMN t_platform_user.platform_type IS '平台类型:1-微信小程序,2-抖音小程序,3-支付宝小程序'; +COMMENT ON COLUMN t_platform_user.platform_openid IS '平台唯一标识(微信openid/抖音open_id)'; +COMMENT ON COLUMN t_platform_user.platform_unionid IS '平台统一标识(微信unionid,多小程序互通用)'; +COMMENT ON COLUMN t_platform_user.platform_session_key IS '平台会话密钥(微信session_key,加密存储)'; +COMMENT ON COLUMN t_platform_user.platform_extra IS '平台扩展字段(如抖音的user_name、微信的city等)'; +COMMENT ON COLUMN t_platform_user.last_login_time IS '最后登录时间'; +COMMENT ON COLUMN t_platform_user.create_time IS '创建时间'; +COMMENT ON COLUMN t_platform_user.update_time IS '更新时间'; +COMMENT ON COLUMN t_platform_user.deleted IS '软删除:0-未删,1-已删'; + +CREATE OR REPLACE FUNCTION set_update_time() +RETURNS TRIGGER AS $$ +BEGIN + NEW.update_time = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER t_platform_user_set_update_time +BEFORE UPDATE ON t_platform_user +FOR EACH ROW +EXECUTE FUNCTION set_update_time(); diff --git a/docs/sql/postgresql/t_user.sql b/docs/sql/postgresql/t_user.sql new file mode 100644 index 0000000..789277c --- /dev/null +++ b/docs/sql/postgresql/t_user.sql @@ -0,0 +1,42 @@ +CREATE TABLE t_user ( + id BIGSERIAL PRIMARY KEY, + username VARCHAR(50), + nickname VARCHAR(100), + avatar_url VARCHAR(500), + phone VARCHAR(20), + password VARCHAR(255), + salt VARCHAR(32), + gender SMALLINT, + status SMALLINT DEFAULT 1, + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted SMALLINT DEFAULT 0, + UNIQUE (phone) +); + +COMMENT ON TABLE t_user IS '用户基础信息表'; +COMMENT ON COLUMN t_user.id IS '全局唯一用户ID(自增)'; +COMMENT ON COLUMN t_user.username IS '用户名(可选,后台管理用)'; +COMMENT ON COLUMN t_user.nickname IS '用户昵称(各平台统一)'; +COMMENT ON COLUMN t_user.avatar_url IS '用户头像URL'; +COMMENT ON COLUMN t_user.phone IS '手机号(脱敏存储,如138****1234)'; +COMMENT ON COLUMN t_user.password IS '登录密码'; +COMMENT ON COLUMN t_user.salt IS '密码盐值'; +COMMENT ON COLUMN t_user.gender IS '性别:0-未知,1-男,2-女'; +COMMENT ON COLUMN t_user.status IS '状态:0-禁用,1-正常'; +COMMENT ON COLUMN t_user.create_time IS '创建时间'; +COMMENT ON COLUMN t_user.update_time IS '更新时间'; +COMMENT ON COLUMN t_user.deleted IS '软删除:0-未删,1-已删'; + +CREATE OR REPLACE FUNCTION set_update_time() +RETURNS TRIGGER AS $$ +BEGIN + NEW.update_time = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER t_user_set_update_time +BEFORE UPDATE ON t_user +FOR EACH ROW +EXECUTE FUNCTION set_update_time(); diff --git a/server/common/base_mapper.go b/server/common/base_mapper.go new file mode 100644 index 0000000..2709b34 --- /dev/null +++ b/server/common/base_mapper.go @@ -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 +} \ No newline at end of file diff --git a/server/common/base_service.go b/server/common/base_service.go new file mode 100644 index 0000000..a5bbef3 --- /dev/null +++ b/server/common/base_service.go @@ -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 +} diff --git a/server/common/constants.go b/server/common/constants.go new file mode 100644 index 0000000..4ac3c6c --- /dev/null +++ b/server/common/constants.go @@ -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"} +) diff --git a/server/common/context.go b/server/common/context.go new file mode 100644 index 0000000..ec84267 --- /dev/null +++ b/server/common/context.go @@ -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 +} diff --git a/server/common/id_utils.go b/server/common/id_utils.go new file mode 100644 index 0000000..1e2b165 --- /dev/null +++ b/server/common/id_utils.go @@ -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) +} diff --git a/server/common/logger.go b/server/common/logger.go new file mode 100644 index 0000000..99567ec --- /dev/null +++ b/server/common/logger.go @@ -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 := ` + + + +应用日志 + + + +

应用日志 - ` + time.Now().Format("2006-01-02") + `

+
+` + h.writer.Write([]byte(html)) + h.initialized = true +} + +func (h *htmlLogWriter) writeLog(timestamp, level, msg string) { + html := fmt.Sprintf(`
[%s] [%s] %s
+`, level, timestamp, level, msg) + h.writer.Write([]byte(html)) +} + +func (h *htmlLogWriter) writeFooter() { + html := `
+ +` + h.writer.Write([]byte(html)) +} diff --git a/server/common/password.go b/server/common/password.go new file mode 100644 index 0000000..4cb7082 --- /dev/null +++ b/server/common/password.go @@ -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(">> 测试失败!") + } +} diff --git a/server/common/response.go b/server/common/response.go new file mode 100644 index 0000000..7dca734 --- /dev/null +++ b/server/common/response.go @@ -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}) +} diff --git a/server/common/score_calculator.go b/server/common/score_calculator.go new file mode 100644 index 0000000..5574b9b --- /dev/null +++ b/server/common/score_calculator.go @@ -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 +} \ No newline at end of file diff --git a/server/common/snowflake/snowflake.go b/server/common/snowflake/snowflake.go new file mode 100644 index 0000000..91df396 --- /dev/null +++ b/server/common/snowflake/snowflake.go @@ -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, + } +} diff --git a/server/common/snowflake/snowflake_test.go b/server/common/snowflake/snowflake_test.go new file mode 100644 index 0000000..8c61648 --- /dev/null +++ b/server/common/snowflake/snowflake_test.go @@ -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"]) +} diff --git a/server/common/sql_utils.go b/server/common/sql_utils.go new file mode 100644 index 0000000..1fa9924 --- /dev/null +++ b/server/common/sql_utils.go @@ -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 +} diff --git a/server/config/config.dev.yaml b/server/config/config.dev.yaml new file mode 100644 index 0000000..f6841ca --- /dev/null +++ b/server/config/config.dev.yaml @@ -0,0 +1,83 @@ +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 + driver: postgresql + host: 10.13.13.1 + #port: 3306 + port: 5432 + database: fast-common-db + username: user_3W72AM + password: "password_KAwdZW" + 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: 2 + +wechat: + mini_program: + # 专升本 + #app_id: "wx88c44c7c66ef6184" + #app_secret: "b4a7ce15d334fd5a6fe7679b8a8019de" + # 艺体志愿宝 + app_id: "wxb9cf28f42ffa35e5" + app_secret: "ed3fd9089dcfbd1d886eddeca69c07bd" + +app_config: + app: + min_version: "1.2.0" + latest_version: "1.3.5" + force_update: true + api: + base_url: "http://127.0.0.1:8081" + version: "2026-03-16" + min_client_version: "1.2.0" + webview: + base_url: "http://127.0.0.1:8082/" + version: "2026-03-16" + min_client_version: "1.2.0" + ttl_seconds: 3600 + disabled: false + disable_reason: "" diff --git a/server/config/config.go b/server/config/config.go new file mode 100644 index 0000000..a0dd86e --- /dev/null +++ b/server/config/config.go @@ -0,0 +1,180 @@ +// 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"` + Wechat WechatConfig `yaml:"wechat"` + AppConfig AppVersionConfig `yaml:"app_config"` +} + +// 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"` +} + +// WechatConfig 微信配置 +type WechatConfig struct { + MiniProgram WechatMiniProgramConfig `yaml:"mini_program"` +} + +// WechatMiniProgramConfig 微信小程序配置 +type WechatMiniProgramConfig struct { + AppID string `yaml:"app_id"` + AppSecret string `yaml:"app_secret"` +} + +// AppVersionConfig 小程序版本与配置中心 +type AppVersionConfig struct { + App AppClientConfig `yaml:"app"` + API AppEndpointConfig `yaml:"api"` + WebView AppEndpointConfig `yaml:"webview"` + TTLSeconds int `yaml:"ttl_seconds"` + Disabled bool `yaml:"disabled"` + DisableReason string `yaml:"disable_reason"` +} + +// AppClientConfig 客户端版本配置 +type AppClientConfig struct { + MinVersion string `yaml:"min_version"` + LatestVersion string `yaml:"latest_version"` + ForceUpdate bool `yaml:"force_update"` +} + +// AppEndpointConfig 接口或 WebView 配置 +type AppEndpointConfig struct { + BaseURL string `yaml:"base_url"` + Version string `yaml:"version"` + MinClientVersion string `yaml:"min_client_version"` +} + +// 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) + } +} diff --git a/server/config/config.prod.yaml b/server/config/config.prod.yaml new file mode 100644 index 0000000..b504224 --- /dev/null +++ b/server/config/config.prod.yaml @@ -0,0 +1,74 @@ +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 + +wechat: + mini_program: + app_id: "wx_your_app_id" + app_secret: "wx_your_app_secret" + +app_config: + app: + min_version: "1.2.0" + latest_version: "1.3.5" + force_update: true + api: + base_url: "https://api.xxx.com" + version: "2026-03-16" + min_client_version: "1.2.0" + webview: + base_url: "https://m.xxx.com" + version: "2026-03-16" + min_client_version: "1.2.0" + ttl_seconds: 3600 + disabled: false + disable_reason: "" diff --git a/server/config/config.test.yaml b/server/config/config.test.yaml new file mode 100644 index 0000000..6247e30 --- /dev/null +++ b/server/config/config.test.yaml @@ -0,0 +1,74 @@ +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 + +wechat: + mini_program: + app_id: "wx_your_app_id" + app_secret: "wx_your_app_secret" + +app_config: + app: + min_version: "1.2.0" + latest_version: "1.3.5" + force_update: true + api: + base_url: "https://api.xxx.com" + version: "2026-03-16" + min_client_version: "1.2.0" + webview: + base_url: "https://m.xxx.com" + version: "2026-03-16" + min_client_version: "1.2.0" + ttl_seconds: 3600 + disabled: false + disable_reason: "" diff --git a/server/config/database.go b/server/config/database.go new file mode 100644 index 0000000..3b21f51 --- /dev/null +++ b/server/config/database.go @@ -0,0 +1,125 @@ +// Package config 配置包,负责应用程序的配置管理 +package config + +import ( + "fmt" + "io" + "log" + "os" + "path/filepath" + "strings" + "time" + + "gorm.io/driver/mysql" + "gorm.io/driver/postgres" + "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 + 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 + driver := strings.ToLower(strings.TrimSpace(dbConfig.Driver)) + switch driver { + case "postgres", "postgresql": + dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=disable TimeZone=Asia/Shanghai", + dbConfig.Host, + dbConfig.Username, + dbConfig.Password, + dbConfig.Database, + dbConfig.Port, + ) + DB, err = gorm.Open(postgres.Open(dsn), gormConfig) + default: + 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, + ) + 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("数据库连接已关闭") + } + } +} diff --git a/server/config/redis.go b/server/config/redis.go new file mode 100644 index 0000000..7cc3172 --- /dev/null +++ b/server/config/redis.go @@ -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连接已关闭") + } +} diff --git a/server/docs/docs.go b/server/docs/docs.go new file mode 100644 index 0000000..f59215a --- /dev/null +++ b/server/docs/docs.go @@ -0,0 +1,3592 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/platform-users": { + "get": { + "tags": [ + "平台用户" + ], + "summary": "获取平台用户列表", + "parameters": [ + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "post": { + "tags": [ + "平台用户" + ], + "summary": "创建平台用户", + "parameters": [ + { + "description": "平台用户信息", + "name": "item", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreatePlatformUserRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/platform-users/{id}": { + "get": { + "tags": [ + "平台用户" + ], + "summary": "获取平台用户详情", + "parameters": [ + { + "type": "integer", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "put": { + "tags": [ + "平台用户" + ], + "summary": "更新平台用户", + "parameters": [ + { + "type": "integer", + "description": "ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "平台用户信息", + "name": "item", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdatePlatformUserRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "delete": { + "tags": [ + "平台用户" + ], + "summary": "删除平台用户", + "parameters": [ + { + "type": "integer", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "patch": { + "tags": [ + "平台用户" + ], + "summary": "动态字段更新", + "parameters": [ + { + "type": "integer", + "description": "ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "要更新的字段", + "name": "fields", + "in": "body", + "required": true, + "schema": { + "type": "object", + "additionalProperties": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/sys-users": { + "get": { + "tags": [ + "用户管理" + ], + "summary": "获取用户列表", + "parameters": [ + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/common.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/vo.SysUserVO" + } + } + } + } + ] + } + } + } + }, + "post": { + "tags": [ + "用户管理" + ], + "summary": "创建用户", + "parameters": [ + { + "description": "用户信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/server_modules_system_dto.CreateUserRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/common.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/vo.SysUserVO" + } + } + } + ] + } + } + } + } + }, + "/sys-users/{id}": { + "get": { + "tags": [ + "用户管理" + ], + "summary": "获取单个用户", + "parameters": [ + { + "type": "string", + "description": "用户ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/common.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/vo.SysUserVO" + } + } + } + ] + } + } + } + }, + "put": { + "tags": [ + "用户管理" + ], + "summary": "更新用户", + "parameters": [ + { + "type": "string", + "description": "用户ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "用户信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/server_modules_system_dto.UpdateUserRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/common.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/vo.SysUserVO" + } + } + } + ] + } + } + } + }, + "delete": { + "tags": [ + "用户管理" + ], + "summary": "删除用户", + "parameters": [ + { + "type": "string", + "description": "用户ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "patch": { + "tags": [ + "用户管理" + ], + "summary": "动态字段更新", + "parameters": [ + { + "type": "string", + "description": "用户ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "要更新的字段", + "name": "fields", + "in": "body", + "required": true, + "schema": { + "type": "object", + "additionalProperties": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/sys-users/{id}/password": { + "put": { + "tags": [ + "用户管理" + ], + "summary": "修改密码", + "parameters": [ + { + "type": "string", + "description": "用户ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "密码信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/controller.UpdatePasswordRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/sys/auth/info": { + "get": { + "tags": [ + "认证" + ], + "summary": "Sys获取当前登录用户信息", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/sys/auth/login": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "认证" + ], + "summary": "Sys用户登录", + "parameters": [ + { + "description": "登录信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/controller.LoginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/sys/auth/logout": { + "post": { + "tags": [ + "认证" + ], + "summary": "Sys用户登出", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/user/auth/info": { + "get": { + "tags": [ + "认证" + ], + "summary": "获取当前登录用户信息", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/user/auth/login": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "认证" + ], + "summary": "用户登录", + "parameters": [ + { + "description": "登录信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/controller.LoginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/user/auth/logout": { + "post": { + "tags": [ + "认证" + ], + "summary": "用户登出", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/user/major/list": { + "get": { + "tags": [ + "用户专业" + ], + "summary": "获取当前用户可检索列表", + "parameters": [ + { + "type": "integer", + "default": 1, + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 10, + "description": "每页数量", + "name": "size", + "in": "query" + }, + { + "type": "string", + "default": "\"\"", + "description": "批次(本科提前批/本科A段/本科B段/本科/高职高专)", + "name": "batch", + "in": "query" + }, + { + "type": "string", + "default": "\"\"", + "description": "录取概率类型(难录取/可冲击/较稳妥/可保底)", + "name": "probability", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/user/major/list_by_school": { + "get": { + "tags": [ + "用户专业" + ], + "summary": "获取当前院校下其他专业数据", + "parameters": [ + { + "type": "integer", + "default": 1, + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 10, + "description": "每页数量", + "name": "size", + "in": "query" + }, + { + "type": "string", + "description": "院校代码", + "name": "school_code", + "in": "query", + "required": true + }, + { + "type": "string", + "default": "\"\"", + "description": "录取概率类型(难录取/可冲击/较稳妥/可保底)", + "name": "probability", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/user/score": { + "get": { + "tags": [ + "用户分数" + ], + "summary": "获取当前用户的激活分数", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/user/score/list": { + "get": { + "tags": [ + "用户分数" + ], + "summary": "获取当前用户的成绩列表", + "parameters": [ + { + "type": "integer", + "default": 1, + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 10, + "description": "每页数量", + "name": "size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/user/score/save-score": { + "post": { + "tags": [ + "用户分数" + ], + "summary": "保存用户成绩", + "parameters": [ + { + "description": "成绩信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SaveScoreRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/user/score/{id}": { + "get": { + "tags": [ + "用户分数" + ], + "summary": "获取指定 ID 的分数", + "parameters": [ + { + "type": "string", + "description": "成绩ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/user/volunteer/delete": { + "delete": { + "tags": [ + "用户志愿" + ], + "summary": "删除志愿单接口", + "parameters": [ + { + "type": "string", + "description": "志愿单ID", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/user/volunteer/detail": { + "get": { + "tags": [ + "用户志愿" + ], + "summary": "获取当前志愿单详情", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/common.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/vo.VolunteerDetailResponse" + } + } + } + ] + } + } + } + } + }, + "/user/volunteer/list": { + "get": { + "tags": [ + "用户志愿" + ], + "summary": "获取当前用户志愿单列表", + "parameters": [ + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/user/volunteer/save": { + "post": { + "tags": [ + "用户志愿" + ], + "summary": "保存志愿明细", + "parameters": [ + { + "description": "志愿键列表", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SaveVolunteerRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/user/volunteer/switch": { + "post": { + "tags": [ + "用户志愿" + ], + "summary": "切换当前志愿单", + "parameters": [ + { + "type": "string", + "description": "志愿单ID", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/user/volunteer/updateName": { + "put": { + "tags": [ + "用户志愿" + ], + "summary": "编辑志愿单名称", + "parameters": [ + { + "type": "string", + "description": "志愿单ID", + "name": "id", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "志愿单名称", + "name": "name", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/users": { + "get": { + "tags": [ + "用户" + ], + "summary": "获取用户列表", + "parameters": [ + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "post": { + "tags": [ + "用户" + ], + "summary": "创建用户", + "parameters": [ + { + "description": "用户信息", + "name": "item", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/server_modules_user_dto.CreateUserRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/users/{id}": { + "get": { + "tags": [ + "用户" + ], + "summary": "获取用户详情", + "parameters": [ + { + "type": "integer", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "put": { + "tags": [ + "用户" + ], + "summary": "更新用户", + "parameters": [ + { + "type": "integer", + "description": "ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "用户信息", + "name": "item", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/server_modules_user_dto.UpdateUserRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "delete": { + "tags": [ + "用户" + ], + "summary": "删除用户", + "parameters": [ + { + "type": "integer", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "patch": { + "tags": [ + "用户" + ], + "summary": "动态字段更新", + "parameters": [ + { + "type": "integer", + "description": "ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "要更新的字段", + "name": "fields", + "in": "body", + "required": true, + "schema": { + "type": "object", + "additionalProperties": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/yx-calculation-majors": { + "get": { + "tags": [ + "计算专业" + ], + "summary": "获取计算专业列表", + "parameters": [ + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/common.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/vo.YxCalculationMajorVO" + } + } + } + } + ] + } + } + } + }, + "post": { + "tags": [ + "计算专业" + ], + "summary": "创建计算专业", + "parameters": [ + { + "description": "计算专业信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreateCalculationMajorRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/common.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/vo.YxCalculationMajorVO" + } + } + } + ] + } + } + } + } + }, + "/yx-calculation-majors/batch": { + "post": { + "tags": [ + "计算专业" + ], + "summary": "批量创建计算专业", + "parameters": [ + { + "description": "计算专业列表", + "name": "items", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.CreateCalculationMajorRequest" + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "delete": { + "tags": [ + "计算专业" + ], + "summary": "批量删除计算专业", + "parameters": [ + { + "description": "ID列表", + "name": "ids", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/yx-calculation-majors/{id}": { + "get": { + "tags": [ + "计算专业" + ], + "summary": "获取单个计算专业", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/common.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/vo.YxCalculationMajorVO" + } + } + } + ] + } + } + } + }, + "put": { + "tags": [ + "计算专业" + ], + "summary": "更新计算专业", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "计算专业信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateCalculationMajorRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/common.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/vo.YxCalculationMajorVO" + } + } + } + ] + } + } + } + }, + "delete": { + "tags": [ + "计算专业" + ], + "summary": "删除计算专业", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "patch": { + "tags": [ + "计算专业" + ], + "summary": "动态字段更新", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "要更新的字段", + "name": "fields", + "in": "body", + "required": true, + "schema": { + "type": "object", + "additionalProperties": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/yx-history-enrolls": { + "get": { + "tags": [ + "历年招生" + ], + "summary": "获取历年招生列表", + "parameters": [ + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "post": { + "tags": [ + "历年招生" + ], + "summary": "创建历年招生记录", + "parameters": [ + { + "description": "历年招生信息", + "name": "item", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entity.YxHistoryMajorEnroll" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/yx-history-enrolls/batch": { + "post": { + "tags": [ + "历年招生" + ], + "summary": "批量创建历年招生记录", + "parameters": [ + { + "description": "历年招生列表", + "name": "items", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/entity.YxHistoryMajorEnroll" + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "delete": { + "tags": [ + "历年招生" + ], + "summary": "批量删除历年招生记录", + "parameters": [ + { + "description": "ID列表", + "name": "ids", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/yx-history-enrolls/{id}": { + "get": { + "tags": [ + "历年招生" + ], + "summary": "获取单个历年招生记录", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "put": { + "tags": [ + "历年招生" + ], + "summary": "更新历年招生记录", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "历年招生信息", + "name": "item", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entity.YxHistoryMajorEnroll" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "delete": { + "tags": [ + "历年招生" + ], + "summary": "删除历年招生记录", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "patch": { + "tags": [ + "历年招生" + ], + "summary": "动态字段更新", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "要更新的字段", + "name": "fields", + "in": "body", + "required": true, + "schema": { + "type": "object", + "additionalProperties": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/yx-school-majors": { + "get": { + "tags": [ + "院校专业" + ], + "summary": "获取院校专业列表", + "parameters": [ + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "post": { + "tags": [ + "院校专业" + ], + "summary": "创建院校专业", + "parameters": [ + { + "description": "院校专业信息", + "name": "item", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entity.YxSchoolMajor" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/yx-school-majors/batch": { + "post": { + "tags": [ + "院校专业" + ], + "summary": "批量创建院校专业", + "parameters": [ + { + "description": "院校专业列表", + "name": "items", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/entity.YxSchoolMajor" + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "delete": { + "tags": [ + "院校专业" + ], + "summary": "批量删除院校专业", + "parameters": [ + { + "description": "ID列表", + "name": "ids", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/yx-school-majors/{id}": { + "get": { + "tags": [ + "院校专业" + ], + "summary": "获取单个院校专业", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "put": { + "tags": [ + "院校专业" + ], + "summary": "更新院校专业", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "院校专业信息", + "name": "item", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entity.YxSchoolMajor" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "delete": { + "tags": [ + "院校专业" + ], + "summary": "删除院校专业", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "patch": { + "tags": [ + "院校专业" + ], + "summary": "动态字段更新", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "要更新的字段", + "name": "fields", + "in": "body", + "required": true, + "schema": { + "type": "object", + "additionalProperties": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/yx-user-scores": { + "get": { + "tags": [ + "用户分数" + ], + "summary": "获取用户分数列表", + "parameters": [ + { + "type": "integer", + "default": 1, + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 10, + "description": "每页数量", + "name": "size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "post": { + "tags": [ + "用户分数" + ], + "summary": "创建用户分数", + "parameters": [ + { + "description": "用户分数信息", + "name": "item", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entity.YxUserScore" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/yx-user-scores/{id}": { + "get": { + "tags": [ + "用户分数" + ], + "summary": "获取单个用户分数", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "put": { + "tags": [ + "用户分数" + ], + "summary": "更新用户分数", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "用户分数信息", + "name": "item", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entity.YxUserScore" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "delete": { + "tags": [ + "用户分数" + ], + "summary": "删除用户分数", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/yx-volunteer-records": { + "get": { + "tags": [ + "志愿明细" + ], + "summary": "获取志愿明细列表", + "parameters": [ + { + "type": "integer", + "default": 1, + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 10, + "description": "每页数量", + "name": "size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "post": { + "tags": [ + "志愿明细" + ], + "summary": "创建志愿明细", + "parameters": [ + { + "description": "志愿明细信息", + "name": "item", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entity.YxVolunteerRecord" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/yx-volunteer-records/{id}": { + "get": { + "tags": [ + "志愿明细" + ], + "summary": "获取单个志愿明细", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "put": { + "tags": [ + "志愿明细" + ], + "summary": "更新志愿明细", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "志愿明细信息", + "name": "item", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entity.YxVolunteerRecord" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "delete": { + "tags": [ + "志愿明细" + ], + "summary": "删除志愿明细", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/yx-volunteers": { + "get": { + "tags": [ + "志愿" + ], + "summary": "获取志愿列表", + "parameters": [ + { + "type": "integer", + "default": 1, + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 10, + "description": "每页数量", + "name": "size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/common.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/vo.YxVolunteerVO" + } + } + } + } + ] + } + } + } + }, + "post": { + "tags": [ + "志愿" + ], + "summary": "创建志愿", + "parameters": [ + { + "description": "志愿信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreateVolunteerRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/common.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/vo.YxVolunteerVO" + } + } + } + ] + } + } + } + } + }, + "/yx-volunteers/{id}": { + "get": { + "tags": [ + "志愿" + ], + "summary": "获取单个志愿", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/common.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/vo.YxVolunteerVO" + } + } + } + ] + } + } + } + }, + "put": { + "tags": [ + "志愿" + ], + "summary": "更新志愿", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "志愿信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateVolunteerRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/common.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/vo.YxVolunteerVO" + } + } + } + ] + } + } + } + }, + "delete": { + "tags": [ + "志愿" + ], + "summary": "删除志愿", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + } + }, + "definitions": { + "common.Response": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "data": {}, + "message": { + "type": "string" + } + } + }, + "controller.LoginRequest": { + "type": "object", + "required": [ + "password", + "username" + ], + "properties": { + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "controller.UpdatePasswordRequest": { + "type": "object", + "required": [ + "newPassword", + "oldPassword" + ], + "properties": { + "newPassword": { + "type": "string" + }, + "oldPassword": { + "type": "string" + } + } + }, + "dto.CreateCalculationMajorRequest": { + "type": "object", + "required": [ + "majorCode", + "schoolCode" + ], + "properties": { + "batch": { + "description": "批次", + "type": "string" + }, + "category": { + "description": "类别", + "type": "string" + }, + "chineseScoreLimitation": { + "description": "语文分数限制", + "type": "number" + }, + "culturalScoreLimitation": { + "description": "文化成绩限制", + "type": "number" + }, + "detail": { + "description": "详情", + "type": "string" + }, + "englishScoreLimitation": { + "description": "英语分数限制", + "type": "number" + }, + "enrollProbability": { + "description": "录取概率", + "type": "number" + }, + "enrollmentCode": { + "description": "招生代码", + "type": "string" + }, + "institutionType": { + "description": "院校类型", + "type": "string" + }, + "kslx": { + "description": "考试类型", + "type": "string" + }, + "limitation": { + "description": "限制条件", + "type": "string" + }, + "mainSubjects": { + "description": "主考科目", + "type": "string" + }, + "majorCode": { + "description": "专业代码", + "type": "string" + }, + "majorName": { + "description": "专业名称", + "type": "string" + }, + "majorType": { + "description": "专业类型", + "type": "string" + }, + "majorTypeChild": { + "description": "子专业类型", + "type": "string" + }, + "planNum": { + "description": "计划人数", + "type": "integer" + }, + "probabilityOperator": { + "description": "概率操作符", + "type": "string" + }, + "professionalScoreLimitation": { + "description": "专业分数限制", + "type": "number" + }, + "province": { + "description": "省份", + "type": "string" + }, + "rulesEnrollProbability": { + "description": "录取规则概率", + "type": "string" + }, + "schoolCode": { + "description": "院校代码", + "type": "string" + }, + "schoolName": { + "description": "院校名称", + "type": "string" + }, + "schoolNature": { + "description": "院校性质", + "type": "string" + }, + "state": { + "description": "状态", + "type": "string" + }, + "studentScore": { + "description": "学生分数", + "type": "number" + }, + "tuition": { + "description": "学费", + "type": "string" + } + } + }, + "dto.CreatePlatformUserRequest": { + "type": "object" + }, + "dto.CreateVolunteerRequest": { + "type": "object", + "required": [ + "scoreId", + "volunteerName" + ], + "properties": { + "createType": { + "description": "生成类型(1.手动生成,2.智能生成)", + "type": "string" + }, + "scoreId": { + "description": "关联成绩ID", + "type": "string" + }, + "volunteerName": { + "description": "志愿单名称", + "type": "string" + } + } + }, + "dto.SaveScoreRequest": { + "type": "object", + "required": [ + "ChineseScore", + "CognitioPolyclinic", + "CulturalScore", + "EnglishScore", + "ProfessionalCategory", + "ProfessionalCategoryChildren", + "ProfessionalCategoryChildrenScore", + "ProfessionalScore", + "Province", + "SubjectList" + ], + "properties": { + "ChineseScore": { + "type": "number" + }, + "CognitioPolyclinic": { + "type": "string" + }, + "CulturalScore": { + "type": "number" + }, + "EnglishScore": { + "type": "number" + }, + "ProfessionalCategory": { + "type": "string" + }, + "ProfessionalCategoryChildren": { + "type": "array", + "items": { + "type": "string" + } + }, + "ProfessionalCategoryChildrenScore": { + "type": "object", + "additionalProperties": { + "type": "number", + "format": "float64" + } + }, + "ProfessionalScore": { + "type": "number" + }, + "Province": { + "type": "string" + }, + "SubjectList": { + "type": "array", + "items": { + "type": "string" + } + }, + "createBy": { + "type": "string" + } + } + }, + "dto.SaveVolunteerRequest": { + "type": "object", + "required": [ + "keys" + ], + "properties": { + "keys": { + "description": "Keys: schoolCode_majorCode_enrollmentCode", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "dto.UpdateCalculationMajorRequest": { + "type": "object", + "properties": { + "batch": { + "description": "批次", + "type": "string" + }, + "category": { + "description": "类别", + "type": "string" + }, + "chineseScoreLimitation": { + "description": "语文分数限制", + "type": "number" + }, + "culturalScoreLimitation": { + "description": "文化成绩限制", + "type": "number" + }, + "detail": { + "description": "详情", + "type": "string" + }, + "englishScoreLimitation": { + "description": "英语分数限制", + "type": "number" + }, + "enrollProbability": { + "description": "录取概率", + "type": "number" + }, + "enrollmentCode": { + "description": "招生代码", + "type": "string" + }, + "institutionType": { + "description": "院校类型", + "type": "string" + }, + "kslx": { + "description": "考试类型", + "type": "string" + }, + "limitation": { + "description": "限制条件", + "type": "string" + }, + "mainSubjects": { + "description": "主考科目", + "type": "string" + }, + "majorName": { + "description": "专业名称", + "type": "string" + }, + "majorType": { + "description": "专业类型", + "type": "string" + }, + "majorTypeChild": { + "description": "子专业类型", + "type": "string" + }, + "planNum": { + "description": "计划人数", + "type": "integer" + }, + "probabilityOperator": { + "description": "概率操作符", + "type": "string" + }, + "professionalScoreLimitation": { + "description": "专业分数限制", + "type": "number" + }, + "province": { + "description": "省份", + "type": "string" + }, + "rulesEnrollProbability": { + "description": "录取规则概率", + "type": "string" + }, + "schoolName": { + "description": "院校名称", + "type": "string" + }, + "schoolNature": { + "description": "院校性质", + "type": "string" + }, + "state": { + "description": "状态", + "type": "string" + }, + "studentScore": { + "description": "学生分数", + "type": "number" + }, + "tuition": { + "description": "学费", + "type": "string" + } + } + }, + "dto.UpdatePlatformUserRequest": { + "type": "object" + }, + "dto.UpdateVolunteerRequest": { + "type": "object", + "properties": { + "state": { + "description": "志愿单状态(0-否,1-正在使用,2-历史)", + "type": "string" + }, + "volunteerName": { + "description": "志愿单名称", + "type": "string" + } + } + }, + "entity.YxHistoryMajorEnroll": { + "type": "object", + "properties": { + "actualPitcherNum": { + "type": "integer" + }, + "admissionLine": { + "type": "number" + }, + "admissionNum": { + "type": "integer" + }, + "batch": { + "type": "string" + }, + "category": { + "type": "string" + }, + "checkMaster": { + "type": "string" + }, + "controlLine": { + "type": "number" + }, + "createBy": { + "type": "string" + }, + "createTime": { + "type": "string" + }, + "detail": { + "type": "string" + }, + "enrollNum": { + "type": "integer" + }, + "enrollmentCode": { + "type": "string" + }, + "id": { + "type": "string" + }, + "institutionCode": { + "type": "string" + }, + "mainSubjects": { + "type": "string" + }, + "majorCode": { + "type": "string" + }, + "majorName": { + "type": "string" + }, + "majorType": { + "type": "string" + }, + "majorTypeChild": { + "type": "string" + }, + "oneVolunteerAdmissionNum": { + "type": "integer" + }, + "probabilityOperator": { + "type": "string" + }, + "rulesEnrollProbability": { + "type": "string" + }, + "schoolCode": { + "type": "string" + }, + "schoolName": { + "type": "string" + }, + "scoreLineDifference": { + "type": "number" + }, + "sysOrgCode": { + "type": "string" + }, + "tuition": { + "type": "string" + }, + "updateBy": { + "type": "string" + }, + "updateTime": { + "type": "string" + }, + "year": { + "type": "string" + } + } + }, + "entity.YxSchoolMajor": { + "type": "object", + "properties": { + "batch": { + "type": "string" + }, + "category": { + "type": "string" + }, + "checkMaster": { + "type": "string" + }, + "chineseScoreLimitation": { + "type": "number" + }, + "createBy": { + "type": "string" + }, + "createTime": { + "type": "string" + }, + "culturalControlLine": { + "type": "number" + }, + "culturalScoreLimitation": { + "type": "number" + }, + "detail": { + "type": "string" + }, + "englishScoreLimitation": { + "type": "number" + }, + "enrollmentCode": { + "type": "string" + }, + "id": { + "type": "string" + }, + "kslx": { + "type": "string" + }, + "limitation": { + "type": "string" + }, + "mainSubjects": { + "type": "string" + }, + "majorCode": { + "type": "string" + }, + "majorName": { + "type": "string" + }, + "majorType": { + "type": "string" + }, + "majorTypeChild": { + "type": "string" + }, + "planNum": { + "type": "integer" + }, + "privateProbabilityOperator": { + "type": "string" + }, + "privateRulesEnrollProbability": { + "type": "string" + }, + "probabilityOperator": { + "type": "string" + }, + "professionalScoreLimitation": { + "type": "number" + }, + "rulesEnrollProbability": { + "type": "string" + }, + "rulesEnrollProbabilitySx": { + "type": "string" + }, + "schoolCode": { + "type": "string" + }, + "schoolName": { + "type": "string" + }, + "semester": { + "type": "string" + }, + "specialControlLine": { + "type": "number" + }, + "state": { + "type": "string" + }, + "tuition": { + "type": "string" + }, + "updateBy": { + "type": "string" + }, + "updateTime": { + "type": "string" + } + } + }, + "entity.YxUserScore": { + "type": "object", + "properties": { + "batch": { + "description": "录取批次", + "type": "string" + }, + "calculationTableName": { + "description": "记录结果表名", + "type": "string" + }, + "chineseScore": { + "description": "语文成绩", + "type": "number" + }, + "cognitioPolyclinic": { + "description": "文理分班(文科/理科)", + "type": "string" + }, + "createBy": { + "description": "创建人", + "type": "string" + }, + "createTime": { + "description": "创建时间", + "type": "string" + }, + "culturalScore": { + "description": "文化成绩分", + "type": "number" + }, + "educationalLevel": { + "description": "学历层次(1-本科,2-专科)", + "type": "string" + }, + "englishScore": { + "description": "英语成绩", + "type": "number" + }, + "fzby": { + "description": "服装表演", + "type": "number" + }, + "id": { + "type": "string" + }, + "jwtNum": { + "description": "较稳妥专业数量", + "type": "integer" + }, + "kbdNum": { + "description": "可保底专业数量", + "type": "integer" + }, + "kcjNum": { + "description": "可冲击专业数量", + "type": "integer" + }, + "nlqNum": { + "description": "难录取专业数量", + "type": "integer" + }, + "professionalCategory": { + "description": "专业类别(美术类/...)", + "type": "string" + }, + "professionalCategoryChildren": { + "description": "子级专业类别", + "type": "string" + }, + "professionalScore": { + "description": "专业成绩分", + "type": "number" + }, + "province": { + "description": "高考省份", + "type": "string" + }, + "ranking": { + "description": "位次", + "type": "integer" + }, + "state": { + "description": "状态(0-未使用,1-使用中)", + "type": "string" + }, + "subjects": { + "description": "选课", + "type": "string" + }, + "type": { + "description": "填报类型(1-普通类 2-艺术类)", + "type": "string" + }, + "updateBy": { + "description": "修改人", + "type": "string" + }, + "updateTime": { + "description": "修改时间", + "type": "string" + }, + "xjysby": { + "description": "戏剧影视表演", + "type": "number" + }, + "xjysdy": { + "description": "戏剧影视导演", + "type": "number" + }, + "yybyqy": { + "description": "音乐表演器乐", + "type": "number" + }, + "yybysy": { + "description": "音乐表演声乐", + "type": "number" + }, + "yyjy": { + "description": "音乐教育", + "type": "number" + } + } + }, + "entity.YxVolunteerRecord": { + "type": "object", + "properties": { + "batch": { + "description": "录取批次", + "type": "string" + }, + "calculationMajorId": { + "description": "专业折算id", + "type": "string" + }, + "createBy": { + "description": "创建人", + "type": "string" + }, + "createTime": { + "description": "创建日期", + "type": "string" + }, + "enrollProbability": { + "description": "录取概率", + "type": "number" + }, + "enrollmentCode": { + "description": "招生代码", + "type": "string" + }, + "fctj": { + "description": "服从调剂", + "type": "integer" + }, + "id": { + "type": "string" + }, + "indexs": { + "description": "志愿顺序", + "type": "integer" + }, + "majorCode": { + "description": "专业编码", + "type": "string" + }, + "schoolCode": { + "description": "学校编码", + "type": "string" + }, + "studentConvertedScore": { + "description": "折合分数", + "type": "number" + }, + "volunteerId": { + "description": "志愿单id", + "type": "string" + } + } + }, + "server_modules_system_dto.CreateUserRequest": { + "type": "object", + "required": [ + "password", + "realname", + "username" + ], + "properties": { + "avatar": { + "description": "头像", + "type": "string" + }, + "birthday": { + "description": "生日", + "type": "string" + }, + "email": { + "description": "电子邮件", + "type": "string" + }, + "orgCode": { + "description": "机构编码", + "type": "string" + }, + "password": { + "description": "密码", + "type": "string", + "minLength": 6 + }, + "phone": { + "description": "电话", + "type": "string" + }, + "realname": { + "description": "真实姓名", + "type": "string" + }, + "sex": { + "description": "性别(0-未知,1-男,2-女)", + "type": "integer" + }, + "username": { + "description": "登录账号", + "type": "string", + "maxLength": 20, + "minLength": 3 + } + } + }, + "server_modules_system_dto.UpdateUserRequest": { + "type": "object", + "properties": { + "avatar": { + "description": "头像", + "type": "string" + }, + "birthday": { + "description": "生日", + "type": "string" + }, + "email": { + "description": "电子邮件", + "type": "string" + }, + "orgCode": { + "description": "机构编码", + "type": "string" + }, + "phone": { + "description": "电话", + "type": "string" + }, + "realname": { + "description": "真实姓名", + "type": "string" + }, + "sex": { + "description": "性别(0-未知,1-男,2-女)", + "type": "integer" + }, + "status": { + "description": "状态(1-正常,2-冻结)", + "type": "integer" + } + } + }, + "server_modules_user_dto.CreateUserRequest": { + "type": "object", + "properties": { + "avatarUrl": { + "type": "string" + }, + "gender": { + "type": "integer" + }, + "nickname": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "username": { + "type": "string" + } + } + }, + "server_modules_user_dto.UpdateUserRequest": { + "type": "object", + "properties": { + "avatarUrl": { + "type": "string" + }, + "gender": { + "type": "integer" + }, + "nickname": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "username": { + "type": "string" + } + } + }, + "vo.SysUserVO": { + "type": "object", + "properties": { + "avatar": { + "description": "头像", + "type": "string" + }, + "birthday": { + "description": "生日", + "type": "string" + }, + "createTime": { + "description": "创建时间", + "type": "string" + }, + "email": { + "description": "电子邮件", + "type": "string" + }, + "id": { + "type": "string" + }, + "orgCode": { + "description": "机构编码", + "type": "string" + }, + "phone": { + "description": "电话", + "type": "string" + }, + "realname": { + "description": "真实姓名", + "type": "string" + }, + "sex": { + "description": "性别(0-未知,1-男,2-女)", + "type": "integer" + }, + "status": { + "description": "状态(1-正常,2-冻结)", + "type": "integer" + }, + "updateTime": { + "description": "更新时间", + "type": "string" + }, + "username": { + "description": "登录账号", + "type": "string" + } + } + }, + "vo.VolunteerDetailResponse": { + "type": "object", + "properties": { + "items": { + "description": "志愿明细列表 *VolunteerItemsVO", + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/definitions/vo.VolunteerDetailVO" + } + } + }, + "volunteer": { + "description": "志愿单信息", + "allOf": [ + { + "$ref": "#/definitions/vo.VolunteerInfoVO" + } + ] + } + } + }, + "vo.VolunteerDetailVO": { + "type": "object", + "properties": { + "batch": { + "description": "批次", + "type": "string" + }, + "calculationMajorId": { + "description": "计算专业ID", + "type": "string" + }, + "createBy": { + "description": "创建人", + "type": "string" + }, + "createTime": { + "description": "创建时间", + "type": "string" + }, + "enrollProbability": { + "description": "录取概率", + "type": "number" + }, + "enrollmentCode": { + "description": "招生代码", + "type": "string" + }, + "id": { + "description": "志愿记录ID", + "type": "string" + }, + "indexs": { + "description": "志愿序号", + "type": "integer" + }, + "institutionType": { + "description": "院校类型", + "type": "string" + }, + "majorCode": { + "description": "专业代码", + "type": "string" + }, + "majorDetail": { + "description": "专业详情", + "type": "string" + }, + "majorName": { + "description": "专业名称", + "type": "string" + }, + "planNum": { + "description": "计划人数", + "type": "integer" + }, + "province": { + "description": "省份", + "type": "string" + }, + "schoolCode": { + "description": "院校代码", + "type": "string" + }, + "schoolIcon": { + "description": "院校图标", + "type": "string" + }, + "schoolName": { + "description": "扩展字段(来自 SchoolMajorDTO)", + "type": "string" + }, + "schoolNature": { + "description": "院校性质", + "type": "string" + }, + "studentConvertedScore": { + "description": "学生折合分", + "type": "number" + }, + "tuition": { + "description": "学费", + "type": "string" + }, + "volunteerId": { + "description": "志愿单ID", + "type": "string" + } + } + }, + "vo.VolunteerInfoVO": { + "type": "object", + "properties": { + "createBy": { + "type": "string" + }, + "createTime": { + "type": "string" + }, + "createType": { + "type": "string" + }, + "id": { + "type": "string" + }, + "scoreId": { + "type": "string" + }, + "state": { + "type": "string" + }, + "updateBy": { + "type": "string" + }, + "updateTime": { + "type": "string" + }, + "volunteerName": { + "type": "string" + } + } + }, + "vo.YxCalculationMajorVO": { + "type": "object", + "properties": { + "batch": { + "description": "批次", + "type": "string" + }, + "category": { + "description": "类别", + "type": "string" + }, + "chineseScoreLimitation": { + "description": "语文分数限制", + "type": "number" + }, + "createBy": { + "description": "创建人", + "type": "string" + }, + "createTime": { + "description": "创建时间", + "type": "string" + }, + "culturalScoreLimitation": { + "description": "文化成绩限制", + "type": "number" + }, + "detail": { + "description": "详情", + "type": "string" + }, + "englishScoreLimitation": { + "description": "英语分数限制", + "type": "number" + }, + "enrollProbability": { + "description": "录取概率", + "type": "number" + }, + "enrollmentCode": { + "description": "招生代码", + "type": "string" + }, + "firstLevelDiscipline": { + "description": "一级学科", + "type": "string" + }, + "id": { + "type": "string" + }, + "institutionType": { + "description": "院校类型", + "type": "string" + }, + "kslx": { + "description": "考试类型", + "type": "string" + }, + "limitation": { + "description": "限制条件", + "type": "string" + }, + "mainSubjects": { + "description": "主考科目", + "type": "string" + }, + "majorCode": { + "description": "专业代码", + "type": "string" + }, + "majorName": { + "description": "专业名称", + "type": "string" + }, + "majorType": { + "description": "专业类型", + "type": "string" + }, + "majorTypeChild": { + "description": "子专业类型", + "type": "string" + }, + "planNum": { + "description": "计划人数", + "type": "integer" + }, + "privateStudentScore": { + "description": "私有学生分数", + "type": "number" + }, + "probabilityOperator": { + "description": "概率操作符", + "type": "string" + }, + "professionalScoreLimitation": { + "description": "专业分数限制", + "type": "number" + }, + "province": { + "description": "省份", + "type": "string" + }, + "rulesEnrollProbability": { + "description": "录取规则概率", + "type": "string" + }, + "schoolCode": { + "description": "院校代码", + "type": "string" + }, + "schoolName": { + "description": "院校名称", + "type": "string" + }, + "schoolNature": { + "description": "院校性质", + "type": "string" + }, + "state": { + "description": "状态", + "type": "string" + }, + "studentConvertedScore": { + "description": "学生折合分", + "type": "number" + }, + "studentScore": { + "description": "学生分数", + "type": "number" + }, + "tuition": { + "description": "学费", + "type": "string" + }, + "updateBy": { + "description": "更新人", + "type": "string" + }, + "updateTime": { + "description": "更新时间", + "type": "string" + } + } + }, + "vo.YxVolunteerVO": { + "type": "object", + "properties": { + "createBy": { + "description": "创建人", + "type": "string" + }, + "createTime": { + "description": "创建日期", + "type": "string" + }, + "createType": { + "description": "生成类型(1.手动生成,2.智能生成)", + "type": "string" + }, + "id": { + "type": "string" + }, + "scoreId": { + "description": "使用成绩id", + "type": "string" + }, + "state": { + "description": "志愿单状态(0-否,1-正在使用,2-历史)", + "type": "string" + }, + "sysOrgCode": { + "description": "所属部门", + "type": "string" + }, + "updateBy": { + "description": "更新人", + "type": "string" + }, + "updateTime": { + "description": "更新日期", + "type": "string" + }, + "volunteerName": { + "description": "志愿单名称", + "type": "string" + } + } + } + }, + "securityDefinitions": { + "Bearer": { + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "2.0", + Host: "localhost:8080", + BasePath: "/api", + Schemes: []string{}, + Title: "Golang Server API", + Description: "提供管理接口", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/server/docs/swagger.json b/server/docs/swagger.json new file mode 100644 index 0000000..f01cb63 --- /dev/null +++ b/server/docs/swagger.json @@ -0,0 +1,3568 @@ +{ + "swagger": "2.0", + "info": { + "description": "提供管理接口", + "title": "Golang Server API", + "contact": {}, + "version": "2.0" + }, + "host": "localhost:8080", + "basePath": "/api", + "paths": { + "/platform-users": { + "get": { + "tags": [ + "平台用户" + ], + "summary": "获取平台用户列表", + "parameters": [ + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "post": { + "tags": [ + "平台用户" + ], + "summary": "创建平台用户", + "parameters": [ + { + "description": "平台用户信息", + "name": "item", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreatePlatformUserRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/platform-users/{id}": { + "get": { + "tags": [ + "平台用户" + ], + "summary": "获取平台用户详情", + "parameters": [ + { + "type": "integer", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "put": { + "tags": [ + "平台用户" + ], + "summary": "更新平台用户", + "parameters": [ + { + "type": "integer", + "description": "ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "平台用户信息", + "name": "item", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdatePlatformUserRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "delete": { + "tags": [ + "平台用户" + ], + "summary": "删除平台用户", + "parameters": [ + { + "type": "integer", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "patch": { + "tags": [ + "平台用户" + ], + "summary": "动态字段更新", + "parameters": [ + { + "type": "integer", + "description": "ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "要更新的字段", + "name": "fields", + "in": "body", + "required": true, + "schema": { + "type": "object", + "additionalProperties": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/sys-users": { + "get": { + "tags": [ + "用户管理" + ], + "summary": "获取用户列表", + "parameters": [ + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/common.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/vo.SysUserVO" + } + } + } + } + ] + } + } + } + }, + "post": { + "tags": [ + "用户管理" + ], + "summary": "创建用户", + "parameters": [ + { + "description": "用户信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/server_modules_system_dto.CreateUserRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/common.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/vo.SysUserVO" + } + } + } + ] + } + } + } + } + }, + "/sys-users/{id}": { + "get": { + "tags": [ + "用户管理" + ], + "summary": "获取单个用户", + "parameters": [ + { + "type": "string", + "description": "用户ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/common.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/vo.SysUserVO" + } + } + } + ] + } + } + } + }, + "put": { + "tags": [ + "用户管理" + ], + "summary": "更新用户", + "parameters": [ + { + "type": "string", + "description": "用户ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "用户信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/server_modules_system_dto.UpdateUserRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/common.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/vo.SysUserVO" + } + } + } + ] + } + } + } + }, + "delete": { + "tags": [ + "用户管理" + ], + "summary": "删除用户", + "parameters": [ + { + "type": "string", + "description": "用户ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "patch": { + "tags": [ + "用户管理" + ], + "summary": "动态字段更新", + "parameters": [ + { + "type": "string", + "description": "用户ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "要更新的字段", + "name": "fields", + "in": "body", + "required": true, + "schema": { + "type": "object", + "additionalProperties": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/sys-users/{id}/password": { + "put": { + "tags": [ + "用户管理" + ], + "summary": "修改密码", + "parameters": [ + { + "type": "string", + "description": "用户ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "密码信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/controller.UpdatePasswordRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/sys/auth/info": { + "get": { + "tags": [ + "认证" + ], + "summary": "Sys获取当前登录用户信息", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/sys/auth/login": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "认证" + ], + "summary": "Sys用户登录", + "parameters": [ + { + "description": "登录信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/controller.LoginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/sys/auth/logout": { + "post": { + "tags": [ + "认证" + ], + "summary": "Sys用户登出", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/user/auth/info": { + "get": { + "tags": [ + "认证" + ], + "summary": "获取当前登录用户信息", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/user/auth/login": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "认证" + ], + "summary": "用户登录", + "parameters": [ + { + "description": "登录信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/controller.LoginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/user/auth/logout": { + "post": { + "tags": [ + "认证" + ], + "summary": "用户登出", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/user/major/list": { + "get": { + "tags": [ + "用户专业" + ], + "summary": "获取当前用户可检索列表", + "parameters": [ + { + "type": "integer", + "default": 1, + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 10, + "description": "每页数量", + "name": "size", + "in": "query" + }, + { + "type": "string", + "default": "\"\"", + "description": "批次(本科提前批/本科A段/本科B段/本科/高职高专)", + "name": "batch", + "in": "query" + }, + { + "type": "string", + "default": "\"\"", + "description": "录取概率类型(难录取/可冲击/较稳妥/可保底)", + "name": "probability", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/user/major/list_by_school": { + "get": { + "tags": [ + "用户专业" + ], + "summary": "获取当前院校下其他专业数据", + "parameters": [ + { + "type": "integer", + "default": 1, + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 10, + "description": "每页数量", + "name": "size", + "in": "query" + }, + { + "type": "string", + "description": "院校代码", + "name": "school_code", + "in": "query", + "required": true + }, + { + "type": "string", + "default": "\"\"", + "description": "录取概率类型(难录取/可冲击/较稳妥/可保底)", + "name": "probability", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/user/score": { + "get": { + "tags": [ + "用户分数" + ], + "summary": "获取当前用户的激活分数", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/user/score/list": { + "get": { + "tags": [ + "用户分数" + ], + "summary": "获取当前用户的成绩列表", + "parameters": [ + { + "type": "integer", + "default": 1, + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 10, + "description": "每页数量", + "name": "size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/user/score/save-score": { + "post": { + "tags": [ + "用户分数" + ], + "summary": "保存用户成绩", + "parameters": [ + { + "description": "成绩信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SaveScoreRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/user/score/{id}": { + "get": { + "tags": [ + "用户分数" + ], + "summary": "获取指定 ID 的分数", + "parameters": [ + { + "type": "string", + "description": "成绩ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/user/volunteer/delete": { + "delete": { + "tags": [ + "用户志愿" + ], + "summary": "删除志愿单接口", + "parameters": [ + { + "type": "string", + "description": "志愿单ID", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/user/volunteer/detail": { + "get": { + "tags": [ + "用户志愿" + ], + "summary": "获取当前志愿单详情", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/common.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/vo.VolunteerDetailResponse" + } + } + } + ] + } + } + } + } + }, + "/user/volunteer/list": { + "get": { + "tags": [ + "用户志愿" + ], + "summary": "获取当前用户志愿单列表", + "parameters": [ + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/user/volunteer/save": { + "post": { + "tags": [ + "用户志愿" + ], + "summary": "保存志愿明细", + "parameters": [ + { + "description": "志愿键列表", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SaveVolunteerRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/user/volunteer/switch": { + "post": { + "tags": [ + "用户志愿" + ], + "summary": "切换当前志愿单", + "parameters": [ + { + "type": "string", + "description": "志愿单ID", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/user/volunteer/updateName": { + "put": { + "tags": [ + "用户志愿" + ], + "summary": "编辑志愿单名称", + "parameters": [ + { + "type": "string", + "description": "志愿单ID", + "name": "id", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "志愿单名称", + "name": "name", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/users": { + "get": { + "tags": [ + "用户" + ], + "summary": "获取用户列表", + "parameters": [ + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "post": { + "tags": [ + "用户" + ], + "summary": "创建用户", + "parameters": [ + { + "description": "用户信息", + "name": "item", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/server_modules_user_dto.CreateUserRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/users/{id}": { + "get": { + "tags": [ + "用户" + ], + "summary": "获取用户详情", + "parameters": [ + { + "type": "integer", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "put": { + "tags": [ + "用户" + ], + "summary": "更新用户", + "parameters": [ + { + "type": "integer", + "description": "ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "用户信息", + "name": "item", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/server_modules_user_dto.UpdateUserRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "delete": { + "tags": [ + "用户" + ], + "summary": "删除用户", + "parameters": [ + { + "type": "integer", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "patch": { + "tags": [ + "用户" + ], + "summary": "动态字段更新", + "parameters": [ + { + "type": "integer", + "description": "ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "要更新的字段", + "name": "fields", + "in": "body", + "required": true, + "schema": { + "type": "object", + "additionalProperties": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/yx-calculation-majors": { + "get": { + "tags": [ + "计算专业" + ], + "summary": "获取计算专业列表", + "parameters": [ + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/common.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/vo.YxCalculationMajorVO" + } + } + } + } + ] + } + } + } + }, + "post": { + "tags": [ + "计算专业" + ], + "summary": "创建计算专业", + "parameters": [ + { + "description": "计算专业信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreateCalculationMajorRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/common.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/vo.YxCalculationMajorVO" + } + } + } + ] + } + } + } + } + }, + "/yx-calculation-majors/batch": { + "post": { + "tags": [ + "计算专业" + ], + "summary": "批量创建计算专业", + "parameters": [ + { + "description": "计算专业列表", + "name": "items", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.CreateCalculationMajorRequest" + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "delete": { + "tags": [ + "计算专业" + ], + "summary": "批量删除计算专业", + "parameters": [ + { + "description": "ID列表", + "name": "ids", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/yx-calculation-majors/{id}": { + "get": { + "tags": [ + "计算专业" + ], + "summary": "获取单个计算专业", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/common.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/vo.YxCalculationMajorVO" + } + } + } + ] + } + } + } + }, + "put": { + "tags": [ + "计算专业" + ], + "summary": "更新计算专业", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "计算专业信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateCalculationMajorRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/common.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/vo.YxCalculationMajorVO" + } + } + } + ] + } + } + } + }, + "delete": { + "tags": [ + "计算专业" + ], + "summary": "删除计算专业", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "patch": { + "tags": [ + "计算专业" + ], + "summary": "动态字段更新", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "要更新的字段", + "name": "fields", + "in": "body", + "required": true, + "schema": { + "type": "object", + "additionalProperties": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/yx-history-enrolls": { + "get": { + "tags": [ + "历年招生" + ], + "summary": "获取历年招生列表", + "parameters": [ + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "post": { + "tags": [ + "历年招生" + ], + "summary": "创建历年招生记录", + "parameters": [ + { + "description": "历年招生信息", + "name": "item", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entity.YxHistoryMajorEnroll" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/yx-history-enrolls/batch": { + "post": { + "tags": [ + "历年招生" + ], + "summary": "批量创建历年招生记录", + "parameters": [ + { + "description": "历年招生列表", + "name": "items", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/entity.YxHistoryMajorEnroll" + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "delete": { + "tags": [ + "历年招生" + ], + "summary": "批量删除历年招生记录", + "parameters": [ + { + "description": "ID列表", + "name": "ids", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/yx-history-enrolls/{id}": { + "get": { + "tags": [ + "历年招生" + ], + "summary": "获取单个历年招生记录", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "put": { + "tags": [ + "历年招生" + ], + "summary": "更新历年招生记录", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "历年招生信息", + "name": "item", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entity.YxHistoryMajorEnroll" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "delete": { + "tags": [ + "历年招生" + ], + "summary": "删除历年招生记录", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "patch": { + "tags": [ + "历年招生" + ], + "summary": "动态字段更新", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "要更新的字段", + "name": "fields", + "in": "body", + "required": true, + "schema": { + "type": "object", + "additionalProperties": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/yx-school-majors": { + "get": { + "tags": [ + "院校专业" + ], + "summary": "获取院校专业列表", + "parameters": [ + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "post": { + "tags": [ + "院校专业" + ], + "summary": "创建院校专业", + "parameters": [ + { + "description": "院校专业信息", + "name": "item", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entity.YxSchoolMajor" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/yx-school-majors/batch": { + "post": { + "tags": [ + "院校专业" + ], + "summary": "批量创建院校专业", + "parameters": [ + { + "description": "院校专业列表", + "name": "items", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/entity.YxSchoolMajor" + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "delete": { + "tags": [ + "院校专业" + ], + "summary": "批量删除院校专业", + "parameters": [ + { + "description": "ID列表", + "name": "ids", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/yx-school-majors/{id}": { + "get": { + "tags": [ + "院校专业" + ], + "summary": "获取单个院校专业", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "put": { + "tags": [ + "院校专业" + ], + "summary": "更新院校专业", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "院校专业信息", + "name": "item", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entity.YxSchoolMajor" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "delete": { + "tags": [ + "院校专业" + ], + "summary": "删除院校专业", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "patch": { + "tags": [ + "院校专业" + ], + "summary": "动态字段更新", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "要更新的字段", + "name": "fields", + "in": "body", + "required": true, + "schema": { + "type": "object", + "additionalProperties": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/yx-user-scores": { + "get": { + "tags": [ + "用户分数" + ], + "summary": "获取用户分数列表", + "parameters": [ + { + "type": "integer", + "default": 1, + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 10, + "description": "每页数量", + "name": "size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "post": { + "tags": [ + "用户分数" + ], + "summary": "创建用户分数", + "parameters": [ + { + "description": "用户分数信息", + "name": "item", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entity.YxUserScore" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/yx-user-scores/{id}": { + "get": { + "tags": [ + "用户分数" + ], + "summary": "获取单个用户分数", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "put": { + "tags": [ + "用户分数" + ], + "summary": "更新用户分数", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "用户分数信息", + "name": "item", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entity.YxUserScore" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "delete": { + "tags": [ + "用户分数" + ], + "summary": "删除用户分数", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/yx-volunteer-records": { + "get": { + "tags": [ + "志愿明细" + ], + "summary": "获取志愿明细列表", + "parameters": [ + { + "type": "integer", + "default": 1, + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 10, + "description": "每页数量", + "name": "size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "post": { + "tags": [ + "志愿明细" + ], + "summary": "创建志愿明细", + "parameters": [ + { + "description": "志愿明细信息", + "name": "item", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entity.YxVolunteerRecord" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/yx-volunteer-records/{id}": { + "get": { + "tags": [ + "志愿明细" + ], + "summary": "获取单个志愿明细", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "put": { + "tags": [ + "志愿明细" + ], + "summary": "更新志愿明细", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "志愿明细信息", + "name": "item", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entity.YxVolunteerRecord" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + }, + "delete": { + "tags": [ + "志愿明细" + ], + "summary": "删除志愿明细", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + }, + "/yx-volunteers": { + "get": { + "tags": [ + "志愿" + ], + "summary": "获取志愿列表", + "parameters": [ + { + "type": "integer", + "default": 1, + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 10, + "description": "每页数量", + "name": "size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/common.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/vo.YxVolunteerVO" + } + } + } + } + ] + } + } + } + }, + "post": { + "tags": [ + "志愿" + ], + "summary": "创建志愿", + "parameters": [ + { + "description": "志愿信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreateVolunteerRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/common.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/vo.YxVolunteerVO" + } + } + } + ] + } + } + } + } + }, + "/yx-volunteers/{id}": { + "get": { + "tags": [ + "志愿" + ], + "summary": "获取单个志愿", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/common.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/vo.YxVolunteerVO" + } + } + } + ] + } + } + } + }, + "put": { + "tags": [ + "志愿" + ], + "summary": "更新志愿", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "志愿信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateVolunteerRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/common.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/vo.YxVolunteerVO" + } + } + } + ] + } + } + } + }, + "delete": { + "tags": [ + "志愿" + ], + "summary": "删除志愿", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/common.Response" + } + } + } + } + } + }, + "definitions": { + "common.Response": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "data": {}, + "message": { + "type": "string" + } + } + }, + "controller.LoginRequest": { + "type": "object", + "required": [ + "password", + "username" + ], + "properties": { + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "controller.UpdatePasswordRequest": { + "type": "object", + "required": [ + "newPassword", + "oldPassword" + ], + "properties": { + "newPassword": { + "type": "string" + }, + "oldPassword": { + "type": "string" + } + } + }, + "dto.CreateCalculationMajorRequest": { + "type": "object", + "required": [ + "majorCode", + "schoolCode" + ], + "properties": { + "batch": { + "description": "批次", + "type": "string" + }, + "category": { + "description": "类别", + "type": "string" + }, + "chineseScoreLimitation": { + "description": "语文分数限制", + "type": "number" + }, + "culturalScoreLimitation": { + "description": "文化成绩限制", + "type": "number" + }, + "detail": { + "description": "详情", + "type": "string" + }, + "englishScoreLimitation": { + "description": "英语分数限制", + "type": "number" + }, + "enrollProbability": { + "description": "录取概率", + "type": "number" + }, + "enrollmentCode": { + "description": "招生代码", + "type": "string" + }, + "institutionType": { + "description": "院校类型", + "type": "string" + }, + "kslx": { + "description": "考试类型", + "type": "string" + }, + "limitation": { + "description": "限制条件", + "type": "string" + }, + "mainSubjects": { + "description": "主考科目", + "type": "string" + }, + "majorCode": { + "description": "专业代码", + "type": "string" + }, + "majorName": { + "description": "专业名称", + "type": "string" + }, + "majorType": { + "description": "专业类型", + "type": "string" + }, + "majorTypeChild": { + "description": "子专业类型", + "type": "string" + }, + "planNum": { + "description": "计划人数", + "type": "integer" + }, + "probabilityOperator": { + "description": "概率操作符", + "type": "string" + }, + "professionalScoreLimitation": { + "description": "专业分数限制", + "type": "number" + }, + "province": { + "description": "省份", + "type": "string" + }, + "rulesEnrollProbability": { + "description": "录取规则概率", + "type": "string" + }, + "schoolCode": { + "description": "院校代码", + "type": "string" + }, + "schoolName": { + "description": "院校名称", + "type": "string" + }, + "schoolNature": { + "description": "院校性质", + "type": "string" + }, + "state": { + "description": "状态", + "type": "string" + }, + "studentScore": { + "description": "学生分数", + "type": "number" + }, + "tuition": { + "description": "学费", + "type": "string" + } + } + }, + "dto.CreatePlatformUserRequest": { + "type": "object" + }, + "dto.CreateVolunteerRequest": { + "type": "object", + "required": [ + "scoreId", + "volunteerName" + ], + "properties": { + "createType": { + "description": "生成类型(1.手动生成,2.智能生成)", + "type": "string" + }, + "scoreId": { + "description": "关联成绩ID", + "type": "string" + }, + "volunteerName": { + "description": "志愿单名称", + "type": "string" + } + } + }, + "dto.SaveScoreRequest": { + "type": "object", + "required": [ + "ChineseScore", + "CognitioPolyclinic", + "CulturalScore", + "EnglishScore", + "ProfessionalCategory", + "ProfessionalCategoryChildren", + "ProfessionalCategoryChildrenScore", + "ProfessionalScore", + "Province", + "SubjectList" + ], + "properties": { + "ChineseScore": { + "type": "number" + }, + "CognitioPolyclinic": { + "type": "string" + }, + "CulturalScore": { + "type": "number" + }, + "EnglishScore": { + "type": "number" + }, + "ProfessionalCategory": { + "type": "string" + }, + "ProfessionalCategoryChildren": { + "type": "array", + "items": { + "type": "string" + } + }, + "ProfessionalCategoryChildrenScore": { + "type": "object", + "additionalProperties": { + "type": "number", + "format": "float64" + } + }, + "ProfessionalScore": { + "type": "number" + }, + "Province": { + "type": "string" + }, + "SubjectList": { + "type": "array", + "items": { + "type": "string" + } + }, + "createBy": { + "type": "string" + } + } + }, + "dto.SaveVolunteerRequest": { + "type": "object", + "required": [ + "keys" + ], + "properties": { + "keys": { + "description": "Keys: schoolCode_majorCode_enrollmentCode", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "dto.UpdateCalculationMajorRequest": { + "type": "object", + "properties": { + "batch": { + "description": "批次", + "type": "string" + }, + "category": { + "description": "类别", + "type": "string" + }, + "chineseScoreLimitation": { + "description": "语文分数限制", + "type": "number" + }, + "culturalScoreLimitation": { + "description": "文化成绩限制", + "type": "number" + }, + "detail": { + "description": "详情", + "type": "string" + }, + "englishScoreLimitation": { + "description": "英语分数限制", + "type": "number" + }, + "enrollProbability": { + "description": "录取概率", + "type": "number" + }, + "enrollmentCode": { + "description": "招生代码", + "type": "string" + }, + "institutionType": { + "description": "院校类型", + "type": "string" + }, + "kslx": { + "description": "考试类型", + "type": "string" + }, + "limitation": { + "description": "限制条件", + "type": "string" + }, + "mainSubjects": { + "description": "主考科目", + "type": "string" + }, + "majorName": { + "description": "专业名称", + "type": "string" + }, + "majorType": { + "description": "专业类型", + "type": "string" + }, + "majorTypeChild": { + "description": "子专业类型", + "type": "string" + }, + "planNum": { + "description": "计划人数", + "type": "integer" + }, + "probabilityOperator": { + "description": "概率操作符", + "type": "string" + }, + "professionalScoreLimitation": { + "description": "专业分数限制", + "type": "number" + }, + "province": { + "description": "省份", + "type": "string" + }, + "rulesEnrollProbability": { + "description": "录取规则概率", + "type": "string" + }, + "schoolName": { + "description": "院校名称", + "type": "string" + }, + "schoolNature": { + "description": "院校性质", + "type": "string" + }, + "state": { + "description": "状态", + "type": "string" + }, + "studentScore": { + "description": "学生分数", + "type": "number" + }, + "tuition": { + "description": "学费", + "type": "string" + } + } + }, + "dto.UpdatePlatformUserRequest": { + "type": "object" + }, + "dto.UpdateVolunteerRequest": { + "type": "object", + "properties": { + "state": { + "description": "志愿单状态(0-否,1-正在使用,2-历史)", + "type": "string" + }, + "volunteerName": { + "description": "志愿单名称", + "type": "string" + } + } + }, + "entity.YxHistoryMajorEnroll": { + "type": "object", + "properties": { + "actualPitcherNum": { + "type": "integer" + }, + "admissionLine": { + "type": "number" + }, + "admissionNum": { + "type": "integer" + }, + "batch": { + "type": "string" + }, + "category": { + "type": "string" + }, + "checkMaster": { + "type": "string" + }, + "controlLine": { + "type": "number" + }, + "createBy": { + "type": "string" + }, + "createTime": { + "type": "string" + }, + "detail": { + "type": "string" + }, + "enrollNum": { + "type": "integer" + }, + "enrollmentCode": { + "type": "string" + }, + "id": { + "type": "string" + }, + "institutionCode": { + "type": "string" + }, + "mainSubjects": { + "type": "string" + }, + "majorCode": { + "type": "string" + }, + "majorName": { + "type": "string" + }, + "majorType": { + "type": "string" + }, + "majorTypeChild": { + "type": "string" + }, + "oneVolunteerAdmissionNum": { + "type": "integer" + }, + "probabilityOperator": { + "type": "string" + }, + "rulesEnrollProbability": { + "type": "string" + }, + "schoolCode": { + "type": "string" + }, + "schoolName": { + "type": "string" + }, + "scoreLineDifference": { + "type": "number" + }, + "sysOrgCode": { + "type": "string" + }, + "tuition": { + "type": "string" + }, + "updateBy": { + "type": "string" + }, + "updateTime": { + "type": "string" + }, + "year": { + "type": "string" + } + } + }, + "entity.YxSchoolMajor": { + "type": "object", + "properties": { + "batch": { + "type": "string" + }, + "category": { + "type": "string" + }, + "checkMaster": { + "type": "string" + }, + "chineseScoreLimitation": { + "type": "number" + }, + "createBy": { + "type": "string" + }, + "createTime": { + "type": "string" + }, + "culturalControlLine": { + "type": "number" + }, + "culturalScoreLimitation": { + "type": "number" + }, + "detail": { + "type": "string" + }, + "englishScoreLimitation": { + "type": "number" + }, + "enrollmentCode": { + "type": "string" + }, + "id": { + "type": "string" + }, + "kslx": { + "type": "string" + }, + "limitation": { + "type": "string" + }, + "mainSubjects": { + "type": "string" + }, + "majorCode": { + "type": "string" + }, + "majorName": { + "type": "string" + }, + "majorType": { + "type": "string" + }, + "majorTypeChild": { + "type": "string" + }, + "planNum": { + "type": "integer" + }, + "privateProbabilityOperator": { + "type": "string" + }, + "privateRulesEnrollProbability": { + "type": "string" + }, + "probabilityOperator": { + "type": "string" + }, + "professionalScoreLimitation": { + "type": "number" + }, + "rulesEnrollProbability": { + "type": "string" + }, + "rulesEnrollProbabilitySx": { + "type": "string" + }, + "schoolCode": { + "type": "string" + }, + "schoolName": { + "type": "string" + }, + "semester": { + "type": "string" + }, + "specialControlLine": { + "type": "number" + }, + "state": { + "type": "string" + }, + "tuition": { + "type": "string" + }, + "updateBy": { + "type": "string" + }, + "updateTime": { + "type": "string" + } + } + }, + "entity.YxUserScore": { + "type": "object", + "properties": { + "batch": { + "description": "录取批次", + "type": "string" + }, + "calculationTableName": { + "description": "记录结果表名", + "type": "string" + }, + "chineseScore": { + "description": "语文成绩", + "type": "number" + }, + "cognitioPolyclinic": { + "description": "文理分班(文科/理科)", + "type": "string" + }, + "createBy": { + "description": "创建人", + "type": "string" + }, + "createTime": { + "description": "创建时间", + "type": "string" + }, + "culturalScore": { + "description": "文化成绩分", + "type": "number" + }, + "educationalLevel": { + "description": "学历层次(1-本科,2-专科)", + "type": "string" + }, + "englishScore": { + "description": "英语成绩", + "type": "number" + }, + "fzby": { + "description": "服装表演", + "type": "number" + }, + "id": { + "type": "string" + }, + "jwtNum": { + "description": "较稳妥专业数量", + "type": "integer" + }, + "kbdNum": { + "description": "可保底专业数量", + "type": "integer" + }, + "kcjNum": { + "description": "可冲击专业数量", + "type": "integer" + }, + "nlqNum": { + "description": "难录取专业数量", + "type": "integer" + }, + "professionalCategory": { + "description": "专业类别(美术类/...)", + "type": "string" + }, + "professionalCategoryChildren": { + "description": "子级专业类别", + "type": "string" + }, + "professionalScore": { + "description": "专业成绩分", + "type": "number" + }, + "province": { + "description": "高考省份", + "type": "string" + }, + "ranking": { + "description": "位次", + "type": "integer" + }, + "state": { + "description": "状态(0-未使用,1-使用中)", + "type": "string" + }, + "subjects": { + "description": "选课", + "type": "string" + }, + "type": { + "description": "填报类型(1-普通类 2-艺术类)", + "type": "string" + }, + "updateBy": { + "description": "修改人", + "type": "string" + }, + "updateTime": { + "description": "修改时间", + "type": "string" + }, + "xjysby": { + "description": "戏剧影视表演", + "type": "number" + }, + "xjysdy": { + "description": "戏剧影视导演", + "type": "number" + }, + "yybyqy": { + "description": "音乐表演器乐", + "type": "number" + }, + "yybysy": { + "description": "音乐表演声乐", + "type": "number" + }, + "yyjy": { + "description": "音乐教育", + "type": "number" + } + } + }, + "entity.YxVolunteerRecord": { + "type": "object", + "properties": { + "batch": { + "description": "录取批次", + "type": "string" + }, + "calculationMajorId": { + "description": "专业折算id", + "type": "string" + }, + "createBy": { + "description": "创建人", + "type": "string" + }, + "createTime": { + "description": "创建日期", + "type": "string" + }, + "enrollProbability": { + "description": "录取概率", + "type": "number" + }, + "enrollmentCode": { + "description": "招生代码", + "type": "string" + }, + "fctj": { + "description": "服从调剂", + "type": "integer" + }, + "id": { + "type": "string" + }, + "indexs": { + "description": "志愿顺序", + "type": "integer" + }, + "majorCode": { + "description": "专业编码", + "type": "string" + }, + "schoolCode": { + "description": "学校编码", + "type": "string" + }, + "studentConvertedScore": { + "description": "折合分数", + "type": "number" + }, + "volunteerId": { + "description": "志愿单id", + "type": "string" + } + } + }, + "server_modules_system_dto.CreateUserRequest": { + "type": "object", + "required": [ + "password", + "realname", + "username" + ], + "properties": { + "avatar": { + "description": "头像", + "type": "string" + }, + "birthday": { + "description": "生日", + "type": "string" + }, + "email": { + "description": "电子邮件", + "type": "string" + }, + "orgCode": { + "description": "机构编码", + "type": "string" + }, + "password": { + "description": "密码", + "type": "string", + "minLength": 6 + }, + "phone": { + "description": "电话", + "type": "string" + }, + "realname": { + "description": "真实姓名", + "type": "string" + }, + "sex": { + "description": "性别(0-未知,1-男,2-女)", + "type": "integer" + }, + "username": { + "description": "登录账号", + "type": "string", + "maxLength": 20, + "minLength": 3 + } + } + }, + "server_modules_system_dto.UpdateUserRequest": { + "type": "object", + "properties": { + "avatar": { + "description": "头像", + "type": "string" + }, + "birthday": { + "description": "生日", + "type": "string" + }, + "email": { + "description": "电子邮件", + "type": "string" + }, + "orgCode": { + "description": "机构编码", + "type": "string" + }, + "phone": { + "description": "电话", + "type": "string" + }, + "realname": { + "description": "真实姓名", + "type": "string" + }, + "sex": { + "description": "性别(0-未知,1-男,2-女)", + "type": "integer" + }, + "status": { + "description": "状态(1-正常,2-冻结)", + "type": "integer" + } + } + }, + "server_modules_user_dto.CreateUserRequest": { + "type": "object", + "properties": { + "avatarUrl": { + "type": "string" + }, + "gender": { + "type": "integer" + }, + "nickname": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "username": { + "type": "string" + } + } + }, + "server_modules_user_dto.UpdateUserRequest": { + "type": "object", + "properties": { + "avatarUrl": { + "type": "string" + }, + "gender": { + "type": "integer" + }, + "nickname": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "username": { + "type": "string" + } + } + }, + "vo.SysUserVO": { + "type": "object", + "properties": { + "avatar": { + "description": "头像", + "type": "string" + }, + "birthday": { + "description": "生日", + "type": "string" + }, + "createTime": { + "description": "创建时间", + "type": "string" + }, + "email": { + "description": "电子邮件", + "type": "string" + }, + "id": { + "type": "string" + }, + "orgCode": { + "description": "机构编码", + "type": "string" + }, + "phone": { + "description": "电话", + "type": "string" + }, + "realname": { + "description": "真实姓名", + "type": "string" + }, + "sex": { + "description": "性别(0-未知,1-男,2-女)", + "type": "integer" + }, + "status": { + "description": "状态(1-正常,2-冻结)", + "type": "integer" + }, + "updateTime": { + "description": "更新时间", + "type": "string" + }, + "username": { + "description": "登录账号", + "type": "string" + } + } + }, + "vo.VolunteerDetailResponse": { + "type": "object", + "properties": { + "items": { + "description": "志愿明细列表 *VolunteerItemsVO", + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/definitions/vo.VolunteerDetailVO" + } + } + }, + "volunteer": { + "description": "志愿单信息", + "allOf": [ + { + "$ref": "#/definitions/vo.VolunteerInfoVO" + } + ] + } + } + }, + "vo.VolunteerDetailVO": { + "type": "object", + "properties": { + "batch": { + "description": "批次", + "type": "string" + }, + "calculationMajorId": { + "description": "计算专业ID", + "type": "string" + }, + "createBy": { + "description": "创建人", + "type": "string" + }, + "createTime": { + "description": "创建时间", + "type": "string" + }, + "enrollProbability": { + "description": "录取概率", + "type": "number" + }, + "enrollmentCode": { + "description": "招生代码", + "type": "string" + }, + "id": { + "description": "志愿记录ID", + "type": "string" + }, + "indexs": { + "description": "志愿序号", + "type": "integer" + }, + "institutionType": { + "description": "院校类型", + "type": "string" + }, + "majorCode": { + "description": "专业代码", + "type": "string" + }, + "majorDetail": { + "description": "专业详情", + "type": "string" + }, + "majorName": { + "description": "专业名称", + "type": "string" + }, + "planNum": { + "description": "计划人数", + "type": "integer" + }, + "province": { + "description": "省份", + "type": "string" + }, + "schoolCode": { + "description": "院校代码", + "type": "string" + }, + "schoolIcon": { + "description": "院校图标", + "type": "string" + }, + "schoolName": { + "description": "扩展字段(来自 SchoolMajorDTO)", + "type": "string" + }, + "schoolNature": { + "description": "院校性质", + "type": "string" + }, + "studentConvertedScore": { + "description": "学生折合分", + "type": "number" + }, + "tuition": { + "description": "学费", + "type": "string" + }, + "volunteerId": { + "description": "志愿单ID", + "type": "string" + } + } + }, + "vo.VolunteerInfoVO": { + "type": "object", + "properties": { + "createBy": { + "type": "string" + }, + "createTime": { + "type": "string" + }, + "createType": { + "type": "string" + }, + "id": { + "type": "string" + }, + "scoreId": { + "type": "string" + }, + "state": { + "type": "string" + }, + "updateBy": { + "type": "string" + }, + "updateTime": { + "type": "string" + }, + "volunteerName": { + "type": "string" + } + } + }, + "vo.YxCalculationMajorVO": { + "type": "object", + "properties": { + "batch": { + "description": "批次", + "type": "string" + }, + "category": { + "description": "类别", + "type": "string" + }, + "chineseScoreLimitation": { + "description": "语文分数限制", + "type": "number" + }, + "createBy": { + "description": "创建人", + "type": "string" + }, + "createTime": { + "description": "创建时间", + "type": "string" + }, + "culturalScoreLimitation": { + "description": "文化成绩限制", + "type": "number" + }, + "detail": { + "description": "详情", + "type": "string" + }, + "englishScoreLimitation": { + "description": "英语分数限制", + "type": "number" + }, + "enrollProbability": { + "description": "录取概率", + "type": "number" + }, + "enrollmentCode": { + "description": "招生代码", + "type": "string" + }, + "firstLevelDiscipline": { + "description": "一级学科", + "type": "string" + }, + "id": { + "type": "string" + }, + "institutionType": { + "description": "院校类型", + "type": "string" + }, + "kslx": { + "description": "考试类型", + "type": "string" + }, + "limitation": { + "description": "限制条件", + "type": "string" + }, + "mainSubjects": { + "description": "主考科目", + "type": "string" + }, + "majorCode": { + "description": "专业代码", + "type": "string" + }, + "majorName": { + "description": "专业名称", + "type": "string" + }, + "majorType": { + "description": "专业类型", + "type": "string" + }, + "majorTypeChild": { + "description": "子专业类型", + "type": "string" + }, + "planNum": { + "description": "计划人数", + "type": "integer" + }, + "privateStudentScore": { + "description": "私有学生分数", + "type": "number" + }, + "probabilityOperator": { + "description": "概率操作符", + "type": "string" + }, + "professionalScoreLimitation": { + "description": "专业分数限制", + "type": "number" + }, + "province": { + "description": "省份", + "type": "string" + }, + "rulesEnrollProbability": { + "description": "录取规则概率", + "type": "string" + }, + "schoolCode": { + "description": "院校代码", + "type": "string" + }, + "schoolName": { + "description": "院校名称", + "type": "string" + }, + "schoolNature": { + "description": "院校性质", + "type": "string" + }, + "state": { + "description": "状态", + "type": "string" + }, + "studentConvertedScore": { + "description": "学生折合分", + "type": "number" + }, + "studentScore": { + "description": "学生分数", + "type": "number" + }, + "tuition": { + "description": "学费", + "type": "string" + }, + "updateBy": { + "description": "更新人", + "type": "string" + }, + "updateTime": { + "description": "更新时间", + "type": "string" + } + } + }, + "vo.YxVolunteerVO": { + "type": "object", + "properties": { + "createBy": { + "description": "创建人", + "type": "string" + }, + "createTime": { + "description": "创建日期", + "type": "string" + }, + "createType": { + "description": "生成类型(1.手动生成,2.智能生成)", + "type": "string" + }, + "id": { + "type": "string" + }, + "scoreId": { + "description": "使用成绩id", + "type": "string" + }, + "state": { + "description": "志愿单状态(0-否,1-正在使用,2-历史)", + "type": "string" + }, + "sysOrgCode": { + "description": "所属部门", + "type": "string" + }, + "updateBy": { + "description": "更新人", + "type": "string" + }, + "updateTime": { + "description": "更新日期", + "type": "string" + }, + "volunteerName": { + "description": "志愿单名称", + "type": "string" + } + } + } + }, + "securityDefinitions": { + "Bearer": { + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +} \ No newline at end of file diff --git a/server/docs/swagger.yaml b/server/docs/swagger.yaml new file mode 100644 index 0000000..967c1d7 --- /dev/null +++ b/server/docs/swagger.yaml @@ -0,0 +1,2360 @@ +basePath: /api +definitions: + common.Response: + properties: + code: + type: integer + data: {} + message: + type: string + type: object + controller.LoginRequest: + properties: + password: + type: string + username: + type: string + required: + - password + - username + type: object + controller.UpdatePasswordRequest: + properties: + newPassword: + type: string + oldPassword: + type: string + required: + - newPassword + - oldPassword + type: object + dto.CreateCalculationMajorRequest: + properties: + batch: + description: 批次 + type: string + category: + description: 类别 + type: string + chineseScoreLimitation: + description: 语文分数限制 + type: number + culturalScoreLimitation: + description: 文化成绩限制 + type: number + detail: + description: 详情 + type: string + englishScoreLimitation: + description: 英语分数限制 + type: number + enrollProbability: + description: 录取概率 + type: number + enrollmentCode: + description: 招生代码 + type: string + institutionType: + description: 院校类型 + type: string + kslx: + description: 考试类型 + type: string + limitation: + description: 限制条件 + type: string + mainSubjects: + description: 主考科目 + type: string + majorCode: + description: 专业代码 + type: string + majorName: + description: 专业名称 + type: string + majorType: + description: 专业类型 + type: string + majorTypeChild: + description: 子专业类型 + type: string + planNum: + description: 计划人数 + type: integer + probabilityOperator: + description: 概率操作符 + type: string + professionalScoreLimitation: + description: 专业分数限制 + type: number + province: + description: 省份 + type: string + rulesEnrollProbability: + description: 录取规则概率 + type: string + schoolCode: + description: 院校代码 + type: string + schoolName: + description: 院校名称 + type: string + schoolNature: + description: 院校性质 + type: string + state: + description: 状态 + type: string + studentScore: + description: 学生分数 + type: number + tuition: + description: 学费 + type: string + required: + - majorCode + - schoolCode + type: object + dto.CreatePlatformUserRequest: + type: object + dto.CreateVolunteerRequest: + properties: + createType: + description: 生成类型(1.手动生成,2.智能生成) + type: string + scoreId: + description: 关联成绩ID + type: string + volunteerName: + description: 志愿单名称 + type: string + required: + - scoreId + - volunteerName + type: object + dto.SaveScoreRequest: + properties: + ChineseScore: + type: number + CognitioPolyclinic: + type: string + CulturalScore: + type: number + EnglishScore: + type: number + ProfessionalCategory: + type: string + ProfessionalCategoryChildren: + items: + type: string + type: array + ProfessionalCategoryChildrenScore: + additionalProperties: + format: float64 + type: number + type: object + ProfessionalScore: + type: number + Province: + type: string + SubjectList: + items: + type: string + type: array + createBy: + type: string + required: + - ChineseScore + - CognitioPolyclinic + - CulturalScore + - EnglishScore + - ProfessionalCategory + - ProfessionalCategoryChildren + - ProfessionalCategoryChildrenScore + - ProfessionalScore + - Province + - SubjectList + type: object + dto.SaveVolunteerRequest: + properties: + keys: + description: 'Keys: schoolCode_majorCode_enrollmentCode' + items: + type: string + type: array + required: + - keys + type: object + dto.UpdateCalculationMajorRequest: + properties: + batch: + description: 批次 + type: string + category: + description: 类别 + type: string + chineseScoreLimitation: + description: 语文分数限制 + type: number + culturalScoreLimitation: + description: 文化成绩限制 + type: number + detail: + description: 详情 + type: string + englishScoreLimitation: + description: 英语分数限制 + type: number + enrollProbability: + description: 录取概率 + type: number + enrollmentCode: + description: 招生代码 + type: string + institutionType: + description: 院校类型 + type: string + kslx: + description: 考试类型 + type: string + limitation: + description: 限制条件 + type: string + mainSubjects: + description: 主考科目 + type: string + majorName: + description: 专业名称 + type: string + majorType: + description: 专业类型 + type: string + majorTypeChild: + description: 子专业类型 + type: string + planNum: + description: 计划人数 + type: integer + probabilityOperator: + description: 概率操作符 + type: string + professionalScoreLimitation: + description: 专业分数限制 + type: number + province: + description: 省份 + type: string + rulesEnrollProbability: + description: 录取规则概率 + type: string + schoolName: + description: 院校名称 + type: string + schoolNature: + description: 院校性质 + type: string + state: + description: 状态 + type: string + studentScore: + description: 学生分数 + type: number + tuition: + description: 学费 + type: string + type: object + dto.UpdatePlatformUserRequest: + type: object + dto.UpdateVolunteerRequest: + properties: + state: + description: 志愿单状态(0-否,1-正在使用,2-历史) + type: string + volunteerName: + description: 志愿单名称 + type: string + type: object + entity.YxHistoryMajorEnroll: + properties: + actualPitcherNum: + type: integer + admissionLine: + type: number + admissionNum: + type: integer + batch: + type: string + category: + type: string + checkMaster: + type: string + controlLine: + type: number + createBy: + type: string + createTime: + type: string + detail: + type: string + enrollNum: + type: integer + enrollmentCode: + type: string + id: + type: string + institutionCode: + type: string + mainSubjects: + type: string + majorCode: + type: string + majorName: + type: string + majorType: + type: string + majorTypeChild: + type: string + oneVolunteerAdmissionNum: + type: integer + probabilityOperator: + type: string + rulesEnrollProbability: + type: string + schoolCode: + type: string + schoolName: + type: string + scoreLineDifference: + type: number + sysOrgCode: + type: string + tuition: + type: string + updateBy: + type: string + updateTime: + type: string + year: + type: string + type: object + entity.YxSchoolMajor: + properties: + batch: + type: string + category: + type: string + checkMaster: + type: string + chineseScoreLimitation: + type: number + createBy: + type: string + createTime: + type: string + culturalControlLine: + type: number + culturalScoreLimitation: + type: number + detail: + type: string + englishScoreLimitation: + type: number + enrollmentCode: + type: string + id: + type: string + kslx: + type: string + limitation: + type: string + mainSubjects: + type: string + majorCode: + type: string + majorName: + type: string + majorType: + type: string + majorTypeChild: + type: string + planNum: + type: integer + privateProbabilityOperator: + type: string + privateRulesEnrollProbability: + type: string + probabilityOperator: + type: string + professionalScoreLimitation: + type: number + rulesEnrollProbability: + type: string + rulesEnrollProbabilitySx: + type: string + schoolCode: + type: string + schoolName: + type: string + semester: + type: string + specialControlLine: + type: number + state: + type: string + tuition: + type: string + updateBy: + type: string + updateTime: + type: string + type: object + entity.YxUserScore: + properties: + batch: + description: 录取批次 + type: string + calculationTableName: + description: 记录结果表名 + type: string + chineseScore: + description: 语文成绩 + type: number + cognitioPolyclinic: + description: 文理分班(文科/理科) + type: string + createBy: + description: 创建人 + type: string + createTime: + description: 创建时间 + type: string + culturalScore: + description: 文化成绩分 + type: number + educationalLevel: + description: 学历层次(1-本科,2-专科) + type: string + englishScore: + description: 英语成绩 + type: number + fzby: + description: 服装表演 + type: number + id: + type: string + jwtNum: + description: 较稳妥专业数量 + type: integer + kbdNum: + description: 可保底专业数量 + type: integer + kcjNum: + description: 可冲击专业数量 + type: integer + nlqNum: + description: 难录取专业数量 + type: integer + professionalCategory: + description: 专业类别(美术类/...) + type: string + professionalCategoryChildren: + description: 子级专业类别 + type: string + professionalScore: + description: 专业成绩分 + type: number + province: + description: 高考省份 + type: string + ranking: + description: 位次 + type: integer + state: + description: 状态(0-未使用,1-使用中) + type: string + subjects: + description: 选课 + type: string + type: + description: 填报类型(1-普通类 2-艺术类) + type: string + updateBy: + description: 修改人 + type: string + updateTime: + description: 修改时间 + type: string + xjysby: + description: 戏剧影视表演 + type: number + xjysdy: + description: 戏剧影视导演 + type: number + yybyqy: + description: 音乐表演器乐 + type: number + yybysy: + description: 音乐表演声乐 + type: number + yyjy: + description: 音乐教育 + type: number + type: object + entity.YxVolunteerRecord: + properties: + batch: + description: 录取批次 + type: string + calculationMajorId: + description: 专业折算id + type: string + createBy: + description: 创建人 + type: string + createTime: + description: 创建日期 + type: string + enrollProbability: + description: 录取概率 + type: number + enrollmentCode: + description: 招生代码 + type: string + fctj: + description: 服从调剂 + type: integer + id: + type: string + indexs: + description: 志愿顺序 + type: integer + majorCode: + description: 专业编码 + type: string + schoolCode: + description: 学校编码 + type: string + studentConvertedScore: + description: 折合分数 + type: number + volunteerId: + description: 志愿单id + type: string + type: object + server_modules_system_dto.CreateUserRequest: + properties: + avatar: + description: 头像 + type: string + birthday: + description: 生日 + type: string + email: + description: 电子邮件 + type: string + orgCode: + description: 机构编码 + type: string + password: + description: 密码 + minLength: 6 + type: string + phone: + description: 电话 + type: string + realname: + description: 真实姓名 + type: string + sex: + description: 性别(0-未知,1-男,2-女) + type: integer + username: + description: 登录账号 + maxLength: 20 + minLength: 3 + type: string + required: + - password + - realname + - username + type: object + server_modules_system_dto.UpdateUserRequest: + properties: + avatar: + description: 头像 + type: string + birthday: + description: 生日 + type: string + email: + description: 电子邮件 + type: string + orgCode: + description: 机构编码 + type: string + phone: + description: 电话 + type: string + realname: + description: 真实姓名 + type: string + sex: + description: 性别(0-未知,1-男,2-女) + type: integer + status: + description: 状态(1-正常,2-冻结) + type: integer + type: object + server_modules_user_dto.CreateUserRequest: + properties: + avatarUrl: + type: string + gender: + type: integer + nickname: + type: string + phone: + type: string + status: + type: integer + username: + type: string + type: object + server_modules_user_dto.UpdateUserRequest: + properties: + avatarUrl: + type: string + gender: + type: integer + nickname: + type: string + phone: + type: string + status: + type: integer + username: + type: string + type: object + vo.SysUserVO: + properties: + avatar: + description: 头像 + type: string + birthday: + description: 生日 + type: string + createTime: + description: 创建时间 + type: string + email: + description: 电子邮件 + type: string + id: + type: string + orgCode: + description: 机构编码 + type: string + phone: + description: 电话 + type: string + realname: + description: 真实姓名 + type: string + sex: + description: 性别(0-未知,1-男,2-女) + type: integer + status: + description: 状态(1-正常,2-冻结) + type: integer + updateTime: + description: 更新时间 + type: string + username: + description: 登录账号 + type: string + type: object + vo.VolunteerDetailResponse: + properties: + items: + additionalProperties: + items: + $ref: '#/definitions/vo.VolunteerDetailVO' + type: array + description: 志愿明细列表 *VolunteerItemsVO + type: object + volunteer: + allOf: + - $ref: '#/definitions/vo.VolunteerInfoVO' + description: 志愿单信息 + type: object + vo.VolunteerDetailVO: + properties: + batch: + description: 批次 + type: string + calculationMajorId: + description: 计算专业ID + type: string + createBy: + description: 创建人 + type: string + createTime: + description: 创建时间 + type: string + enrollProbability: + description: 录取概率 + type: number + enrollmentCode: + description: 招生代码 + type: string + id: + description: 志愿记录ID + type: string + indexs: + description: 志愿序号 + type: integer + institutionType: + description: 院校类型 + type: string + majorCode: + description: 专业代码 + type: string + majorDetail: + description: 专业详情 + type: string + majorName: + description: 专业名称 + type: string + planNum: + description: 计划人数 + type: integer + province: + description: 省份 + type: string + schoolCode: + description: 院校代码 + type: string + schoolIcon: + description: 院校图标 + type: string + schoolName: + description: 扩展字段(来自 SchoolMajorDTO) + type: string + schoolNature: + description: 院校性质 + type: string + studentConvertedScore: + description: 学生折合分 + type: number + tuition: + description: 学费 + type: string + volunteerId: + description: 志愿单ID + type: string + type: object + vo.VolunteerInfoVO: + properties: + createBy: + type: string + createTime: + type: string + createType: + type: string + id: + type: string + scoreId: + type: string + state: + type: string + updateBy: + type: string + updateTime: + type: string + volunteerName: + type: string + type: object + vo.YxCalculationMajorVO: + properties: + batch: + description: 批次 + type: string + category: + description: 类别 + type: string + chineseScoreLimitation: + description: 语文分数限制 + type: number + createBy: + description: 创建人 + type: string + createTime: + description: 创建时间 + type: string + culturalScoreLimitation: + description: 文化成绩限制 + type: number + detail: + description: 详情 + type: string + englishScoreLimitation: + description: 英语分数限制 + type: number + enrollProbability: + description: 录取概率 + type: number + enrollmentCode: + description: 招生代码 + type: string + firstLevelDiscipline: + description: 一级学科 + type: string + id: + type: string + institutionType: + description: 院校类型 + type: string + kslx: + description: 考试类型 + type: string + limitation: + description: 限制条件 + type: string + mainSubjects: + description: 主考科目 + type: string + majorCode: + description: 专业代码 + type: string + majorName: + description: 专业名称 + type: string + majorType: + description: 专业类型 + type: string + majorTypeChild: + description: 子专业类型 + type: string + planNum: + description: 计划人数 + type: integer + privateStudentScore: + description: 私有学生分数 + type: number + probabilityOperator: + description: 概率操作符 + type: string + professionalScoreLimitation: + description: 专业分数限制 + type: number + province: + description: 省份 + type: string + rulesEnrollProbability: + description: 录取规则概率 + type: string + schoolCode: + description: 院校代码 + type: string + schoolName: + description: 院校名称 + type: string + schoolNature: + description: 院校性质 + type: string + state: + description: 状态 + type: string + studentConvertedScore: + description: 学生折合分 + type: number + studentScore: + description: 学生分数 + type: number + tuition: + description: 学费 + type: string + updateBy: + description: 更新人 + type: string + updateTime: + description: 更新时间 + type: string + type: object + vo.YxVolunteerVO: + properties: + createBy: + description: 创建人 + type: string + createTime: + description: 创建日期 + type: string + createType: + description: 生成类型(1.手动生成,2.智能生成) + type: string + id: + type: string + scoreId: + description: 使用成绩id + type: string + state: + description: 志愿单状态(0-否,1-正在使用,2-历史) + type: string + sysOrgCode: + description: 所属部门 + type: string + updateBy: + description: 更新人 + type: string + updateTime: + description: 更新日期 + type: string + volunteerName: + description: 志愿单名称 + type: string + type: object +host: localhost:8080 +info: + contact: {} + description: 提供管理接口 + title: Golang Server API + version: "2.0" +paths: + /platform-users: + get: + parameters: + - description: 页码 + in: query + name: page + type: integer + - description: 每页数量 + in: query + name: size + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 获取平台用户列表 + tags: + - 平台用户 + post: + parameters: + - description: 平台用户信息 + in: body + name: item + required: true + schema: + $ref: '#/definitions/dto.CreatePlatformUserRequest' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 创建平台用户 + tags: + - 平台用户 + /platform-users/{id}: + delete: + parameters: + - description: ID + in: path + name: id + required: true + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 删除平台用户 + tags: + - 平台用户 + get: + parameters: + - description: ID + in: path + name: id + required: true + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 获取平台用户详情 + tags: + - 平台用户 + patch: + parameters: + - description: ID + in: path + name: id + required: true + type: integer + - description: 要更新的字段 + in: body + name: fields + required: true + schema: + additionalProperties: true + type: object + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 动态字段更新 + tags: + - 平台用户 + put: + parameters: + - description: ID + in: path + name: id + required: true + type: integer + - description: 平台用户信息 + in: body + name: item + required: true + schema: + $ref: '#/definitions/dto.UpdatePlatformUserRequest' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 更新平台用户 + tags: + - 平台用户 + /sys-users: + get: + parameters: + - description: 页码 + in: query + name: page + type: integer + - description: 每页数量 + in: query + name: size + type: integer + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/common.Response' + - properties: + data: + items: + $ref: '#/definitions/vo.SysUserVO' + type: array + type: object + summary: 获取用户列表 + tags: + - 用户管理 + post: + parameters: + - description: 用户信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/server_modules_system_dto.CreateUserRequest' + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/common.Response' + - properties: + data: + $ref: '#/definitions/vo.SysUserVO' + type: object + summary: 创建用户 + tags: + - 用户管理 + /sys-users/{id}: + delete: + parameters: + - description: 用户ID + in: path + name: id + required: true + type: string + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 删除用户 + tags: + - 用户管理 + get: + parameters: + - description: 用户ID + in: path + name: id + required: true + type: string + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/common.Response' + - properties: + data: + $ref: '#/definitions/vo.SysUserVO' + type: object + summary: 获取单个用户 + tags: + - 用户管理 + patch: + parameters: + - description: 用户ID + in: path + name: id + required: true + type: string + - description: 要更新的字段 + in: body + name: fields + required: true + schema: + additionalProperties: true + type: object + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 动态字段更新 + tags: + - 用户管理 + put: + parameters: + - description: 用户ID + in: path + name: id + required: true + type: string + - description: 用户信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/server_modules_system_dto.UpdateUserRequest' + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/common.Response' + - properties: + data: + $ref: '#/definitions/vo.SysUserVO' + type: object + summary: 更新用户 + tags: + - 用户管理 + /sys-users/{id}/password: + put: + parameters: + - description: 用户ID + in: path + name: id + required: true + type: string + - description: 密码信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/controller.UpdatePasswordRequest' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 修改密码 + tags: + - 用户管理 + /sys/auth/info: + get: + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: Sys获取当前登录用户信息 + tags: + - 认证 + /sys/auth/login: + post: + consumes: + - application/json + parameters: + - description: 登录信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/controller.LoginRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: Sys用户登录 + tags: + - 认证 + /sys/auth/logout: + post: + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: Sys用户登出 + tags: + - 认证 + /user/auth/info: + get: + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 获取当前登录用户信息 + tags: + - 认证 + /user/auth/login: + post: + consumes: + - application/json + parameters: + - description: 登录信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/controller.LoginRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 用户登录 + tags: + - 认证 + /user/auth/logout: + post: + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 用户登出 + tags: + - 认证 + /user/major/list: + get: + parameters: + - default: 1 + description: 页码 + in: query + name: page + type: integer + - default: 10 + description: 每页数量 + in: query + name: size + type: integer + - default: '""' + description: 批次(本科提前批/本科A段/本科B段/本科/高职高专) + in: query + name: batch + type: string + - default: '""' + description: 录取概率类型(难录取/可冲击/较稳妥/可保底) + in: query + name: probability + type: string + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 获取当前用户可检索列表 + tags: + - 用户专业 + /user/major/list_by_school: + get: + parameters: + - default: 1 + description: 页码 + in: query + name: page + type: integer + - default: 10 + description: 每页数量 + in: query + name: size + type: integer + - description: 院校代码 + in: query + name: school_code + required: true + type: string + - default: '""' + description: 录取概率类型(难录取/可冲击/较稳妥/可保底) + in: query + name: probability + type: string + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 获取当前院校下其他专业数据 + tags: + - 用户专业 + /user/score: + get: + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 获取当前用户的激活分数 + tags: + - 用户分数 + /user/score/{id}: + get: + parameters: + - description: 成绩ID + in: path + name: id + required: true + type: string + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 获取指定 ID 的分数 + tags: + - 用户分数 + /user/score/list: + get: + parameters: + - default: 1 + description: 页码 + in: query + name: page + type: integer + - default: 10 + description: 每页数量 + in: query + name: size + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 获取当前用户的成绩列表 + tags: + - 用户分数 + /user/score/save-score: + post: + parameters: + - description: 成绩信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.SaveScoreRequest' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 保存用户成绩 + tags: + - 用户分数 + /user/volunteer/delete: + delete: + parameters: + - description: 志愿单ID + in: query + name: id + required: true + type: string + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 删除志愿单接口 + tags: + - 用户志愿 + /user/volunteer/detail: + get: + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/common.Response' + - properties: + data: + $ref: '#/definitions/vo.VolunteerDetailResponse' + type: object + summary: 获取当前志愿单详情 + tags: + - 用户志愿 + /user/volunteer/list: + get: + parameters: + - description: 页码 + in: query + name: page + type: integer + - description: 每页数量 + in: query + name: size + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 获取当前用户志愿单列表 + tags: + - 用户志愿 + /user/volunteer/save: + post: + parameters: + - description: 志愿键列表 + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.SaveVolunteerRequest' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 保存志愿明细 + tags: + - 用户志愿 + /user/volunteer/switch: + post: + parameters: + - description: 志愿单ID + in: query + name: id + required: true + type: string + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 切换当前志愿单 + tags: + - 用户志愿 + /user/volunteer/updateName: + put: + parameters: + - description: 志愿单ID + in: query + name: id + required: true + type: string + - description: 志愿单名称 + in: query + name: name + required: true + type: string + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 编辑志愿单名称 + tags: + - 用户志愿 + /users: + get: + parameters: + - description: 页码 + in: query + name: page + type: integer + - description: 每页数量 + in: query + name: size + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 获取用户列表 + tags: + - 用户 + post: + parameters: + - description: 用户信息 + in: body + name: item + required: true + schema: + $ref: '#/definitions/server_modules_user_dto.CreateUserRequest' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 创建用户 + tags: + - 用户 + /users/{id}: + delete: + parameters: + - description: ID + in: path + name: id + required: true + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 删除用户 + tags: + - 用户 + get: + parameters: + - description: ID + in: path + name: id + required: true + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 获取用户详情 + tags: + - 用户 + patch: + parameters: + - description: ID + in: path + name: id + required: true + type: integer + - description: 要更新的字段 + in: body + name: fields + required: true + schema: + additionalProperties: true + type: object + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 动态字段更新 + tags: + - 用户 + put: + parameters: + - description: ID + in: path + name: id + required: true + type: integer + - description: 用户信息 + in: body + name: item + required: true + schema: + $ref: '#/definitions/server_modules_user_dto.UpdateUserRequest' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 更新用户 + tags: + - 用户 + /yx-calculation-majors: + get: + parameters: + - description: 页码 + in: query + name: page + type: integer + - description: 每页数量 + in: query + name: size + type: integer + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/common.Response' + - properties: + data: + items: + $ref: '#/definitions/vo.YxCalculationMajorVO' + type: array + type: object + summary: 获取计算专业列表 + tags: + - 计算专业 + post: + parameters: + - description: 计算专业信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.CreateCalculationMajorRequest' + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/common.Response' + - properties: + data: + $ref: '#/definitions/vo.YxCalculationMajorVO' + type: object + summary: 创建计算专业 + tags: + - 计算专业 + /yx-calculation-majors/{id}: + delete: + parameters: + - description: ID + in: path + name: id + required: true + type: string + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 删除计算专业 + tags: + - 计算专业 + get: + parameters: + - description: ID + in: path + name: id + required: true + type: string + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/common.Response' + - properties: + data: + $ref: '#/definitions/vo.YxCalculationMajorVO' + type: object + summary: 获取单个计算专业 + tags: + - 计算专业 + patch: + parameters: + - description: ID + in: path + name: id + required: true + type: string + - description: 要更新的字段 + in: body + name: fields + required: true + schema: + additionalProperties: true + type: object + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 动态字段更新 + tags: + - 计算专业 + put: + parameters: + - description: ID + in: path + name: id + required: true + type: string + - description: 计算专业信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.UpdateCalculationMajorRequest' + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/common.Response' + - properties: + data: + $ref: '#/definitions/vo.YxCalculationMajorVO' + type: object + summary: 更新计算专业 + tags: + - 计算专业 + /yx-calculation-majors/batch: + delete: + parameters: + - description: ID列表 + in: body + name: ids + required: true + schema: + items: + type: string + type: array + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 批量删除计算专业 + tags: + - 计算专业 + post: + parameters: + - description: 计算专业列表 + in: body + name: items + required: true + schema: + items: + $ref: '#/definitions/dto.CreateCalculationMajorRequest' + type: array + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 批量创建计算专业 + tags: + - 计算专业 + /yx-history-enrolls: + get: + parameters: + - description: 页码 + in: query + name: page + type: integer + - description: 每页数量 + in: query + name: size + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 获取历年招生列表 + tags: + - 历年招生 + post: + parameters: + - description: 历年招生信息 + in: body + name: item + required: true + schema: + $ref: '#/definitions/entity.YxHistoryMajorEnroll' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 创建历年招生记录 + tags: + - 历年招生 + /yx-history-enrolls/{id}: + delete: + parameters: + - description: ID + in: path + name: id + required: true + type: string + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 删除历年招生记录 + tags: + - 历年招生 + get: + parameters: + - description: ID + in: path + name: id + required: true + type: string + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 获取单个历年招生记录 + tags: + - 历年招生 + patch: + parameters: + - description: ID + in: path + name: id + required: true + type: string + - description: 要更新的字段 + in: body + name: fields + required: true + schema: + additionalProperties: true + type: object + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 动态字段更新 + tags: + - 历年招生 + put: + parameters: + - description: ID + in: path + name: id + required: true + type: string + - description: 历年招生信息 + in: body + name: item + required: true + schema: + $ref: '#/definitions/entity.YxHistoryMajorEnroll' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 更新历年招生记录 + tags: + - 历年招生 + /yx-history-enrolls/batch: + delete: + parameters: + - description: ID列表 + in: body + name: ids + required: true + schema: + items: + type: string + type: array + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 批量删除历年招生记录 + tags: + - 历年招生 + post: + parameters: + - description: 历年招生列表 + in: body + name: items + required: true + schema: + items: + $ref: '#/definitions/entity.YxHistoryMajorEnroll' + type: array + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 批量创建历年招生记录 + tags: + - 历年招生 + /yx-school-majors: + get: + parameters: + - description: 页码 + in: query + name: page + type: integer + - description: 每页数量 + in: query + name: size + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 获取院校专业列表 + tags: + - 院校专业 + post: + parameters: + - description: 院校专业信息 + in: body + name: item + required: true + schema: + $ref: '#/definitions/entity.YxSchoolMajor' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 创建院校专业 + tags: + - 院校专业 + /yx-school-majors/{id}: + delete: + parameters: + - description: ID + in: path + name: id + required: true + type: string + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 删除院校专业 + tags: + - 院校专业 + get: + parameters: + - description: ID + in: path + name: id + required: true + type: string + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 获取单个院校专业 + tags: + - 院校专业 + patch: + parameters: + - description: ID + in: path + name: id + required: true + type: string + - description: 要更新的字段 + in: body + name: fields + required: true + schema: + additionalProperties: true + type: object + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 动态字段更新 + tags: + - 院校专业 + put: + parameters: + - description: ID + in: path + name: id + required: true + type: string + - description: 院校专业信息 + in: body + name: item + required: true + schema: + $ref: '#/definitions/entity.YxSchoolMajor' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 更新院校专业 + tags: + - 院校专业 + /yx-school-majors/batch: + delete: + parameters: + - description: ID列表 + in: body + name: ids + required: true + schema: + items: + type: string + type: array + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 批量删除院校专业 + tags: + - 院校专业 + post: + parameters: + - description: 院校专业列表 + in: body + name: items + required: true + schema: + items: + $ref: '#/definitions/entity.YxSchoolMajor' + type: array + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 批量创建院校专业 + tags: + - 院校专业 + /yx-user-scores: + get: + parameters: + - default: 1 + description: 页码 + in: query + name: page + type: integer + - default: 10 + description: 每页数量 + in: query + name: size + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 获取用户分数列表 + tags: + - 用户分数 + post: + parameters: + - description: 用户分数信息 + in: body + name: item + required: true + schema: + $ref: '#/definitions/entity.YxUserScore' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 创建用户分数 + tags: + - 用户分数 + /yx-user-scores/{id}: + delete: + parameters: + - description: ID + in: path + name: id + required: true + type: string + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 删除用户分数 + tags: + - 用户分数 + get: + parameters: + - description: ID + in: path + name: id + required: true + type: string + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 获取单个用户分数 + tags: + - 用户分数 + put: + parameters: + - description: ID + in: path + name: id + required: true + type: string + - description: 用户分数信息 + in: body + name: item + required: true + schema: + $ref: '#/definitions/entity.YxUserScore' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 更新用户分数 + tags: + - 用户分数 + /yx-volunteer-records: + get: + parameters: + - default: 1 + description: 页码 + in: query + name: page + type: integer + - default: 10 + description: 每页数量 + in: query + name: size + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 获取志愿明细列表 + tags: + - 志愿明细 + post: + parameters: + - description: 志愿明细信息 + in: body + name: item + required: true + schema: + $ref: '#/definitions/entity.YxVolunteerRecord' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 创建志愿明细 + tags: + - 志愿明细 + /yx-volunteer-records/{id}: + delete: + parameters: + - description: ID + in: path + name: id + required: true + type: string + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 删除志愿明细 + tags: + - 志愿明细 + get: + parameters: + - description: ID + in: path + name: id + required: true + type: string + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 获取单个志愿明细 + tags: + - 志愿明细 + put: + parameters: + - description: ID + in: path + name: id + required: true + type: string + - description: 志愿明细信息 + in: body + name: item + required: true + schema: + $ref: '#/definitions/entity.YxVolunteerRecord' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 更新志愿明细 + tags: + - 志愿明细 + /yx-volunteers: + get: + parameters: + - default: 1 + description: 页码 + in: query + name: page + type: integer + - default: 10 + description: 每页数量 + in: query + name: size + type: integer + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/common.Response' + - properties: + data: + items: + $ref: '#/definitions/vo.YxVolunteerVO' + type: array + type: object + summary: 获取志愿列表 + tags: + - 志愿 + post: + parameters: + - description: 志愿信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.CreateVolunteerRequest' + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/common.Response' + - properties: + data: + $ref: '#/definitions/vo.YxVolunteerVO' + type: object + summary: 创建志愿 + tags: + - 志愿 + /yx-volunteers/{id}: + delete: + parameters: + - description: ID + in: path + name: id + required: true + type: string + responses: + "200": + description: OK + schema: + $ref: '#/definitions/common.Response' + summary: 删除志愿 + tags: + - 志愿 + get: + parameters: + - description: ID + in: path + name: id + required: true + type: string + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/common.Response' + - properties: + data: + $ref: '#/definitions/vo.YxVolunteerVO' + type: object + summary: 获取单个志愿 + tags: + - 志愿 + put: + parameters: + - description: ID + in: path + name: id + required: true + type: string + - description: 志愿信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.UpdateVolunteerRequest' + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/common.Response' + - properties: + data: + $ref: '#/definitions/vo.YxVolunteerVO' + type: object + summary: 更新志愿 + tags: + - 志愿 +securityDefinitions: + Bearer: + in: header + name: Authorization + type: apiKey +swagger: "2.0" diff --git a/server/go.mod b/server/go.mod new file mode 100644 index 0000000..8a98085 --- /dev/null +++ b/server/go.mod @@ -0,0 +1,82 @@ +module server + +go 1.25.0 + +require ( + github.com/gin-gonic/gin v1.12.0 + github.com/google/uuid v1.6.0 + github.com/redis/go-redis/v9 v9.3.0 + github.com/swaggo/files v1.0.1 + github.com/swaggo/gin-swagger v1.6.1 + github.com/swaggo/swag v1.16.6 + gopkg.in/yaml.v3 v3.0.1 + gorm.io/driver/mysql v1.5.6 + gorm.io/gorm v1.30.0 +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/PuerkitoBio/purell v1.2.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.15.0 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/gabriel-vasile/mimetype v1.4.13 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-openapi/jsonpointer v0.22.5 // indirect + github.com/go-openapi/jsonreference v0.21.5 // indirect + github.com/go-openapi/spec v0.22.4 // indirect + github.com/go-openapi/swag v0.25.5 // indirect + github.com/go-openapi/swag/conv v0.25.5 // indirect + github.com/go-openapi/swag/jsonname v0.25.5 // indirect + github.com/go-openapi/swag/jsonutils v0.25.5 // indirect + github.com/go-openapi/swag/loading v0.25.5 // indirect + github.com/go-openapi/swag/stringutils v0.25.5 // indirect + github.com/go-openapi/swag/typeutils v0.25.5 // indirect + github.com/go-openapi/swag/yamlutils v0.25.5 // 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.30.1 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/goccy/go-json v0.10.6 // indirect + github.com/goccy/go-yaml v1.19.2 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.6.0 // indirect + github.com/jackc/puddle/v2 v2.2.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.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mailru/easyjson v0.9.1 // indirect + github.com/mattn/go-isatty v0.0.20 // 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.2.4 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.59.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.1 // indirect + go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/arch v0.25.0 // indirect + golang.org/x/crypto v0.49.0 // indirect + golang.org/x/mod v0.34.0 // indirect + golang.org/x/net v0.52.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.35.0 // indirect + golang.org/x/tools v0.43.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gorm.io/datatypes v1.2.7 // indirect + gorm.io/driver/postgres v1.6.0 // indirect +) diff --git a/server/go.sum b/server/go.sum new file mode 100644 index 0000000..677f493 --- /dev/null +++ b/server/go.sum @@ -0,0 +1,324 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +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/purell v1.2.1 h1:QsZ4TjvwiMpat6gBCBxEQI0rcS9ehtkKtSpiUnd9N28= +github.com/PuerkitoBio/purell v1.2.1/go.mod h1:ZwHcC/82TOaovDi//J/804umJFFmbOHPngi8iYYv/Eo= +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/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +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/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +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/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= +github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +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/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= +github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= +github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +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/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= +github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +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-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +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/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= +github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= +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/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA= +github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0= +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/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= +github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= +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/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ= +github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ= +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-openapi/swag v0.25.5 h1:pNkwbUEeGwMtcgxDr+2GBPAk4kT+kJ+AaB+TMKAg+TU= +github.com/go-openapi/swag v0.25.5/go.mod h1:B3RT6l8q7X803JRxa2e59tHOiZlX1t8viplOcs9CwTA= +github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g= +github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k= +github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo= +github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU= +github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo= +github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4= +github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU= +github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g= +github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M= +github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII= +github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E= +github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc= +github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ= +github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ= +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-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +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/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +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/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= +github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +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/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +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/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +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/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +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/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +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/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +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/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/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/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +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/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +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/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +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/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY= +github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw= +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/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= +github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= +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/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= +github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= +go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +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/arch v0.25.0 h1:qnk6Ksugpi5Bz32947rkUgDt9/s5qvqDPl/gBKdMJLE= +golang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8= +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/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +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/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +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/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +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/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +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/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +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/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +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/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= +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= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +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/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk= +gorm.io/datatypes v1.2.7/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY= +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/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8= +gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= +gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= +gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= +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= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= +gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/server/main.go b/server/main.go new file mode 100644 index 0000000..14df254 --- /dev/null +++ b/server/main.go @@ -0,0 +1,180 @@ +// Package main 应用程序入口 +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "server/common" + "server/config" + _ "server/docs" + "server/middleware" + apiController "server/modules/api/controller" + 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 Golang Server 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) + + // 注册 对外 API 模块路由 + apiController.NewWechatMiniProgramController().RegisterRoutes(api) + apiController.NewOpenAuthController().RegisterRoutes(api) + apiController.NewAppConfigController().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) + userController.NewUserController().RegisterRoutes(api) + userController.NewPlatformUserController().RegisterRoutes(api) + userController.NewUserProfileController().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) + } +} diff --git a/server/middleware/auth.go b/server/middleware/auth.go new file mode 100644 index 0000000..36f0cd2 --- /dev/null +++ b/server/middleware/auth.go @@ -0,0 +1,73 @@ +// 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", + "/api/open/wechat/mini/login", + "/api/open/user/login", + "/api/open/app/config", + "/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...) +} diff --git a/server/middleware/cors.go b/server/middleware/cors.go new file mode 100644 index 0000000..bc6d174 --- /dev/null +++ b/server/middleware/cors.go @@ -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() + } +} diff --git a/server/middleware/ratelimit.go b/server/middleware/ratelimit.go new file mode 100644 index 0000000..e77bcbb --- /dev/null +++ b/server/middleware/ratelimit.go @@ -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...) +} diff --git a/server/middleware/security.go b/server/middleware/security.go new file mode 100644 index 0000000..aede502 --- /dev/null +++ b/server/middleware/security.go @@ -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...) +} diff --git a/server/modules/api/controller/app_config_controller.go b/server/modules/api/controller/app_config_controller.go new file mode 100644 index 0000000..4f224f6 --- /dev/null +++ b/server/modules/api/controller/app_config_controller.go @@ -0,0 +1,33 @@ +// Package controller 控制器层 +package controller + +import ( + "server/common" + "server/modules/api/service" + + "github.com/gin-gonic/gin" +) + +type AppConfigController struct { + service *service.AppConfigService +} + +func NewAppConfigController() *AppConfigController { + return &AppConfigController{service: service.NewAppConfigService()} +} + +func (ctrl *AppConfigController) RegisterRoutes(r *gin.RouterGroup) { + group := r.Group("/open") + group.GET("/app/config", ctrl.GetConfig) +} + +// GetConfig 获取小程序配置 +// @Summary 获取小程序配置 +// @Tags 对外接口 +// @Produce json +// @Success 200 {object} common.Response +// @Router /open/app/config [get] +func (ctrl *AppConfigController) GetConfig(c *gin.Context) { + data := ctrl.service.GetConfig() + common.Success(c, data) +} diff --git a/server/modules/api/controller/user_auth_controller.go b/server/modules/api/controller/user_auth_controller.go new file mode 100644 index 0000000..d495673 --- /dev/null +++ b/server/modules/api/controller/user_auth_controller.go @@ -0,0 +1,48 @@ +// Package controller 控制器层 +package controller + +import ( + "server/common" + apiDto "server/modules/api/dto" + "server/modules/user/service" + + "github.com/gin-gonic/gin" +) + +type OpenAuthController struct { + userService *service.UserService +} + +func NewOpenAuthController() *OpenAuthController { + return &OpenAuthController{userService: service.NewUserService()} +} + +func (ctrl *OpenAuthController) RegisterRoutes(r *gin.RouterGroup) { + group := r.Group("/open") + group.POST("/user/login", ctrl.LoginByPhone) +} + +// LoginByPhone 手机号密码登录 +// @Summary 手机号密码登录 +// @Tags 对外接口 +// @Accept json +// @Produce json +// @Param request body dto.UserPasswordLoginRequest true "登录信息" +// @Success 200 {object} common.Response +// @Router /open/user/login [post] +func (ctrl *OpenAuthController) LoginByPhone(c *gin.Context) { + var req apiDto.UserPasswordLoginRequest + if err := c.ShouldBindJSON(&req); err != nil { + common.Error(c, 400, "手机号和密码不能为空") + return + } + loginUser, token, err := ctrl.userService.LoginByPhonePassword(req.Phone, req.Password) + if err != nil { + common.Error(c, 500, err.Error()) + return + } + common.Success(c, gin.H{ + "token": token, + "user": loginUser, + }) +} diff --git a/server/modules/api/controller/wechat_controller.go b/server/modules/api/controller/wechat_controller.go new file mode 100644 index 0000000..db721c5 --- /dev/null +++ b/server/modules/api/controller/wechat_controller.go @@ -0,0 +1,45 @@ +// Package controller 控制器层 +package controller + +import ( + "server/common" + apiDto "server/modules/api/dto" + "server/modules/api/service" + + "github.com/gin-gonic/gin" +) + +type WechatMiniProgramController struct { + service *service.WechatMiniProgramService +} + +func NewWechatMiniProgramController() *WechatMiniProgramController { + return &WechatMiniProgramController{service: service.NewWechatMiniProgramService()} +} + +func (ctrl *WechatMiniProgramController) RegisterRoutes(r *gin.RouterGroup) { + group := r.Group("/open") + group.POST("/wechat/mini/login", ctrl.MiniLogin) +} + +// MiniLogin 微信小程序登录 +// @Summary 微信小程序登录 +// @Tags 对外接口 +// @Accept json +// @Produce json +// @Param request body dto.WechatMiniLoginRequest true "微信小程序登录请求" +// @Success 200 {object} common.Response +// @Router /open/wechat/mini/login [post] +func (ctrl *WechatMiniProgramController) MiniLogin(c *gin.Context) { + var req apiDto.WechatMiniLoginRequest + if err := c.ShouldBindJSON(&req); err != nil { + common.Error(c, 400, "参数错误") + return + } + resp, err := ctrl.service.Login(&req) + if err != nil { + common.Error(c, 400, err.Error()) + return + } + common.Success(c, resp) +} diff --git a/server/modules/api/dto/app_config_dto.go b/server/modules/api/dto/app_config_dto.go new file mode 100644 index 0000000..71389e2 --- /dev/null +++ b/server/modules/api/dto/app_config_dto.go @@ -0,0 +1,26 @@ +// Package dto 请求参数 +package dto + +// AppConfigResponse 配置中心响应 +type AppConfigResponse struct { + App AppClientConfig `json:"app"` + API AppEndpointConfig `json:"api"` + WebView AppEndpointConfig `json:"webview"` + TTLSeconds int `json:"ttlSeconds"` + Disabled bool `json:"disabled"` + DisableReason string `json:"disableReason"` +} + +// AppClientConfig 客户端版本配置 +type AppClientConfig struct { + MinVersion string `json:"minVersion"` + LatestVersion string `json:"latestVersion"` + ForceUpdate bool `json:"forceUpdate"` +} + +// AppEndpointConfig 接口或 WebView 配置 +type AppEndpointConfig struct { + BaseURL string `json:"baseUrl"` + Version string `json:"version"` + MinClientVersion string `json:"minClientVersion"` +} diff --git a/server/modules/api/dto/user_login_dto.go b/server/modules/api/dto/user_login_dto.go new file mode 100644 index 0000000..bd3d7b4 --- /dev/null +++ b/server/modules/api/dto/user_login_dto.go @@ -0,0 +1,8 @@ +// Package dto 请求参数 +package dto + +// UserPasswordLoginRequest 手机号密码登录请求 +type UserPasswordLoginRequest struct { + Phone string `json:"phone" binding:"required"` + Password string `json:"password" binding:"required"` +} diff --git a/server/modules/api/dto/wechat_dto.go b/server/modules/api/dto/wechat_dto.go new file mode 100644 index 0000000..85f1723 --- /dev/null +++ b/server/modules/api/dto/wechat_dto.go @@ -0,0 +1,30 @@ +// Package dto 请求参数 +package dto + +import "gorm.io/datatypes" + +// WechatMiniLoginRequest 微信小程序登录请求 +type WechatMiniLoginRequest struct { + Code string `json:"code" binding:"required"` + PhoneCode string `json:"phoneCode"` + EncryptedData string `json:"encryptedData"` + IV string `json:"iv"` + Nickname string `json:"nickname"` + AvatarURL string `json:"avatarUrl"` + Phone *string `json:"phone"` + Gender *int8 `json:"gender"` + PlatformExtra datatypes.JSONMap `json:"platformExtra"` +} + +// WechatMiniLoginResponse 微信小程序登录响应 +type WechatMiniLoginResponse struct { + UserID int64 `json:"userId"` + PlatformUserID int64 `json:"platformUserId"` + OpenID string `json:"openid"` + UnionID string `json:"unionid"` + SessionKey string `json:"sessionKey"` + Phone string `json:"phone"` + Token string `json:"token"` + IsNewPlatform bool `json:"isNewPlatform"` + IsNewUser bool `json:"isNewUser"` +} diff --git a/server/modules/api/service/app_config_service.go b/server/modules/api/service/app_config_service.go new file mode 100644 index 0000000..36668be --- /dev/null +++ b/server/modules/api/service/app_config_service.go @@ -0,0 +1,38 @@ +// Package service 业务逻辑层 +package service + +import ( + "server/config" + apiDto "server/modules/api/dto" +) + +type AppConfigService struct{} + +func NewAppConfigService() *AppConfigService { + return &AppConfigService{} +} + +// GetConfig 获取配置中心信息 +func (s *AppConfigService) GetConfig() apiDto.AppConfigResponse { + cfg := config.AppConfig.AppConfig + return apiDto.AppConfigResponse{ + App: apiDto.AppClientConfig{ + MinVersion: cfg.App.MinVersion, + LatestVersion: cfg.App.LatestVersion, + ForceUpdate: cfg.App.ForceUpdate, + }, + API: apiDto.AppEndpointConfig{ + BaseURL: cfg.API.BaseURL, + Version: cfg.API.Version, + MinClientVersion: cfg.API.MinClientVersion, + }, + WebView: apiDto.AppEndpointConfig{ + BaseURL: cfg.WebView.BaseURL, + Version: cfg.WebView.Version, + MinClientVersion: cfg.WebView.MinClientVersion, + }, + TTLSeconds: cfg.TTLSeconds, + Disabled: cfg.Disabled, + DisableReason: cfg.DisableReason, + } +} diff --git a/server/modules/api/service/wechat_service.go b/server/modules/api/service/wechat_service.go new file mode 100644 index 0000000..1cd1d51 --- /dev/null +++ b/server/modules/api/service/wechat_service.go @@ -0,0 +1,474 @@ +// Package service 业务逻辑层 +package service + +import ( + "context" + "crypto/aes" + "crypto/cipher" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "server/common" + "server/config" + apiDto "server/modules/api/dto" + systemEntity "server/modules/system/entity" + userEntity "server/modules/user/entity" + userMapper "server/modules/user/mapper" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +const ( + wechatMiniProgramType int8 = 1 +) + +type WechatMiniProgramService struct { + userMapper *userMapper.UserMapper + platformUserMapper *userMapper.PlatformUserMapper + httpClient *http.Client +} + +func NewWechatMiniProgramService() *WechatMiniProgramService { + return &WechatMiniProgramService{ + userMapper: userMapper.NewUserMapper(), + platformUserMapper: userMapper.NewPlatformUserMapper(), + httpClient: &http.Client{ + Timeout: 5 * time.Second, + }, + } +} + +// Login 微信小程序登录 +func (s *WechatMiniProgramService) Login(req *apiDto.WechatMiniLoginRequest) (*apiDto.WechatMiniLoginResponse, error) { + cfg := config.AppConfig.Wechat.MiniProgram + if strings.TrimSpace(cfg.AppID) == "" || strings.TrimSpace(cfg.AppSecret) == "" { + return nil, errors.New("微信小程序配置缺失") + } + + session, err := s.exchangeCodeForSession(req.Code, cfg.AppID, cfg.AppSecret) + if err != nil { + return nil, err + } + + now := time.Now() + isNewPlatform := false + isNewUser := false + var userID int64 + var platformUserID int64 + var phone string + + if req.PhoneCode != "" { + phoneResp, err := s.getPhoneNumberByCode(req.PhoneCode, cfg.AppID, cfg.AppSecret) + if err != nil { + return nil, err + } + phone = phoneResp.PhoneInfo.PhoneNumber + } else if req.EncryptedData != "" && req.IV != "" { + phoneResp, err := decryptPhoneNumber(req.EncryptedData, req.IV, session.SessionKey) + if err != nil { + return nil, err + } + phone = phoneResp.PhoneNumber + } else { + phone = normalizeString(req.Phone) + } + + phonePtr := normalizeOptionalString(&phone) + + platformUser, err := s.platformUserMapper.FindByPlatformOpenID(wechatMiniProgramType, session.OpenID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + isNewPlatform = true + isNewUser = true + nickname := req.Nickname + if phone != "" { + nickname = maskPhone(phone) + } + user := &userEntity.User{ + Username: generateWechatUsername(), + Nickname: nickname, + AvatarURL: chooseAvatarURL(req.AvatarURL), + Phone: phonePtr, + Gender: 0, + Status: 1, + Deleted: 0, + } + if phone != "" { + salt := uuid.NewString()[:8] + encrypted, err := common.Encrypt(phone, "123456", salt) + if err != nil { + return nil, err + } + user.Password = &encrypted + user.Salt = &salt + } + if req.Gender != nil { + user.Gender = *req.Gender + } + if err := s.userMapper.Create(user); err != nil { + return nil, err + } + + platform := &userEntity.PlatformUser{ + UserID: user.ID, + PlatformType: wechatMiniProgramType, + PlatformOpenID: session.OpenID, + PlatformUnionID: session.UnionID, + PlatformSessionKey: session.SessionKey, + PlatformExtra: req.PlatformExtra, + LastLoginTime: &now, + Deleted: 0, + } + if err := s.platformUserMapper.Create(platform); err != nil { + return nil, err + } + + userID = user.ID + platformUserID = platform.ID + } else { + return nil, err + } + } else { + userID = platformUser.UserID + platformUserID = platformUser.ID + fields := map[string]interface{}{ + "platform_session_key": session.SessionKey, + "last_login_time": now, + } + if session.UnionID != "" { + fields["platform_unionid"] = session.UnionID + } + if req.PlatformExtra != nil { + fields["platform_extra"] = req.PlatformExtra + } + if err := s.platformUserMapper.UpdateFields(platformUser.ID, fields); err != nil { + return nil, err + } + + userFields := map[string]interface{}{} + if req.Nickname != "" { + userFields["nickname"] = req.Nickname + } + if req.AvatarURL != "" { + userFields["avatar_url"] = req.AvatarURL + } + if req.Gender != nil { + userFields["gender"] = *req.Gender + } + if phonePtr != nil { + userFields["phone"] = phonePtr + } + if len(userFields) > 0 { + userFields["update_time"] = now + _ = s.userMapper.UpdateFields(userID, userFields) + } + } + + token, err := s.saveLoginUser(userID, req, session, phone) + if err != nil { + return nil, err + } + + return &apiDto.WechatMiniLoginResponse{ + UserID: userID, + PlatformUserID: platformUserID, + OpenID: session.OpenID, + UnionID: session.UnionID, + SessionKey: session.SessionKey, + Phone: phone, + Token: token, + IsNewPlatform: isNewPlatform, + IsNewUser: isNewUser, + }, nil +} + +type wechatSessionResponse struct { + OpenID string `json:"openid"` + SessionKey string `json:"session_key"` + UnionID string `json:"unionid"` + ErrCode int `json:"errcode"` + ErrMsg string `json:"errmsg"` +} + +type wechatAccessTokenResponse struct { + AccessToken string `json:"access_token"` + ExpiresIn int64 `json:"expires_in"` + ErrCode int `json:"errcode"` + ErrMsg string `json:"errmsg"` +} + +type wechatPhoneNumberResponse struct { + ErrCode int `json:"errcode"` + ErrMsg string `json:"errmsg"` + PhoneInfo struct { + PhoneNumber string `json:"phoneNumber"` + PurePhoneNumber string `json:"purePhoneNumber"` + CountryCode string `json:"countryCode"` + } `json:"phone_info"` +} + +func (s *WechatMiniProgramService) exchangeCodeForSession(code, appID, appSecret string) (*wechatSessionResponse, error) { + query := url.Values{} + query.Set("appid", appID) + query.Set("secret", appSecret) + query.Set("js_code", code) + query.Set("grant_type", "authorization_code") + + endpoint := fmt.Sprintf("https://api.weixin.qq.com/sns/jscode2session?%s", query.Encode()) + resp, err := s.httpClient.Get(endpoint) + if err != nil { + return nil, fmt.Errorf("请求微信接口失败: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("微信接口返回异常状态: %d", resp.StatusCode) + } + + var data wechatSessionResponse + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return nil, fmt.Errorf("解析微信接口响应失败: %w", err) + } + if data.ErrCode != 0 { + return nil, fmt.Errorf("微信接口错误: %d %s", data.ErrCode, data.ErrMsg) + } + if data.OpenID == "" || data.SessionKey == "" { + return nil, errors.New("微信接口返回数据不完整") + } + return &data, nil +} + +func normalizeOptionalString(val *string) *string { + if val == nil { + return nil + } + trimmed := strings.TrimSpace(*val) + if trimmed == "" { + return nil + } + return &trimmed +} + +func normalizeString(val *string) string { + if val == nil { + return "" + } + return strings.TrimSpace(*val) +} + +func generateWechatUsername() string { + return "wx_" + time.Now().Format("20060102150405") + "_" + uuid.NewString()[:8] +} + +var defaultAvatarURLs = []string{ + "https://oss-1322049369.cos.ap-beijing.myqcloud.com/images/xianxingnvxuesheng.png?imageSlim", + "https://oss-1322049369.cos.ap-beijing.myqcloud.com/images/xianxingnanxuesheng.png?imageSlim", + "https://oss-1322049369.cos.ap-beijing.myqcloud.com/images/nvxuesheng.png?imageSlim", + "https://oss-1322049369.cos.ap-beijing.myqcloud.com/images/nvxuesheng_1.png?imageSlim", + "https://oss-1322049369.cos.ap-beijing.myqcloud.com/images/nanxuesheng.png?imageSlim", + "https://oss-1322049369.cos.ap-beijing.myqcloud.com/images/nanxuesheng_1.png?imageSlim", + "https://oss-1322049369.cos.ap-beijing.myqcloud.com/images/a-389363773.png?imageSlim", + "https://oss-1322049369.cos.ap-beijing.myqcloud.com/images/a-389363772.png?imageSlim", + "https://oss-1322049369.cos.ap-beijing.myqcloud.com/images/a-389363771.png?imageSlim", + "https://oss-1322049369.cos.ap-beijing.myqcloud.com/images/a-389363770.png?imageSlim", + "https://oss-1322049369.cos.ap-beijing.myqcloud.com/images/a-389363769.png?imageSlim", + "https://oss-1322049369.cos.ap-beijing.myqcloud.com/images/a-389363768.png?imageSlim", + "https://oss-1322049369.cos.ap-beijing.myqcloud.com/images/a-389363767.png?imageSlim", + "https://oss-1322049369.cos.ap-beijing.myqcloud.com/images/a-8nvxuesheng.png?imageSlim", + "https://oss-1322049369.cos.ap-beijing.myqcloud.com/images/a-8nanxuesheng.png?imageSlim", +} + +func chooseAvatarURL(input string) string { + if strings.TrimSpace(input) != "" { + return input + } + return defaultAvatarURLs[time.Now().UnixNano()%int64(len(defaultAvatarURLs))] +} + +func maskPhone(phone string) string { + phone = strings.TrimSpace(phone) + if len(phone) < 7 { + return phone + } + return phone[:3] + "****" + phone[len(phone)-4:] +} + +func (s *WechatMiniProgramService) getAccessToken(appID, appSecret string) (string, error) { + ctx := context.Background() + cacheKey := "wechat:access_token" + if token, err := config.RDB.Get(ctx, cacheKey).Result(); err == nil && strings.TrimSpace(token) != "" { + return token, nil + } + + query := url.Values{} + query.Set("grant_type", "client_credential") + query.Set("appid", appID) + query.Set("secret", appSecret) + endpoint := fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/token?%s", query.Encode()) + + resp, err := s.httpClient.Get(endpoint) + if err != nil { + return "", fmt.Errorf("请求微信 access_token 失败: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("微信 access_token 返回异常状态: %d", resp.StatusCode) + } + + var data wechatAccessTokenResponse + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return "", fmt.Errorf("解析微信 access_token 响应失败: %w", err) + } + if data.ErrCode != 0 { + return "", fmt.Errorf("微信 access_token 错误: %d %s", data.ErrCode, data.ErrMsg) + } + if strings.TrimSpace(data.AccessToken) == "" { + return "", errors.New("微信 access_token 返回为空") + } + + expire := time.Duration(data.ExpiresIn) * time.Second + if expire <= 0 { + expire = time.Hour + } + _ = config.RDB.Set(ctx, cacheKey, data.AccessToken, expire-time.Minute).Err() + return data.AccessToken, nil +} + +func (s *WechatMiniProgramService) getPhoneNumberByCode(phoneCode, appID, appSecret string) (*wechatPhoneNumberResponse, error) { + token, err := s.getAccessToken(appID, appSecret) + if err != nil { + return nil, err + } + + endpoint := fmt.Sprintf("https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=%s", url.QueryEscape(token)) + payload := map[string]string{ + "code": phoneCode, + } + body, _ := json.Marshal(payload) + resp, err := s.httpClient.Post(endpoint, "application/json", strings.NewReader(string(body))) + if err != nil { + return nil, fmt.Errorf("请求微信手机号接口失败: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("微信手机号接口返回异常状态: %d", resp.StatusCode) + } + + var data wechatPhoneNumberResponse + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return nil, fmt.Errorf("解析微信手机号接口响应失败: %w", err) + } + if data.ErrCode != 0 { + return nil, fmt.Errorf("微信手机号接口错误: %d %s", data.ErrCode, data.ErrMsg) + } + if data.PhoneInfo.PhoneNumber == "" { + return nil, errors.New("微信手机号接口返回为空") + } + return &data, nil +} + +type wechatPhoneDecrypted struct { + PhoneNumber string `json:"phoneNumber"` + PurePhoneNumber string `json:"purePhoneNumber"` + CountryCode string `json:"countryCode"` + Watermark struct { + AppID string `json:"appid"` + Timestamp int64 `json:"timestamp"` + } `json:"watermark"` +} + +func decryptPhoneNumber(encryptedData, iv, sessionKey string) (*wechatPhoneDecrypted, error) { + cipherText, err := base64.StdEncoding.DecodeString(encryptedData) + if err != nil { + return nil, fmt.Errorf("encryptedData 解码失败: %w", err) + } + ivBytes, err := base64.StdEncoding.DecodeString(iv) + if err != nil { + return nil, fmt.Errorf("iv 解码失败: %w", err) + } + keyBytes, err := base64.StdEncoding.DecodeString(sessionKey) + if err != nil { + return nil, fmt.Errorf("session_key 解码失败: %w", err) + } + if len(keyBytes) != 16 { + return nil, errors.New("session_key 长度不正确") + } + + block, err := aes.NewCipher(keyBytes) + if err != nil { + return nil, fmt.Errorf("创建 AES cipher 失败: %w", err) + } + if len(cipherText)%aes.BlockSize != 0 { + return nil, errors.New("密文长度不正确") + } + mode := cipher.NewCBCDecrypter(block, ivBytes) + plainText := make([]byte, len(cipherText)) + mode.CryptBlocks(plainText, cipherText) + plainText, err = pkcs7Unpad(plainText) + if err != nil { + return nil, err + } + + var data wechatPhoneDecrypted + if err := json.Unmarshal(plainText, &data); err != nil { + return nil, fmt.Errorf("手机号解密结果解析失败: %w", err) + } + if data.PhoneNumber == "" { + return nil, errors.New("手机号解密结果为空") + } + return &data, nil +} + +func pkcs7Unpad(data []byte) ([]byte, error) { + if len(data) == 0 { + return nil, errors.New("PKCS7 数据为空") + } + padding := int(data[len(data)-1]) + if padding == 0 || padding > len(data) { + return nil, errors.New("PKCS7 padding 不合法") + } + for i := 0; i < padding; i++ { + if data[len(data)-1-i] != byte(padding) { + return nil, errors.New("PKCS7 padding 校验失败") + } + } + return data[:len(data)-padding], nil +} + +func (s *WechatMiniProgramService) saveLoginUser(userID int64, req *apiDto.WechatMiniLoginRequest, session *wechatSessionResponse, phone string) (string, error) { + token := uuid.NewString() + loginUser := &systemEntity.LoginUser{ + ID: strconv.FormatInt(userID, 10), + Username: chooseUsername(phone, session.OpenID), + Realname: req.Nickname, + Avatar: req.AvatarURL, + Phone: phone, + Email: "", + Token: token, + } + data, err := json.Marshal(loginUser) + if err != nil { + return "", err + } + if err := config.RDB.Set(context.Background(), common.RedisTokenPrefix+token, data, common.RedisTokenExpire).Err(); err != nil { + return "", err + } + return token, nil +} + +func chooseUsername(phone, openid string) string { + if strings.TrimSpace(phone) != "" { + return phone + } + return openid +} diff --git a/server/modules/system/controller/sys_auth_controller.go b/server/modules/system/controller/sys_auth_controller.go new file mode 100644 index 0000000..9ec4424 --- /dev/null +++ b/server/modules/system/controller/sys_auth_controller.go @@ -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) +} diff --git a/server/modules/system/controller/sys_user_controller.go b/server/modules/system/controller/sys_user_controller.go new file mode 100644 index 0000000..4a97e30 --- /dev/null +++ b/server/modules/system/controller/sys_user_controller.go @@ -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) +} diff --git a/server/modules/system/dto/auth_dto.go b/server/modules/system/dto/auth_dto.go new file mode 100644 index 0000000..995c6fc --- /dev/null +++ b/server/modules/system/dto/auth_dto.go @@ -0,0 +1,7 @@ +package dto + +// LoginRequest 登录请求 +type LoginRequest struct { + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required"` +} \ No newline at end of file diff --git a/server/modules/system/dto/sys_user_dto.go b/server/modules/system/dto/sys_user_dto.go new file mode 100644 index 0000000..a6f7bd6 --- /dev/null +++ b/server/modules/system/dto/sys_user_dto.go @@ -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"` // 机构编码 +} \ No newline at end of file diff --git a/server/modules/system/entity/sys_user.go b/server/modules/system/entity/sys_user.go new file mode 100644 index 0000000..ea299a7 --- /dev/null +++ b/server/modules/system/entity/sys_user.go @@ -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"` +} diff --git a/server/modules/system/mapper/sys_user_mapper.go b/server/modules/system/mapper/sys_user_mapper.go new file mode 100644 index 0000000..a9f5b1e --- /dev/null +++ b/server/modules/system/mapper/sys_user_mapper.go @@ -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 +} diff --git a/server/modules/system/service/sys_user_service.go b/server/modules/system/service/sys_user_service.go new file mode 100644 index 0000000..6b33a15 --- /dev/null +++ b/server/modules/system/service/sys_user_service.go @@ -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 等敏感字段 + } +} diff --git a/server/modules/system/vo/login_user_vo.go b/server/modules/system/vo/login_user_vo.go new file mode 100644 index 0000000..a50efa8 --- /dev/null +++ b/server/modules/system/vo/login_user_vo.go @@ -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 应该在单独的响应结构中 +} \ No newline at end of file diff --git a/server/modules/system/vo/sys_user_vo.go b/server/modules/system/vo/sys_user_vo.go new file mode 100644 index 0000000..f826d24 --- /dev/null +++ b/server/modules/system/vo/sys_user_vo.go @@ -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 等敏感字段 +} \ No newline at end of file diff --git a/server/modules/user/controller/platform_user_controller.go b/server/modules/user/controller/platform_user_controller.go new file mode 100644 index 0000000..6cafc7d --- /dev/null +++ b/server/modules/user/controller/platform_user_controller.go @@ -0,0 +1,158 @@ +// Package controller 控制器层 +package controller + +import ( + "strconv" + + "server/common" + "server/modules/user/dto" + "server/modules/user/service" + + "github.com/gin-gonic/gin" +) + +type PlatformUserController struct { + service *service.PlatformUserService +} + +func NewPlatformUserController() *PlatformUserController { + return &PlatformUserController{service: service.NewPlatformUserService()} +} + +func (ctrl *PlatformUserController) RegisterRoutes(r *gin.RouterGroup) { + group := r.Group("/platform-users") + group.GET("", ctrl.List) + group.GET("/:id", ctrl.GetByID) + group.POST("", ctrl.Create) + group.PUT("/:id", ctrl.Update) + group.PATCH("/:id", ctrl.UpdateFields) + group.DELETE("/:id", ctrl.Delete) +} + +// List 获取平台用户列表 +// @Summary 获取平台用户列表 +// @Tags 平台用户 +// @Param page query int false "页码" +// @Param size query int false "每页数量" +// @Success 200 {object} common.Response +// @Router /platform-users [get] +func (ctrl *PlatformUserController) 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 int true "ID" +// @Success 200 {object} common.Response +// @Router /platform-users/{id} [get] +func (ctrl *PlatformUserController) GetByID(c *gin.Context) { + id, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + common.Error(c, 400, "ID格式错误") + return + } + item, err := ctrl.service.GetByID(id) + if err != nil { + common.Error(c, 404, "未找到") + return + } + common.Success(c, item) +} + +// Create 创建平台用户 +// @Summary 创建平台用户 +// @Tags 平台用户 +// @Param item body dto.CreatePlatformUserRequest true "平台用户信息" +// @Success 200 {object} common.Response +// @Router /platform-users [post] +func (ctrl *PlatformUserController) Create(c *gin.Context) { + var req dto.CreatePlatformUserRequest + if err := c.ShouldBindJSON(&req); err != nil { + common.Error(c, 400, "参数错误") + return + } + item, err := ctrl.service.Create(&req) + if err != nil { + common.Error(c, 500, err.Error()) + return + } + common.Success(c, item) +} + +// Update 更新平台用户 +// @Summary 更新平台用户 +// @Tags 平台用户 +// @Param id path int true "ID" +// @Param item body dto.UpdatePlatformUserRequest true "平台用户信息" +// @Success 200 {object} common.Response +// @Router /platform-users/{id} [put] +func (ctrl *PlatformUserController) Update(c *gin.Context) { + id, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + common.Error(c, 400, "ID格式错误") + return + } + var req dto.UpdatePlatformUserRequest + if err := c.ShouldBindJSON(&req); err != nil { + common.Error(c, 400, "参数错误") + return + } + item, err := ctrl.service.Update(id, &req) + if err != nil { + common.Error(c, 500, err.Error()) + return + } + common.Success(c, item) +} + +// UpdateFields 动态字段更新 +// @Summary 动态字段更新 +// @Tags 平台用户 +// @Param id path int true "ID" +// @Param fields body map[string]interface{} true "要更新的字段" +// @Success 200 {object} common.Response +// @Router /platform-users/{id} [patch] +func (ctrl *PlatformUserController) UpdateFields(c *gin.Context) { + id, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + common.Error(c, 400, "ID格式错误") + return + } + var fields map[string]interface{} + if err := c.ShouldBindJSON(&fields); err != nil { + common.Error(c, 400, "参数错误") + return + } + if err := ctrl.service.UpdateFields(id, fields); err != nil { + common.Error(c, 500, err.Error()) + return + } + common.Success(c, nil) +} + +// Delete 删除平台用户 +// @Summary 删除平台用户 +// @Tags 平台用户 +// @Param id path int true "ID" +// @Success 200 {object} common.Response +// @Router /platform-users/{id} [delete] +func (ctrl *PlatformUserController) Delete(c *gin.Context) { + id, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + common.Error(c, 400, "ID格式错误") + return + } + if err := ctrl.service.Delete(id); err != nil { + common.Error(c, 500, err.Error()) + return + } + common.Success(c, nil) +} diff --git a/server/modules/user/controller/user_auth_controller.go b/server/modules/user/controller/user_auth_controller.go new file mode 100644 index 0000000..2f51ced --- /dev/null +++ b/server/modules/user/controller/user_auth_controller.go @@ -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) +} diff --git a/server/modules/user/controller/user_controller.go b/server/modules/user/controller/user_controller.go new file mode 100644 index 0000000..964c8ce --- /dev/null +++ b/server/modules/user/controller/user_controller.go @@ -0,0 +1,158 @@ +// Package controller 控制器层 +package controller + +import ( + "strconv" + + "server/common" + "server/modules/user/dto" + "server/modules/user/service" + + "github.com/gin-gonic/gin" +) + +type UserController struct { + service *service.UserService +} + +func NewUserController() *UserController { + return &UserController{service: service.NewUserService()} +} + +func (ctrl *UserController) RegisterRoutes(r *gin.RouterGroup) { + group := r.Group("/users") + group.GET("", ctrl.List) + group.GET("/:id", ctrl.GetByID) + group.POST("", ctrl.Create) + group.PUT("/:id", ctrl.Update) + group.PATCH("/:id", ctrl.UpdateFields) + group.DELETE("/:id", ctrl.Delete) +} + +// List 获取用户列表 +// @Summary 获取用户列表 +// @Tags 用户 +// @Param page query int false "页码" +// @Param size query int false "每页数量" +// @Success 200 {object} common.Response +// @Router /users [get] +func (ctrl *UserController) 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 int true "ID" +// @Success 200 {object} common.Response +// @Router /users/{id} [get] +func (ctrl *UserController) GetByID(c *gin.Context) { + id, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + common.Error(c, 400, "ID格式错误") + return + } + item, err := ctrl.service.GetByID(id) + if err != nil { + common.Error(c, 404, "未找到") + return + } + common.Success(c, item) +} + +// Create 创建用户 +// @Summary 创建用户 +// @Tags 用户 +// @Param item body dto.CreateUserRequest true "用户信息" +// @Success 200 {object} common.Response +// @Router /users [post] +func (ctrl *UserController) Create(c *gin.Context) { + var req dto.CreateUserRequest + if err := c.ShouldBindJSON(&req); err != nil { + common.Error(c, 400, "参数错误") + return + } + item, err := ctrl.service.Create(&req) + if err != nil { + common.Error(c, 500, err.Error()) + return + } + common.Success(c, item) +} + +// Update 更新用户 +// @Summary 更新用户 +// @Tags 用户 +// @Param id path int true "ID" +// @Param item body dto.UpdateUserRequest true "用户信息" +// @Success 200 {object} common.Response +// @Router /users/{id} [put] +func (ctrl *UserController) Update(c *gin.Context) { + id, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + common.Error(c, 400, "ID格式错误") + return + } + var req dto.UpdateUserRequest + if err := c.ShouldBindJSON(&req); err != nil { + common.Error(c, 400, "参数错误") + return + } + item, err := ctrl.service.Update(id, &req) + if err != nil { + common.Error(c, 500, err.Error()) + return + } + common.Success(c, item) +} + +// UpdateFields 动态字段更新 +// @Summary 动态字段更新 +// @Tags 用户 +// @Param id path int true "ID" +// @Param fields body map[string]interface{} true "要更新的字段" +// @Success 200 {object} common.Response +// @Router /users/{id} [patch] +func (ctrl *UserController) UpdateFields(c *gin.Context) { + id, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + common.Error(c, 400, "ID格式错误") + return + } + var fields map[string]interface{} + if err := c.ShouldBindJSON(&fields); err != nil { + common.Error(c, 400, "参数错误") + return + } + if err := ctrl.service.UpdateFields(id, fields); err != nil { + common.Error(c, 500, err.Error()) + return + } + common.Success(c, nil) +} + +// Delete 删除用户 +// @Summary 删除用户 +// @Tags 用户 +// @Param id path int true "ID" +// @Success 200 {object} common.Response +// @Router /users/{id} [delete] +func (ctrl *UserController) Delete(c *gin.Context) { + id, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + common.Error(c, 400, "ID格式错误") + return + } + if err := ctrl.service.Delete(id); err != nil { + common.Error(c, 500, err.Error()) + return + } + common.Success(c, nil) +} diff --git a/server/modules/user/controller/user_major_controller.go b/server/modules/user/controller/user_major_controller.go new file mode 100644 index 0000000..d0876ea --- /dev/null +++ b/server/modules/user/controller/user_major_controller.go @@ -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) +} diff --git a/server/modules/user/controller/user_profile_controller.go b/server/modules/user/controller/user_profile_controller.go new file mode 100644 index 0000000..9797592 --- /dev/null +++ b/server/modules/user/controller/user_profile_controller.go @@ -0,0 +1,51 @@ +// Package controller 控制器层 +package controller + +import ( + "strconv" + + "server/common" + "server/modules/user/service" + + "github.com/gin-gonic/gin" +) + +type UserProfileController struct { + service *service.UserService +} + +func NewUserProfileController() *UserProfileController { + return &UserProfileController{service: service.NewUserService()} +} + +func (ctrl *UserProfileController) RegisterRoutes(r *gin.RouterGroup) { + group := r.Group("/user") + group.GET("/profile", ctrl.GetProfile) +} + +// GetProfile 获取登录用户信息 +// @Summary 获取登录用户信息 +// @Tags 用户 +// @Param platformType query int false "平台类型(默认1-微信小程序)" +// @Success 200 {object} common.Response +// @Router /user/profile [get] +func (ctrl *UserProfileController) GetProfile(c *gin.Context) { + loginUser := common.GetLoginUser(c) + if loginUser == nil || loginUser.ID == "" { + common.Error(c, 401, "未登录") + return + } + userID, err := strconv.ParseInt(loginUser.ID, 10, 64) + if err != nil { + common.Error(c, 400, "用户ID格式错误") + return + } + + platformType, _ := strconv.ParseInt(c.DefaultQuery("platformType", "1"), 10, 8) + profile, err := ctrl.service.GetProfile(userID, int8(platformType)) + if err != nil { + common.Error(c, 500, err.Error()) + return + } + common.Success(c, profile) +} diff --git a/server/modules/user/controller/user_score_controller.go b/server/modules/user/controller/user_score_controller.go new file mode 100644 index 0000000..7ca0224 --- /dev/null +++ b/server/modules/user/controller/user_score_controller.go @@ -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, + }) +} diff --git a/server/modules/user/controller/user_volunteer_controller.go b/server/modules/user/controller/user_volunteer_controller.go new file mode 100644 index 0000000..c34083e --- /dev/null +++ b/server/modules/user/controller/user_volunteer_controller.go @@ -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, "切换成功") +} diff --git a/server/modules/user/dto/platform_user_dto.go b/server/modules/user/dto/platform_user_dto.go new file mode 100644 index 0000000..0b3ace2 --- /dev/null +++ b/server/modules/user/dto/platform_user_dto.go @@ -0,0 +1,30 @@ +// Package dto 请求参数 +package dto + +import ( + "time" + + "gorm.io/datatypes" +) + +// CreatePlatformUserRequest 创建平台用户请求 +type CreatePlatformUserRequest struct { + UserID int64 `json:"userId"` + PlatformType int8 `json:"platformType"` + PlatformOpenID string `json:"platformOpenid"` + PlatformUnionID string `json:"platformUnionid"` + PlatformSessionKey string `json:"platformSessionKey"` + PlatformExtra datatypes.JSONMap `json:"platformExtra"` + LastLoginTime *time.Time `json:"lastLoginTime"` +} + +// UpdatePlatformUserRequest 更新平台用户请求 +type UpdatePlatformUserRequest struct { + UserID *int64 `json:"userId"` + PlatformType *int8 `json:"platformType"` + PlatformOpenID *string `json:"platformOpenid"` + PlatformUnionID *string `json:"platformUnionid"` + PlatformSessionKey *string `json:"platformSessionKey"` + PlatformExtra datatypes.JSONMap `json:"platformExtra"` + LastLoginTime *time.Time `json:"lastLoginTime"` +} diff --git a/server/modules/user/dto/user_dto.go b/server/modules/user/dto/user_dto.go new file mode 100644 index 0000000..bda6e90 --- /dev/null +++ b/server/modules/user/dto/user_dto.go @@ -0,0 +1,22 @@ +// Package dto 请求参数 +package dto + +// CreateUserRequest 创建用户请求 +type CreateUserRequest struct { + Username string `json:"username"` + Nickname string `json:"nickname"` + AvatarURL string `json:"avatarUrl"` + Phone *string `json:"phone"` + Gender *int8 `json:"gender"` + Status *int8 `json:"status"` +} + +// UpdateUserRequest 更新用户请求 +type UpdateUserRequest struct { + Username *string `json:"username"` + Nickname *string `json:"nickname"` + AvatarURL *string `json:"avatarUrl"` + Phone *string `json:"phone"` + Gender *int8 `json:"gender"` + Status *int8 `json:"status"` +} diff --git a/server/modules/user/dto/user_major_dto.go b/server/modules/user/dto/user_major_dto.go new file mode 100644 index 0000000..b6ad873 --- /dev/null +++ b/server/modules/user/dto/user_major_dto.go @@ -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"` +} diff --git a/server/modules/user/dto/user_volunteer_dto.go b/server/modules/user/dto/user_volunteer_dto.go new file mode 100644 index 0000000..c641a1d --- /dev/null +++ b/server/modules/user/dto/user_volunteer_dto.go @@ -0,0 +1,6 @@ +package dto + +// SaveVolunteerRequest 保存志愿请求 +type SaveVolunteerRequest struct { + Keys []string `json:"keys" binding:"required"` // Keys: schoolCode_majorCode_enrollmentCode +} \ No newline at end of file diff --git a/server/modules/user/entity/platform_user.go b/server/modules/user/entity/platform_user.go new file mode 100644 index 0000000..71af688 --- /dev/null +++ b/server/modules/user/entity/platform_user.go @@ -0,0 +1,28 @@ +// Package entity 数据实体 +package entity + +import ( + "time" + + "gorm.io/datatypes" +) + +// PlatformUser 平台用户关联信息 +type PlatformUser struct { + ID int64 `gorm:"column:id;primaryKey;autoIncrement;comment:平台用户ID(自增)" json:"id"` + UserID int64 `gorm:"column:user_id;comment:关联t_user.id" json:"userId"` + PlatformType int8 `gorm:"column:platform_type;comment:平台类型:1-微信小程序,2-抖音小程序,3-支付宝小程序" json:"platformType"` + PlatformOpenID string `gorm:"column:platform_openid;comment:平台唯一标识(微信openid/抖音open_id)" json:"platformOpenid"` + PlatformUnionID string `gorm:"column:platform_unionid;comment:平台统一标识(微信unionid,多小程序互通用)" json:"platformUnionid"` + PlatformSessionKey string `gorm:"column:platform_session_key;comment:平台会话密钥(微信session_key,加密存储)" json:"platformSessionKey"` + PlatformExtra datatypes.JSONMap `gorm:"column:platform_extra;comment:平台扩展字段(如抖音的user_name、微信的city等)" json:"platformExtra"` + LastLoginTime *time.Time `gorm:"column:last_login_time;comment:最后登录时间" json:"lastLoginTime"` + CreateTime time.Time `gorm:"column:create_time;autoCreateTime;comment:创建时间" json:"createTime"` + UpdateTime time.Time `gorm:"column:update_time;autoUpdateTime;comment:更新时间" json:"updateTime"` + Deleted int8 `gorm:"column:deleted;comment:软删除:0-未删,1-已删" json:"deleted"` +} + +// TableName 指定表名 +func (PlatformUser) TableName() string { + return "t_platform_user" +} diff --git a/server/modules/user/entity/user.go b/server/modules/user/entity/user.go new file mode 100644 index 0000000..3e06711 --- /dev/null +++ b/server/modules/user/entity/user.go @@ -0,0 +1,25 @@ +// Package entity 数据实体 +package entity + +import "time" + +// User 用户基础信息 +type User struct { + ID int64 `gorm:"column:id;primaryKey;autoIncrement;comment:全局唯一用户ID(自增)" json:"id"` + Username string `gorm:"column:username;comment:用户名(可选,后台管理用)" json:"username"` + Nickname string `gorm:"column:nickname;comment:用户昵称(各平台统一)" json:"nickname"` + AvatarURL string `gorm:"column:avatar_url;comment:用户头像URL" json:"avatarUrl"` + Phone *string `gorm:"column:phone;comment:手机号(脱敏存储,如138****1234)" json:"phone"` + Password *string `gorm:"column:password;comment:登录密码" json:"-"` + Salt *string `gorm:"column:salt;comment:密码盐值" json:"-"` + Gender int8 `gorm:"column:gender;comment:性别:0-未知,1-男,2-女" json:"gender"` + Status int8 `gorm:"column:status;comment:状态:0-禁用,1-正常" json:"status"` + CreateTime time.Time `gorm:"column:create_time;autoCreateTime;comment:创建时间" json:"createTime"` + UpdateTime time.Time `gorm:"column:update_time;autoUpdateTime;comment:更新时间" json:"updateTime"` + Deleted int8 `gorm:"column:deleted;comment:软删除:0-未删,1-已删" json:"deleted"` +} + +// TableName 指定表名 +func (User) TableName() string { + return "t_user" +} diff --git a/server/modules/user/mapper/platform_user_mapper.go b/server/modules/user/mapper/platform_user_mapper.go new file mode 100644 index 0000000..3f03fe0 --- /dev/null +++ b/server/modules/user/mapper/platform_user_mapper.go @@ -0,0 +1,77 @@ +// Package mapper 数据访问层 +package mapper + +import ( + "server/config" + "server/modules/user/entity" + + "gorm.io/gorm" +) + +type PlatformUserMapper struct { + db *gorm.DB +} + +func NewPlatformUserMapper() *PlatformUserMapper { + return &PlatformUserMapper{db: config.DB} +} + +func (m *PlatformUserMapper) baseDB() *gorm.DB { + return m.db +} + +// GetDB 获取数据库实例,默认过滤软删除 +func (m *PlatformUserMapper) GetDB() *gorm.DB { + return m.baseDB().Where("deleted = 0") +} + +// FindAll 分页查询 +func (m *PlatformUserMapper) FindAll(page, size int) ([]entity.PlatformUser, int64, error) { + var items []entity.PlatformUser + var total int64 + query := m.GetDB().Model(&entity.PlatformUser{}) + query.Count(&total) + err := query.Offset((page - 1) * size).Limit(size).Find(&items).Error + return items, total, err +} + +// FindByID 根据 ID 查询 +func (m *PlatformUserMapper) FindByID(id int64) (*entity.PlatformUser, error) { + var item entity.PlatformUser + err := m.GetDB().First(&item, "id = ?", id).Error + return &item, err +} + +// FindByPlatformOpenID 根据平台类型与 openid 查询 +func (m *PlatformUserMapper) FindByPlatformOpenID(platformType int8, openid string) (*entity.PlatformUser, error) { + var item entity.PlatformUser + err := m.GetDB().First(&item, "platform_type = ? AND platform_openid = ?", platformType, openid).Error + return &item, err +} + +// FindByUserIDAndPlatformType 根据用户ID与平台类型查询 +func (m *PlatformUserMapper) FindByUserIDAndPlatformType(userID int64, platformType int8) (*entity.PlatformUser, error) { + var item entity.PlatformUser + err := m.GetDB().First(&item, "user_id = ? AND platform_type = ?", userID, platformType).Error + return &item, err +} + +// Create 创建记录 +func (m *PlatformUserMapper) Create(item *entity.PlatformUser) error { + return m.baseDB().Create(item).Error +} + +// Update 更新记录 +func (m *PlatformUserMapper) Update(item *entity.PlatformUser) error { + return m.baseDB().Save(item).Error +} + +// UpdateFields 更新指定字段 +func (m *PlatformUserMapper) UpdateFields(id int64, fields map[string]interface{}) error { + return m.baseDB().Model(&entity.PlatformUser{}).Where("id = ?", id).Updates(fields).Error +} + +// Delete 逻辑删除 +func (m *PlatformUserMapper) Delete(id int64) error { + return m.baseDB().Model(&entity.PlatformUser{}).Where("id = ?", id).Update("deleted", 1).Error +} diff --git a/server/modules/user/mapper/user_mapper.go b/server/modules/user/mapper/user_mapper.go new file mode 100644 index 0000000..9f07336 --- /dev/null +++ b/server/modules/user/mapper/user_mapper.go @@ -0,0 +1,70 @@ +// Package mapper 数据访问层 +package mapper + +import ( + "server/config" + "server/modules/user/entity" + + "gorm.io/gorm" +) + +type UserMapper struct { + db *gorm.DB +} + +func NewUserMapper() *UserMapper { + return &UserMapper{db: config.DB} +} + +func (m *UserMapper) baseDB() *gorm.DB { + return m.db +} + +// GetDB 获取数据库实例,默认过滤软删除 +func (m *UserMapper) GetDB() *gorm.DB { + return m.baseDB().Where("deleted = 0") +} + +// FindAll 分页查询 +func (m *UserMapper) FindAll(page, size int) ([]entity.User, int64, error) { + var items []entity.User + var total int64 + query := m.GetDB().Model(&entity.User{}) + query.Count(&total) + err := query.Offset((page - 1) * size).Limit(size).Find(&items).Error + return items, total, err +} + +// FindByID 根据 ID 查询 +func (m *UserMapper) FindByID(id int64) (*entity.User, error) { + var item entity.User + err := m.GetDB().First(&item, "id = ?", id).Error + return &item, err +} + +// FindByPhone 根据手机号查询 +func (m *UserMapper) FindByPhone(phone string) (*entity.User, error) { + var item entity.User + err := m.GetDB().First(&item, "phone = ?", phone).Error + return &item, err +} + +// Create 创建记录 +func (m *UserMapper) Create(item *entity.User) error { + return m.baseDB().Create(item).Error +} + +// Update 更新记录 +func (m *UserMapper) Update(item *entity.User) error { + return m.baseDB().Save(item).Error +} + +// UpdateFields 更新指定字段 +func (m *UserMapper) UpdateFields(id int64, fields map[string]interface{}) error { + return m.baseDB().Model(&entity.User{}).Where("id = ?", id).Updates(fields).Error +} + +// Delete 逻辑删除 +func (m *UserMapper) Delete(id int64) error { + return m.baseDB().Model(&entity.User{}).Where("id = ?", id).Update("deleted", 1).Error +} diff --git a/server/modules/user/service/platform_user_service.go b/server/modules/user/service/platform_user_service.go new file mode 100644 index 0000000..1264af2 --- /dev/null +++ b/server/modules/user/service/platform_user_service.go @@ -0,0 +1,94 @@ +// Package service 业务逻辑层 +package service + +import ( + "time" + + "server/modules/user/dto" + "server/modules/user/entity" + "server/modules/user/mapper" +) + +type PlatformUserService struct { + mapper *mapper.PlatformUserMapper +} + +func NewPlatformUserService() *PlatformUserService { + return &PlatformUserService{mapper: mapper.NewPlatformUserMapper()} +} + +// List 获取平台用户列表 +func (s *PlatformUserService) List(page, size int) ([]entity.PlatformUser, int64, error) { + return s.mapper.FindAll(page, size) +} + +// GetByID 获取平台用户详情 +func (s *PlatformUserService) GetByID(id int64) (*entity.PlatformUser, error) { + return s.mapper.FindByID(id) +} + +// Create 创建平台用户 +func (s *PlatformUserService) Create(req *dto.CreatePlatformUserRequest) (*entity.PlatformUser, error) { + item := &entity.PlatformUser{ + UserID: req.UserID, + PlatformType: req.PlatformType, + PlatformOpenID: req.PlatformOpenID, + PlatformUnionID: req.PlatformUnionID, + PlatformSessionKey: req.PlatformSessionKey, + PlatformExtra: req.PlatformExtra, + LastLoginTime: req.LastLoginTime, + Deleted: 0, + } + if err := s.mapper.Create(item); err != nil { + return nil, err + } + return item, nil +} + +// Update 更新平台用户 +func (s *PlatformUserService) Update(id int64, req *dto.UpdatePlatformUserRequest) (*entity.PlatformUser, error) { + fields := make(map[string]interface{}) + if req.UserID != nil { + fields["user_id"] = *req.UserID + } + if req.PlatformType != nil { + fields["platform_type"] = *req.PlatformType + } + if req.PlatformOpenID != nil { + fields["platform_openid"] = *req.PlatformOpenID + } + if req.PlatformUnionID != nil { + fields["platform_unionid"] = *req.PlatformUnionID + } + if req.PlatformSessionKey != nil { + fields["platform_session_key"] = *req.PlatformSessionKey + } + if req.PlatformExtra != nil { + fields["platform_extra"] = req.PlatformExtra + } + if req.LastLoginTime != nil { + fields["last_login_time"] = *req.LastLoginTime + } + if len(fields) == 0 { + return s.mapper.FindByID(id) + } + fields["update_time"] = time.Now() + if err := s.mapper.UpdateFields(id, fields); err != nil { + return nil, err + } + return s.mapper.FindByID(id) +} + +// UpdateFields 动态字段更新 +func (s *PlatformUserService) UpdateFields(id int64, fields map[string]interface{}) error { + if len(fields) == 0 { + return nil + } + fields["update_time"] = time.Now() + return s.mapper.UpdateFields(id, fields) +} + +// Delete 删除平台用户 +func (s *PlatformUserService) Delete(id int64) error { + return s.mapper.Delete(id) +} diff --git a/server/modules/user/service/user_score_service.go b/server/modules/user/service/user_score_service.go new file mode 100644 index 0000000..b4a3dc8 --- /dev/null +++ b/server/modules/user/service/user_score_service.go @@ -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 +} diff --git a/server/modules/user/service/user_service.go b/server/modules/user/service/user_service.go new file mode 100644 index 0000000..65a6467 --- /dev/null +++ b/server/modules/user/service/user_service.go @@ -0,0 +1,249 @@ +// Package service 业务逻辑层 +package service + +import ( + "context" + "encoding/json" + "errors" + "strconv" + "strings" + "time" + + "server/common" + "server/config" + systemEntity "server/modules/system/entity" + "server/modules/user/dto" + "server/modules/user/entity" + "server/modules/user/mapper" + "server/modules/user/vo" + + "github.com/google/uuid" + "gorm.io/datatypes" + "gorm.io/gorm" +) + +type UserService struct { + mapper *mapper.UserMapper + platformUserMapper *mapper.PlatformUserMapper +} + +func NewUserService() *UserService { + return &UserService{ + mapper: mapper.NewUserMapper(), + platformUserMapper: mapper.NewPlatformUserMapper(), + } +} + +// List 获取用户列表 +func (s *UserService) List(page, size int) ([]entity.User, int64, error) { + return s.mapper.FindAll(page, size) +} + +// GetByID 获取用户详情 +func (s *UserService) GetByID(id int64) (*entity.User, error) { + return s.mapper.FindByID(id) +} + +// Create 创建用户 +func (s *UserService) Create(req *dto.CreateUserRequest) (*entity.User, error) { + phone := normalizeOptionalString(req.Phone) + user := &entity.User{ + Username: req.Username, + Nickname: req.Nickname, + AvatarURL: req.AvatarURL, + Phone: phone, + Gender: 0, + Status: 1, + Deleted: 0, + } + if req.Gender != nil { + user.Gender = *req.Gender + } + if req.Status != nil { + user.Status = *req.Status + } + if err := s.mapper.Create(user); err != nil { + return nil, err + } + return user, nil +} + +// Update 更新用户 +func (s *UserService) Update(id int64, req *dto.UpdateUserRequest) (*entity.User, error) { + fields := make(map[string]interface{}) + if req.Username != nil { + fields["username"] = *req.Username + } + if req.Nickname != nil { + fields["nickname"] = *req.Nickname + } + if req.AvatarURL != nil { + fields["avatar_url"] = *req.AvatarURL + } + if req.Phone != nil { + fields["phone"] = normalizeOptionalString(req.Phone) + } + if req.Gender != nil { + fields["gender"] = *req.Gender + } + if req.Status != nil { + fields["status"] = *req.Status + } + if len(fields) == 0 { + return s.mapper.FindByID(id) + } + fields["update_time"] = time.Now() + if err := s.mapper.UpdateFields(id, fields); err != nil { + return nil, err + } + return s.mapper.FindByID(id) +} + +// UpdateFields 动态字段更新 +func (s *UserService) UpdateFields(id int64, fields map[string]interface{}) error { + if len(fields) == 0 { + return nil + } + fields["update_time"] = time.Now() + return s.mapper.UpdateFields(id, fields) +} + +// Delete 删除用户 +func (s *UserService) Delete(id int64) error { + return s.mapper.Delete(id) +} + +func normalizeOptionalString(val *string) *string { + if val == nil { + return nil + } + trimmed := strings.TrimSpace(*val) + if trimmed == "" { + return nil + } + return &trimmed +} + +// LoginByPhonePassword 手机号密码登录 +func (s *UserService) LoginByPhonePassword(phone, password string) (*systemEntity.LoginUser, string, error) { + phone = strings.TrimSpace(phone) + if phone == "" || strings.TrimSpace(password) == "" { + return nil, "", errors.New("手机号或密码不能为空") + } + + user, err := s.mapper.FindByPhone(phone) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, "", errors.New("用户不存在") + } + return nil, "", err + } + if user.Status == 0 { + return nil, "", errors.New("账号已被禁用") + } + if user.Password == nil || strings.TrimSpace(*user.Password) == "" || user.Salt == nil || strings.TrimSpace(*user.Salt) == "" { + return nil, "", errors.New("用户未设置密码") + } + + encrypted, err := common.Encrypt(phone, password, *user.Salt) + if err != nil { + return nil, "", errors.New("密码校验失败") + } + if encrypted != *user.Password { + return nil, "", errors.New("手机号或密码错误") + } + + token := uuid.NewString() + loginUser := &systemEntity.LoginUser{ + ID: strconv.FormatInt(user.ID, 10), + Username: chooseUsernameForLogin(user.Username, phone), + Realname: user.Nickname, + Avatar: user.AvatarURL, + Phone: phone, + Email: "", + Token: token, + } + if err := s.saveLoginUser(token, loginUser); err != nil { + return nil, "", errors.New("登录失败,请重试") + } + return loginUser, token, nil +} + +func chooseUsernameForLogin(username, phone string) string { + if strings.TrimSpace(username) != "" { + return username + } + return phone +} + +func (s *UserService) saveLoginUser(token string, user *systemEntity.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() +} + +// GetProfile 获取登录用户信息 +func (s *UserService) GetProfile(userID int64, platformType int8) (*vo.UserProfileVO, error) { + user, err := s.mapper.FindByID(userID) + if err != nil { + return nil, err + } + + var platform *entity.PlatformUser + platform, err = s.platformUserMapper.FindByUserIDAndPlatformType(userID, platformType) + if err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + platform = nil + } + + profile := &vo.UserProfileVO{ + UserID: user.ID, + Username: user.Username, + Nickname: user.Nickname, + AvatarURL: user.AvatarURL, + Phone: normalizeValue(user.Phone), + Gender: user.Gender, + } + + if platform != nil { + profile.PlatformType = platform.PlatformType + profile.PlatformOpenID = platform.PlatformOpenID + profile.PlatformUnionID = platform.PlatformUnionID + profile.PlatformExtra = platform.PlatformExtra + profile.Region = buildRegion(platform.PlatformExtra) + } + + return profile, nil +} + +func normalizeValue(val *string) string { + if val == nil { + return "" + } + return strings.TrimSpace(*val) +} + +func buildRegion(extra datatypes.JSONMap) string { + if extra == nil { + return "" + } + if region, ok := extra["region"].(string); ok && strings.TrimSpace(region) != "" { + return strings.TrimSpace(region) + } + province, _ := extra["province"].(string) + city, _ := extra["city"].(string) + country, _ := extra["country"].(string) + + parts := []string{} + for _, item := range []string{country, province, city} { + if strings.TrimSpace(item) != "" { + parts = append(parts, strings.TrimSpace(item)) + } + } + return strings.Join(parts, " ") +} diff --git a/server/modules/user/vo/user_profile_vo.go b/server/modules/user/vo/user_profile_vo.go new file mode 100644 index 0000000..208d1bb --- /dev/null +++ b/server/modules/user/vo/user_profile_vo.go @@ -0,0 +1,19 @@ +// Package vo 视图对象 +package vo + +import "gorm.io/datatypes" + +// UserProfileVO 登录用户信息 +type UserProfileVO struct { + UserID int64 `json:"userId"` + Username string `json:"username"` + Nickname string `json:"nickname"` + AvatarURL string `json:"avatarUrl"` + Phone string `json:"phone"` + Gender int8 `json:"gender"` + Region string `json:"region"` + PlatformType int8 `json:"platformType"` + PlatformOpenID string `json:"platformOpenid"` + PlatformUnionID string `json:"platformUnionid"` + PlatformExtra datatypes.JSONMap `json:"platformExtra"` +} diff --git a/server/modules/user/vo/user_score_vo.go b/server/modules/user/vo/user_score_vo.go new file mode 100644 index 0000000..4735b1c --- /dev/null +++ b/server/modules/user/vo/user_score_vo.go @@ -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"` // 计算表名称 +} diff --git a/server/modules/user/vo/volunteer_detail_vo.go b/server/modules/user/vo/volunteer_detail_vo.go new file mode 100644 index 0000000..1b2eb7a --- /dev/null +++ b/server/modules/user/vo/volunteer_detail_vo.go @@ -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"` // 专科批 +} diff --git a/server/modules/yx/controller/yx_calculation_major_controller.go b/server/modules/yx/controller/yx_calculation_major_controller.go new file mode 100644 index 0000000..1d389a8 --- /dev/null +++ b/server/modules/yx/controller/yx_calculation_major_controller.go @@ -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) +} diff --git a/server/modules/yx/controller/yx_history_major_enroll_controller.go b/server/modules/yx/controller/yx_history_major_enroll_controller.go new file mode 100644 index 0000000..34b5550 --- /dev/null +++ b/server/modules/yx/controller/yx_history_major_enroll_controller.go @@ -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) +} diff --git a/server/modules/yx/controller/yx_school_major_controller.go b/server/modules/yx/controller/yx_school_major_controller.go new file mode 100644 index 0000000..e13d96b --- /dev/null +++ b/server/modules/yx/controller/yx_school_major_controller.go @@ -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) +} diff --git a/server/modules/yx/controller/yx_user_score_controller.go b/server/modules/yx/controller/yx_user_score_controller.go new file mode 100644 index 0000000..abd4aa0 --- /dev/null +++ b/server/modules/yx/controller/yx_user_score_controller.go @@ -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) +} diff --git a/server/modules/yx/controller/yx_volunteer_controller.go b/server/modules/yx/controller/yx_volunteer_controller.go new file mode 100644 index 0000000..24312ff --- /dev/null +++ b/server/modules/yx/controller/yx_volunteer_controller.go @@ -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) +} diff --git a/server/modules/yx/controller/yx_volunteer_record_controller.go b/server/modules/yx/controller/yx_volunteer_record_controller.go new file mode 100644 index 0000000..bd1e773 --- /dev/null +++ b/server/modules/yx/controller/yx_volunteer_record_controller.go @@ -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) +} diff --git a/server/modules/yx/dto/probability_count_dto.go b/server/modules/yx/dto/probability_count_dto.go new file mode 100644 index 0000000..4d256c3 --- /dev/null +++ b/server/modules/yx/dto/probability_count_dto.go @@ -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) +} diff --git a/server/modules/yx/dto/yx_calculation_major_dto.go b/server/modules/yx/dto/yx_calculation_major_dto.go new file mode 100644 index 0000000..4cfc867 --- /dev/null +++ b/server/modules/yx/dto/yx_calculation_major_dto.go @@ -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"` // 学生分数 +} \ No newline at end of file diff --git a/server/modules/yx/dto/yx_school_major_dto.go b/server/modules/yx/dto/yx_school_major_dto.go new file mode 100644 index 0000000..1f9e93c --- /dev/null +++ b/server/modules/yx/dto/yx_school_major_dto.go @@ -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"` // 对应的 院校代码 +} diff --git a/server/modules/yx/dto/yx_user_score_dto.go b/server/modules/yx/dto/yx_user_score_dto.go new file mode 100644 index 0000000..1895a00 --- /dev/null +++ b/server/modules/yx/dto/yx_user_score_dto.go @@ -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 +} diff --git a/server/modules/yx/dto/yx_volunteer_dto.go b/server/modules/yx/dto/yx_volunteer_dto.go new file mode 100644 index 0000000..2d5a256 --- /dev/null +++ b/server/modules/yx/dto/yx_volunteer_dto.go @@ -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-历史) +} \ No newline at end of file diff --git a/server/modules/yx/entity/yx_calculation_major.go b/server/modules/yx/entity/yx_calculation_major.go new file mode 100644 index 0000000..1f34efa --- /dev/null +++ b/server/modules/yx/entity/yx_calculation_major.go @@ -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" +} diff --git a/server/modules/yx/entity/yx_history_major_enroll.go b/server/modules/yx/entity/yx_history_major_enroll.go new file mode 100644 index 0000000..51cd22c --- /dev/null +++ b/server/modules/yx/entity/yx_history_major_enroll.go @@ -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" +} diff --git a/server/modules/yx/entity/yx_history_score_control_line.go b/server/modules/yx/entity/yx_history_score_control_line.go new file mode 100644 index 0000000..c4702f1 --- /dev/null +++ b/server/modules/yx/entity/yx_history_score_control_line.go @@ -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" +} diff --git a/server/modules/yx/entity/yx_school_major.go b/server/modules/yx/entity/yx_school_major.go new file mode 100644 index 0000000..5754770 --- /dev/null +++ b/server/modules/yx/entity/yx_school_major.go @@ -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" +} diff --git a/server/modules/yx/entity/yx_user_score.go b/server/modules/yx/entity/yx_user_score.go new file mode 100644 index 0000000..47272e2 --- /dev/null +++ b/server/modules/yx/entity/yx_user_score.go @@ -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" +} diff --git a/server/modules/yx/entity/yx_volunteer.go b/server/modules/yx/entity/yx_volunteer.go new file mode 100644 index 0000000..feb4d35 --- /dev/null +++ b/server/modules/yx/entity/yx_volunteer.go @@ -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" +} diff --git a/server/modules/yx/entity/yx_volunteer_record.go b/server/modules/yx/entity/yx_volunteer_record.go new file mode 100644 index 0000000..948cc62 --- /dev/null +++ b/server/modules/yx/entity/yx_volunteer_record.go @@ -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" +} diff --git a/server/modules/yx/mapper/yx_calculation_major_mapper.go b/server/modules/yx/mapper/yx_calculation_major_mapper.go new file mode 100644 index 0000000..a6d8f66 --- /dev/null +++ b/server/modules/yx/mapper/yx_calculation_major_mapper.go @@ -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 +} diff --git a/server/modules/yx/mapper/yx_history_major_enroll_mapper.go b/server/modules/yx/mapper/yx_history_major_enroll_mapper.go new file mode 100644 index 0000000..cb012f5 --- /dev/null +++ b/server/modules/yx/mapper/yx_history_major_enroll_mapper.go @@ -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 +} diff --git a/server/modules/yx/mapper/yx_history_score_control_line_mapper.go b/server/modules/yx/mapper/yx_history_score_control_line_mapper.go new file mode 100644 index 0000000..cb317d7 --- /dev/null +++ b/server/modules/yx/mapper/yx_history_score_control_line_mapper.go @@ -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 +} diff --git a/server/modules/yx/mapper/yx_school_major_mapper.go b/server/modules/yx/mapper/yx_school_major_mapper.go new file mode 100644 index 0000000..b30092a --- /dev/null +++ b/server/modules/yx/mapper/yx_school_major_mapper.go @@ -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 +} diff --git a/server/modules/yx/mapper/yx_user_score_mapper.go b/server/modules/yx/mapper/yx_user_score_mapper.go new file mode 100644 index 0000000..e58bf62 --- /dev/null +++ b/server/modules/yx/mapper/yx_user_score_mapper.go @@ -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 +} diff --git a/server/modules/yx/mapper/yx_volunteer_mapper.go b/server/modules/yx/mapper/yx_volunteer_mapper.go new file mode 100644 index 0000000..40eb168 --- /dev/null +++ b/server/modules/yx/mapper/yx_volunteer_mapper.go @@ -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 +} diff --git a/server/modules/yx/mapper/yx_volunteer_record_mapper.go b/server/modules/yx/mapper/yx_volunteer_record_mapper.go new file mode 100644 index 0000000..344a31d --- /dev/null +++ b/server/modules/yx/mapper/yx_volunteer_record_mapper.go @@ -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) +} diff --git a/server/modules/yx/service/yx_calculation_major_service.go b/server/modules/yx/service/yx_calculation_major_service.go new file mode 100644 index 0000000..ceb3b48 --- /dev/null +++ b/server/modules/yx/service/yx_calculation_major_service.go @@ -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 +} diff --git a/server/modules/yx/service/yx_history_major_enroll_service.go b/server/modules/yx/service/yx_history_major_enroll_service.go new file mode 100644 index 0000000..8861fac --- /dev/null +++ b/server/modules/yx/service/yx_history_major_enroll_service.go @@ -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) +} diff --git a/server/modules/yx/service/yx_history_score_control_line_service.go b/server/modules/yx/service/yx_history_score_control_line_service.go new file mode 100644 index 0000000..321053c --- /dev/null +++ b/server/modules/yx/service/yx_history_score_control_line_service.go @@ -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 +} diff --git a/server/modules/yx/service/yx_school_major_service.go b/server/modules/yx/service/yx_school_major_service.go new file mode 100644 index 0000000..cc51a09 --- /dev/null +++ b/server/modules/yx/service/yx_school_major_service.go @@ -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) +} diff --git a/server/modules/yx/service/yx_user_score_service.go b/server/modules/yx/service/yx_user_score_service.go new file mode 100644 index 0000000..419828e --- /dev/null +++ b/server/modules/yx/service/yx_user_score_service.go @@ -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) +} diff --git a/server/modules/yx/service/yx_volunteer_record_service.go b/server/modules/yx/service/yx_volunteer_record_service.go new file mode 100644 index 0000000..b8631af --- /dev/null +++ b/server/modules/yx/service/yx_volunteer_record_service.go @@ -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) +} diff --git a/server/modules/yx/service/yx_volunteer_service.go b/server/modules/yx/service/yx_volunteer_service.go new file mode 100644 index 0000000..d9375ac --- /dev/null +++ b/server/modules/yx/service/yx_volunteer_service.go @@ -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, + } +} diff --git a/server/modules/yx/vo/user_volunteer_vo.go b/server/modules/yx/vo/user_volunteer_vo.go new file mode 100644 index 0000000..e5b0b53 --- /dev/null +++ b/server/modules/yx/vo/user_volunteer_vo.go @@ -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"` // 专业成绩分 +} diff --git a/server/modules/yx/vo/yx_calculation_major_vo.go b/server/modules/yx/vo/yx_calculation_major_vo.go new file mode 100644 index 0000000..16cb845 --- /dev/null +++ b/server/modules/yx/vo/yx_calculation_major_vo.go @@ -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"` // 更新时间 +} \ No newline at end of file diff --git a/server/modules/yx/vo/yx_history_major_enroll_vo.go b/server/modules/yx/vo/yx_history_major_enroll_vo.go new file mode 100644 index 0000000..aa4bb47 --- /dev/null +++ b/server/modules/yx/vo/yx_history_major_enroll_vo.go @@ -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"` // 录取规则概率(山东) + // 其他字段... +} \ No newline at end of file diff --git a/server/modules/yx/vo/yx_volunteer_vo.go b/server/modules/yx/vo/yx_volunteer_vo.go new file mode 100644 index 0000000..9783746 --- /dev/null +++ b/server/modules/yx/vo/yx_volunteer_vo.go @@ -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"` // 所属部门 +} \ No newline at end of file diff --git a/server/tests/help.md b/server/tests/help.md new file mode 100644 index 0000000..b83867d --- /dev/null +++ b/server/tests/help.md @@ -0,0 +1,6 @@ +``` +cd server/tests +go test -v -run TestRedisOperation # 运行 Redis 测试 +go test -v -run TestSchoolMajorQuery # 运行 SQL 查询测试 +go test -v # 运行所有测试 +``` \ No newline at end of file diff --git a/server/tests/init_test.go b/server/tests/init_test.go new file mode 100644 index 0000000..d1f278d --- /dev/null +++ b/server/tests/init_test.go @@ -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() +} diff --git a/server/tests/service_test.go b/server/tests/service_test.go new file mode 100644 index 0000000..021ff01 --- /dev/null +++ b/server/tests/service_test.go @@ -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) +} diff --git a/server/types/date_time.go b/server/types/date_time.go new file mode 100644 index 0000000..97042b3 --- /dev/null +++ b/server/types/date_time.go @@ -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()) +} \ No newline at end of file diff --git a/task_detail_2026_03_14.md b/task_detail_2026_03_14.md new file mode 100644 index 0000000..f80b429 --- /dev/null +++ b/task_detail_2026_03_14.md @@ -0,0 +1,215 @@ +# 任务执行摘要 + +## 会话 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+点击跳转与路径打开建议 + + + +# 任务执行摘要 + +## 会话 ID: local +- [2026-03-14 20:59:52] +- **执行原因**: PostgreSQL 适配与 t_user/t_platform_user 全链路开发 +- **执行过程**: + 1. 新增 PostgreSQL 驱动并按配置切换数据源。 + 2. 创建 t_user、t_platform_user 的 MySQL/PostgreSQL 建表脚本。 + 3. 完成实体、Mapper、Service、Controller 与路由注册。 +- **执行结果**: 支持 MySQL/PostgreSQL 切换并新增两张表的全链路接口 + + +# 任务执行摘要 + +## 会话 ID: local +- [2026-03-14 21:14:19] +- **执行原因**: 处理 swag 命令缺失问题并提供安装指引 +- **执行过程**: + 1. 分析终端报错为 swag 未安装或 PATH 未配置。 + 2. 给出 go install 安装与 PATH 配置步骤。 +- **执行结果**: 提供 swag 安装与环境变量修复指引 + + +# 任务执行摘要 + +## 会话 ID: local +- [2026-03-14 21:15:10] +- **执行原因**: 用户使用 asdf 管理 Go,提供适配的 swag 安装步骤 +- **执行过程**: + 1. 提供 asdf 环境下 go install 与 reshim 步骤。 + 2. 补充 PATH 配置与 swag 版本检查。 +- **执行结果**: 输出 asdf 场景的 swag 安装与修复指引 + + +# 任务执行摘要 + +## 会话 ID: local +- [2026-03-14 21:35:21] +- **执行原因**: 新增对外 API 模块与微信小程序登录接口,并补充配置 +- **执行过程**: + 1. 新增对外 API 模块控制器与微信小程序登录服务。 + 2. 接入微信 jscode2session 并落库 t_user/t_platform_user。 + 3. 配置文件新增微信小程序 app_id/app_secret 并加入鉴权白名单。 +- **执行结果**: 对外 API 与微信小程序登录接口可用 + + +# 任务执行摘要 + +## 会话 ID: local +- [2026-03-14 21:44:16] +- **执行原因**: 输出微信小程序登录对接文档供前端使用 +- **执行过程**: + 1. 编写接口路径、请求/响应示例与字段说明。 + 2. 补充安全校验与配置项说明。 +- **执行结果**: 已生成微信小程序登录接口文档 + + +# 任务执行摘要 + +## 会话 ID: local +- [2026-03-14 22:15:31] +- **执行原因**: 支持微信小程序快速手机号登录并返回鉴权 token +- **执行过程**: + 1. 扩展登录请求支持 phoneCode/encryptedData/iv 并解密手机号。 + 2. 接入微信手机号与 access_token 接口,缓存 token。 + 3. 登录后写入 Redis 并返回 token,同时更新接口文档。 +- **执行结果**: 小程序登录支持手机号解密与鉴权 token 返回 + + +# 任务执行摘要 + +## 会话 ID: local +- [2026-03-14 22:30:24] +- **执行原因**: 修复 go test 报错的手机号字段读取问题 +- **执行过程**: + 1. 将手机号返回结构读取改为 phone_info.phoneNumber。 +- **执行结果**: 修复编译错误,代码可继续测试 + + +# 任务执行摘要 + +## 会话 ID: local +- [2026-03-14 23:00:16] +- **执行原因**: 微信小程序登录新用户随机用户名与头像 +- **执行过程**: + 1. 新增随机用户名生成逻辑。 + 2. 默认头像从 docs/avatar.md 列表中随机选择。 + 3. 更新接口文档说明。 +- **执行结果**: 新用户自动生成用户名并随机头像 + + +# 任务执行摘要 + +## 会话 ID: local +- [2026-03-14 23:12:32] +- **执行原因**: 新增登录后获取用户信息接口 +- **执行过程**: + 1. 新增用户资料 VO 与平台用户查询方法。 + 2. 增加用户信息查询服务并拼装平台扩展字段。 + 3. 添加 /api/user/profile 接口并注册路由。 +- **执行结果**: 登录后可通过接口获取用户信息 + + +# 任务执行摘要 + +## 会话 ID: local +- [2026-03-15 11:14:41] +- **执行原因**: t_user 增加密码并新增手机号密码登录接口 +- **执行过程**: + 1. 为 t_user 增加 password/salt 字段并更新建表脚本。 + 2. 新增手机号密码登录服务与对外接口。 + 3. 登录后写入 Redis 并加入鉴权白名单。 +- **执行结果**: 支持手机号密码登录与 token 鉴权 + + +# 任务执行摘要 + +## 会话 ID: local +- [2026-03-15 11:21:08] +- **执行原因**: 输出登录后获取用户信息接口文档给前端 +- **执行过程**: + 1. 编写 /api/user/profile 的请求参数与响应说明。 + 2. 补充示例与错误码说明。 +- **执行结果**: 已生成用户信息接口文档 + + +# 任务执行摘要 + +## 会话 ID: local +- [2026-03-15 14:35:43] +- **执行原因**: 输出手机号密码登录接口文档给前端 +- **执行过程**: + 1. 编写 /api/open/user/login 的请求与响应说明。 + 2. 补充示例与安全校验说明。 +- **执行结果**: 已生成手机号密码登录接口文档 + + +# 任务执行摘要 + +## 会话 ID: local +- [2026-03-15 23:09:37] +- **执行原因**: 微信快速登录新用户设置默认密码与手机号昵称 +- **执行过程**: + 1. 新用户创建时生成密码盐并设置默认密码 123456。 + 2. 新用户昵称按手机号掩码格式设置。 + 3. 更新微信登录接口文档说明。 +- **执行结果**: 新用户默认密码与昵称规则生效 + + +# 任务执行摘要 + +## 会话 ID: local +- [2026-03-16 09:31:10] +- **执行原因**: 提供小程序获取接口与 WebView 地址的版本一致性技术方案 +- **执行过程**: + 1. 设计版本配置中心与接口协议。 + 2. 给出客户端缓存、校验与提示策略。 +- **执行结果**: 输出可落地的版本一致性方案 + + +# 任务执行摘要 + +## 会话 ID: local +- [2026-03-16 09:32:32] +- **执行原因**: 将版本一致性方案写入 Markdown 文档 +- **执行过程**: + 1. 整理配置中心接口与字段说明。 + 2. 给出启动校验、缓存兜底与提示策略。 +- **执行结果**: 已生成方案文档 + + +# 任务执行摘要 + +## 会话 ID: local +- [2026-03-16 09:40:33] +- **执行原因**: 落地小程序配置中心后端接口并补充前端任务说明 +- **执行过程**: + 1. 新增 app_config 配置结构与 YAML 示例。 + 2. 增加 /api/open/app/config 接口与路由。 + 3. 文档补充前端待完成事项。 +- **执行结果**: 后端配置接口就绪,前端任务已写入文档