This commit is contained in:
zwt13703 2026-03-16 16:29:48 +08:00
parent 693dfb9c2b
commit 433b4469de
122 changed files with 21494 additions and 1 deletions

169
.gitignore vendored
View File

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

217
AGENTS.md Normal file
View File

@ -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)
- 生产环境务必开启安全校验
- 需要配置合适的限流规则防止恶意请求
- 定期清理日志文件避免磁盘空间不足

46
Help.md Normal file
View File

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

226
IFLOW.md Normal file
View File

@ -0,0 +1,226 @@
# IFLOW.md - 核心工作规则
## Global Protocols
所有操作必须严格遵循以下系统约束:
- **交互语言**:技术术语、工具与模型交互强制使用 **English**;用户输出强制使用 **中文**
- **最小改动**:仅对需求做针对性改动,严禁影响用户现有的其他功能。
- **风格一致**:遵循项目现有的代码风格,使用项目已有的工具函数。
## Tool Priority
在执行任何操作前,必须按照以下顺序选择工具,严禁跳级使用:
**1. MCP 工具**:当 MCP 工具能够完成任务时,必须使用 MCP禁止降级到内置工具或 Shell 命令。
**2. 内置工具**:仅当 MCP 工具**无法覆盖**该功能时,使用内置工具。
**3. Shell 命令**Shell 命令是最后手段,同时遵循以下规则:
- 只读类安全操作允许直接执行
| 类别 | 安全操作示例 |
| ---------------- | ------------------------------------------------- |
| Git 只读操作 | `git status`、`git log`、`git diff`、`git branch` |
| 包管理器只读操作 | `npm list`、`pnpm why`、`pip show` |
| 容器只读操作 | `docker ps`、`docker logs` |
| 环境检查 | `node -v`、`python -version`、`which xxx` |
- 写入/删除/修改/安装等危险操作必须征得用户同意
| 类别 | 危险操作示例 |
| ------------ | ------------------------------------------------------------ |
| Git 写操作 | `commit`、`push`、`pull`、`merge`、`rebase`、`reset`、`checkout <branch>` |
| 文件删除 | `rm`、`rmdir`、清空目录 |
| 批量文件修改 | `sed -i`(多文件)、批量重命名 |
| 包管理写操作 | `pnpm install/uninstall`、`pnpm add/remove`、`uv add/remove` |
| 容器写操作 | `docker rm`、`docker rmi`、`docker-compose down` |
| 系统级操作 | 修改环境变量、修改系统配置文件 |
- 触发危险操作时告知用户
```
# 告知示例
!!!即将执行危险操作!!!
命令git push origin main
影响:将本地 main 分支的提交推送到远程仓库
是否继续?请回复"确认"或"取消"
```
## Technology Stack
如果是对已有项目二次开发/修改bug则遵循项目已有技术栈。
如果是从0到1开发新的项目尽可能使用下方给出的技术栈
### 后端 - Go主力
| 配置项 | 要求 |
| -------- | -------------------------------------- |
| 语言版本 | Go 1.21+ |
| 开发框架 | Gin |
| ORM框架 | GORM |
| 代码规范 | Google Go 编程规范 |
### 后端 - Java
| 配置项 | 要求 |
| -------- | -------------------------------------- |
| 语言版本 | Java 17 |
| 开发框架 | Spring Boot 3.x + Spring Cloud Alibaba |
| ORM框架 | MyBatis Plus |
| 包管理器 | Maven |
| 代码规范 | 阿里巴巴Java开发手册嵩山版 |
### 后端 - Python辅助/小工具)
| 配置项 | 要求 |
| ---------- | ------------------------------------------------------------ |
| 语言版本 | Python 3.10+ |
| 开发框架 | FastAPI轻量级API/ TyperCLI工具/ Streamlit数据可视化 |
| 包管理工具 | uv |
| 代码规范 | PEP 8 + Google Python Style Guide |
| 虚拟环境 | **强制启用**uv venv |
### 后端 - 其他组件
| 组件 | 选型 |
| -------- | --------- |
| 数据库 | MySQL 8.x |
| 缓存 | Redis |
### 前端 - TypeScript + Vue 3
| 配置项 | 要求 |
| -------- | ---------------------------- |
| 语言版本 | TypeScript 5.x |
| 开发框架 | Vue 3Composition API |
| UI组件库 | TailWind CSS |
| 包管理器 | pnpm |
| 构建工具 | Vite |
| 代码规范 | ESLint严格模式+ Prettier |
### 桌面端 - Electron
| 配置项 | 要求 |
| -------- | ------------------ |
| 基础框架 | Vue 3 + TypeScript |
| 打包工具 | electron-builder |
## Workflow
在开发过程中,严格按照以下阶段顺序执行任务。
**格式要求**: 每次回复必须在开头标注 `【当前阶段: [阶段名称]】`
---
### Phase 0上下文全量检索
**执行条件**:在生成任何建议或代码前。
**调用工具**`mcp__auggie-mcp__codebase-retrieval`
**检索策略**
- 禁止基于假设Assumption回答。
- 使用自然语言NL构建语义查询Where/What/How
- **完整性检查**:必须获取相关类、函数、变量的完整定义与签名。若上下文不足,触发递归检索。
**需求对齐**:若检索后需求仍有模糊空间,**必须**向用户输出引导性问题列表,直至需求边界清晰(无遗漏、无冗余)。
---
### Phase 1 产品需求分析
**角色**:产品经理
**方法**:通过`AskUserQuestion`工具进行多轮提问引导,直到需求完全量化。
**最小维度**
- 目标用户与使用场景。
- 核心功能清单(按优先级 P0/P1/P2 排列)。
- 业务规则与约束条件。
**输出**`requirement.md`(需求规格书)
---
### Phase 2 UI/UX 设计
**角色**UI/UX 设计师
**方法**:基于`requirement.md`,通过多轮提问引导,定义交互与视觉规范。
**最小维度**
- 核心用户流程。
- 页面结构与布局。
- 组件状态定义。
**冲突检测**:与`requirement.md`中的约束进行一致性校验,如有冲突,必须提问澄清后再继续。
**输出**`ui_ux_specifications.md`UI/UX 规范)
---
### Phase 3 架构设计
**角色**:系统架构师
**方法**:基于`requirement.md`和`ui_ux_specifications.md`,通过多轮提问引导,设计技术方案。
**最小维度**
- 技术栈选型(遵循本文档`Technology Stack`章节)。
- 系统分层、模块划分、目录结构。
- API 契约定义。
**冲突检测**:与`requirement.md`中的约束进行一致性校验,如有冲突,必须提问澄清后再继续。
**输出**`architecture_design_document.md`(架构设计文档)
---
### Phase 4 代码实现
**角色**:全栈开发工程师
**方法**
1. 根据 `requirement.md``architecture_design_document.md`,拆分开发任务
2. 在 `task_list.md` 中记录任务清单,将**待开发/已开发/跳过**的任务通过不同的复选框进行标记
3. 逐个任务开发,每个任务完成后更新状态
**输出**`task_list.md`(任务清单,持续更新)、`deployment.md`(部署文档)
---
### Phase 5 代码审计
**执行条件**:每个任务模块开发完成后进行增量审计,全部完成后进行最终审计。
**角色**:代码审计工程师
**方法**:根据`task_list.md`,逐个对已完成代码进行 Code Review。
**审计范围**
- 功能完整性:是否覆盖`requirement.md`对应功能的全部需求
- 代码质量:命名规范、无重复代码、适当抽象、注释完整
- 安全检查输入验证、SQL注入防护、XSS防护、敏感数据处理、权限控制
- 性能检查:算法效率、数据库查询优化、资源释放
**问题分级与处理**
| 级别 | 定义 | 处理方式 |
| ---- | -------------------------------- | ------------------ |
| P0 | 安全漏洞、数据风险、核心功能缺失 | 阻断发布,立即修复 |
| P1 | 功能不完整、明显性能问题 | 当前迭代必须修复 |
| P2 | 代码规范、可维护性问题 | 可选 |
| P3 | 优化建议 | 可选 |
**输出**`audit_report.md`(审计报告)、`fix_changelog.md`(修复记录)

184
README.md
View File

@ -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 # 新的一天
```

View File

@ -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` 用于维护提示。

74
docs/Task1.md Normal file
View File

@ -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. **精准性提升**:明确了文件命名、目录层级、数据类型适配等细节,避免执行过程中的歧义,同时保留了原有的核心要求和参考建表语句。

70
docs/UserPasswordLogin.md Normal file
View File

@ -0,0 +1,70 @@
# 手机号+密码登录接口文档
## 概述
用于移动端/前端通过手机号和密码登录,成功后返回 `token` 与用户信息,后续请求携带 `Authorization: Bearer <token>`
## 基础信息
- 方法: `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

85
docs/UserProfile.md Normal file
View File

@ -0,0 +1,85 @@
# 登录后获取用户信息接口文档
## 概述
登录成功后,前端携带 `token` 调用该接口获取用户基础信息与平台扩展信息(头像、昵称、性别、地区等)。
## 基础信息
- 方法: `GET`
- 路径: `/api/user/profile`
- Content-Type: `application/json`
## 请求头
- `Authorization: Bearer <token>`
## 请求参数Query
| 字段 | 类型 | 必填 | 说明 |
| ------------ | ---- | ---- | -------------------------------- |
| platformType | int | 否 | 平台类型,默认 `1`(微信小程序) |
### 请求示例
```
GET /api/user/profile?platformType=1
Authorization: Bearer <token>
```
## 响应参数
| 字段 | 类型 | 说明 |
| --------------- | ------ | -------------------------------------------------------- |
| userId | int64 | 用户IDt_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` 可能为空。

111
docs/WeChatMiniLogin.md Normal file
View File

@ -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 | 用户IDt_user.id |
| platformUserId | int64 | 平台用户IDt_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 <token>`)。
- 若未传 `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可在接口层扩展

15
docs/avatar.md Normal file
View File

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

View File

@ -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='平台用户关联表(微信/抖音小程序用户信息)';

14
docs/sql/mysql/t_user.sql Normal file
View File

@ -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='用户基础信息表';

View File

@ -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();

View File

@ -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();

View File

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

View File

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

View File

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

62
server/common/context.go Normal file
View File

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

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

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

210
server/common/logger.go Normal file
View File

@ -0,0 +1,210 @@
// Package common 日志工具
package common
import (
"fmt"
"io"
"os"
"path/filepath"
"sync"
"time"
"server/config"
)
// 日志级别
const (
LevelDebug = iota
LevelInfo
LevelWarn
LevelError
)
var levelNames = map[int]string{
LevelDebug: "DEBUG",
LevelInfo: "INFO",
LevelWarn: "WARN",
LevelError: "ERROR",
}
var levelValues = map[string]int{
"debug": LevelDebug,
"info": LevelInfo,
"warn": LevelWarn,
"error": LevelError,
}
// Logger 日志记录器
type Logger struct {
level int
file *os.File
htmlWriter *htmlLogWriter
mu sync.Mutex
console bool
}
var (
defaultLogger *Logger
startCount int
onceInit sync.Once
)
// InitLogger 初始化日志
func InitLogger() {
once.Do(func() {
cfg := config.AppConfig.Log
level := levelValues[cfg.Level]
// 创建日志目录
if err := os.MkdirAll(cfg.Dir, 0755); err != nil {
fmt.Println("创建日志目录失败:", err)
return
}
// 计算启动次数
startCount = getStartCount(cfg.Dir)
// 创建日志文件
filename := fmt.Sprintf("%s-%d.html", time.Now().Format("2006-01-02"), startCount)
logFilepath := filepath.Join(cfg.Dir, filename)
file, err := os.OpenFile(logFilepath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
fmt.Println("创建日志文件失败:", err)
return
}
htmlWriter := newHtmlLogWriter(file)
defaultLogger = &Logger{
level: level,
file: file,
htmlWriter: htmlWriter,
console: cfg.Console,
}
fmt.Printf("日志文件: %s\n", logFilepath)
})
}
// getStartCount 获取今日启动次数
func getStartCount(dir string) int {
today := time.Now().Format("2006-01-02")
pattern := filepath.Join(dir, today+"*.html")
matches, _ := filepath.Glob(pattern)
return len(matches) + 1
}
// CloseLogger 关闭日志
func CloseLogger() {
if defaultLogger != nil && defaultLogger.file != nil {
defaultLogger.htmlWriter.writeFooter()
defaultLogger.file.Close()
}
}
func (l *Logger) log(level int, format string, args ...interface{}) {
if l == nil || level < l.level {
return
}
l.mu.Lock()
defer l.mu.Unlock()
msg := fmt.Sprintf(format, args...)
timestamp := time.Now().Format("2006-01-02 15:04:05.000")
levelName := levelNames[level]
// 写入HTML文件
l.htmlWriter.writeLog(timestamp, levelName, msg)
// 输出到控制台
if l.console {
fmt.Printf("[%s] [%s] %s\n", timestamp, levelName, msg)
}
}
// LogDebug 调试日志
func LogDebug(format string, args ...interface{}) {
if defaultLogger != nil {
defaultLogger.log(LevelDebug, format, args...)
}
}
// LogInfo 信息日志
func LogInfo(format string, args ...interface{}) {
if defaultLogger != nil {
defaultLogger.log(LevelInfo, format, args...)
}
}
// LogWarn 警告日志
func LogWarn(format string, args ...interface{}) {
if defaultLogger != nil {
defaultLogger.log(LevelWarn, format, args...)
}
}
// LogError 错误日志
func LogError(format string, args ...interface{}) {
if defaultLogger != nil {
defaultLogger.log(LevelError, format, args...)
}
}
// 简短别名
var (
Debug = LogDebug
Info = LogInfo
Warn = LogWarn
)
// htmlLogWriter HTML日志写入器
type htmlLogWriter struct {
writer io.Writer
initialized bool
}
func newHtmlLogWriter(w io.Writer) *htmlLogWriter {
hw := &htmlLogWriter{writer: w}
hw.writeHeader()
return hw
}
func (h *htmlLogWriter) writeHeader() {
html := `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>应用日志</title>
<style>
body { font-family: Consolas, monospace; background: #1e1e1e; color: #d4d4d4; padding: 20px; }
.log { margin: 2px 0; padding: 4px 8px; border-radius: 3px; }
.DEBUG { background: #2d2d2d; color: #9cdcfe; }
.INFO { background: #1e3a1e; color: #4ec9b0; }
.WARN { background: #3a3a1e; color: #dcdcaa; }
.ERROR { background: #3a1e1e; color: #f14c4c; }
.time { color: #808080; }
.level { font-weight: bold; width: 60px; display: inline-block; }
</style>
</head>
<body>
<h2>应用日志 - ` + time.Now().Format("2006-01-02") + `</h2>
<div id="logs">
`
h.writer.Write([]byte(html))
h.initialized = true
}
func (h *htmlLogWriter) writeLog(timestamp, level, msg string) {
html := fmt.Sprintf(`<div class="log %s"><span class="time">[%s]</span> <span class="level">[%s]</span> %s</div>
`, level, timestamp, level, msg)
h.writer.Write([]byte(html))
}
func (h *htmlLogWriter) writeFooter() {
html := `</div>
</body>
</html>`
h.writer.Write([]byte(html))
}

190
server/common/password.go Normal file
View File

@ -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(">> 测试失败!")
}
}

35
server/common/response.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

180
server/config/config.go Normal file
View File

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

View File

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

View File

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

125
server/config/database.go Normal file
View File

@ -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("数据库连接已关闭")
}
}
}

43
server/config/redis.go Normal file
View File

@ -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连接已关闭")
}
}

3592
server/docs/docs.go Normal file

File diff suppressed because it is too large Load Diff

3568
server/docs/swagger.json Normal file

File diff suppressed because it is too large Load Diff

2360
server/docs/swagger.yaml Normal file

File diff suppressed because it is too large Load Diff

82
server/go.mod Normal file
View File

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

324
server/go.sum Normal file
View File

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

180
server/main.go Normal file
View File

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

73
server/middleware/auth.go Normal file
View File

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

31
server/middleware/cors.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,8 @@
// Package dto 请求参数
package dto
// UserPasswordLoginRequest 手机号密码登录请求
type UserPasswordLoginRequest struct {
Phone string `json:"phone" binding:"required"`
Password string `json:"password" binding:"required"`
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
package dto
// LoginRequest 登录请求
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}

View File

@ -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"` // 机构编码
}

View File

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

View File

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

View File

@ -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 等敏感字段
}
}

View File

@ -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"`
// 注意:不包含 TokenToken 应该在单独的响应结构中
}

View File

@ -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 等敏感字段
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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, "切换成功")
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
package dto
// SaveVolunteerRequest 保存志愿请求
type SaveVolunteerRequest struct {
Keys []string `json:"keys" binding:"required"` // Keys: schoolCode_majorCode_enrollmentCode
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"` // 计算表名称
}

View File

@ -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"` // 专科批
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"` // 学生分数
}

View File

@ -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"` // 对应的 院校代码
}

View File

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

View File

@ -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-历史)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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)
// 协程1COUNT 查询
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
}

Some files were not shown because too many files have changed in this diff Show More