default
This commit is contained in:
parent
693dfb9c2b
commit
433b4469de
|
|
@ -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.*
|
||||
|
|
|
|||
|
|
@ -0,0 +1,217 @@
|
|||
# 艺考招生管理系统 API
|
||||
|
||||
## 项目概述
|
||||
|
||||
艺考招生管理系统是一个基于 Go + Gin + GORM + Redis 的 RESTful API 服务,用于管理和计算艺考志愿填报、成绩统计和录取概率等功能。
|
||||
|
||||
### 核心技术栈
|
||||
|
||||
- **Go 1.21+** - 主要开发语言
|
||||
- **Gin** - Web 框架
|
||||
- **GORM** - ORM 框架
|
||||
- **MySQL 8.0** - 数据存储
|
||||
- **Redis** - 会话存储和限流
|
||||
- **Swaggo** - API 文档生成
|
||||
|
||||
### 项目架构
|
||||
|
||||
项目采用典型的分层架构模式:
|
||||
|
||||
```
|
||||
server/
|
||||
├── main.go # 应用程序入口
|
||||
├── config/ # 配置文件
|
||||
│ ├── config.go # 应用配置
|
||||
│ ├── database.go # MySQL 配置
|
||||
│ └── redis.go # Redis 配置
|
||||
├── common/ # 公共组件
|
||||
│ ├── response.go # 统一响应格式
|
||||
│ ├── context.go # 上下文工具
|
||||
│ ├── logger.go # 日志工具
|
||||
│ └── password.go # 密码加密
|
||||
├── middleware/ # 中间件
|
||||
│ ├── auth.go # 登录鉴权
|
||||
│ ├── security.go # 安全校验
|
||||
│ ├── ratelimit.go # 接口限流
|
||||
│ └── cors.go # 跨域处理
|
||||
├── modules/ # 业务模块
|
||||
│ ├── system/ # 系统模块 (用户认证、权限管理)
|
||||
│ ├── user/ # 用户模块 (用户相关功能)
|
||||
│ └── yx/ # 艺考模块 (核心业务)
|
||||
├── docs/ # API 文档
|
||||
└── logs/ # 日志目录
|
||||
```
|
||||
|
||||
### 业务模块
|
||||
|
||||
#### 1. 系统模块 (system)
|
||||
- 用户认证 (登录/登出/用户信息)
|
||||
- 用户管理
|
||||
|
||||
#### 2. 用户模块 (user)
|
||||
- 成绩管理
|
||||
- 专业选择
|
||||
- 志愿填报
|
||||
- 用户认证
|
||||
|
||||
#### 3. 艺考模块 (yx)
|
||||
- 院校专业管理
|
||||
- 历年招生数据
|
||||
- 计算专业逻辑
|
||||
- 用户成绩管理
|
||||
- 志愿管理
|
||||
- 志愿记录管理
|
||||
|
||||
## 部署与运行
|
||||
|
||||
### 环境要求
|
||||
- Go 1.21+
|
||||
- MySQL 8.0
|
||||
- Redis
|
||||
|
||||
### 快速启动
|
||||
|
||||
```bash
|
||||
cd server
|
||||
go mod tidy
|
||||
swag init
|
||||
go run main.go
|
||||
```
|
||||
|
||||
### 配置说明
|
||||
|
||||
系统支持多环境配置文件:
|
||||
|
||||
- `config/config.dev.yaml` - 开发环境
|
||||
- `config/config.test.yaml` - 测试环境
|
||||
- `config/config.prod.yaml` - 生产环境
|
||||
|
||||
配置文件格式示例:
|
||||
```yaml
|
||||
server:
|
||||
port: 8081
|
||||
|
||||
database:
|
||||
driver: mysql
|
||||
host: localhost
|
||||
port: 3306
|
||||
database: yitisheng
|
||||
username: root
|
||||
password: "password"
|
||||
|
||||
redis:
|
||||
addr: localhost:6379
|
||||
password: "password"
|
||||
db: 1
|
||||
```
|
||||
|
||||
## 安全机制
|
||||
|
||||
### 1. 登录鉴权
|
||||
- 基于 JWT Token 的身份验证
|
||||
- 支持白名单配置,允许特定接口无需登录访问
|
||||
- Token 存储在 Redis 中,支持过期时间设置
|
||||
|
||||
### 2. 安全校验
|
||||
- 请求头需携带签名进行验证
|
||||
- 防暴力破解机制
|
||||
- 支持白名单配置
|
||||
|
||||
### 3. 接口限流
|
||||
- 基于 Redis 滑动窗口算法
|
||||
- 支持按用户ID或IP限流
|
||||
- 可为不同接口配置不同限流规则
|
||||
|
||||
## API 接口
|
||||
|
||||
### 认证相关
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| POST | /api/user/auth/login | 用户登录 |
|
||||
| POST | /api/user/auth/logout | 用户登出 |
|
||||
| GET | /api/user/auth/info | 获取当前用户信息 |
|
||||
|
||||
### 系统管理
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | /api/sys-users | 获取用户列表 |
|
||||
| POST | /api/sys-users | 创建用户 |
|
||||
| PUT | /api/sys-users/:id | 更新用户 |
|
||||
| DELETE | /api/sys-users/:id | 删除用户 |
|
||||
|
||||
### 艺考相关
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | /api/yx-school-majors | 获取院校专业列表 |
|
||||
| GET | /api/yx-history-enrolls | 获取历年招生数据 |
|
||||
| GET | /api/yx-calculation-majors | 获取计算专业数据 |
|
||||
| GET | /api/yx-user-scores | 获取用户成绩 |
|
||||
| GET | /api/yx-volunteers | 获取志愿列表 |
|
||||
| POST | /api/yx-volunteers | 创建志愿 |
|
||||
| PUT | /api/yx-volunteers/:id | 更新志愿 |
|
||||
| DELETE | /api/yx-volunteers/:id | 删除志愿 |
|
||||
|
||||
### 中间件执行顺序
|
||||
|
||||
```
|
||||
请求 -> 安全校验 -> 限流 -> 登录鉴权 -> Controller
|
||||
```
|
||||
|
||||
## 数据实体
|
||||
|
||||
### YxVolunteer (志愿表)
|
||||
- ID: 主键
|
||||
- VolunteerName: 志愿单名称
|
||||
- ScoreId: 关联成绩ID
|
||||
- CreateType: 生成类型 (1.手动生成, 2.智能生成)
|
||||
- State: 状态 (0-未使用, 1-使用中, 2-历史)
|
||||
- CreateBy: 创建人
|
||||
- CreateTime: 创建时间
|
||||
- UpdateBy: 更新人
|
||||
- UpdateTime: 更新时间
|
||||
|
||||
## 常量定义
|
||||
|
||||
### 业务状态常量
|
||||
- StateActive ("1") - 使用中
|
||||
- StateInactive ("0") - 未使用/已删除
|
||||
- StateHistory ("2") - 历史记录
|
||||
|
||||
### 令牌相关
|
||||
- TokenHeader: "Authorization"
|
||||
- HeaderTokenPrefix: "Bearer "
|
||||
- RedisTokenExpire: 24小时
|
||||
|
||||
## 日志系统
|
||||
|
||||
- 支持 debug/info/warn/error 级别
|
||||
- 输出到 HTML 文件,按日期和启动次数命名
|
||||
- 可配置是否同时输出到控制台
|
||||
|
||||
## 开发规范
|
||||
|
||||
### 代码风格
|
||||
- 使用 Go 语言标准格式 (go fmt)
|
||||
- 遵循 Go 语言命名规范
|
||||
- 使用有意义的变量和函数名
|
||||
|
||||
### 错误处理
|
||||
- 统一使用 common.Error(c, code, message) 返回错误
|
||||
- 业务错误码使用 HTTP 标准码或自定义业务码
|
||||
- 记录详细错误日志便于调试
|
||||
|
||||
### 接口文档
|
||||
- 使用 Swaggo 注解生成 API 文档
|
||||
- 所有公开接口都应有完整的文档注释
|
||||
- 包含请求参数、响应格式和错误码说明
|
||||
|
||||
## 额外说明
|
||||
|
||||
项目还包含一些 Java 代码文件,如 `ScoreUtil.java`,这表明项目可能正在从 Java 版本迁移到 Go 版本,或者 Java 版本作为参考。Go 版本中的 `server/modules/yx/service/score_util.go` 文件可能包含与 Java 版本相同功能的实现。
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 开发环境中的安全校验默认关闭 (enable: false)
|
||||
- 生产环境务必开启安全校验
|
||||
- 需要配置合适的限流规则防止恶意请求
|
||||
- 定期清理日志文件避免磁盘空间不足
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
#### 关于 Swagger 文档更新流程:
|
||||
|
||||
新增接口后更新文档步骤:
|
||||
|
||||
在 Controller 方法上添加 Swagger 注解(@Summary、@Param、@Router 等)
|
||||
在 server 目录运行 swag init 重新生成文档
|
||||
重启服务器
|
||||
常用 Swagger 注解说明:
|
||||
|
||||
```go
|
||||
// @Summary 接口简介
|
||||
// @Description 详细描述
|
||||
// @Tags 分组标签
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param name query/path/body 类型 是否必填 "描述"
|
||||
// @Success 200 {object} Response "成功描述"
|
||||
// @Failure 400 {object} Response "失败描述"
|
||||
// @Router /path [get/post/put/delete]
|
||||
```
|
||||
|
||||
#### 批量修改/新增/删除
|
||||
|
||||
Mapper 层: | 方法 | 说明 | |------|------| | UpdateFields(id, fields) | 动态字段更新,只更新指定字段 | | BatchCreate(items, batchSize) | 批量插入,分批处理 | | BatchUpdate(items) | 批量更新 (根据主键) | | BatchUpsert(items, updateColumns) | 批量插入或更新 (存在则更新) | | BatchDelete(ids) | 批量删除 |
|
||||
|
||||
Service 层: 同样的方法,自动处理 UUID 生成。
|
||||
|
||||
使用示例:
|
||||
|
||||
```
|
||||
// 动态字段更新 - 只更新指定字段
|
||||
service.UpdateFields("xxx-id", map[string]interface{}{
|
||||
"school_name": "新名称",
|
||||
"plan_num": 100,
|
||||
})
|
||||
|
||||
// 批量插入
|
||||
items := []entity.SchoolMajor{{...}, {...}}
|
||||
service.BatchCreate(items)
|
||||
|
||||
// 批量插入或更新 (存在则更新指定字段)
|
||||
service.BatchUpsert(items, []string{"school_name", "plan_num"})
|
||||
|
||||
// 批量删除
|
||||
service.BatchDelete([]string{"id1", "id2", "id3"})
|
||||
```
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
# IFLOW.md - 核心工作规则
|
||||
|
||||
## Global Protocols
|
||||
|
||||
所有操作必须严格遵循以下系统约束:
|
||||
|
||||
- **交互语言**:技术术语、工具与模型交互强制使用 **English**;用户输出强制使用 **中文**。
|
||||
- **最小改动**:仅对需求做针对性改动,严禁影响用户现有的其他功能。
|
||||
- **风格一致**:遵循项目现有的代码风格,使用项目已有的工具函数。
|
||||
|
||||
## Tool Priority
|
||||
|
||||
在执行任何操作前,必须按照以下顺序选择工具,严禁跳级使用:
|
||||
|
||||
**1. MCP 工具**:当 MCP 工具能够完成任务时,必须使用 MCP,禁止降级到内置工具或 Shell 命令。
|
||||
|
||||
**2. 内置工具**:仅当 MCP 工具**无法覆盖**该功能时,使用内置工具。
|
||||
|
||||
**3. Shell 命令**:Shell 命令是最后手段,同时遵循以下规则:
|
||||
|
||||
- 只读类安全操作允许直接执行
|
||||
|
||||
| 类别 | 安全操作示例 |
|
||||
| ---------------- | ------------------------------------------------- |
|
||||
| Git 只读操作 | `git status`、`git log`、`git diff`、`git branch` |
|
||||
| 包管理器只读操作 | `npm list`、`pnpm why`、`pip show` |
|
||||
| 容器只读操作 | `docker ps`、`docker logs` |
|
||||
| 环境检查 | `node -v`、`python -version`、`which xxx` |
|
||||
|
||||
- 写入/删除/修改/安装等危险操作必须征得用户同意
|
||||
|
||||
| 类别 | 危险操作示例 |
|
||||
| ------------ | ------------------------------------------------------------ |
|
||||
| Git 写操作 | `commit`、`push`、`pull`、`merge`、`rebase`、`reset`、`checkout <branch>` |
|
||||
| 文件删除 | `rm`、`rmdir`、清空目录 |
|
||||
| 批量文件修改 | `sed -i`(多文件)、批量重命名 |
|
||||
| 包管理写操作 | `pnpm install/uninstall`、`pnpm add/remove`、`uv add/remove` |
|
||||
| 容器写操作 | `docker rm`、`docker rmi`、`docker-compose down` |
|
||||
| 系统级操作 | 修改环境变量、修改系统配置文件 |
|
||||
|
||||
- 触发危险操作时告知用户
|
||||
|
||||
```
|
||||
# 告知示例
|
||||
!!!即将执行危险操作!!!:
|
||||
命令:git push origin main
|
||||
影响:将本地 main 分支的提交推送到远程仓库
|
||||
|
||||
是否继续?请回复"确认"或"取消"
|
||||
```
|
||||
|
||||
## Technology Stack
|
||||
|
||||
如果是对已有项目二次开发/修改bug,则遵循项目已有技术栈。
|
||||
|
||||
如果是从0到1开发新的项目,尽可能使用下方给出的技术栈:
|
||||
|
||||
### 后端 - Go(主力)
|
||||
|
||||
| 配置项 | 要求 |
|
||||
| -------- | -------------------------------------- |
|
||||
| 语言版本 | Go 1.21+ |
|
||||
| 开发框架 | Gin |
|
||||
| ORM框架 | GORM |
|
||||
| 代码规范 | Google Go 编程规范 |
|
||||
|
||||
### 后端 - Java
|
||||
|
||||
| 配置项 | 要求 |
|
||||
| -------- | -------------------------------------- |
|
||||
| 语言版本 | Java 17 |
|
||||
| 开发框架 | Spring Boot 3.x + Spring Cloud Alibaba |
|
||||
| ORM框架 | MyBatis Plus |
|
||||
| 包管理器 | Maven |
|
||||
| 代码规范 | 阿里巴巴Java开发手册(嵩山版) |
|
||||
|
||||
### 后端 - Python(辅助/小工具)
|
||||
|
||||
| 配置项 | 要求 |
|
||||
| ---------- | ------------------------------------------------------------ |
|
||||
| 语言版本 | Python 3.10+ |
|
||||
| 开发框架 | FastAPI(轻量级API)/ Typer(CLI工具)/ Streamlit(数据可视化) |
|
||||
| 包管理工具 | uv |
|
||||
| 代码规范 | PEP 8 + Google Python Style Guide |
|
||||
| 虚拟环境 | **强制启用**(uv venv) |
|
||||
|
||||
### 后端 - 其他组件
|
||||
|
||||
| 组件 | 选型 |
|
||||
| -------- | --------- |
|
||||
| 数据库 | MySQL 8.x |
|
||||
| 缓存 | Redis |
|
||||
|
||||
### 前端 - TypeScript + Vue 3
|
||||
|
||||
| 配置项 | 要求 |
|
||||
| -------- | ---------------------------- |
|
||||
| 语言版本 | TypeScript 5.x |
|
||||
| 开发框架 | Vue 3(Composition API) |
|
||||
| UI组件库 | TailWind CSS |
|
||||
| 包管理器 | pnpm |
|
||||
| 构建工具 | Vite |
|
||||
| 代码规范 | ESLint(严格模式)+ Prettier |
|
||||
|
||||
### 桌面端 - Electron
|
||||
|
||||
| 配置项 | 要求 |
|
||||
| -------- | ------------------ |
|
||||
| 基础框架 | Vue 3 + TypeScript |
|
||||
| 打包工具 | electron-builder |
|
||||
|
||||
## Workflow
|
||||
|
||||
在开发过程中,严格按照以下阶段顺序执行任务。
|
||||
|
||||
**格式要求**: 每次回复必须在开头标注 `【当前阶段: [阶段名称]】`
|
||||
|
||||
---
|
||||
|
||||
### Phase 0:上下文全量检索
|
||||
|
||||
**执行条件**:在生成任何建议或代码前。
|
||||
|
||||
**调用工具**:`mcp__auggie-mcp__codebase-retrieval`
|
||||
|
||||
**检索策略**:
|
||||
|
||||
- 禁止基于假设(Assumption)回答。
|
||||
- 使用自然语言(NL)构建语义查询(Where/What/How)。
|
||||
- **完整性检查**:必须获取相关类、函数、变量的完整定义与签名。若上下文不足,触发递归检索。
|
||||
|
||||
**需求对齐**:若检索后需求仍有模糊空间,**必须**向用户输出引导性问题列表,直至需求边界清晰(无遗漏、无冗余)。
|
||||
|
||||
---
|
||||
|
||||
### Phase 1: 产品需求分析
|
||||
|
||||
**角色**:产品经理
|
||||
|
||||
**方法**:通过`AskUserQuestion`工具进行多轮提问引导,直到需求完全量化。
|
||||
|
||||
**最小维度**:
|
||||
|
||||
- 目标用户与使用场景。
|
||||
- 核心功能清单(按优先级 P0/P1/P2 排列)。
|
||||
- 业务规则与约束条件。
|
||||
|
||||
**输出**:`requirement.md`(需求规格书)
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: UI/UX 设计
|
||||
|
||||
**角色**:UI/UX 设计师
|
||||
|
||||
**方法**:基于`requirement.md`,通过多轮提问引导,定义交互与视觉规范。
|
||||
|
||||
**最小维度**:
|
||||
|
||||
- 核心用户流程。
|
||||
- 页面结构与布局。
|
||||
- 组件状态定义。
|
||||
|
||||
**冲突检测**:与`requirement.md`中的约束进行一致性校验,如有冲突,必须提问澄清后再继续。
|
||||
|
||||
**输出**:`ui_ux_specifications.md`(UI/UX 规范)
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: 架构设计
|
||||
|
||||
**角色**:系统架构师
|
||||
|
||||
**方法**:基于`requirement.md`和`ui_ux_specifications.md`,通过多轮提问引导,设计技术方案。
|
||||
|
||||
**最小维度**:
|
||||
|
||||
- 技术栈选型(遵循本文档`Technology Stack`章节)。
|
||||
- 系统分层、模块划分、目录结构。
|
||||
- API 契约定义。
|
||||
|
||||
**冲突检测**:与`requirement.md`中的约束进行一致性校验,如有冲突,必须提问澄清后再继续。
|
||||
|
||||
**输出**:`architecture_design_document.md`(架构设计文档)
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: 代码实现
|
||||
|
||||
**角色**:全栈开发工程师
|
||||
|
||||
**方法**:
|
||||
|
||||
1. 根据 `requirement.md` 和 `architecture_design_document.md`,拆分开发任务
|
||||
2. 在 `task_list.md` 中记录任务清单,将**待开发/已开发/跳过**的任务通过不同的复选框进行标记
|
||||
3. 逐个任务开发,每个任务完成后更新状态
|
||||
|
||||
**输出**:`task_list.md`(任务清单,持续更新)、`deployment.md`(部署文档)
|
||||
|
||||
---
|
||||
|
||||
### Phase 5: 代码审计
|
||||
|
||||
**执行条件**:每个任务模块开发完成后进行增量审计,全部完成后进行最终审计。
|
||||
|
||||
**角色**:代码审计工程师
|
||||
|
||||
**方法**:根据`task_list.md`,逐个对已完成代码进行 Code Review。
|
||||
|
||||
**审计范围**:
|
||||
|
||||
- 功能完整性:是否覆盖`requirement.md`对应功能的全部需求
|
||||
- 代码质量:命名规范、无重复代码、适当抽象、注释完整
|
||||
- 安全检查:输入验证、SQL注入防护、XSS防护、敏感数据处理、权限控制
|
||||
- 性能检查:算法效率、数据库查询优化、资源释放
|
||||
|
||||
**问题分级与处理**:
|
||||
|
||||
| 级别 | 定义 | 处理方式 |
|
||||
| ---- | -------------------------------- | ------------------ |
|
||||
| P0 | 安全漏洞、数据风险、核心功能缺失 | 阻断发布,立即修复 |
|
||||
| P1 | 功能不完整、明显性能问题 | 当前迭代必须修复 |
|
||||
| P2 | 代码规范、可维护性问题 | 可选 |
|
||||
| P3 | 优化建议 | 可选 |
|
||||
|
||||
**输出**:`audit_report.md`(审计报告)、`fix_changelog.md`(修复记录)
|
||||
184
README.md
184
README.md
|
|
@ -1,2 +1,184 @@
|
|||
# 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 # 新的一天
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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` 用于维护提示。
|
||||
|
|
@ -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. **精准性提升**:明确了文件命名、目录层级、数据类型适配等细节,避免执行过程中的歧义,同时保留了原有的核心要求和参考建表语句。
|
||||
|
|
@ -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)。
|
||||
|
|
@ -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 | 用户ID(t_user.id) |
|
||||
| username | string | 用户名 |
|
||||
| nickname | string | 昵称 |
|
||||
| avatarUrl | string | 头像URL |
|
||||
| phone | string | 手机号 |
|
||||
| gender | int | 性别:0-未知,1-男,2-女 |
|
||||
| region | string | 地区(优先 `platform_extra.region`,否则拼接国家/省/市) |
|
||||
| platformType | int | 平台类型 |
|
||||
| platformOpenid | string | 平台 openid |
|
||||
| platformUnionid | string | 平台 unionid |
|
||||
| platformExtra | object | 平台扩展字段(原样返回) |
|
||||
|
||||
### 成功响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"userId": 10001,
|
||||
"username": "wx_20260315121500_ab12cd34",
|
||||
"nickname": "张三",
|
||||
"avatarUrl": "https://oss-xxx/avatar.png",
|
||||
"phone": "13800000000",
|
||||
"gender": 1,
|
||||
"region": "中国 广东 深圳",
|
||||
"platformType": 1,
|
||||
"platformOpenid": "oL1oU5gS6Q...",
|
||||
"platformUnionid": "o6_bmasdasds...",
|
||||
"platformExtra": {
|
||||
"country": "中国",
|
||||
"province": "广东",
|
||||
"city": "深圳"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 错误响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 401,
|
||||
"message": "未登录",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
## 备注
|
||||
|
||||
- 需要先登录并获取 `token`。
|
||||
- `platformExtra` 为空时,`region` 可能为空。
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
# 微信小程序登录接口文档
|
||||
|
||||
## 概述
|
||||
- 接口用于微信小程序登录,前端通过 `code` 换取 `openid/session_key`,后端自动创建或更新 `t_user` 与 `t_platform_user`。
|
||||
- 成功后返回用户ID与平台用户ID,供前端后续业务使用。
|
||||
|
||||
## 基础信息
|
||||
- 方法: `POST`
|
||||
- 路径: `/api/open/wechat/mini/login`
|
||||
- Content-Type: `application/json`
|
||||
|
||||
## 请求头
|
||||
- `Content-Type: application/json`
|
||||
- `Authorization`: 不需要(已加入登录白名单)
|
||||
- 安全校验(当 `security.enable: true` 时必须):
|
||||
- `X-App-Timestamp`: 毫秒时间戳
|
||||
- `X-App-Sign`: MD5(`timestamp` + `secret_key`)
|
||||
|
||||
## 请求参数
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| code | string | 是 | 微信登录 `wx.login` 返回的 `code` |
|
||||
| phoneCode | string | 否 | 快速手机号登录返回的 `phoneCode`(优先使用) |
|
||||
| encryptedData | string | 否 | `getPhoneNumber` 返回的加密数据 |
|
||||
| iv | string | 否 | `getPhoneNumber` 返回的解密向量 |
|
||||
| nickname | string | 否 | 用户昵称 |
|
||||
| avatarUrl | string | 否 | 用户头像URL |
|
||||
| phone | string | 否 | 手机号(不传或空字符串将写入为 NULL) |
|
||||
| gender | int | 否 | 性别:0-未知,1-男,2-女 |
|
||||
| platformExtra | object | 否 | 平台扩展字段(如城市、语言等) |
|
||||
|
||||
### 请求示例
|
||||
```json
|
||||
{
|
||||
"code": "wx_login_code",
|
||||
"phoneCode": "8064f569d035bf3a9b4c7fc223bbbc589bf442c0557e0ae51039031d883668f5",
|
||||
"encryptedData": "EZyKBdrHgQoAjgMDPNGXRGOjsPrB8LHcupwFCztA3IBNvbdkrSsk6iU6FqQsrn5TfpMJeTzHJ2l7lg6e+EBqqDXVgVEgQxkTWlxBS6mwUN4NgRI2FanA0wPFAWMZGmn7jKgEJhu8xKGSihcI111Y/mq+3T6gDzjZvkz32MhngL5/wBEjDQbBdBXfvY6FveyV7CinM0j17wE7pbjNIFrExw==",
|
||||
"iv": "DMzzMUAx5ke1S8nFItBHSg==",
|
||||
"nickname": "张三",
|
||||
"avatarUrl": "https://example.com/avatar.png",
|
||||
"phone": "13800000000",
|
||||
"gender": 1,
|
||||
"platformExtra": {
|
||||
"city": "Shenzhen",
|
||||
"language": "zh_CN"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 响应参数
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| userId | int64 | 用户ID(t_user.id) |
|
||||
| platformUserId | int64 | 平台用户ID(t_platform_user.id) |
|
||||
| openid | string | 微信 openid |
|
||||
| unionid | string | 微信 unionid(可能为空) |
|
||||
| sessionKey | string | 微信 session_key |
|
||||
| phone | string | 手机号(如成功获取) |
|
||||
| token | string | 登录令牌(用于鉴权) |
|
||||
| isNewPlatform | bool | 是否新创建平台用户 |
|
||||
| isNewUser | bool | 是否新创建用户 |
|
||||
|
||||
### 成功响应示例
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"userId": 10001,
|
||||
"platformUserId": 20001,
|
||||
"openid": "oL1oU5gS6Q...",
|
||||
"unionid": "o6_bmasdasds...",
|
||||
"sessionKey": "HKq6lZzG2u...",
|
||||
"phone": "13800000000",
|
||||
"token": "c6f7f1e4-5a3b-4f4e-9d0b-6b3f7b8c5e3a",
|
||||
"isNewPlatform": false,
|
||||
"isNewUser": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 错误响应示例
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "微信接口错误: 40163 code been used",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
## 业务说明
|
||||
- 首次登录时会创建 `t_user` 与 `t_platform_user`。
|
||||
- 之后登录会更新 `platform_session_key`、`last_login_time`,并按需更新 `nickname/avatarUrl/gender/phone`。
|
||||
- `phoneCode` 优先于 `encryptedData+iv` 用于获取手机号;两者都不提供时不更新手机号。
|
||||
- 返回 `token` 可直接用于鉴权(`Authorization: Bearer <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,可在接口层扩展)。
|
||||
|
|
@ -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
|
||||
|
|
@ -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='平台用户关联表(微信/抖音小程序用户信息)';
|
||||
|
|
@ -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='用户基础信息表';
|
||||
|
|
@ -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();
|
||||
|
|
@ -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();
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
// Package common 公共组件
|
||||
package common
|
||||
|
||||
import (
|
||||
"server/config"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
// DefaultBatchSize 默认批量操作大小
|
||||
const DefaultBatchSize = 100
|
||||
|
||||
// BaseMapper 泛型 Mapper 基类,封装通用 CRUD 操作
|
||||
type BaseMapper[T any] struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewBaseMapper 创建 BaseMapper 实例
|
||||
func NewBaseMapper[T any]() *BaseMapper[T] {
|
||||
return &BaseMapper[T]{
|
||||
db: config.DB,
|
||||
}
|
||||
}
|
||||
|
||||
// GetDB 获取数据库实例(允许子类覆盖)
|
||||
func (m *BaseMapper[T]) GetDB() *gorm.DB {
|
||||
return m.db
|
||||
}
|
||||
|
||||
// FindAll 分页查询
|
||||
func (m *BaseMapper[T]) FindAll(page, size int) ([]T, int64, error) {
|
||||
var items []T
|
||||
var total int64
|
||||
query := m.GetDB().Model(new(T))
|
||||
query.Count(&total)
|
||||
err := query.Offset((page - 1) * size).Limit(size).Find(&items).Error
|
||||
return items, total, err
|
||||
}
|
||||
|
||||
// FindByID 根据 ID 查询
|
||||
func (m *BaseMapper[T]) FindByID(id string) (*T, error) {
|
||||
var item T
|
||||
err := m.GetDB().First(&item, "id = ?", id).Error
|
||||
return &item, err
|
||||
}
|
||||
|
||||
// Create 创建单个记录
|
||||
func (m *BaseMapper[T]) Create(item *T) error {
|
||||
return m.GetDB().Create(item).Error
|
||||
}
|
||||
|
||||
// Update 更新单个记录
|
||||
func (m *BaseMapper[T]) Update(item *T) error {
|
||||
return m.GetDB().Save(item).Error
|
||||
}
|
||||
|
||||
// UpdateFields 更新指定字段
|
||||
func (m *BaseMapper[T]) UpdateFields(id string, fields map[string]interface{}) error {
|
||||
return m.GetDB().Model(new(T)).Where("id = ?", id).Updates(fields).Error
|
||||
}
|
||||
|
||||
// Delete 删除记录
|
||||
func (m *BaseMapper[T]) Delete(id string) error {
|
||||
return m.GetDB().Delete(new(T), "id = ?", id).Error
|
||||
}
|
||||
|
||||
// BatchCreate 批量创建
|
||||
func (m *BaseMapper[T]) BatchCreate(items []T, batchSize int) error {
|
||||
if batchSize <= 0 {
|
||||
batchSize = DefaultBatchSize
|
||||
}
|
||||
return m.GetDB().CreateInBatches(items, batchSize).Error
|
||||
}
|
||||
|
||||
// BatchUpdate 批量更新
|
||||
func (m *BaseMapper[T]) BatchUpdate(items []T) error {
|
||||
return m.GetDB().Save(items).Error
|
||||
}
|
||||
|
||||
// BatchUpsert 批量插入或更新
|
||||
func (m *BaseMapper[T]) BatchUpsert(items []T, updateColumns []string) error {
|
||||
return m.GetDB().Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "id"}},
|
||||
DoUpdates: clause.AssignmentColumns(updateColumns),
|
||||
}).CreateInBatches(items, DefaultBatchSize).Error
|
||||
}
|
||||
|
||||
// BatchDelete 批量删除
|
||||
func (m *BaseMapper[T]) BatchDelete(ids []string) error {
|
||||
return m.GetDB().Delete(new(T), "id IN ?", ids).Error
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
// Package common 公共组件
|
||||
package common
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// BaseService 泛型 Service 基类,封装通用业务逻辑
|
||||
type BaseService[T any] struct {
|
||||
mapper *BaseMapper[T]
|
||||
}
|
||||
|
||||
// NewBaseService 创建 BaseService 实例
|
||||
func NewBaseService[T any]() *BaseService[T] {
|
||||
return &BaseService[T]{
|
||||
mapper: NewBaseMapper[T](),
|
||||
}
|
||||
}
|
||||
|
||||
// GetMapper 获取 Mapper 实例
|
||||
func (s *BaseService[T]) GetMapper() *BaseMapper[T] {
|
||||
return s.mapper
|
||||
}
|
||||
|
||||
// List 分页查询
|
||||
func (s *BaseService[T]) List(page, size int) ([]T, int64, error) {
|
||||
return s.mapper.FindAll(page, size)
|
||||
}
|
||||
|
||||
// GetByID 根据 ID 获取
|
||||
func (s *BaseService[T]) GetByID(id string) (*T, error) {
|
||||
return s.mapper.FindByID(id)
|
||||
}
|
||||
|
||||
// Create 创建记录(自动生成 ID)
|
||||
func (s *BaseService[T]) Create(item *T) error {
|
||||
// 通过反射设置 ID 字段
|
||||
if err := setID(item); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.mapper.Create(item)
|
||||
}
|
||||
|
||||
// Update 更新记录
|
||||
func (s *BaseService[T]) Update(item *T) error {
|
||||
return s.mapper.Update(item)
|
||||
}
|
||||
|
||||
// UpdateFields 更新指定字段
|
||||
func (s *BaseService[T]) UpdateFields(id string, fields map[string]interface{}) error {
|
||||
return s.mapper.UpdateFields(id, fields)
|
||||
}
|
||||
|
||||
// Delete 删除记录
|
||||
func (s *BaseService[T]) Delete(id string) error {
|
||||
return s.mapper.Delete(id)
|
||||
}
|
||||
|
||||
// BatchCreate 批量创建(自动生成 ID)
|
||||
func (s *BaseService[T]) BatchCreate(items []T) error {
|
||||
for i := range items {
|
||||
if err := setID(&items[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return s.mapper.BatchCreate(items, DefaultBatchSize)
|
||||
}
|
||||
|
||||
// BatchUpdate 批量更新
|
||||
func (s *BaseService[T]) BatchUpdate(items []T) error {
|
||||
return s.mapper.BatchUpdate(items)
|
||||
}
|
||||
|
||||
// BatchUpsert 批量插入或更新
|
||||
func (s *BaseService[T]) BatchUpsert(items []T, updateColumns []string) error {
|
||||
for i := range items {
|
||||
if err := setID(&items[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return s.mapper.BatchUpsert(items, updateColumns)
|
||||
}
|
||||
|
||||
// BatchDelete 批量删除
|
||||
func (s *BaseService[T]) BatchDelete(ids []string) error {
|
||||
return s.mapper.BatchDelete(ids)
|
||||
}
|
||||
|
||||
// setID 通过反射设置 ID 字段
|
||||
func setID(item interface{}) error {
|
||||
val := reflect.ValueOf(item).Elem()
|
||||
|
||||
// 如果当前类型是指针,再次解引用以获取实际的 Struct
|
||||
if val.Kind() == reflect.Ptr {
|
||||
val = val.Elem()
|
||||
}
|
||||
|
||||
if val.Kind() == reflect.Struct {
|
||||
idField := val.FieldByName("ID")
|
||||
if idField.IsValid() && idField.Kind() == reflect.String {
|
||||
// 如果 ID 为空,生成新 ID
|
||||
if idField.String() == "" {
|
||||
idField.SetString(GenerateStringID())
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package common
|
||||
|
||||
import "time"
|
||||
|
||||
// Redis 相关常量
|
||||
const (
|
||||
// RedisTokenPrefix Redis中Token前缀
|
||||
RedisTokenPrefix = "login:token:"
|
||||
// RedisTokenExpire Token过期时间
|
||||
RedisTokenExpire = 24 * time.Hour
|
||||
|
||||
// RedisUserScorePrefix Redis中用户成绩前缀
|
||||
RedisUserScorePrefix = "user:score:"
|
||||
// RedisUserScoreExpire 用户成绩过期时间
|
||||
RedisUserScoreExpire = 8 * time.Hour
|
||||
)
|
||||
|
||||
// HTTP/Context 相关常量
|
||||
const (
|
||||
// ContextUserKey 上下文中存储用户信息的key
|
||||
ContextUserKey = "loginUser"
|
||||
// TokenHeader 请求头中Token的key
|
||||
// "X-Access-Token"
|
||||
TokenHeader = "Authorization"
|
||||
// HeaderTokenPrefix Token前缀 (如有需要)
|
||||
HeaderTokenPrefix = "Bearer "
|
||||
)
|
||||
|
||||
// 业务状态常量
|
||||
const (
|
||||
StateActive = "1" // 使用中
|
||||
StateInactive = "0" // 未使用/已删除
|
||||
StateHistory = "2" // 历史记录
|
||||
)
|
||||
|
||||
// 数值常量
|
||||
const (
|
||||
Number0 = 0
|
||||
Number0p75 = 0.75
|
||||
Number0p5 = 0.5
|
||||
Number5 = 5
|
||||
Number7p5 = 7.5
|
||||
Number100 = 100
|
||||
)
|
||||
|
||||
// 数据类型常量
|
||||
const (
|
||||
TypeNormal = "1" // 普通类
|
||||
TypeArt = "2" // 艺术类
|
||||
|
||||
// YxConstant 相关常量
|
||||
NowYear = "2026"
|
||||
|
||||
// 录取方式常量
|
||||
CulturalControlLineGuo = "文线专排"
|
||||
SpecialControlLineGuo = "专过文排"
|
||||
CulturalControlLineGuoMain = "文过专排主科"
|
||||
|
||||
W1Z1 = "文*1+专*1"
|
||||
W1JiaZ1 = "文+专"
|
||||
)
|
||||
|
||||
var (
|
||||
OldYearList = []string{"2025", "2024"}
|
||||
ShowOldYearList = []string{"2025", "2024", "2023"}
|
||||
)
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
// Package common 公共包
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"server/modules/system/entity"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GetLoginUser 从上下文获取当前登录用户
|
||||
// 在Controller中使用: user := common.GetLoginUser(c)
|
||||
func GetLoginUser(c *gin.Context) *entity.LoginUser {
|
||||
value, exists := c.Get(ContextUserKey)
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
if user, ok := value.(*entity.LoginUser); ok {
|
||||
return user
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetLoginUserID 获取当前登录用户ID
|
||||
func GetLoginUserID(c *gin.Context) string {
|
||||
user := GetLoginUser(c)
|
||||
if user != nil {
|
||||
return user.ID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetLoginUsername 获取当前登录用户名
|
||||
func GetLoginUsername(c *gin.Context) string {
|
||||
user := GetLoginUser(c)
|
||||
if user != nil {
|
||||
return user.Username
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetPage 从上下文获取页码
|
||||
func GetPage(c *gin.Context) int {
|
||||
pageStr := c.DefaultQuery("page", "1")
|
||||
var page int
|
||||
fmt.Sscanf(pageStr, "%d", &page)
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
return page
|
||||
}
|
||||
|
||||
// GetSize 从上下文获取每页大小
|
||||
func GetSize(c *gin.Context) int {
|
||||
sizeStr := c.DefaultQuery("size", "10")
|
||||
var size int
|
||||
fmt.Sscanf(sizeStr, "%d", &size)
|
||||
if size < 1 {
|
||||
size = 10
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"server/common/snowflake"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultSnowflake *snowflake.Snowflake
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
// InitGenerator 初始化雪花算法生成器
|
||||
// workerId: 工作机器ID (0 ~ 31)
|
||||
// datacenterId: 数据中心ID (0 ~ 31)
|
||||
// 如果不需要区分数据中心,可以将 datacenterId 设置为 0
|
||||
func InitGenerator(workerId, datacenterId int64) error {
|
||||
// 先校验参数
|
||||
if workerId < 0 || workerId > 31 {
|
||||
return errors.New("workerId must be between 0 and 31")
|
||||
}
|
||||
if datacenterId < 0 || datacenterId > 31 {
|
||||
return errors.New("datacenterId must be between 0 and 31")
|
||||
}
|
||||
|
||||
// 执行初始化
|
||||
once.Do(func() {
|
||||
var err error
|
||||
defaultSnowflake, err = snowflake.NewSnowflake(workerId, datacenterId)
|
||||
if err != nil {
|
||||
panic("InitGenerator failed: " + err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// InitGeneratorWithWorkerID 仅使用 workerId 初始化(兼容旧版本)
|
||||
// datacenterId 默认为 0
|
||||
func InitGeneratorWithWorkerID(workerID int64) error {
|
||||
return InitGenerator(workerID, 0)
|
||||
}
|
||||
|
||||
// getInstance 获取单例实例
|
||||
func getInstance() *snowflake.Snowflake {
|
||||
once.Do(func() {
|
||||
// 默认值:workerId=1, datacenterId=0
|
||||
var err error
|
||||
defaultSnowflake, err = snowflake.NewSnowflake(1, 0)
|
||||
if err != nil {
|
||||
// 默认参数如果还失败,直接 panic
|
||||
panic("Snowflake getInstance failed: " + err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
// 防御性编程:如果 once.Do 已经执行过(例如被 InitGenerator 执行了),
|
||||
// 但因为 panic 或其他异常导致 defaultSnowflake 仍为 nil,这里进行补救
|
||||
if defaultSnowflake == nil {
|
||||
// 此时忽略 sync.Once,直接强制初始化,防止 nil pointer crash
|
||||
// 使用默认安全值 (1, 0)
|
||||
defaultSnowflake, _ = snowflake.NewSnowflake(1, 0)
|
||||
}
|
||||
|
||||
return defaultSnowflake
|
||||
}
|
||||
|
||||
// GenerateLongID 生成 64 位整型 ID
|
||||
func GenerateLongID() int64 {
|
||||
id, err := getInstance().NextId()
|
||||
if err != nil {
|
||||
// 极端情况:时间回拨
|
||||
// 返回 0 或使用时间戳作为备用方案
|
||||
panic("GenerateLongID failed: " + err.Error())
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
// GenerateStringID 生成字符串 ID
|
||||
func GenerateStringID() string {
|
||||
return strconv.FormatInt(GenerateLongID(), 10)
|
||||
}
|
||||
|
|
@ -0,0 +1,210 @@
|
|||
// Package common 日志工具
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"server/config"
|
||||
)
|
||||
|
||||
// 日志级别
|
||||
const (
|
||||
LevelDebug = iota
|
||||
LevelInfo
|
||||
LevelWarn
|
||||
LevelError
|
||||
)
|
||||
|
||||
var levelNames = map[int]string{
|
||||
LevelDebug: "DEBUG",
|
||||
LevelInfo: "INFO",
|
||||
LevelWarn: "WARN",
|
||||
LevelError: "ERROR",
|
||||
}
|
||||
|
||||
var levelValues = map[string]int{
|
||||
"debug": LevelDebug,
|
||||
"info": LevelInfo,
|
||||
"warn": LevelWarn,
|
||||
"error": LevelError,
|
||||
}
|
||||
|
||||
// Logger 日志记录器
|
||||
type Logger struct {
|
||||
level int
|
||||
file *os.File
|
||||
htmlWriter *htmlLogWriter
|
||||
mu sync.Mutex
|
||||
console bool
|
||||
}
|
||||
|
||||
var (
|
||||
defaultLogger *Logger
|
||||
startCount int
|
||||
onceInit sync.Once
|
||||
)
|
||||
|
||||
// InitLogger 初始化日志
|
||||
func InitLogger() {
|
||||
once.Do(func() {
|
||||
cfg := config.AppConfig.Log
|
||||
level := levelValues[cfg.Level]
|
||||
|
||||
// 创建日志目录
|
||||
if err := os.MkdirAll(cfg.Dir, 0755); err != nil {
|
||||
fmt.Println("创建日志目录失败:", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 计算启动次数
|
||||
startCount = getStartCount(cfg.Dir)
|
||||
|
||||
// 创建日志文件
|
||||
filename := fmt.Sprintf("%s-%d.html", time.Now().Format("2006-01-02"), startCount)
|
||||
logFilepath := filepath.Join(cfg.Dir, filename)
|
||||
|
||||
file, err := os.OpenFile(logFilepath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
fmt.Println("创建日志文件失败:", err)
|
||||
return
|
||||
}
|
||||
|
||||
htmlWriter := newHtmlLogWriter(file)
|
||||
|
||||
defaultLogger = &Logger{
|
||||
level: level,
|
||||
file: file,
|
||||
htmlWriter: htmlWriter,
|
||||
console: cfg.Console,
|
||||
}
|
||||
|
||||
fmt.Printf("日志文件: %s\n", logFilepath)
|
||||
})
|
||||
}
|
||||
|
||||
// getStartCount 获取今日启动次数
|
||||
func getStartCount(dir string) int {
|
||||
today := time.Now().Format("2006-01-02")
|
||||
pattern := filepath.Join(dir, today+"*.html")
|
||||
matches, _ := filepath.Glob(pattern)
|
||||
return len(matches) + 1
|
||||
}
|
||||
|
||||
// CloseLogger 关闭日志
|
||||
func CloseLogger() {
|
||||
if defaultLogger != nil && defaultLogger.file != nil {
|
||||
defaultLogger.htmlWriter.writeFooter()
|
||||
defaultLogger.file.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) log(level int, format string, args ...interface{}) {
|
||||
if l == nil || level < l.level {
|
||||
return
|
||||
}
|
||||
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
timestamp := time.Now().Format("2006-01-02 15:04:05.000")
|
||||
levelName := levelNames[level]
|
||||
|
||||
// 写入HTML文件
|
||||
l.htmlWriter.writeLog(timestamp, levelName, msg)
|
||||
|
||||
// 输出到控制台
|
||||
if l.console {
|
||||
fmt.Printf("[%s] [%s] %s\n", timestamp, levelName, msg)
|
||||
}
|
||||
}
|
||||
|
||||
// LogDebug 调试日志
|
||||
func LogDebug(format string, args ...interface{}) {
|
||||
if defaultLogger != nil {
|
||||
defaultLogger.log(LevelDebug, format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// LogInfo 信息日志
|
||||
func LogInfo(format string, args ...interface{}) {
|
||||
if defaultLogger != nil {
|
||||
defaultLogger.log(LevelInfo, format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// LogWarn 警告日志
|
||||
func LogWarn(format string, args ...interface{}) {
|
||||
if defaultLogger != nil {
|
||||
defaultLogger.log(LevelWarn, format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// LogError 错误日志
|
||||
func LogError(format string, args ...interface{}) {
|
||||
if defaultLogger != nil {
|
||||
defaultLogger.log(LevelError, format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// 简短别名
|
||||
var (
|
||||
Debug = LogDebug
|
||||
Info = LogInfo
|
||||
Warn = LogWarn
|
||||
)
|
||||
|
||||
// htmlLogWriter HTML日志写入器
|
||||
type htmlLogWriter struct {
|
||||
writer io.Writer
|
||||
initialized bool
|
||||
}
|
||||
|
||||
func newHtmlLogWriter(w io.Writer) *htmlLogWriter {
|
||||
hw := &htmlLogWriter{writer: w}
|
||||
hw.writeHeader()
|
||||
return hw
|
||||
}
|
||||
|
||||
func (h *htmlLogWriter) writeHeader() {
|
||||
html := `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>应用日志</title>
|
||||
<style>
|
||||
body { font-family: Consolas, monospace; background: #1e1e1e; color: #d4d4d4; padding: 20px; }
|
||||
.log { margin: 2px 0; padding: 4px 8px; border-radius: 3px; }
|
||||
.DEBUG { background: #2d2d2d; color: #9cdcfe; }
|
||||
.INFO { background: #1e3a1e; color: #4ec9b0; }
|
||||
.WARN { background: #3a3a1e; color: #dcdcaa; }
|
||||
.ERROR { background: #3a1e1e; color: #f14c4c; }
|
||||
.time { color: #808080; }
|
||||
.level { font-weight: bold; width: 60px; display: inline-block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>应用日志 - ` + time.Now().Format("2006-01-02") + `</h2>
|
||||
<div id="logs">
|
||||
`
|
||||
h.writer.Write([]byte(html))
|
||||
h.initialized = true
|
||||
}
|
||||
|
||||
func (h *htmlLogWriter) writeLog(timestamp, level, msg string) {
|
||||
html := fmt.Sprintf(`<div class="log %s"><span class="time">[%s]</span> <span class="level">[%s]</span> %s</div>
|
||||
`, level, timestamp, level, msg)
|
||||
h.writer.Write([]byte(html))
|
||||
}
|
||||
|
||||
func (h *htmlLogWriter) writeFooter() {
|
||||
html := `</div>
|
||||
</body>
|
||||
</html>`
|
||||
h.writer.Write([]byte(html))
|
||||
}
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/cipher"
|
||||
"crypto/des"
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// PasswordUtil 对应 Java 类的常量
|
||||
const (
|
||||
// DefaultSalt 对应 Java 中的 SALT = "63293188"
|
||||
DefaultSalt = "63293188"
|
||||
// IterationCount 对应 Java 中的 ITERATIONCOUNT = 1000
|
||||
IterationCount = 1000
|
||||
)
|
||||
|
||||
// GetSalt 生成 8 字节的随机盐
|
||||
func GetSalt() ([]byte, error) {
|
||||
salt := make([]byte, 8)
|
||||
_, err := rand.Read(salt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return salt, nil
|
||||
}
|
||||
|
||||
// GetStaticSalt 获取静态盐
|
||||
func GetStaticSalt() []byte {
|
||||
return []byte(DefaultSalt)
|
||||
}
|
||||
|
||||
// Encrypt 加密
|
||||
// plaintext: 明文
|
||||
// password: 密码
|
||||
// salt: 盐 (传入 string, 对应 Java 的 salt 参数)
|
||||
func Encrypt(plaintext, password, salt string) (string, error) {
|
||||
// 1. 生成 Key 和 IV
|
||||
key, iv := deriveKeyAndIV(password, salt, IterationCount)
|
||||
|
||||
// 2. 创建 DES Cipher
|
||||
block, err := des.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 3. 处理数据 (UTF-8 转 bytes 并填充)
|
||||
data := []byte(plaintext)
|
||||
data = pkcs5Padding(data, block.BlockSize())
|
||||
|
||||
// 4. 加密 (CBC 模式)
|
||||
blockMode := cipher.NewCBCEncrypter(block, iv)
|
||||
crypted := make([]byte, len(data))
|
||||
blockMode.CryptBlocks(crypted, data)
|
||||
|
||||
// 5. 转十六进制字符串 (对应 Java 的 bytesToHexString)
|
||||
// Java 的 Integer.toHexString 产生的是小写,这里保持一致,
|
||||
// 但通常十六进制可以互通。
|
||||
return hex.EncodeToString(crypted), nil
|
||||
}
|
||||
|
||||
// Decrypt 解密
|
||||
// ciphertext: 密文 (Hex 字符串)
|
||||
// password: 密码
|
||||
// salt: 盐
|
||||
func Decrypt(ciphertext, password, salt string) (string, error) {
|
||||
// 1. 生成 Key 和 IV
|
||||
key, iv := deriveKeyAndIV(password, salt, IterationCount)
|
||||
|
||||
// 2. Hex 字符串转 bytes
|
||||
decodedBytes, err := hex.DecodeString(ciphertext)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 3. 创建 DES Cipher
|
||||
block, err := des.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 4. 解密 (CBC 模式)
|
||||
blockMode := cipher.NewCBCDecrypter(block, iv)
|
||||
// 密文长度检查
|
||||
if len(decodedBytes)%block.BlockSize() != 0 {
|
||||
return "", errors.New("ciphertext is not a multiple of the block size")
|
||||
}
|
||||
origData := make([]byte, len(decodedBytes))
|
||||
blockMode.CryptBlocks(origData, decodedBytes)
|
||||
|
||||
// 5. 去除填充
|
||||
origData, err = pkcs5UnPadding(origData)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(origData), nil
|
||||
}
|
||||
|
||||
// deriveKeyAndIV 模拟 Java PBEWithMD5AndDES 的密钥派生逻辑
|
||||
// 逻辑:MD5(password || salt) -> 迭代 -> 结果前8字节是Key,后8字节是IV
|
||||
func deriveKeyAndIV(password, salt string, iterations int) ([]byte, []byte) {
|
||||
// Java PBEKeySpec 处理 password 为 char[],通常转 byte 时依赖编码,
|
||||
// 此处假设使用 UTF-8 兼容(Java 默认行为通常如此,或者 ASCII)
|
||||
passBytes := []byte(password)
|
||||
saltBytes := []byte(salt)
|
||||
|
||||
// 第一次迭代: Hash(pass + salt)
|
||||
hash := md5.New()
|
||||
hash.Write(passBytes)
|
||||
hash.Write(saltBytes)
|
||||
derived := hash.Sum(nil)
|
||||
|
||||
// 后续迭代: Hash(prev_hash)
|
||||
for i := 1; i < iterations; i++ {
|
||||
hash.Reset()
|
||||
hash.Write(derived)
|
||||
derived = hash.Sum(nil)
|
||||
}
|
||||
|
||||
// MD5 结果是 16 字节
|
||||
// DES Key 需要 8 字节,IV 需要 8 字节
|
||||
// 正好平分
|
||||
key := derived[:8]
|
||||
iv := derived[8:]
|
||||
|
||||
return key, iv
|
||||
}
|
||||
|
||||
// pkcs5Padding 填充
|
||||
func pkcs5Padding(ciphertext []byte, blockSize int) []byte {
|
||||
padding := blockSize - len(ciphertext)%blockSize
|
||||
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||
return append(ciphertext, padtext...)
|
||||
}
|
||||
|
||||
// pkcs5UnPadding 去除填充
|
||||
func pkcs5UnPadding(origData []byte) ([]byte, error) {
|
||||
length := len(origData)
|
||||
if length == 0 {
|
||||
return nil, errors.New("decryption error: output length is zero")
|
||||
}
|
||||
unpadding := int(origData[length-1])
|
||||
if length < unpadding {
|
||||
return nil, errors.New("decryption error: invalid padding")
|
||||
}
|
||||
return origData[:(length - unpadding)], nil
|
||||
}
|
||||
|
||||
// --- 测试主函数 ---
|
||||
func main() {
|
||||
// 测试数据
|
||||
plaintext := "admin"
|
||||
password := "Wang5322570"
|
||||
//salt := DefaultSalt
|
||||
salt := "RCGTeGiH"
|
||||
|
||||
fmt.Println("原文:", plaintext)
|
||||
fmt.Println("密码:", password)
|
||||
fmt.Println("盐值:", salt)
|
||||
|
||||
// 加密
|
||||
encrypted, err := Encrypt(plaintext, password, salt)
|
||||
if err != nil {
|
||||
fmt.Println("加密失败:", err)
|
||||
return
|
||||
}
|
||||
// Java代码可能输出小写或手动处理,这里使用 hex.EncodeToString (小写)
|
||||
// 如果需要大写可以使用 strings.ToUpper(encrypted)
|
||||
fmt.Println("加密密文 (Hex):", encrypted)
|
||||
|
||||
// 解密
|
||||
decrypted, err := Decrypt(encrypted, password, salt)
|
||||
if err != nil {
|
||||
fmt.Println("解密失败:", err)
|
||||
return
|
||||
}
|
||||
fmt.Println("解密原文:", decrypted)
|
||||
|
||||
// 验证一致性
|
||||
if plaintext == decrypted {
|
||||
fmt.Println(">> 测试通过!Go版本与逻辑一致。")
|
||||
} else {
|
||||
fmt.Println(">> 测试失败!")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
// Package common 公共包
|
||||
package common
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
// Response 统一响应结构
|
||||
type Response struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
// PageResponse 分页响应
|
||||
type PageResponse struct {
|
||||
List interface{} `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
Size int `json:"size"`
|
||||
}
|
||||
|
||||
func Success(c *gin.Context, data interface{}) {
|
||||
c.JSON(200, Response{Code: 200, Message: "success", Data: data})
|
||||
}
|
||||
|
||||
func SuccessPage(c *gin.Context, list interface{}, total int64, page, size int) {
|
||||
c.JSON(200, Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: PageResponse{List: list, Total: total, Page: page, Size: size},
|
||||
})
|
||||
}
|
||||
|
||||
func Error(c *gin.Context, code int, message string) {
|
||||
c.JSON(code, Response{Code: code, Message: message, Data: nil})
|
||||
}
|
||||
|
|
@ -0,0 +1,197 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"math"
|
||||
"regexp"
|
||||
"server/modules/user/vo"
|
||||
"server/modules/yx/dto"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ScoreCalculator 分数计算工具函数集合
|
||||
// 移植自 Java 版本的 ScoreUtil.java
|
||||
|
||||
var (
|
||||
BigDecimal0 = 0.0
|
||||
BigDecimal05 = 0.5
|
||||
BigDecimal075 = 0.75
|
||||
BigDecimal0133 = 0.133
|
||||
BigDecimal0667 = 0.667
|
||||
BigDecimal004 = 0.04
|
||||
BigDecimal0467 = 0.467
|
||||
BigDecimal0067 = 0.067
|
||||
BigDecimal0333 = 0.333
|
||||
BigDecimal0093 = 0.093
|
||||
BigDecimal02 = 0.2
|
||||
BigDecimal07 = 0.7
|
||||
BigDecimal03 = 0.3
|
||||
BigDecimal04 = 0.4
|
||||
BigDecimal06 = 0.6
|
||||
BigDecimal1 = 1.0
|
||||
BigDecimal100 = 100.0
|
||||
BigDecimal150 = 150.0
|
||||
BigDecimal95x = 95.0
|
||||
BigDecimal85x = 85.0
|
||||
BigDecimal7p5 = 7.5
|
||||
BigDecimal5 = 5.0
|
||||
)
|
||||
|
||||
// ComputeHistoryMajorEnrollScoreLineDifferenceWithRulesEnrollProbability 计算历年录取分差
|
||||
func ComputeHistoryMajorEnrollScoreLineDifferenceWithRulesEnrollProbability(majorType string,
|
||||
rulesEnrollProbability, probabilityOperator string,
|
||||
HistoryMajorEnrollMap map[string]dto.YxHistoryMajorEnrollDTO) map[string]any {
|
||||
|
||||
if len(HistoryMajorEnrollMap) == 0 {
|
||||
return map[string]any{"scoreDifference": 0.0}
|
||||
}
|
||||
|
||||
sum := 0.0
|
||||
validYearCount := 0
|
||||
|
||||
for _, enrollData := range HistoryMajorEnrollMap {
|
||||
admissionLine := enrollData.AdmissionLine
|
||||
controlLine := enrollData.ControlLine
|
||||
if admissionLine <= 0 || controlLine <= 0 {
|
||||
continue
|
||||
}
|
||||
// 计算分差
|
||||
currentDiff := admissionLine - controlLine
|
||||
|
||||
// currentDiff := enrollData.ScoreLineDifference
|
||||
|
||||
// 特殊逻辑:高职高专(非体育类)需计算分差率(分差/省控线)
|
||||
// Java: boolean isVocationalCollege = "高职高专".equals(enrollData.getBatch());
|
||||
// boolean isSportsMajor = "体育类".equals(enrollData.getMajorType());
|
||||
if "体育类" == majorType {
|
||||
if "2024" == enrollData.Year && "专过文排" != enrollData.EnrollmentCode && "文过专排" != enrollData.EnrollmentCode {
|
||||
currentDiff = currentDiff * Number7p5
|
||||
} else if "2024" == enrollData.Year && "文过专排" == enrollData.EnrollmentCode { // Placeholder
|
||||
currentDiff = currentDiff * Number5
|
||||
} else if "2023" == enrollData.Year {
|
||||
continue
|
||||
} else if rulesEnrollProbability == enrollData.RulesEnrollProbability { // Need field
|
||||
sum += currentDiff
|
||||
validYearCount++
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
// 非高职高专 or 体育类:检查录取方式是否一致
|
||||
if probabilityOperator != enrollData.ProbabilityOperator {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
sum += currentDiff
|
||||
validYearCount++
|
||||
}
|
||||
|
||||
averageDiff := 0.0
|
||||
if validYearCount > 0 {
|
||||
averageDiff = sum / float64(validYearCount)
|
||||
}
|
||||
|
||||
return map[string]any{"scoreDifference": averageDiff}
|
||||
}
|
||||
|
||||
// ConvertIntoScore 计算折合分
|
||||
func ConvertIntoScore(rulesEnrollProbability string, culturalScore, professionalScore float64, operator string) float64 {
|
||||
score := 0.0
|
||||
if operator != "" {
|
||||
// operator: 例如: "文*0.5+专*0.5"
|
||||
parts := strings.Split(operator, "+")
|
||||
for _, part := range parts {
|
||||
subParts := strings.Split(part, "*")
|
||||
if len(subParts) == 2 {
|
||||
ratio, _ := strconv.ParseFloat(subParts[1], 64)
|
||||
if strings.Contains(subParts[0], "文") {
|
||||
score += culturalScore * ratio
|
||||
} else {
|
||||
score += professionalScore * ratio
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return round(score, 4)
|
||||
}
|
||||
|
||||
// CrossingControlLine 判断是否过省控线
|
||||
func CrossingControlLine(rulesEnrollProbability string, culturalScore, professionalScore, culturalControlLine, specialControlLine float64) bool {
|
||||
if rulesEnrollProbability == CulturalControlLineGuo {
|
||||
return culturalScore >= culturalControlLine
|
||||
} else if rulesEnrollProbability == SpecialControlLineGuo {
|
||||
return professionalScore >= specialControlLine
|
||||
} else if rulesEnrollProbability == CulturalControlLineGuoMain {
|
||||
return culturalScore >= specialControlLine
|
||||
}
|
||||
return culturalScore >= culturalControlLine && professionalScore >= specialControlLine
|
||||
}
|
||||
|
||||
// CommonCheckEnrollProbability 计算录取率
|
||||
func CommonCheckEnrollProbability(nowYearDiff, historyThreeYearDiff float64) float64 {
|
||||
enrollProbability := 0.0
|
||||
if nowYearDiff == 0 && historyThreeYearDiff > 0 {
|
||||
enrollProbability = 75.0 / historyThreeYearDiff
|
||||
} else if nowYearDiff == 0 && historyThreeYearDiff <= 0 {
|
||||
enrollProbability = 50.0
|
||||
} else if nowYearDiff > 0 && historyThreeYearDiff <= 0 {
|
||||
enrollProbability = nowYearDiff * 100 * 0.75
|
||||
} else if nowYearDiff < 0 && historyThreeYearDiff <= 0 {
|
||||
enrollProbability = 0.0
|
||||
} else {
|
||||
// (当前年分差/去年分差)*0.75 * 100
|
||||
if historyThreeYearDiff != 0 {
|
||||
enrollProbability = (nowYearDiff / historyThreeYearDiff) * 100 * 0.75
|
||||
}
|
||||
}
|
||||
return enrollProbability
|
||||
}
|
||||
|
||||
// CommonCheckEnrollProbabilityBeilv 录取率倍率调整
|
||||
func CommonCheckEnrollProbabilityBeilv(enrollProbability float64) float64 {
|
||||
if enrollProbability > 150 {
|
||||
return 95.0
|
||||
} else if enrollProbability > 100 {
|
||||
return 85.0
|
||||
} else if enrollProbability <= 0 {
|
||||
return 0.0
|
||||
}
|
||||
return enrollProbability
|
||||
}
|
||||
|
||||
// OtherScoreJudge 其他录取要求
|
||||
func OtherScoreJudge(professionalScore float64, userScoreVO vo.UserScoreVO, schoolMajorDTO dto.SchoolMajorDTO) bool {
|
||||
// 简单实现,参考 Java 逻辑
|
||||
if schoolMajorDTO.EnglishScoreLimitation > 0 && userScoreVO.EnglishScore < schoolMajorDTO.EnglishScoreLimitation {
|
||||
return false
|
||||
}
|
||||
if schoolMajorDTO.CulturalScoreLimitation > 0 && userScoreVO.CulturalScore < schoolMajorDTO.CulturalScoreLimitation {
|
||||
return false
|
||||
}
|
||||
// 专业分限制等...
|
||||
return true
|
||||
}
|
||||
|
||||
// HasComputeEnrollProbabilityPermissions 判断是否有权限计算
|
||||
func HasComputeEnrollProbabilityPermissions(nowBatch, majorBatch string) bool {
|
||||
return "" != nowBatch && "" != majorBatch
|
||||
}
|
||||
|
||||
// round 四舍五入
|
||||
func round(val float64, precision int) float64 {
|
||||
ratio := math.Pow(10, float64(precision))
|
||||
return math.Round(val*ratio) / ratio
|
||||
}
|
||||
|
||||
// ReplaceLastZeroChar 格式化操作符字符串 (Java: replaceLastZeroChar)
|
||||
func ReplaceLastZeroChar(input string) string {
|
||||
if input == "" {
|
||||
return ""
|
||||
}
|
||||
re := regexp.MustCompile(`(\d+\.\d*?)0+(\D|$)`)
|
||||
input = re.ReplaceAllString(input, "$1$2")
|
||||
input = strings.ReplaceAll(input, "1.", "1")
|
||||
// 简化处理
|
||||
return input
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
package snowflake
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 定义常量
|
||||
const (
|
||||
// 位数分配
|
||||
sequenceBits = 12 // 序列号占用的位数
|
||||
workerIdBits = 5 // 工作机器ID占用的位数
|
||||
datacenterIdBits = 5 // 数据中心ID占用的位数
|
||||
|
||||
// 最大值
|
||||
maxSequence = -1 ^ (-1 << sequenceBits) // 4095
|
||||
maxWorkerId = -1 ^ (-1 << workerIdBits) // 31
|
||||
maxDatacenterId = -1 ^ (-1 << datacenterIdBits) // 31
|
||||
|
||||
// 位移偏移量
|
||||
workerIdShift = sequenceBits // 12
|
||||
datacenterIdShift = sequenceBits + workerIdBits // 12 + 5 = 17
|
||||
timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits // 12 + 5 + 5 = 22
|
||||
)
|
||||
|
||||
// 起始时间戳 (纪元),可以使用程序上线的时间,这里设置为 2020-01-01 00:00:00 UTC
|
||||
var epoch int64 = 1577836800000
|
||||
|
||||
// Snowflake 结构体
|
||||
type Snowflake struct {
|
||||
mu sync.Mutex // 互斥锁,保证并发安全
|
||||
lastTime int64 // 上次生成ID的时间戳
|
||||
workerId int64 // 工作机器ID
|
||||
datacenterId int64 // 数据中心ID
|
||||
sequence int64 // 当前毫秒内的序列号
|
||||
}
|
||||
|
||||
// NewSnowflake 初始化一个 Snowflake 实例
|
||||
// workerId: 工作机器ID (0 ~ 31)
|
||||
// datacenterId: 数据中心ID (0 ~ 31)
|
||||
func NewSnowflake(workerId, datacenterId int64) (*Snowflake, error) {
|
||||
if workerId < 0 || workerId > maxWorkerId {
|
||||
return nil, errors.New(fmt.Sprintf("worker Id can't be greater than %d or less than 0", maxWorkerId))
|
||||
}
|
||||
if datacenterId < 0 || datacenterId > maxDatacenterId {
|
||||
return nil, errors.New(fmt.Sprintf("datacenter Id can't be greater than %d or less than 0", maxDatacenterId))
|
||||
}
|
||||
|
||||
return &Snowflake{
|
||||
lastTime: 0,
|
||||
workerId: workerId,
|
||||
datacenterId: datacenterId,
|
||||
sequence: 0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NextId 生成下一个 ID
|
||||
func (s *Snowflake) NextId() (int64, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
// 获取当前时间戳(毫秒)
|
||||
now := time.Now().UnixMilli()
|
||||
|
||||
// 如果当前时间小于上次生成ID的时间,说明时钟回拨,抛出异常
|
||||
if now < s.lastTime {
|
||||
return 0, errors.New(fmt.Sprintf("Clock moved backwards. Refusing to generate id for %d milliseconds", s.lastTime-now))
|
||||
}
|
||||
|
||||
// 如果是同一毫秒内
|
||||
if now == s.lastTime {
|
||||
// 序列号自增
|
||||
s.sequence = (s.sequence + 1) & maxSequence
|
||||
// 如果序列号溢出(超过4095),则等待下一毫秒
|
||||
if s.sequence == 0 {
|
||||
now = s.waitNextMillis(now)
|
||||
}
|
||||
} else {
|
||||
// 不同毫秒,序列号重置为0
|
||||
s.sequence = 0
|
||||
}
|
||||
|
||||
// 更新最后时间戳
|
||||
s.lastTime = now
|
||||
|
||||
// 组装 ID
|
||||
// (当前时间 - 起始时间) << 时间戳位移 | 数据中心ID << 数据中心位移 | 工作ID << 工作位移 | 序列号
|
||||
id := ((now - epoch) << timestampLeftShift) |
|
||||
(s.datacenterId << datacenterIdShift) |
|
||||
(s.workerId << workerIdShift) |
|
||||
s.sequence
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// waitNextMillis 阻塞等待下一毫秒
|
||||
func (s *Snowflake) waitNextMillis(lastTime int64) int64 {
|
||||
now := time.Now().UnixMilli()
|
||||
for now <= lastTime {
|
||||
now = time.Now().UnixMilli()
|
||||
}
|
||||
return now
|
||||
}
|
||||
|
||||
// ParseId 解析 ID,用于调试或查看 ID 组成部分
|
||||
func ParseId(id int64) map[string]interface{} {
|
||||
timestamp := (id >> timestampLeftShift) + epoch
|
||||
datacenterId := (id >> datacenterIdShift) & maxDatacenterId
|
||||
workerId := (id >> workerIdShift) & maxWorkerId
|
||||
sequence := id & maxSequence
|
||||
|
||||
return map[string]interface{}{
|
||||
"id": id,
|
||||
"timestamp": timestamp,
|
||||
"time_str": time.UnixMilli(timestamp).Format("2006-01-02 15:04:05.000"),
|
||||
"datacenterId": datacenterId,
|
||||
"workerId": workerId,
|
||||
"sequence": sequence,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package snowflake // 注意:这里必须是 package snowflake,不能是 main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// 这是一个测试函数,用于验证功能
|
||||
func TestGenerateID(t *testing.T) {
|
||||
// 1. 初始化生成器
|
||||
sf, err := NewSnowflake(1, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("初始化失败: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println("=== 开始生成 ID ===")
|
||||
|
||||
// 2. 生成几个 ID
|
||||
for i := 0; i < 5; i++ {
|
||||
id, err := sf.NextId()
|
||||
if err != nil {
|
||||
t.Errorf("生成 ID 失败: %v", err)
|
||||
} else {
|
||||
fmt.Printf("生成 ID: %d\n", id)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 解析 ID 查看详情
|
||||
id, _ := sf.NextId()
|
||||
info := ParseId(id)
|
||||
fmt.Printf("\nID 详情解析:\n")
|
||||
fmt.Printf("ID: %d\n", info["id"])
|
||||
fmt.Printf("时间: %s\n", info["time_str"])
|
||||
fmt.Printf("数据中心: %d\n", info["datacenterId"])
|
||||
fmt.Printf("工作机器: %d\n", info["workerId"])
|
||||
fmt.Printf("序列号: %d\n", info["sequence"])
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package common
|
||||
|
||||
import "regexp"
|
||||
|
||||
// 验证表名格式的辅助函数
|
||||
func IsValidTableName(tableName string) bool {
|
||||
if tableName == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// 表名只能包含字母、数字、下划线和点号,且长度合理
|
||||
matched, err := regexp.MatchString(`^[a-zA-Z_][a-zA-Z0-9_.]{0,100}$`, tableName)
|
||||
return err == nil && matched
|
||||
}
|
||||
|
|
@ -0,0 +1,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: ""
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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: ""
|
||||
|
|
@ -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: ""
|
||||
|
|
@ -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("数据库连接已关闭")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
// Package config Redis配置
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// RDB 全局Redis客户端
|
||||
var RDB *redis.Client
|
||||
|
||||
// InitRedis 初始化Redis连接
|
||||
func InitRedis() {
|
||||
redisConfig := AppConfig.Redis
|
||||
RDB = redis.NewClient(&redis.Options{
|
||||
Addr: redisConfig.Addr,
|
||||
Password: redisConfig.Password,
|
||||
DB: redisConfig.DB,
|
||||
PoolSize: 10, // 连接池大小
|
||||
})
|
||||
|
||||
// 测试连接
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, err := RDB.Ping(ctx).Result()
|
||||
if err != nil {
|
||||
log.Fatal("Redis连接失败:", err)
|
||||
}
|
||||
fmt.Println("Redis连接成功")
|
||||
}
|
||||
|
||||
// CloseRedis 关闭Redis连接
|
||||
func CloseRedis() {
|
||||
if RDB != nil {
|
||||
RDB.Close()
|
||||
fmt.Println("Redis连接已关闭")
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,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
|
||||
)
|
||||
|
|
@ -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=
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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...)
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// CorsMiddleware 跨域中间件
|
||||
func CorsMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
method := c.Request.Method
|
||||
origin := c.Request.Header.Get("Origin")
|
||||
|
||||
if origin != "" {
|
||||
// 允许所有来源,生产环境请修改为特定域名
|
||||
c.Header("Access-Control-Allow-Origin", origin)
|
||||
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
|
||||
c.Header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization, X-App-Sign")
|
||||
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Cache-Control, Content-Language, Content-Type")
|
||||
c.Header("Access-Control-Allow-Credentials", "true")
|
||||
}
|
||||
|
||||
// 放行 OPTIONS 请求
|
||||
if method == "OPTIONS" {
|
||||
c.AbortWithStatus(http.StatusNoContent)
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
// Package middleware 限流中间件
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/common"
|
||||
"server/config"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// RateLimitMiddleware 限流中间件
|
||||
// 基于 Redis 实现,支持按用户ID或IP限流
|
||||
// 不同接口可配置不同的限流规则
|
||||
func RateLimitMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
cfg := config.AppConfig.RateLimit
|
||||
|
||||
// 未启用则跳过
|
||||
if !cfg.Enable {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 白名单路径跳过
|
||||
path := c.Request.URL.Path
|
||||
if isRateLimitWhitelist(path) {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 获取限流规则
|
||||
rule := getRule(path, cfg)
|
||||
|
||||
// 获取限流key (优先用户ID,否则用IP)
|
||||
key := getRateLimitKey(c, path)
|
||||
|
||||
// 检查是否超过限制
|
||||
if !checkRateLimit(key, rule) {
|
||||
common.Warn("请求过于频繁: Key=%s Path=%s", key, path)
|
||||
c.JSON(429, map[string]interface{}{
|
||||
"code": 429,
|
||||
"message": "操作过快,请稍后再试",
|
||||
"data": nil,
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// getRule 获取路径对应的限流规则
|
||||
func getRule(path string, cfg config.RateLimitConfig) config.RateLimitRule {
|
||||
// 精确匹配
|
||||
if rule, ok := cfg.Rules[path]; ok {
|
||||
return rule
|
||||
}
|
||||
|
||||
// 前缀匹配
|
||||
for rulePath, rule := range cfg.Rules {
|
||||
if strings.HasPrefix(path, rulePath) {
|
||||
return rule
|
||||
}
|
||||
}
|
||||
|
||||
// 返回默认规则
|
||||
return cfg.Default
|
||||
}
|
||||
|
||||
// getRateLimitKey 获取限流key
|
||||
func getRateLimitKey(c *gin.Context, path string) string {
|
||||
// 优先使用用户ID
|
||||
if user := common.GetLoginUser(c); user != nil {
|
||||
return fmt.Sprintf("ratelimit:%s:%s", user.ID, path)
|
||||
}
|
||||
// 否则使用IP
|
||||
return fmt.Sprintf("ratelimit:%s:%s", c.ClientIP(), path)
|
||||
}
|
||||
|
||||
// checkRateLimit 检查是否超过限流
|
||||
// 使用 Redis 滑动窗口算法
|
||||
func checkRateLimit(key string, rule config.RateLimitRule) bool {
|
||||
ctx := context.Background()
|
||||
rdb := config.RDB
|
||||
|
||||
now := time.Now().UnixMilli()
|
||||
windowStart := now - int64(rule.Interval*1000)
|
||||
|
||||
// 使用 Redis 事务
|
||||
pipe := rdb.Pipeline()
|
||||
|
||||
// 移除窗口外的记录
|
||||
pipe.ZRemRangeByScore(ctx, key, "0", fmt.Sprintf("%d", windowStart))
|
||||
|
||||
// 获取当前窗口内的请求数
|
||||
countCmd := pipe.ZCard(ctx, key)
|
||||
|
||||
// 添加当前请求
|
||||
pipe.ZAdd(ctx, key, redis.Z{
|
||||
Score: float64(now),
|
||||
Member: fmt.Sprintf("%d", now),
|
||||
})
|
||||
|
||||
// 设置过期时间
|
||||
pipe.Expire(ctx, key, time.Duration(rule.Interval)*time.Second)
|
||||
|
||||
_, err := pipe.Exec(ctx)
|
||||
if err != nil {
|
||||
common.LogError("限流检查失败: %v", err)
|
||||
return true // 出错时放行
|
||||
}
|
||||
|
||||
count := countCmd.Val()
|
||||
return count < int64(rule.MaxRequests)
|
||||
}
|
||||
|
||||
// 限流白名单
|
||||
var rateLimitWhitelist = []string{
|
||||
"/swagger/",
|
||||
"/api/auth/logout",
|
||||
}
|
||||
|
||||
func isRateLimitWhitelist(path string) bool {
|
||||
for _, white := range rateLimitWhitelist {
|
||||
if strings.HasPrefix(path, white) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// AddRateLimitWhitelist 添加限流白名单
|
||||
func AddRateLimitWhitelist(paths ...string) {
|
||||
rateLimitWhitelist = append(rateLimitWhitelist, paths...)
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
// Package middleware 安全校验中间件
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"server/common"
|
||||
"server/config"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// SecurityMiddleware 安全校验中间件
|
||||
// 防止暴力入侵,校验请求头签名
|
||||
// 请求头需携带:
|
||||
// - X-App-Sign: 签名值 = MD5(timestamp + secretKey)
|
||||
// - X-App-Timestamp: 时间戳(毫秒)
|
||||
func SecurityMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
cfg := config.AppConfig.Security
|
||||
|
||||
// 未启用则跳过
|
||||
if !cfg.Enable {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 白名单路径跳过
|
||||
path := c.Request.URL.Path
|
||||
if isSecurityWhitelist(path) {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 获取签名和时间戳
|
||||
sign := c.GetHeader(cfg.HeaderKey)
|
||||
timestamp := c.GetHeader("X-App-Timestamp")
|
||||
|
||||
if sign == "" || timestamp == "" {
|
||||
common.Warn("安全校验失败: 缺少签名头 IP=%s Path=%s", c.ClientIP(), path)
|
||||
common.Error(c, 403, "非法请求")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 验证时间戳 (5分钟内有效)
|
||||
ts, err := strconv.ParseInt(timestamp, 10, 64)
|
||||
if err != nil {
|
||||
common.Warn("安全校验失败: 时间戳格式错误 IP=%s", c.ClientIP())
|
||||
common.Error(c, 403, "非法请求")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now().UnixMilli()
|
||||
if abs(now-ts) > 5*60*1000 { // 5分钟
|
||||
common.Warn("安全校验失败: 时间戳过期 IP=%s Timestamp=%d", c.ClientIP(), ts)
|
||||
common.Error(c, 403, "请求已过期")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 验证签名
|
||||
expectedSign := generateSign(timestamp, cfg.SecretKey)
|
||||
if sign != expectedSign {
|
||||
common.Warn("安全校验失败: 签名错误 IP=%s Sign=%s Expected=%s", c.ClientIP(), sign, expectedSign)
|
||||
common.Error(c, 403, "签名错误")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// generateSign 生成签名
|
||||
func generateSign(timestamp, secretKey string) string {
|
||||
data := timestamp + secretKey
|
||||
hash := md5.Sum([]byte(data))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
// 安全校验白名单
|
||||
var securityWhitelist = []string{
|
||||
"/swagger/",
|
||||
"/swagger/index.html",
|
||||
}
|
||||
|
||||
func isSecurityWhitelist(path string) bool {
|
||||
for _, white := range securityWhitelist {
|
||||
if len(path) >= len(white) && path[:len(white)] == white {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func abs(n int64) int64 {
|
||||
if n < 0 {
|
||||
return -n
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// AddSecurityWhitelist 添加安全校验白名单
|
||||
func AddSecurityWhitelist(paths ...string) {
|
||||
securityWhitelist = append(securityWhitelist, paths...)
|
||||
}
|
||||
|
|
@ -0,0 +1,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)
|
||||
}
|
||||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
// Package dto 请求参数
|
||||
package dto
|
||||
|
||||
// UserPasswordLoginRequest 手机号密码登录请求
|
||||
type UserPasswordLoginRequest struct {
|
||||
Phone string `json:"phone" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
// Package controller 控制器层
|
||||
package controller
|
||||
|
||||
import (
|
||||
"server/common"
|
||||
"server/modules/system/dto"
|
||||
"server/modules/system/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// AuthController 认证控制器
|
||||
type AuthController struct {
|
||||
userService *service.SysUserService
|
||||
}
|
||||
|
||||
func NewAuthController() *AuthController {
|
||||
return &AuthController{userService: service.NewSysUserService()}
|
||||
}
|
||||
|
||||
func (ctrl *AuthController) RegisterRoutes(r *gin.RouterGroup) {
|
||||
r.POST("/sys/auth/login", ctrl.SysLogin)
|
||||
r.POST("/sys/auth/logout", ctrl.Logout)
|
||||
r.GET("/sys/auth/info", ctrl.GetUserInfo)
|
||||
}
|
||||
|
||||
// SysLogin 用户登录
|
||||
// @Summary Sys用户登录
|
||||
// @Tags 认证
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body LoginRequest true "登录信息"
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /sys/auth/login [post]
|
||||
func (ctrl *AuthController) SysLogin(c *gin.Context) {
|
||||
var req dto.LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.Error(c, 400, "用户名和密码不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
loginUser, token, err := ctrl.userService.SysLogin(req.Username, req.Password)
|
||||
if err != nil {
|
||||
common.Error(c, 401, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
common.Success(c, gin.H{
|
||||
"token": token,
|
||||
"user": loginUser,
|
||||
})
|
||||
}
|
||||
|
||||
// Logout 用户登出
|
||||
// @Summary Sys用户登出
|
||||
// @Tags 认证
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /sys/auth/logout [post]
|
||||
func (ctrl *AuthController) Logout(c *gin.Context) {
|
||||
token := c.GetHeader("Authorization")
|
||||
if len(token) > 7 {
|
||||
token = token[7:] // 去除 "Bearer "
|
||||
}
|
||||
|
||||
ctrl.userService.Logout(token)
|
||||
common.Success(c, nil)
|
||||
}
|
||||
|
||||
// GetUserInfo 获取当前登录用户信息
|
||||
// @Summary Sys获取当前登录用户信息
|
||||
// @Tags 认证
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /sys/auth/info [get]
|
||||
func (ctrl *AuthController) GetUserInfo(c *gin.Context) {
|
||||
user := common.GetLoginUser(c)
|
||||
if user == nil {
|
||||
common.Error(c, 401, "未登录")
|
||||
return
|
||||
}
|
||||
common.Success(c, user)
|
||||
}
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
// Package controller 控制器层
|
||||
package controller
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"server/common"
|
||||
"server/modules/system/dto"
|
||||
"server/modules/system/service"
|
||||
"server/modules/system/vo" // 用于 Swagger 注解
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type SysUserController struct {
|
||||
service *service.SysUserService
|
||||
}
|
||||
|
||||
func NewSysUserController() *SysUserController {
|
||||
return &SysUserController{service: service.NewSysUserService()}
|
||||
}
|
||||
|
||||
// _ 确保 vo 包被导入(用于 Swagger 注解)
|
||||
var _ = vo.SysUserVO{}
|
||||
|
||||
func (ctrl *SysUserController) RegisterRoutes(r *gin.RouterGroup) {
|
||||
r.GET("/sys-users", ctrl.List)
|
||||
r.GET("/sys-users/:id", ctrl.GetByID)
|
||||
r.POST("/sys-users", ctrl.Create)
|
||||
r.PUT("/sys-users/:id", ctrl.Update)
|
||||
r.PATCH("/sys-users/:id", ctrl.UpdateFields)
|
||||
r.DELETE("/sys-users/:id", ctrl.Delete)
|
||||
r.PUT("/sys-users/:id/password", ctrl.UpdatePassword)
|
||||
}
|
||||
|
||||
// @Summary 获取用户列表
|
||||
// @Tags 用户管理
|
||||
// @Param page query int false "页码"
|
||||
// @Param size query int false "每页数量"
|
||||
// @Success 200 {object} common.Response{data=[]vo.SysUserVO}
|
||||
// @Router /sys-users [get]
|
||||
func (ctrl *SysUserController) List(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
size, _ := strconv.Atoi(c.DefaultQuery("size", "10"))
|
||||
items, total, err := ctrl.service.ListUsers(page, size)
|
||||
if err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
common.SuccessPage(c, items, total, page, size)
|
||||
}
|
||||
|
||||
// @Summary 获取单个用户
|
||||
// @Tags 用户管理
|
||||
// @Param id path string true "用户ID"
|
||||
// @Success 200 {object} common.Response{data=vo.SysUserVO}
|
||||
// @Router /sys-users/{id} [get]
|
||||
func (ctrl *SysUserController) GetByID(c *gin.Context) {
|
||||
item, err := ctrl.service.GetUserByID(c.Param("id"))
|
||||
if err != nil {
|
||||
common.Error(c, 404, "未找到")
|
||||
return
|
||||
}
|
||||
common.Success(c, item)
|
||||
}
|
||||
|
||||
// @Summary 创建用户
|
||||
// @Tags 用户管理
|
||||
// @Param request body dto.CreateUserRequest true "用户信息"
|
||||
// @Success 200 {object} common.Response{data=vo.SysUserVO}
|
||||
// @Router /sys-users [post]
|
||||
func (ctrl *SysUserController) Create(c *gin.Context) {
|
||||
var req dto.CreateUserRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.Error(c, 400, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
item, err := ctrl.service.CreateUser(&req, common.GetLoginUserID(c))
|
||||
if err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
common.Success(c, item)
|
||||
}
|
||||
|
||||
// @Summary 更新用户
|
||||
// @Tags 用户管理
|
||||
// @Param id path string true "用户ID"
|
||||
// @Param request body dto.UpdateUserRequest true "用户信息"
|
||||
// @Success 200 {object} common.Response{data=vo.SysUserVO}
|
||||
// @Router /sys-users/{id} [put]
|
||||
func (ctrl *SysUserController) Update(c *gin.Context) {
|
||||
var req dto.UpdateUserRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.Error(c, 400, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
item, err := ctrl.service.UpdateUser(c.Param("id"), &req)
|
||||
if err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
common.Success(c, item)
|
||||
}
|
||||
|
||||
// @Summary 动态字段更新
|
||||
// @Tags 用户管理
|
||||
// @Param id path string true "用户ID"
|
||||
// @Param fields body map[string]interface{} true "要更新的字段"
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /sys-users/{id} [patch]
|
||||
func (ctrl *SysUserController) UpdateFields(c *gin.Context) {
|
||||
var fields map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&fields); err != nil {
|
||||
common.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if err := ctrl.service.UpdateFields(c.Param("id"), fields); err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
common.Success(c, nil)
|
||||
}
|
||||
|
||||
// @Summary 删除用户
|
||||
// @Tags 用户管理
|
||||
// @Param id path string true "用户ID"
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /sys-users/{id} [delete]
|
||||
func (ctrl *SysUserController) Delete(c *gin.Context) {
|
||||
if err := ctrl.service.Delete(c.Param("id")); err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
common.Success(c, nil)
|
||||
}
|
||||
|
||||
// UpdatePasswordRequest 修改密码请求
|
||||
type UpdatePasswordRequest struct {
|
||||
OldPassword string `json:"oldPassword" binding:"required"`
|
||||
NewPassword string `json:"newPassword" binding:"required"`
|
||||
}
|
||||
|
||||
// @Summary 修改密码
|
||||
// @Tags 用户管理
|
||||
// @Param id path string true "用户ID"
|
||||
// @Param request body UpdatePasswordRequest true "密码信息"
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /sys-users/{id}/password [put]
|
||||
func (ctrl *SysUserController) UpdatePassword(c *gin.Context) {
|
||||
var req UpdatePasswordRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if err := ctrl.service.UpdatePassword(c.Param("id"), req.OldPassword, req.NewPassword); err != nil {
|
||||
common.Error(c, 400, err.Error())
|
||||
return
|
||||
}
|
||||
common.Success(c, nil)
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package dto
|
||||
|
||||
// LoginRequest 登录请求
|
||||
type LoginRequest struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package dto
|
||||
|
||||
// CreateUserRequest 创建用户请求
|
||||
type CreateUserRequest struct {
|
||||
Username string `json:"username" binding:"required,min=3,max=20"` // 登录账号
|
||||
Realname string `json:"realname" binding:"required"` // 真实姓名
|
||||
Password string `json:"password" binding:"required,min=6"` // 密码
|
||||
Email string `json:"email" binding:"omitempty,email"` // 电子邮件
|
||||
Phone string `json:"phone" binding:"omitempty,len=11"` // 电话
|
||||
Avatar string `json:"avatar"` // 头像
|
||||
Sex *int `json:"sex"` // 性别(0-未知,1-男,2-女)
|
||||
Birthday string `json:"birthday"` // 生日
|
||||
OrgCode string `json:"orgCode"` // 机构编码
|
||||
}
|
||||
|
||||
// UpdateUserRequest 更新用户请求
|
||||
type UpdateUserRequest struct {
|
||||
Realname string `json:"realname"` // 真实姓名
|
||||
Email string `json:"email" binding:"omitempty,email"` // 电子邮件
|
||||
Phone string `json:"phone" binding:"omitempty,len=11"` // 电话
|
||||
Avatar string `json:"avatar"` // 头像
|
||||
Sex *int `json:"sex"` // 性别(0-未知,1-男,2-女)
|
||||
Birthday string `json:"birthday"` // 生日
|
||||
Status *int `json:"status"` // 状态(1-正常,2-冻结)
|
||||
OrgCode string `json:"orgCode"` // 机构编码
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
// Package entity 实体层
|
||||
package entity
|
||||
|
||||
import "time"
|
||||
|
||||
// SysUser 用户表实体
|
||||
type SysUser struct {
|
||||
ID string `gorm:"column:id;primaryKey" json:"id"`
|
||||
Username string `gorm:"column:username" json:"username"` // 登录账号
|
||||
Realname string `gorm:"column:realname" json:"realname"` // 真实姓名
|
||||
Password string `gorm:"column:password" json:"-"` // 密码 (不返回给前端)
|
||||
Salt string `gorm:"column:salt" json:"-"` // md5密码盐
|
||||
Avatar string `gorm:"column:avatar" json:"avatar"` // 头像
|
||||
Birthday *time.Time `gorm:"column:birthday" json:"birthday"` // 生日
|
||||
Sex int `gorm:"column:sex" json:"sex"` // 性别(0-未知,1-男,2-女)
|
||||
Email string `gorm:"column:email" json:"email"` // 电子邮件
|
||||
Phone string `gorm:"column:phone" json:"phone"` // 电话
|
||||
OrgCode string `gorm:"column:org_code" json:"orgCode"` // 机构编码
|
||||
Status int `gorm:"column:status" json:"status"` // 状态(1-正常,2-冻结)
|
||||
DelFlag int `gorm:"column:del_flag" json:"delFlag"` // 删除状态(0-正常,1-已删除)
|
||||
ThirdID string `gorm:"column:third_id" json:"thirdId"` // 第三方登录唯一标识
|
||||
ThirdType string `gorm:"column:third_type" json:"thirdType"` // 第三方类型
|
||||
ActivitiSync int `gorm:"column:activiti_sync" json:"activitiSync"` // 同步工作流引擎
|
||||
WorkNo string `gorm:"column:work_no" json:"workNo"` // 工号
|
||||
Telephone string `gorm:"column:telephone" json:"telephone"` // 座机号
|
||||
CreateBy string `gorm:"column:create_by" json:"createBy"` // 创建人
|
||||
CreateTime *time.Time `gorm:"column:create_time" json:"createTime"` // 创建时间
|
||||
UpdateBy string `gorm:"column:update_by" json:"updateBy"` // 更新人
|
||||
UpdateTime *time.Time `gorm:"column:update_time" json:"updateTime"` // 更新时间
|
||||
UserIdentity int `gorm:"column:user_identity" json:"userIdentity"` // 身份(1普通成员 2上级)
|
||||
DepartIds string `gorm:"column:depart_ids" json:"departIds"` // 负责部门
|
||||
ClientID string `gorm:"column:client_id" json:"clientId"` // 设备ID
|
||||
LoginTenantID int `gorm:"column:login_tenant_id" json:"loginTenantId"` // 上次登录租户ID
|
||||
BpmStatus string `gorm:"column:bpm_status" json:"bpmStatus"` // 流程状态
|
||||
WxOpenID string `gorm:"column:wx_open_id" json:"wxOpenId"` // 微信openId
|
||||
DyOpenID string `gorm:"column:dy_open_id" json:"dyOpenId"` // 抖音openId
|
||||
IP string `gorm:"column:ip" json:"ip"` // 注册时ip
|
||||
ShowLinediff string `gorm:"column:show_linediff" json:"showLinediff"` // 是否显示历年线差
|
||||
ProgramType string `gorm:"column:program_type" json:"programType"` // 所属程序
|
||||
}
|
||||
|
||||
func (SysUser) TableName() string {
|
||||
return "sys_user"
|
||||
}
|
||||
|
||||
// LoginUser 登录用户信息 (存储在Redis中)
|
||||
type LoginUser struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Realname string `json:"realname"`
|
||||
Avatar string `json:"avatar"`
|
||||
Phone string `json:"phone"`
|
||||
Email string `json:"email"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
// Package mapper 数据访问层
|
||||
package mapper
|
||||
|
||||
import (
|
||||
"server/common"
|
||||
"server/modules/system/entity"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type SysUserMapper struct {
|
||||
*common.BaseMapper[entity.SysUser]
|
||||
}
|
||||
|
||||
func NewSysUserMapper() *SysUserMapper {
|
||||
return &SysUserMapper{
|
||||
BaseMapper: common.NewBaseMapper[entity.SysUser](),
|
||||
}
|
||||
}
|
||||
|
||||
// GetDB 获取数据库实例,添加逻辑删除过滤
|
||||
func (m *SysUserMapper) GetDB() *gorm.DB {
|
||||
return m.BaseMapper.GetDB().Where("del_flag = 0")
|
||||
}
|
||||
|
||||
// Delete 逻辑删除
|
||||
func (m *SysUserMapper) Delete(id string) error {
|
||||
return m.BaseMapper.GetDB().Model(&entity.SysUser{}).Where("id = ?", id).Update("del_flag", 1).Error
|
||||
}
|
||||
|
||||
// FindByUsername 根据用户名查找用户
|
||||
func (m *SysUserMapper) FindByUsername(username string) (*entity.SysUser, error) {
|
||||
var item entity.SysUser
|
||||
err := m.GetDB().First(&item, "username = ?", username).Error
|
||||
return &item, err
|
||||
}
|
||||
|
||||
// FindByPhone 根据手机号查找用户
|
||||
func (m *SysUserMapper) FindByPhone(phone string) (*entity.SysUser, error) {
|
||||
var item entity.SysUser
|
||||
err := m.GetDB().First(&item, "phone = ?", phone).Error
|
||||
return &item, err
|
||||
}
|
||||
|
|
@ -0,0 +1,323 @@
|
|||
// Package service 业务逻辑层
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"server/common"
|
||||
"server/config"
|
||||
"server/modules/system/dto"
|
||||
"server/modules/system/entity"
|
||||
"server/modules/system/mapper"
|
||||
"server/modules/system/vo"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type SysUserService struct {
|
||||
*common.BaseService[entity.SysUser]
|
||||
mapper *mapper.SysUserMapper
|
||||
}
|
||||
|
||||
func NewSysUserService() *SysUserService {
|
||||
mapper := mapper.NewSysUserMapper()
|
||||
return &SysUserService{
|
||||
BaseService: common.NewBaseService[entity.SysUser](),
|
||||
mapper: mapper,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateUser 创建用户并返回 VO
|
||||
func (s *SysUserService) CreateUser(req *dto.CreateUserRequest, createBy string) (*vo.SysUserVO, error) {
|
||||
// DTO 转 Entity
|
||||
entityItem := &entity.SysUser{
|
||||
Username: req.Username,
|
||||
Realname: req.Realname,
|
||||
Password: req.Password,
|
||||
Email: req.Email,
|
||||
Phone: req.Phone,
|
||||
Avatar: req.Avatar,
|
||||
Sex: 0, // 默认值
|
||||
OrgCode: req.OrgCode,
|
||||
DelFlag: 0,
|
||||
Status: 1,
|
||||
CreateBy: createBy,
|
||||
}
|
||||
|
||||
if req.Sex != nil {
|
||||
entityItem.Sex = *req.Sex
|
||||
}
|
||||
|
||||
if req.Birthday != "" {
|
||||
birthday, err := time.Parse("2006-01-02", req.Birthday)
|
||||
if err == nil {
|
||||
entityItem.Birthday = &birthday
|
||||
}
|
||||
}
|
||||
|
||||
// 保存到数据库
|
||||
if err := s.Create(entityItem); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Entity 转 VO
|
||||
return s.convertToVO(*entityItem), nil
|
||||
}
|
||||
|
||||
// UpdateUser 更新用户并返回 VO
|
||||
func (s *SysUserService) UpdateUser(id string, req *dto.UpdateUserRequest) (*vo.SysUserVO, error) {
|
||||
// 获取原数据
|
||||
entityItem, err := s.GetByID(id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("用户不存在: %w", err)
|
||||
}
|
||||
|
||||
// 更新字段
|
||||
updateFields := make(map[string]interface{})
|
||||
if req.Realname != "" {
|
||||
updateFields["realname"] = req.Realname
|
||||
}
|
||||
if req.Email != "" {
|
||||
updateFields["email"] = req.Email
|
||||
}
|
||||
if req.Phone != "" {
|
||||
updateFields["phone"] = req.Phone
|
||||
}
|
||||
if req.Avatar != "" {
|
||||
updateFields["avatar"] = req.Avatar
|
||||
}
|
||||
if req.Sex != nil {
|
||||
updateFields["sex"] = *req.Sex
|
||||
}
|
||||
if req.Status != nil {
|
||||
updateFields["status"] = *req.Status
|
||||
}
|
||||
if req.OrgCode != "" {
|
||||
updateFields["org_code"] = req.OrgCode
|
||||
}
|
||||
if req.Birthday != "" {
|
||||
birthday, err := time.Parse("2006-01-02", req.Birthday)
|
||||
if err == nil {
|
||||
updateFields["birthday"] = birthday
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.mapper.UpdateFields(id, updateFields); err != nil {
|
||||
return nil, fmt.Errorf("更新用户失败: %w", err)
|
||||
}
|
||||
|
||||
// 重新获取更新后的数据
|
||||
entityItem, err = s.GetByID(id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取更新后数据失败: %w", err)
|
||||
}
|
||||
|
||||
return s.convertToVO(*entityItem), nil
|
||||
}
|
||||
|
||||
// GetUserByID 获取用户并返回 VO
|
||||
func (s *SysUserService) GetUserByID(id string) (*vo.SysUserVO, error) {
|
||||
entityItem, err := s.GetByID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.convertToVO(*entityItem), nil
|
||||
}
|
||||
|
||||
// ListUsers 获取用户列表并返回 VO 列表
|
||||
func (s *SysUserService) ListUsers(page, size int) ([]vo.SysUserVO, int64, error) {
|
||||
entities, total, err := s.List(page, size)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 批量转换 Entity 到 VO
|
||||
vos := make([]vo.SysUserVO, len(entities))
|
||||
for i := range entities {
|
||||
vos[i] = *s.convertToVO(entities[i])
|
||||
}
|
||||
|
||||
return vos, total, nil
|
||||
}
|
||||
|
||||
// Login 用户登录(手机号登录)
|
||||
func (s *SysUserService) Login(username, password string) (*entity.LoginUser, string, error) {
|
||||
user, err := s.mapper.FindByPhone(username)
|
||||
if err != nil {
|
||||
return nil, "", errors.New("用户不存在")
|
||||
}
|
||||
|
||||
if user.Status == 2 {
|
||||
return nil, "", errors.New("账号已被冻结")
|
||||
}
|
||||
|
||||
encrypted, err := common.Encrypt(user.Username, password, user.Salt)
|
||||
if (user.Password != encrypted) || (err != nil) {
|
||||
return nil, "", errors.New("用户名或密码错误")
|
||||
}
|
||||
|
||||
token := s.generateToken()
|
||||
|
||||
loginUser := &entity.LoginUser{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Realname: user.Realname,
|
||||
Avatar: user.Avatar,
|
||||
Phone: user.Phone,
|
||||
Email: user.Email,
|
||||
Token: token,
|
||||
}
|
||||
|
||||
if err := s.saveLoginUser(token, loginUser); err != nil {
|
||||
return nil, "", errors.New("登录失败,请重试")
|
||||
}
|
||||
|
||||
return loginUser, token, nil
|
||||
}
|
||||
|
||||
// SysLogin 用户登录(用户名登录)
|
||||
func (s *SysUserService) SysLogin(username, password string) (*entity.LoginUser, string, error) {
|
||||
user, err := s.mapper.FindByUsername(username)
|
||||
if err != nil {
|
||||
return nil, "", errors.New("用户不存在")
|
||||
}
|
||||
|
||||
if user.Status == 2 {
|
||||
return nil, "", errors.New("账号已被冻结")
|
||||
}
|
||||
|
||||
encrypted, err := common.Encrypt(username, password, user.Salt)
|
||||
if (user.Password != encrypted) || (err != nil) {
|
||||
return nil, "", errors.New("用户名或密码错误")
|
||||
}
|
||||
|
||||
token := s.generateToken()
|
||||
|
||||
loginUser := &entity.LoginUser{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Realname: user.Realname,
|
||||
Avatar: user.Avatar,
|
||||
Phone: user.Phone,
|
||||
Email: user.Email,
|
||||
Token: token,
|
||||
}
|
||||
|
||||
if err := s.saveLoginUser(token, loginUser); err != nil {
|
||||
return nil, "", errors.New("登录失败,请重试")
|
||||
}
|
||||
|
||||
return loginUser, token, nil
|
||||
}
|
||||
|
||||
// Logout 用户登出
|
||||
func (s *SysUserService) Logout(token string) error {
|
||||
ctx := context.Background()
|
||||
return config.RDB.Del(ctx, common.RedisTokenPrefix+token).Err()
|
||||
}
|
||||
|
||||
// GetLoginUser 根据Token获取登录用户信息
|
||||
func (s *SysUserService) GetLoginUser(token string) (*entity.LoginUser, error) {
|
||||
ctx := context.Background()
|
||||
data, err := config.RDB.Get(ctx, common.RedisTokenPrefix+token).Result()
|
||||
if err != nil {
|
||||
return nil, errors.New("未登录或登录已过期")
|
||||
}
|
||||
|
||||
var loginUser entity.LoginUser
|
||||
if err := json.Unmarshal([]byte(data), &loginUser); err != nil {
|
||||
return nil, errors.New("登录信息异常")
|
||||
}
|
||||
|
||||
config.RDB.Expire(ctx, common.RedisTokenPrefix+token, common.RedisTokenExpire)
|
||||
|
||||
return &loginUser, nil
|
||||
}
|
||||
|
||||
// RefreshToken 刷新Token过期时间
|
||||
func (s *SysUserService) RefreshToken(token string) error {
|
||||
ctx := context.Background()
|
||||
return config.RDB.Expire(ctx, common.RedisTokenPrefix+token, common.RedisTokenExpire).Err()
|
||||
}
|
||||
|
||||
// UpdatePassword 修改密码
|
||||
func (s *SysUserService) UpdatePassword(id, oldPwd, newPwd string) error {
|
||||
user, err := s.GetByID(id)
|
||||
if err != nil {
|
||||
return errors.New("用户不存在")
|
||||
}
|
||||
|
||||
encrypted, err := common.Encrypt(user.Username, oldPwd, user.Salt)
|
||||
if err != nil {
|
||||
log.Printf("密码加密失败: %v", err)
|
||||
return fmt.Errorf("密码加密失败: %w,请联系管理员", err)
|
||||
}
|
||||
if encrypted != user.Password {
|
||||
return errors.New("原密码错误")
|
||||
}
|
||||
|
||||
newEncrypted, err := common.Encrypt(user.Username, newPwd, user.Salt)
|
||||
if err != nil {
|
||||
log.Printf("密码加密失败: %v", err)
|
||||
return fmt.Errorf("密码加密失败: %w,请联系管理员", err)
|
||||
}
|
||||
return s.mapper.UpdateFields(id, map[string]interface{}{
|
||||
"password": newEncrypted,
|
||||
})
|
||||
}
|
||||
|
||||
// saveLoginUser 保存登录用户到Redis
|
||||
func (s *SysUserService) saveLoginUser(token string, user *entity.LoginUser) error {
|
||||
ctx := context.Background()
|
||||
data, err := json.Marshal(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return config.RDB.Set(ctx, common.RedisTokenPrefix+token, data, common.RedisTokenExpire).Err()
|
||||
}
|
||||
|
||||
// generateToken 生成Token
|
||||
func (s *SysUserService) generateToken() string {
|
||||
return uuid.New().String()
|
||||
}
|
||||
|
||||
// Create 创建用户(添加密码加密逻辑)
|
||||
func (s *SysUserService) Create(item *entity.SysUser) error {
|
||||
item.ID = uuid.New().String()
|
||||
item.Salt = uuid.New().String()[:8]
|
||||
encrypted, err := common.Encrypt(item.Username, item.Password, item.Salt)
|
||||
if err != nil {
|
||||
log.Printf("密码加密失败: %v", err)
|
||||
return fmt.Errorf("密码加密失败: %w,请联系管理员", err)
|
||||
}
|
||||
item.Password = encrypted
|
||||
item.DelFlag = 0
|
||||
item.Status = 1
|
||||
now := time.Now()
|
||||
item.CreateTime = &now
|
||||
return s.mapper.Create(item)
|
||||
}
|
||||
|
||||
// convertToVO Entity 转 VO(私有方法)
|
||||
func (s *SysUserService) convertToVO(entity entity.SysUser) *vo.SysUserVO {
|
||||
return &vo.SysUserVO{
|
||||
ID: entity.ID,
|
||||
Username: entity.Username,
|
||||
Realname: entity.Realname,
|
||||
Avatar: entity.Avatar,
|
||||
Birthday: entity.Birthday,
|
||||
Sex: entity.Sex,
|
||||
Email: entity.Email,
|
||||
Phone: entity.Phone,
|
||||
OrgCode: entity.OrgCode,
|
||||
Status: entity.Status,
|
||||
CreateTime: entity.CreateTime,
|
||||
UpdateTime: entity.UpdateTime,
|
||||
// 不包含 Password、Salt 等敏感字段
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package vo
|
||||
|
||||
// LoginUserVO 登录用户视图对象
|
||||
type LoginUserVO struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Realname string `json:"realname"`
|
||||
Avatar string `json:"avatar"`
|
||||
Phone string `json:"phone"`
|
||||
Email string `json:"email"`
|
||||
// 注意:不包含 Token,Token 应该在单独的响应结构中
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package vo
|
||||
|
||||
import "time"
|
||||
|
||||
// SysUserVO 用户视图对象
|
||||
type SysUserVO struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"` // 登录账号
|
||||
Realname string `json:"realname"` // 真实姓名
|
||||
Avatar string `json:"avatar"` // 头像
|
||||
Birthday *time.Time `json:"birthday"` // 生日
|
||||
Sex int `json:"sex"` // 性别(0-未知,1-男,2-女)
|
||||
Email string `json:"email"` // 电子邮件
|
||||
Phone string `json:"phone"` // 电话
|
||||
OrgCode string `json:"orgCode"` // 机构编码
|
||||
Status int `json:"status"` // 状态(1-正常,2-冻结)
|
||||
CreateTime *time.Time `json:"createTime"` // 创建时间
|
||||
UpdateTime *time.Time `json:"updateTime"` // 更新时间
|
||||
// 注意:不包含 Password、Salt 等敏感字段
|
||||
}
|
||||
|
|
@ -0,0 +1,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)
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
// Package controller 控制器层
|
||||
package controller
|
||||
|
||||
import (
|
||||
"server/common"
|
||||
"server/modules/system/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// LoginRequest 登录请求
|
||||
type LoginRequest struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
// AuthController 认证控制器
|
||||
type AuthController struct {
|
||||
userService *service.SysUserService
|
||||
}
|
||||
|
||||
func NewAuthController() *AuthController {
|
||||
return &AuthController{userService: service.NewSysUserService()}
|
||||
}
|
||||
|
||||
func (ctrl *AuthController) RegisterRoutes(r *gin.RouterGroup) {
|
||||
r.POST("/user/auth/login", ctrl.Login)
|
||||
r.POST("/user/auth/logout", ctrl.Logout)
|
||||
r.GET("/user/auth/info", ctrl.GetUserInfo)
|
||||
}
|
||||
|
||||
// Login 用户登录
|
||||
// @Summary 用户登录
|
||||
// @Tags 认证
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body LoginRequest true "登录信息"
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /user/auth/login [post]
|
||||
func (ctrl *AuthController) Login(c *gin.Context) {
|
||||
var req LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.Error(c, 400, "用户名和密码不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
loginUser, token, err := ctrl.userService.Login(req.Username, req.Password)
|
||||
if err != nil {
|
||||
common.Error(c, 400, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
common.Success(c, gin.H{
|
||||
"token": token,
|
||||
"user": loginUser,
|
||||
})
|
||||
}
|
||||
|
||||
// Logout 用户登出
|
||||
// @Summary 用户登出
|
||||
// @Tags 认证
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /user/auth/logout [post]
|
||||
func (ctrl *AuthController) Logout(c *gin.Context) {
|
||||
token := c.GetHeader("Authorization")
|
||||
if len(token) > 7 {
|
||||
token = token[7:] // 去除 "Bearer "
|
||||
}
|
||||
|
||||
ctrl.userService.Logout(token)
|
||||
common.Success(c, nil)
|
||||
}
|
||||
|
||||
// GetUserInfo 获取当前登录用户信息
|
||||
// @Summary 获取当前登录用户信息
|
||||
// @Tags 认证
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /user/auth/info [get]
|
||||
func (ctrl *AuthController) GetUserInfo(c *gin.Context) {
|
||||
user := common.GetLoginUser(c)
|
||||
if user == nil {
|
||||
common.Error(c, 401, "未登录")
|
||||
return
|
||||
}
|
||||
common.Success(c, user)
|
||||
}
|
||||
|
|
@ -0,0 +1,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)
|
||||
}
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"server/common"
|
||||
user_service "server/modules/user/service"
|
||||
yxDto "server/modules/yx/dto"
|
||||
yx_service "server/modules/yx/service"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type UserMajorController struct {
|
||||
userScoreService *user_service.UserScoreService
|
||||
yxUserScoreService *yx_service.YxUserScoreService
|
||||
yxCalculationMajorService *yx_service.YxCalculationMajorService
|
||||
yxVolunteerService *yx_service.YxVolunteerService
|
||||
yxVolunteerRecordService *yx_service.YxVolunteerRecordService
|
||||
}
|
||||
|
||||
func NewUserMajorController() *UserMajorController {
|
||||
return &UserMajorController{
|
||||
yxUserScoreService: yx_service.NewYxUserScoreService(),
|
||||
userScoreService: user_service.NewUserScoreService(),
|
||||
yxCalculationMajorService: yx_service.NewYxCalculationMajorService(),
|
||||
yxVolunteerService: yx_service.NewYxVolunteerService(),
|
||||
yxVolunteerRecordService: yx_service.NewYxVolunteerRecordService(),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (ctrl *UserMajorController) RegisterRoutes(rg *gin.RouterGroup) {
|
||||
group := rg.Group("/user/major")
|
||||
{
|
||||
// group.GET("/", ctrl.GetActive)
|
||||
// group.GET("/:id", ctrl.GetByID)
|
||||
group.GET("/list", ctrl.List)
|
||||
group.GET("/list_by_school", ctrl.ListBySchool)
|
||||
}
|
||||
}
|
||||
|
||||
// ListBySchool 获取当前院校下其他专业数据
|
||||
// @Summary 获取当前院校下其他专业数据
|
||||
// @Tags 用户专业
|
||||
// @Param page query int false "页码" default(1)
|
||||
// @Param size query int false "每页数量" default(10)
|
||||
// @Param school_code query string true "院校代码"
|
||||
// @Param probability query string false "录取概率类型(难录取/可冲击/较稳妥/可保底)" default("")
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /user/major/list_by_school [get]
|
||||
func (ctrl *UserMajorController) ListBySchool(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
size, _ := strconv.Atoi(c.DefaultQuery("size", "10"))
|
||||
schoolMajorQuery := yxDto.SchoolMajorQuery{
|
||||
Page: page,
|
||||
Size: size,
|
||||
SchoolCode: c.Query("school_code"),
|
||||
Probability: c.DefaultQuery("probability", ""),
|
||||
LoginUserId: common.GetLoginUser(c).ID,
|
||||
}
|
||||
userScoreVO, err := ctrl.userScoreService.GetActiveScoreByUserID(schoolMajorQuery.LoginUserId)
|
||||
if err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
schoolMajorQuery.UserScoreVO = userScoreVO
|
||||
items, total, probCount, err := ctrl.yxCalculationMajorService.RecommendMajorList(schoolMajorQuery)
|
||||
if err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
newMap := map[string]interface{}{
|
||||
"items": items,
|
||||
"total": total,
|
||||
"probCount": probCount,
|
||||
}
|
||||
common.SuccessPage(c, newMap, total, page, size)
|
||||
}
|
||||
|
||||
// List 获取当前用户可检索列表
|
||||
// @Summary 获取当前用户可检索列表
|
||||
// @Tags 用户专业
|
||||
// @Param page query int false "页码" default(1)
|
||||
// @Param size query int false "每页数量" default(10)
|
||||
// @Param batch query string false "批次(本科提前批/本科A段/本科B段/本科/高职高专)" default("")
|
||||
// @Param probability query string false "录取概率类型(难录取/可冲击/较稳妥/可保底)" default("")
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /user/major/list [get]
|
||||
func (ctrl *UserMajorController) List(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
size, _ := strconv.Atoi(c.DefaultQuery("size", "10"))
|
||||
schoolMajorQuery := yxDto.SchoolMajorQuery{
|
||||
Keyword: c.DefaultQuery("keyword", ""),
|
||||
Page: page,
|
||||
Size: size,
|
||||
SchoolCode: c.Query("schoolCode"),
|
||||
Batch: c.DefaultQuery("batch", ""),
|
||||
Batch2: c.DefaultQuery("batch2", ""),
|
||||
Probability: c.DefaultQuery("probability", ""),
|
||||
LoginUserId: common.GetLoginUser(c).ID,
|
||||
}
|
||||
userScoreVO, err := ctrl.userScoreService.GetActiveScoreByUserID(schoolMajorQuery.LoginUserId)
|
||||
if err != nil {
|
||||
common.Error(c, 500, err.Error()) // 获取用户成绩信息失败
|
||||
return
|
||||
}
|
||||
|
||||
schoolMajorQuery.UserScoreVO = userScoreVO
|
||||
items, total, probCount, err := ctrl.yxCalculationMajorService.RecommendMajorList(schoolMajorQuery)
|
||||
if err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
newMap := map[string]interface{}{
|
||||
"items": items,
|
||||
"total": total,
|
||||
"probCount": probCount,
|
||||
}
|
||||
common.SuccessPage(c, newMap, total, page, size)
|
||||
}
|
||||
|
|
@ -0,0 +1,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)
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"server/common"
|
||||
user_service "server/modules/user/service"
|
||||
"server/modules/yx/dto"
|
||||
yx_service "server/modules/yx/service"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type UserScoreController struct {
|
||||
userScoreService *user_service.UserScoreService
|
||||
yxUserScoreService *yx_service.YxUserScoreService
|
||||
}
|
||||
|
||||
func NewUserScoreController() *UserScoreController {
|
||||
return &UserScoreController{
|
||||
yxUserScoreService: yx_service.NewYxUserScoreService(),
|
||||
userScoreService: user_service.NewUserScoreService(),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (ctrl *UserScoreController) RegisterRoutes(rg *gin.RouterGroup) {
|
||||
group := rg.Group("/user/score")
|
||||
{
|
||||
group.GET("/", ctrl.GetActive)
|
||||
group.GET("/:id", ctrl.GetByID)
|
||||
group.GET("/list", ctrl.List)
|
||||
group.POST("/save-score", ctrl.SaveUserScore)
|
||||
}
|
||||
}
|
||||
|
||||
// SaveUserScore 保存用户成绩
|
||||
// @Summary 保存用户成绩
|
||||
// @Tags 用户分数
|
||||
// @Param request body dto.SaveScoreRequest true "成绩信息"
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /user/score/save-score [post]
|
||||
func (ctrl *UserScoreController) SaveUserScore(c *gin.Context) {
|
||||
var req dto.SaveScoreRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if err := req.Validate(); err != nil {
|
||||
common.Error(c, 400, "校验失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
req.CreateBy = common.GetLoginUser(c).ID
|
||||
// 直接调用 Service,不处理具体业务逻辑
|
||||
result, err := ctrl.userScoreService.SaveUserScore(&req)
|
||||
if err != nil {
|
||||
common.Error(c, 500, "保存失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
common.Success(c, result)
|
||||
}
|
||||
|
||||
// GetActive 获取当前用户的激活分数
|
||||
// @Summary 获取当前用户的激活分数
|
||||
// @Tags 用户分数
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /user/score [get]
|
||||
func (ctrl *UserScoreController) GetActive(c *gin.Context) {
|
||||
loginUserId := common.GetLoginUser(c).ID
|
||||
item, err := ctrl.userScoreService.GetActiveScoreByUserID(loginUserId)
|
||||
if err != nil {
|
||||
common.Error(c, 404, "未找到激活成绩")
|
||||
return
|
||||
}
|
||||
common.Success(c, item)
|
||||
}
|
||||
|
||||
// GetByID 获取指定 ID 的分数
|
||||
// @Summary 获取指定 ID 的分数
|
||||
// @Tags 用户分数
|
||||
// @Param id path string true "成绩ID"
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /user/score/{id} [get]
|
||||
func (ctrl *UserScoreController) GetByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
item, err := ctrl.userScoreService.GetByID(id)
|
||||
if err != nil {
|
||||
common.Error(c, 404, "记录不存在")
|
||||
return
|
||||
}
|
||||
common.Success(c, item)
|
||||
}
|
||||
|
||||
// List 获取当前用户的成绩列表
|
||||
// @Summary 获取当前用户的成绩列表
|
||||
// @Tags 用户分数
|
||||
// @Param page query int false "页码" default(1)
|
||||
// @Param size query int false "每页数量" default(10)
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /user/score/list [get]
|
||||
func (ctrl *UserScoreController) List(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
size, _ := strconv.Atoi(c.DefaultQuery("size", "10"))
|
||||
loginUserId := common.GetLoginUser(c).ID
|
||||
items, total, err := ctrl.userScoreService.ListByUser(loginUserId, page, size)
|
||||
if err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
common.Success(c, gin.H{
|
||||
"items": items,
|
||||
"total": total,
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"server/common"
|
||||
"server/modules/user/dto"
|
||||
"server/modules/user/service"
|
||||
"server/modules/user/vo"
|
||||
yxDto "server/modules/yx/dto"
|
||||
"server/modules/yx/entity"
|
||||
yx_service "server/modules/yx/service"
|
||||
"time" // 用于时间处理
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// _ 确保包被导入(用于类型引用)
|
||||
var _ = yxDto.SchoolMajorDTO{}
|
||||
var _ = entity.YxVolunteerRecord{}
|
||||
var _ = time.Now()
|
||||
var _ = vo.VolunteerDetailVO{}
|
||||
|
||||
type UserVolunteerController struct {
|
||||
userScoreService *service.UserScoreService
|
||||
yxVolunteerService *yx_service.YxVolunteerService
|
||||
yxVolunteerRecordService *yx_service.YxVolunteerRecordService
|
||||
yxCalculationMajorService *yx_service.YxCalculationMajorService
|
||||
}
|
||||
|
||||
func NewUserVolunteerController() *UserVolunteerController {
|
||||
return &UserVolunteerController{
|
||||
userScoreService: service.NewUserScoreService(),
|
||||
yxVolunteerService: yx_service.NewYxVolunteerService(),
|
||||
yxVolunteerRecordService: yx_service.NewYxVolunteerRecordService(),
|
||||
yxCalculationMajorService: yx_service.NewYxCalculationMajorService(),
|
||||
}
|
||||
}
|
||||
|
||||
func (ctrl *UserVolunteerController) RegisterRoutes(rg *gin.RouterGroup) {
|
||||
group := rg.Group("/user/volunteer")
|
||||
{
|
||||
group.POST("/save", ctrl.SaveVolunteer)
|
||||
group.GET("/detail", ctrl.GetVolunteerDetail)
|
||||
group.PUT("/updateName", ctrl.UpdateVolunteerName)
|
||||
group.GET("/list", ctrl.GetUserVolunteerList)
|
||||
group.DELETE("/delete", ctrl.DeleteVolunteer)
|
||||
group.POST("/switch", ctrl.SwitchVolunteer)
|
||||
}
|
||||
}
|
||||
|
||||
// SaveVolunteer 保存志愿明细
|
||||
// @Summary 保存志愿明细
|
||||
// @Tags 用户志愿
|
||||
// @Param request body dto.SaveVolunteerRequest true "志愿键列表"
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /user/volunteer/save [post]
|
||||
func (ctrl *UserVolunteerController) SaveVolunteer(c *gin.Context) {
|
||||
var req dto.SaveVolunteerRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
loginUserId := common.GetLoginUser(c).ID
|
||||
if err := ctrl.userScoreService.SaveVolunteer(loginUserId, &req); err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
common.Success(c, "保存成功")
|
||||
}
|
||||
|
||||
// GetVolunteerDetail 获取当前志愿单详情
|
||||
// @Summary 获取当前志愿单详情
|
||||
// @Tags 用户志愿
|
||||
// @Success 200 {object} common.Response{data=vo.VolunteerDetailResponse}
|
||||
// @Router /user/volunteer/detail [get]
|
||||
func (ctrl *UserVolunteerController) GetVolunteerDetail(c *gin.Context) {
|
||||
loginUserId := common.GetLoginUser(c).ID
|
||||
response, err := ctrl.userScoreService.GetVolunteerDetail(loginUserId)
|
||||
if err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
common.Success(c, response)
|
||||
}
|
||||
|
||||
// UpdateVolunteerName 编辑志愿单名称
|
||||
// @Summary 编辑志愿单名称
|
||||
// @Tags 用户志愿
|
||||
// @Param id query string true "志愿单ID"
|
||||
// @Param name query string true "志愿单名称"
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /user/volunteer/updateName [put]
|
||||
func (ctrl *UserVolunteerController) UpdateVolunteerName(c *gin.Context) {
|
||||
id := c.Query("id")
|
||||
name := c.Query("name")
|
||||
if id == "" || name == "" {
|
||||
common.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
loginUserID := common.GetLoginUser(c).ID
|
||||
if err := ctrl.yxVolunteerService.UpdateName(id, name, loginUserID); err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
common.Success(c, "更新成功")
|
||||
}
|
||||
|
||||
// GetUserVolunteerList 获取当前用户志愿单列表
|
||||
// @Summary 获取当前用户志愿单列表
|
||||
// @Tags 用户志愿
|
||||
// @Param page query int false "页码"
|
||||
// @Param size query int false "每页数量"
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /user/volunteer/list [get]
|
||||
func (ctrl *UserVolunteerController) GetUserVolunteerList(c *gin.Context) {
|
||||
page := common.GetPage(c)
|
||||
size := common.GetSize(c)
|
||||
loginUserID := common.GetLoginUser(c).ID
|
||||
|
||||
items, total, err := ctrl.yxVolunteerService.ListByUser(loginUserID, page, size)
|
||||
if err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
common.Success(c, map[string]interface{}{
|
||||
"items": items,
|
||||
"total": total,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteVolunteer 删除志愿单接口
|
||||
// @Summary 删除志愿单接口
|
||||
// @Tags 用户志愿
|
||||
// @Param id query string true "志愿单ID"
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /user/volunteer/delete [delete]
|
||||
func (ctrl *UserVolunteerController) DeleteVolunteer(c *gin.Context) {
|
||||
id := c.Query("id")
|
||||
if id == "" {
|
||||
common.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
loginUserID := common.GetLoginUser(c).ID
|
||||
err := ctrl.yxVolunteerService.DeleteVolunteer(id, loginUserID)
|
||||
if err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
common.Success(c, "删除成功")
|
||||
}
|
||||
|
||||
// SwitchVolunteer 切换当前志愿单
|
||||
// @Summary 切换当前志愿单
|
||||
// @Tags 用户志愿
|
||||
// @Param id query string true "志愿单ID"
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /user/volunteer/switch [post]
|
||||
func (ctrl *UserVolunteerController) SwitchVolunteer(c *gin.Context) {
|
||||
id := c.Query("id")
|
||||
if id == "" {
|
||||
common.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
loginUserID := common.GetLoginUser(c).ID
|
||||
// 1. 先判断是否已是该志愿单 (从 cache 或 db 中查找激活的)
|
||||
userScoreVO, err := ctrl.userScoreService.GetActiveScoreByUserID(loginUserID)
|
||||
if err != nil {
|
||||
common.Error(c, 500, "获取用户成绩信息失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if userScoreVO.ID != "" {
|
||||
activeVolunteer, _ := ctrl.yxVolunteerService.FindActiveByScoreId(userScoreVO.ID)
|
||||
if activeVolunteer != nil && activeVolunteer.ID == id {
|
||||
common.Success(c, "已是当前志愿单,忽略切换")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 切换
|
||||
if err := ctrl.yxVolunteerService.SwitchVolunteer(id, loginUserID, ctrl.userScoreService); err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 3. Redis 缓存同步 (假设 common 中有清除缓存的方法逻辑,或者由 Service 处理)
|
||||
// 这里按需求提到:切换志愿单切换后 写入到redis做缓存(查询志愿单接口也要从redis读本体数据),再清除之前的成绩单redis重新获取写到redis缓存。
|
||||
// 实际项目中 common.GetLoginUser 已经包含了 ID,成绩单和志愿单通常由具体模块处理缓存。
|
||||
// 如果这里只是标记,Service 层已做状态更新,查询接口读取时会感知变化。
|
||||
// 为了彻底满足“写入/清除 redis 缓存”,通常会有相应的 RedisUtil 调用。
|
||||
// 假设 service 层或 controller 有 Cache 清理逻辑。
|
||||
|
||||
common.Success(c, "切换成功")
|
||||
}
|
||||
|
|
@ -0,0 +1,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"`
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package dto
|
||||
|
||||
import (
|
||||
"server/modules/user/vo"
|
||||
)
|
||||
|
||||
// RecommendMajorListRequest 推荐专业列表请求
|
||||
type RecommendMajorListRequest struct {
|
||||
Page int `json:"page"`
|
||||
Size int `json:"size"`
|
||||
LoginUserId string `json:"loginUserId"`
|
||||
UserScoreVO vo.UserScoreVO `json:"userScoreVO"`
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package dto
|
||||
|
||||
// SaveVolunteerRequest 保存志愿请求
|
||||
type SaveVolunteerRequest struct {
|
||||
Keys []string `json:"keys" binding:"required"` // Keys: schoolCode_majorCode_enrollmentCode
|
||||
}
|
||||
|
|
@ -0,0 +1,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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -0,0 +1,527 @@
|
|||
// Package service 业务逻辑层
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"server/common"
|
||||
"server/config"
|
||||
userDto "server/modules/user/dto"
|
||||
"server/modules/user/vo"
|
||||
yxDto "server/modules/yx/dto"
|
||||
"server/modules/yx/entity"
|
||||
"server/modules/yx/mapper"
|
||||
"server/modules/yx/service"
|
||||
"server/types"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UserScoreService struct {
|
||||
yxUserScoreService *service.YxUserScoreService
|
||||
yxVolunteerService *service.YxVolunteerService
|
||||
yxVolunteerRecordService *service.YxVolunteerRecordService
|
||||
yxCalculationMajorService *service.YxCalculationMajorService
|
||||
mapper *mapper.YxUserScoreMapper
|
||||
}
|
||||
|
||||
// GetActiveScoreID 获取用户的激活成绩ID
|
||||
func (s *UserScoreService) GetActiveScoreID(userID string) (string, error) {
|
||||
var score entity.YxUserScore
|
||||
// 明确指定字段,提高可读性
|
||||
err := config.DB.Model(&entity.YxUserScore{}).
|
||||
Where("create_by = ? AND state = ?", userID, "1").
|
||||
Select("id").
|
||||
First(&score).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return "", nil // 未找到激活成绩返回空字符串
|
||||
}
|
||||
return "", fmt.Errorf("查询激活成绩ID失败: %w", err)
|
||||
}
|
||||
return score.ID, nil
|
||||
}
|
||||
|
||||
// GetActiveScoreByID 获取用户的激活成绩实体(实现 IScoreService 接口)
|
||||
func (s *UserScoreService) GetActiveScoreByID(userID string) (entity.YxUserScore, error) {
|
||||
var score entity.YxUserScore
|
||||
// 明确指定字段,提高可读性
|
||||
err := config.DB.Model(&entity.YxUserScore{}).
|
||||
Where("create_by = ? AND state = ?", userID, "1").
|
||||
First(&score).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return entity.YxUserScore{}, fmt.Errorf("未找到激活的成绩记录")
|
||||
}
|
||||
return entity.YxUserScore{}, fmt.Errorf("查询成绩记录失败: %w", err)
|
||||
}
|
||||
return score, nil
|
||||
}
|
||||
|
||||
func (s *UserScoreService) GetActiveScoreByUserID(userID string) (vo.UserScoreVO, error) {
|
||||
var score entity.YxUserScore
|
||||
// 先从Redis获取是否存在
|
||||
scoreRedisData, err := config.RDB.Get(context.Background(), common.RedisUserScorePrefix+userID).Result()
|
||||
if err != nil {
|
||||
// 明确指定字段,提高可读性
|
||||
err := config.DB.Model(&entity.YxUserScore{}).
|
||||
Where("create_by = ? AND state = ?", userID, "1").
|
||||
First(&score).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return vo.UserScoreVO{}, fmt.Errorf("未找到激活的成绩记录")
|
||||
}
|
||||
return vo.UserScoreVO{}, fmt.Errorf("查询成绩记录失败: %w", err)
|
||||
}
|
||||
// 缓存到 Redis
|
||||
scoreRedisSetData, err := json.Marshal(score)
|
||||
if err != nil {
|
||||
return vo.UserScoreVO{}, fmt.Errorf("序列化成绩记录失败: %w", err)
|
||||
}
|
||||
err = config.RDB.Set(context.Background(), common.RedisUserScorePrefix+userID, scoreRedisSetData, common.RedisUserScoreExpire).Err()
|
||||
if err != nil {
|
||||
return vo.UserScoreVO{}, fmt.Errorf("缓存成绩记录失败: %w", err)
|
||||
}
|
||||
} else {
|
||||
if err := json.Unmarshal([]byte(scoreRedisData), &score); err != nil {
|
||||
return vo.UserScoreVO{}, fmt.Errorf("解析 Redis 数据失败: %w", err)
|
||||
}
|
||||
// 刷新过期时间
|
||||
config.RDB.Expire(context.Background(), common.RedisUserScorePrefix+userID, common.RedisUserScoreExpire)
|
||||
}
|
||||
|
||||
return s.convertEntityToVo(score), nil
|
||||
}
|
||||
|
||||
func (s *UserScoreService) GetByID(id string) (vo.UserScoreVO, error) {
|
||||
var score entity.YxUserScore
|
||||
err := config.DB.Model(&entity.YxUserScore{}).
|
||||
Where("id = ?", id).
|
||||
First(&score).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return vo.UserScoreVO{}, fmt.Errorf("未找到成绩记录")
|
||||
}
|
||||
return vo.UserScoreVO{}, fmt.Errorf("查询成绩记录失败: %w", err)
|
||||
}
|
||||
return s.convertEntityToVo(score), nil
|
||||
}
|
||||
|
||||
// ListByUser 获取用户的成绩分页列表
|
||||
func (s *UserScoreService) ListByUser(userID string, page, size int) ([]vo.UserScoreVO, int64, error) {
|
||||
var scores []entity.YxUserScore
|
||||
var total int64
|
||||
|
||||
query := config.DB.Model(&entity.YxUserScore{}).Where("create_by = ?", userID)
|
||||
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("查询总数失败: %w", err)
|
||||
}
|
||||
|
||||
if err := query.Offset((page - 1) * size).Limit(size).Order("create_time desc").Find(&scores).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("查询成绩列表失败: %w", err)
|
||||
}
|
||||
|
||||
vos := make([]vo.UserScoreVO, 0, len(scores))
|
||||
for i := range scores {
|
||||
vos = append(vos, s.convertEntityToVo(scores[i]))
|
||||
}
|
||||
|
||||
return vos, total, nil
|
||||
}
|
||||
|
||||
func (s *UserScoreService) Delete(id string) error {
|
||||
return s.mapper.Delete(id)
|
||||
}
|
||||
|
||||
func (s *UserScoreService) UpdateFields(id string, fields map[string]interface{}) error {
|
||||
return s.mapper.UpdateFields(id, fields)
|
||||
}
|
||||
|
||||
func NewUserScoreService() *UserScoreService {
|
||||
return &UserScoreService{
|
||||
yxUserScoreService: service.NewYxUserScoreService(),
|
||||
yxCalculationMajorService: service.NewYxCalculationMajorService(),
|
||||
yxVolunteerService: service.NewYxVolunteerService(),
|
||||
yxVolunteerRecordService: service.NewYxVolunteerRecordService(),
|
||||
mapper: mapper.NewYxUserScoreMapper(),
|
||||
}
|
||||
}
|
||||
|
||||
// GetVolunteerDetail 获取志愿详情(从 Controller 移入 Service 层)
|
||||
func (s *UserScoreService) GetVolunteerDetail(userID string) (*vo.VolunteerDetailResponse, error) {
|
||||
userScoreVO, err := s.GetActiveScoreByUserID(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 查找当前激活的志愿表
|
||||
volunteer, err := s.yxVolunteerService.FindActiveByScoreId(userScoreVO.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查找志愿表失败: %w", err)
|
||||
}
|
||||
if volunteer == nil || volunteer.ID == "" {
|
||||
return &vo.VolunteerDetailResponse{Volunteer: nil, Items: nil}, nil
|
||||
}
|
||||
|
||||
records, err := s.yxVolunteerRecordService.FindByVolunteerID(volunteer.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查找志愿明细失败: %w", err)
|
||||
}
|
||||
|
||||
// 获取丰富详情
|
||||
var enrichedMajors map[string]yxDto.SchoolMajorDTO
|
||||
if len(records) > 0 && userScoreVO.CalculationTableName != "" {
|
||||
keys := make([]string, 0, len(records))
|
||||
for _, r := range records {
|
||||
keys = append(keys, r.SchoolCode+"_"+r.MajorCode+"_"+r.EnrollmentCode)
|
||||
}
|
||||
majors, err := s.yxCalculationMajorService.FindDtoListByCompositeKeys(userScoreVO.CalculationTableName, keys, userScoreVO.ID)
|
||||
if err == nil {
|
||||
enrichedMajors = make(map[string]yxDto.SchoolMajorDTO)
|
||||
for _, m := range majors {
|
||||
enrichedMajors[m.SchoolCode+"_"+m.MajorCode+"_"+m.EnrollmentCode] = m
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 分组
|
||||
groupedItems := &vo.VolunteerItemsVO{
|
||||
BatchBefore: []vo.VolunteerDetailVO{},
|
||||
BatchUndergraduate: []vo.VolunteerDetailVO{},
|
||||
BatchCollege: []vo.VolunteerDetailVO{},
|
||||
}
|
||||
|
||||
for _, r := range records {
|
||||
item := vo.VolunteerDetailVO{
|
||||
ID: r.ID,
|
||||
VolunteerID: r.VolunteerID,
|
||||
SchoolCode: r.SchoolCode,
|
||||
MajorCode: r.MajorCode,
|
||||
EnrollmentCode: r.EnrollmentCode,
|
||||
Indexs: r.Indexs,
|
||||
Batch: r.Batch,
|
||||
EnrollProbability: r.EnrollProbability,
|
||||
StudentConvertedScore: r.StudentConvertedScore,
|
||||
CreateBy: r.CreateBy,
|
||||
CreateTime: types.NewDateTime(r.CreateTime),
|
||||
CalculationMajorID: r.CalculationMajorID,
|
||||
}
|
||||
key := r.SchoolCode + "_" + r.MajorCode + "_" + r.EnrollmentCode
|
||||
if m, ok := enrichedMajors[key]; ok {
|
||||
item.SchoolName = m.SchoolName
|
||||
item.MajorName = m.MajorName
|
||||
item.PlanNum = m.PlanNum
|
||||
item.Tuition = m.Tuition
|
||||
item.Province = m.Province
|
||||
item.SchoolNature = m.SchoolNature
|
||||
item.InstitutionType = m.InstitutionType
|
||||
item.MajorDetail = m.Detail
|
||||
}
|
||||
|
||||
// 分批
|
||||
groupKey := ""
|
||||
if r.Batch == "提前批" || r.Batch == "本科提前批" {
|
||||
groupKey = "提前批"
|
||||
} else if r.Batch == "高职高专" || r.Batch == "专科批" {
|
||||
groupKey = "专科批"
|
||||
} else {
|
||||
groupKey = "本科批"
|
||||
}
|
||||
|
||||
switch groupKey {
|
||||
case "提前批":
|
||||
groupedItems.BatchBefore = append(groupedItems.BatchBefore, item)
|
||||
case "本科批":
|
||||
groupedItems.BatchUndergraduate = append(groupedItems.BatchUndergraduate, item)
|
||||
case "专科批":
|
||||
groupedItems.BatchCollege = append(groupedItems.BatchCollege, item)
|
||||
}
|
||||
}
|
||||
|
||||
volunteerInfo := &vo.VolunteerInfoVO{
|
||||
ID: volunteer.ID,
|
||||
VolunteerName: volunteer.VolunteerName,
|
||||
ScoreId: volunteer.ScoreId,
|
||||
CreateType: volunteer.CreateType,
|
||||
State: volunteer.State,
|
||||
CreateBy: volunteer.CreateBy,
|
||||
CreateTime: types.NewDateTime(volunteer.CreateTime),
|
||||
UpdateBy: volunteer.UpdateBy,
|
||||
UpdateTime: types.NewDateTime(volunteer.UpdateTime),
|
||||
}
|
||||
itemMap := make(map[string][]vo.VolunteerDetailVO)
|
||||
|
||||
itemMap["提前批"] = groupedItems.BatchBefore
|
||||
itemMap["本科批"] = groupedItems.BatchUndergraduate
|
||||
itemMap["专科批"] = groupedItems.BatchCollege
|
||||
|
||||
return &vo.VolunteerDetailResponse{
|
||||
Volunteer: volunteerInfo,
|
||||
Items: itemMap,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SaveVolunteer 保存志愿(从 Controller 移入 Service 层)
|
||||
func (s *UserScoreService) SaveVolunteer(userID string, req *userDto.SaveVolunteerRequest) error {
|
||||
// 数据去重
|
||||
seen := make(map[string]bool)
|
||||
var uniqueKeys []string
|
||||
for _, key := range req.Keys {
|
||||
if !seen[key] {
|
||||
seen[key] = true
|
||||
uniqueKeys = append(uniqueKeys, key)
|
||||
}
|
||||
}
|
||||
|
||||
loginUserId := userID
|
||||
userScoreVO, err := s.GetActiveScoreByUserID(loginUserId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if userScoreVO.CalculationTableName == "" {
|
||||
return fmt.Errorf("未找到计算表名")
|
||||
}
|
||||
|
||||
// 查找当前激活的志愿表
|
||||
volunteer, err := s.yxVolunteerService.FindActiveByScoreId(userScoreVO.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查找志愿表失败: %w", err)
|
||||
}
|
||||
if volunteer == nil || volunteer.ID == "" {
|
||||
return fmt.Errorf("请先创建志愿表")
|
||||
}
|
||||
|
||||
// 查找专业信息
|
||||
majors, err := s.yxCalculationMajorService.FindListByCompositeKeys(userScoreVO.CalculationTableName, uniqueKeys, userScoreVO.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查找专业信息失败: %w", err)
|
||||
}
|
||||
|
||||
// 构建 Map 用于保持顺序
|
||||
majorMap := make(map[string]entity.YxCalculationMajor)
|
||||
for _, major := range majors {
|
||||
k := major.SchoolCode + "_" + major.MajorCode + "_" + major.EnrollmentCode
|
||||
majorMap[k] = major
|
||||
}
|
||||
|
||||
var records []entity.YxVolunteerRecord
|
||||
for i, key := range uniqueKeys {
|
||||
if major, ok := majorMap[key]; ok {
|
||||
record := entity.YxVolunteerRecord{
|
||||
VolunteerID: volunteer.ID,
|
||||
SchoolCode: major.SchoolCode,
|
||||
MajorCode: major.MajorCode,
|
||||
EnrollmentCode: major.EnrollmentCode,
|
||||
Indexs: i + 1,
|
||||
CreateBy: loginUserId,
|
||||
CreateTime: time.Now(),
|
||||
Batch: major.Batch,
|
||||
EnrollProbability: major.EnrollProbability,
|
||||
StudentConvertedScore: major.StudentConvertedScore,
|
||||
CalculationMajorID: major.ID,
|
||||
}
|
||||
records = append(records, record)
|
||||
}
|
||||
}
|
||||
|
||||
// 先删除旧数据
|
||||
if err := s.yxVolunteerRecordService.DeleteByVolunteerID(volunteer.ID); err != nil {
|
||||
return fmt.Errorf("删除旧数据失败: %w", err)
|
||||
}
|
||||
|
||||
// 批量插入新数据
|
||||
if len(records) > 0 {
|
||||
if err := s.yxVolunteerRecordService.BatchCreate(records); err != nil {
|
||||
return fmt.Errorf("保存失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveUserScore 保存用户成绩并返回 VO
|
||||
func (s *UserScoreService) SaveUserScore(req *yxDto.SaveScoreRequest) (vo.UserScoreVO, error) {
|
||||
// 1. 业务验证
|
||||
if err := req.Validate(); err != nil {
|
||||
return vo.UserScoreVO{}, err
|
||||
}
|
||||
|
||||
// 2. DTO 转 Entity
|
||||
entityItem := s.convertDtoToEntity(req)
|
||||
entityItem.CalculationTableName = "yx_calculation_major_2025_2"
|
||||
entityItem.ID = common.GenerateStringID() // 使用新封装的 ID 生成工具
|
||||
entityItem.CreateTime = time.Now()
|
||||
entityItem.UpdateTime = time.Now()
|
||||
|
||||
// 3. 执行保存操作(可以包含事务)
|
||||
tx := config.DB.Begin()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Printf("【PANIC】事务执行过程中发生panic: %v", r)
|
||||
// 记录详细的栈信息
|
||||
fmt.Printf("【PANIC】尝试回滚事务")
|
||||
tx.Rollback()
|
||||
fmt.Printf("【PANIC】事务已回滚")
|
||||
}
|
||||
}()
|
||||
|
||||
// 标记该用户的所有旧成绩为历史状态
|
||||
if err := tx.Model(&entity.YxUserScore{}).
|
||||
Where("create_by = ? AND state = ?", req.CreateBy, "1").
|
||||
Updates(map[string]interface{}{"state": "2"}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return vo.UserScoreVO{}, fmt.Errorf("更新旧记录失败: %w", err)
|
||||
}
|
||||
|
||||
// 保存新的成绩记录
|
||||
if err := tx.Create(entityItem).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return vo.UserScoreVO{}, fmt.Errorf("保存记录失败: %w", err)
|
||||
}
|
||||
userScoreVO := s.convertEntityToVo(*entityItem)
|
||||
|
||||
// 提交事务 - ✅ 快速释放数据库连接
|
||||
// if err := tx.Commit().Error; err != nil {
|
||||
// return vo.UserScoreVO{}, fmt.Errorf("提交事务失败: %w", err)
|
||||
// }
|
||||
|
||||
// 根据成绩计算用户的专业信息
|
||||
schoolMajorItems, err := s.yxCalculationMajorService.ListByUserQueryType(userScoreVO.ProfessionalCategory,
|
||||
userScoreVO.CognitioPolyclinic, userScoreVO.ProfessionalCategoryChildren)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return vo.UserScoreVO{}, fmt.Errorf("查询专业信息失败: %w", err)
|
||||
}
|
||||
|
||||
// 检查录取概率
|
||||
s.yxCalculationMajorService.CheckEnrollProbability(&schoolMajorItems, userScoreVO)
|
||||
|
||||
// 插入到数据库
|
||||
err = s.yxCalculationMajorService.BatchCreateBySchoolMajorDTO(entityItem.CalculationTableName, schoolMajorItems, userScoreVO.ID)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return vo.UserScoreVO{}, fmt.Errorf("保存专业信息失败: %w", err)
|
||||
}
|
||||
|
||||
// 创建志愿表
|
||||
err = s.yxVolunteerService.CreateByScoreId(entityItem.ID, req.CreateBy)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return vo.UserScoreVO{}, fmt.Errorf("创建志愿表失败: %w", err)
|
||||
}
|
||||
|
||||
// 提交事务
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
tx.Rollback()
|
||||
return vo.UserScoreVO{}, fmt.Errorf("提交事务失败: %w", err)
|
||||
}
|
||||
|
||||
// 清除之前的Redis成绩缓存
|
||||
config.RDB.Del(context.Background(), common.RedisUserScorePrefix+req.CreateBy)
|
||||
|
||||
// 更新Redis 数据
|
||||
scoreRedisSetData, err := json.Marshal(entityItem)
|
||||
err = config.RDB.Set(context.Background(), common.RedisUserScorePrefix+req.CreateBy, scoreRedisSetData, common.RedisUserScoreExpire).Err()
|
||||
if err != nil {
|
||||
return vo.UserScoreVO{}, fmt.Errorf("缓存成绩记录失败: %w", err)
|
||||
}
|
||||
return userScoreVO, nil
|
||||
}
|
||||
|
||||
// 私有方法:DTO 转 Entity
|
||||
func (s *UserScoreService) convertDtoToEntity(req *yxDto.SaveScoreRequest) *entity.YxUserScore {
|
||||
entityItem := entity.YxUserScore{
|
||||
CognitioPolyclinic: req.CognitioPolyclinic,
|
||||
Subjects: strings.Join(req.SubjectList, ","),
|
||||
ProfessionalCategory: req.ProfessionalCategory,
|
||||
ProfessionalCategoryChildren: strings.Join(req.ProfessionalCategoryChildren, ","),
|
||||
ProfessionalScore: *req.ProfessionalScore,
|
||||
CulturalScore: *req.CulturalScore,
|
||||
EnglishScore: *req.EnglishScore,
|
||||
ChineseScore: *req.ChineseScore,
|
||||
Province: req.Province,
|
||||
State: "1", // 默认状态
|
||||
CreateBy: req.CreateBy,
|
||||
}
|
||||
|
||||
// 映射子专业成绩到具体字段
|
||||
if v, ok := req.ProfessionalCategoryChildrenScore["音乐表演声乐"]; ok {
|
||||
entityItem.Yybysy = v
|
||||
}
|
||||
if v, ok := req.ProfessionalCategoryChildrenScore["音乐表演器乐"]; ok {
|
||||
entityItem.Yybyqy = v
|
||||
}
|
||||
if v, ok := req.ProfessionalCategoryChildrenScore["音乐教育"]; ok {
|
||||
entityItem.Yyjy = v
|
||||
}
|
||||
if v, ok := req.ProfessionalCategoryChildrenScore["戏剧影视导演"]; ok {
|
||||
entityItem.Xjysdy = v
|
||||
}
|
||||
if v, ok := req.ProfessionalCategoryChildrenScore["戏剧影视表演"]; ok {
|
||||
entityItem.Xjysby = v
|
||||
}
|
||||
if v, ok := req.ProfessionalCategoryChildrenScore["服装表演"]; ok {
|
||||
entityItem.Fzby = v
|
||||
}
|
||||
|
||||
return &entityItem
|
||||
}
|
||||
|
||||
// 私有方法:Entity 转 VO
|
||||
func (s *UserScoreService) convertEntityToVo(item entity.YxUserScore) vo.UserScoreVO {
|
||||
voItem := vo.UserScoreVO{
|
||||
ID: item.ID,
|
||||
Type: item.Type,
|
||||
EducationalLevel: item.EducationalLevel,
|
||||
CognitioPolyclinic: item.CognitioPolyclinic,
|
||||
ProfessionalCategory: item.ProfessionalCategory,
|
||||
ProfessionalCategoryChildren: []string{},
|
||||
ProfessionalCategoryChildrenScore: make(map[string]float64),
|
||||
ProfessionalScore: item.ProfessionalScore,
|
||||
CulturalScore: item.CulturalScore,
|
||||
EnglishScore: item.EnglishScore,
|
||||
ChineseScore: item.ChineseScore,
|
||||
Province: item.Province,
|
||||
State: item.State,
|
||||
CalculationTableName: item.CalculationTableName,
|
||||
}
|
||||
if item.Subjects == "" {
|
||||
voItem.SubjectList = []string{}
|
||||
} else {
|
||||
voItem.SubjectList = strings.Split(item.Subjects, ",")
|
||||
}
|
||||
|
||||
if item.ProfessionalCategoryChildren == "" {
|
||||
voItem.ProfessionalCategoryChildren = []string{}
|
||||
} else {
|
||||
voItem.ProfessionalCategoryChildren = strings.Split(item.ProfessionalCategoryChildren, ",")
|
||||
}
|
||||
|
||||
// 映射具体字段到子专业成绩 Map
|
||||
if item.Yybysy > 0 {
|
||||
voItem.ProfessionalCategoryChildrenScore["音乐表演声乐"] = item.Yybysy
|
||||
}
|
||||
if item.Yybyqy > 0 {
|
||||
voItem.ProfessionalCategoryChildrenScore["音乐表演器乐"] = item.Yybyqy
|
||||
}
|
||||
if item.Yyjy > 0 {
|
||||
voItem.ProfessionalCategoryChildrenScore["音乐教育"] = item.Yyjy
|
||||
}
|
||||
if item.Xjysdy > 0 {
|
||||
voItem.ProfessionalCategoryChildrenScore["戏剧影视导演"] = item.Xjysdy
|
||||
}
|
||||
if item.Xjysby > 0 {
|
||||
voItem.ProfessionalCategoryChildrenScore["戏剧影视表演"] = item.Xjysby
|
||||
}
|
||||
if item.Fzby > 0 {
|
||||
voItem.ProfessionalCategoryChildrenScore["服装表演"] = item.Fzby
|
||||
}
|
||||
|
||||
return voItem
|
||||
}
|
||||
|
|
@ -0,0 +1,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, " ")
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package vo
|
||||
|
||||
// UserScoreVO 用户成绩展示对象
|
||||
type UserScoreVO struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"` // 填报类型(1-普通类 2-艺术类)
|
||||
EducationalLevel string `json:"educationalLevel"` // 学历层次(1-本科,2-专科)
|
||||
CognitioPolyclinic string `json:"cognitioPolyclinic"` // 文理分班(文科/理科)
|
||||
SubjectList []string `json:"subjectList"` // 选课列表
|
||||
ProfessionalCategory string `json:"professionalCategory"` // 专业类别
|
||||
ProfessionalCategoryChildren []string `json:"professionalCategoryChildren"` // 子级专业类别
|
||||
ProfessionalCategoryChildrenScore map[string]float64 `json:"professionalCategoryChildrenScore"` // 子级专业成绩
|
||||
ProfessionalScore float64 `json:"professionalScore"` // 专业总分
|
||||
CulturalScore float64 `json:"culturalScore"` // 文化成绩分
|
||||
EnglishScore float64 `json:"englishScore"` // 英语成绩
|
||||
ChineseScore float64 `json:"chineseScore"` // 语文成绩
|
||||
Province string `json:"province"` // 高考省份
|
||||
State string `json:"state"` // 状态
|
||||
CalculationTableName string `json:"calculationTableName"` // 计算表名称
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package vo
|
||||
|
||||
import "server/types"
|
||||
|
||||
// VolunteerDetailVO 志愿详情视图对象
|
||||
type VolunteerDetailVO struct {
|
||||
ID string `json:"id"` // 志愿记录ID
|
||||
VolunteerID string `json:"volunteerId"` // 志愿单ID
|
||||
SchoolCode string `json:"schoolCode"` // 院校代码
|
||||
MajorCode string `json:"majorCode"` // 专业代码
|
||||
EnrollmentCode string `json:"enrollmentCode"` // 招生代码
|
||||
Indexs int `json:"indexs"` // 志愿序号
|
||||
Batch string `json:"batch"` // 批次
|
||||
EnrollProbability float64 `json:"enrollProbability"` // 录取概率
|
||||
StudentConvertedScore float64 `json:"studentConvertedScore"` // 学生折合分
|
||||
CreateBy string `json:"createBy"` // 创建人
|
||||
CreateTime types.DateTime `json:"createTime"` // 创建时间
|
||||
CalculationMajorID string `json:"calculationMajorId"` // 计算专业ID
|
||||
// 扩展字段(来自 SchoolMajorDTO)
|
||||
SchoolName string `json:"schoolName"` // 院校名称
|
||||
MajorName string `json:"majorName"` // 专业名称
|
||||
PlanNum int `json:"planNum"` // 计划人数
|
||||
Tuition string `json:"tuition"` // 学费
|
||||
SchoolIcon string `json:"schoolIcon"` // 院校图标
|
||||
Province string `json:"province"` // 省份
|
||||
SchoolNature string `json:"schoolNature"` // 院校性质
|
||||
InstitutionType string `json:"institutionType"` // 院校类型
|
||||
MajorDetail string `json:"majorDetail"` // 专业详情
|
||||
}
|
||||
|
||||
// VolunteerDetailResponse 志愿详情响应
|
||||
type VolunteerDetailResponse struct {
|
||||
Volunteer *VolunteerInfoVO `json:"volunteer"` // 志愿单信息
|
||||
Items map[string][]VolunteerDetailVO `json:"items"` // 志愿明细列表 *VolunteerItemsVO
|
||||
}
|
||||
|
||||
// VolunteerInfoVO 志愿单信息视图对象
|
||||
type VolunteerInfoVO struct {
|
||||
ID string `json:"id"`
|
||||
VolunteerName string `json:"volunteerName"`
|
||||
ScoreId string `json:"scoreId"`
|
||||
CreateType string `json:"createType"`
|
||||
State string `json:"state"`
|
||||
CreateBy string `json:"createBy"`
|
||||
CreateTime types.DateTime `json:"createTime"`
|
||||
UpdateBy string `json:"updateBy"`
|
||||
UpdateTime types.DateTime `json:"updateTime"`
|
||||
}
|
||||
|
||||
// VolunteerItemsVO 志愿明细列表视图对象
|
||||
type VolunteerItemsVO struct {
|
||||
BatchBefore []VolunteerDetailVO `json:"batchBefore"` // 提前批
|
||||
BatchUndergraduate []VolunteerDetailVO `json:"batchUndergraduate"` // 本科批
|
||||
BatchCollege []VolunteerDetailVO `json:"batchCollege"` // 专科批
|
||||
}
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
// Package controller 控制器层
|
||||
package controller
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"server/common"
|
||||
"server/modules/yx/dto"
|
||||
"server/modules/yx/service"
|
||||
yxVO "server/modules/yx/vo"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type YxCalculationMajorController struct {
|
||||
service *service.YxCalculationMajorService
|
||||
}
|
||||
|
||||
func NewYxCalculationMajorController() *YxCalculationMajorController {
|
||||
return &YxCalculationMajorController{service: service.NewYxCalculationMajorService()}
|
||||
}
|
||||
|
||||
// _ 确保 vo 包被导入(用于 Swagger 注解)
|
||||
var _ = yxVO.YxCalculationMajorVO{}
|
||||
|
||||
func (ctrl *YxCalculationMajorController) RegisterRoutes(r *gin.RouterGroup) {
|
||||
r.GET("/yx-calculation-majors", ctrl.List)
|
||||
r.GET("/yx-calculation-majors/:id", ctrl.GetByID)
|
||||
r.POST("/yx-calculation-majors", ctrl.Create)
|
||||
r.PUT("/yx-calculation-majors/:id", ctrl.Update)
|
||||
r.PATCH("/yx-calculation-majors/:id", ctrl.UpdateFields)
|
||||
r.DELETE("/yx-calculation-majors/:id", ctrl.Delete)
|
||||
r.POST("/yx-calculation-majors/batch", ctrl.BatchCreate)
|
||||
r.DELETE("/yx-calculation-majors/batch", ctrl.BatchDelete)
|
||||
}
|
||||
|
||||
// @Summary 获取计算专业列表
|
||||
// @Tags 计算专业
|
||||
// @Param page query int false "页码"
|
||||
// @Param size query int false "每页数量"
|
||||
// @Success 200 {object} common.Response{data=[]vo.YxCalculationMajorVO}
|
||||
// @Router /yx-calculation-majors [get]
|
||||
func (ctrl *YxCalculationMajorController) List(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
size, _ := strconv.Atoi(c.DefaultQuery("size", "10"))
|
||||
items, total, err := ctrl.service.ListCalculationMajors(page, size)
|
||||
if err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
common.SuccessPage(c, items, total, page, size)
|
||||
}
|
||||
|
||||
// @Summary 获取单个计算专业
|
||||
// @Tags 计算专业
|
||||
// @Param id path string true "ID"
|
||||
// @Success 200 {object} common.Response{data=vo.YxCalculationMajorVO}
|
||||
// @Router /yx-calculation-majors/{id} [get]
|
||||
func (ctrl *YxCalculationMajorController) GetByID(c *gin.Context) {
|
||||
item, err := ctrl.service.GetCalculationMajorByID(c.Param("id"))
|
||||
if err != nil {
|
||||
common.Error(c, 404, "未找到")
|
||||
return
|
||||
}
|
||||
common.Success(c, item)
|
||||
}
|
||||
|
||||
// @Summary 创建计算专业
|
||||
// @Tags 计算专业
|
||||
// @Param request body dto.CreateCalculationMajorRequest true "计算专业信息"
|
||||
// @Success 200 {object} common.Response{data=vo.YxCalculationMajorVO}
|
||||
// @Router /yx-calculation-majors [post]
|
||||
func (ctrl *YxCalculationMajorController) Create(c *gin.Context) {
|
||||
var req dto.CreateCalculationMajorRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.Error(c, 400, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
item, err := ctrl.service.CreateCalculationMajor(&req)
|
||||
if err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
common.Success(c, item)
|
||||
}
|
||||
|
||||
// @Summary 更新计算专业
|
||||
// @Tags 计算专业
|
||||
// @Param id path string true "ID"
|
||||
// @Param request body dto.UpdateCalculationMajorRequest true "计算专业信息"
|
||||
// @Success 200 {object} common.Response{data=vo.YxCalculationMajorVO}
|
||||
// @Router /yx-calculation-majors/{id} [put]
|
||||
func (ctrl *YxCalculationMajorController) Update(c *gin.Context) {
|
||||
var req dto.UpdateCalculationMajorRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.Error(c, 400, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
item, err := ctrl.service.UpdateCalculationMajor(c.Param("id"), &req)
|
||||
if err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
common.Success(c, item)
|
||||
}
|
||||
|
||||
// @Summary 动态字段更新
|
||||
// @Tags 计算专业
|
||||
// @Param id path string true "ID"
|
||||
// @Param fields body map[string]interface{} true "要更新的字段"
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /yx-calculation-majors/{id} [patch]
|
||||
func (ctrl *YxCalculationMajorController) UpdateFields(c *gin.Context) {
|
||||
var fields map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&fields); err != nil {
|
||||
common.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if err := ctrl.service.UpdateFields(c.Param("id"), fields); err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
common.Success(c, nil)
|
||||
}
|
||||
|
||||
// @Summary 删除计算专业
|
||||
// @Tags 计算专业
|
||||
// @Param id path string true "ID"
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /yx-calculation-majors/{id} [delete]
|
||||
func (ctrl *YxCalculationMajorController) Delete(c *gin.Context) {
|
||||
if err := ctrl.service.Delete(c.Param("id")); err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
common.Success(c, nil)
|
||||
}
|
||||
|
||||
// @Summary 批量创建计算专业
|
||||
// @Tags 计算专业
|
||||
// @Param items body []dto.CreateCalculationMajorRequest true "计算专业列表"
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /yx-calculation-majors/batch [post]
|
||||
func (ctrl *YxCalculationMajorController) BatchCreate(c *gin.Context) {
|
||||
var reqs []dto.CreateCalculationMajorRequest
|
||||
if err := c.ShouldBindJSON(&reqs); err != nil {
|
||||
common.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if err := ctrl.service.BatchCreateCalculationMajors(&reqs); err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
common.Success(c, nil)
|
||||
}
|
||||
|
||||
// @Summary 批量删除计算专业
|
||||
// @Tags 计算专业
|
||||
// @Param ids body []string true "ID列表"
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /yx-calculation-majors/batch [delete]
|
||||
func (ctrl *YxCalculationMajorController) BatchDelete(c *gin.Context) {
|
||||
var ids []string
|
||||
if err := c.ShouldBindJSON(&ids); err != nil {
|
||||
common.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if err := ctrl.service.BatchDelete(ids); err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
common.Success(c, nil)
|
||||
}
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
// Package controller 控制器层
|
||||
package controller
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"server/common"
|
||||
"server/modules/yx/entity"
|
||||
"server/modules/yx/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type YxHistoryMajorEnrollController struct {
|
||||
service *service.YxHistoryMajorEnrollService
|
||||
}
|
||||
|
||||
func NewYxHistoryMajorEnrollController() *YxHistoryMajorEnrollController {
|
||||
return &YxHistoryMajorEnrollController{service: service.NewYxHistoryMajorEnrollService()}
|
||||
}
|
||||
|
||||
func (ctrl *YxHistoryMajorEnrollController) RegisterRoutes(r *gin.RouterGroup) {
|
||||
r.GET("/yx-history-enrolls", ctrl.List)
|
||||
r.GET("/yx-history-enrolls/:id", ctrl.GetByID)
|
||||
r.POST("/yx-history-enrolls", ctrl.Create)
|
||||
r.PUT("/yx-history-enrolls/:id", ctrl.Update)
|
||||
r.PATCH("/yx-history-enrolls/:id", ctrl.UpdateFields)
|
||||
r.DELETE("/yx-history-enrolls/:id", ctrl.Delete)
|
||||
r.POST("/yx-history-enrolls/batch", ctrl.BatchCreate)
|
||||
r.DELETE("/yx-history-enrolls/batch", ctrl.BatchDelete)
|
||||
}
|
||||
|
||||
// @Summary 获取历年招生列表
|
||||
// @Tags 历年招生
|
||||
// @Param page query int false "页码"
|
||||
// @Param size query int false "每页数量"
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /yx-history-enrolls [get]
|
||||
func (ctrl *YxHistoryMajorEnrollController) List(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
size, _ := strconv.Atoi(c.DefaultQuery("size", "10"))
|
||||
items, total, err := ctrl.service.List(page, size)
|
||||
if err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
common.SuccessPage(c, items, total, page, size)
|
||||
}
|
||||
|
||||
// @Summary 获取单个历年招生记录
|
||||
// @Tags 历年招生
|
||||
// @Param id path string true "ID"
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /yx-history-enrolls/{id} [get]
|
||||
func (ctrl *YxHistoryMajorEnrollController) GetByID(c *gin.Context) {
|
||||
item, err := ctrl.service.GetByID(c.Param("id"))
|
||||
if err != nil {
|
||||
common.Error(c, 404, "未找到")
|
||||
return
|
||||
}
|
||||
common.Success(c, item)
|
||||
}
|
||||
|
||||
// @Summary 创建历年招生记录
|
||||
// @Tags 历年招生
|
||||
// @Param item body entity.YxHistoryMajorEnroll true "历年招生信息"
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /yx-history-enrolls [post]
|
||||
func (ctrl *YxHistoryMajorEnrollController) Create(c *gin.Context) {
|
||||
var item entity.YxHistoryMajorEnroll
|
||||
if err := c.ShouldBindJSON(&item); err != nil {
|
||||
common.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if err := ctrl.service.Create(&item); err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
common.Success(c, item)
|
||||
}
|
||||
|
||||
// @Summary 更新历年招生记录
|
||||
// @Tags 历年招生
|
||||
// @Param id path string true "ID"
|
||||
// @Param item body entity.YxHistoryMajorEnroll true "历年招生信息"
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /yx-history-enrolls/{id} [put]
|
||||
func (ctrl *YxHistoryMajorEnrollController) Update(c *gin.Context) {
|
||||
var item entity.YxHistoryMajorEnroll
|
||||
if err := c.ShouldBindJSON(&item); err != nil {
|
||||
common.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
item.ID = c.Param("id")
|
||||
if err := ctrl.service.Update(&item); err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
common.Success(c, item)
|
||||
}
|
||||
|
||||
// @Summary 动态字段更新
|
||||
// @Tags 历年招生
|
||||
// @Param id path string true "ID"
|
||||
// @Param fields body map[string]interface{} true "要更新的字段"
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /yx-history-enrolls/{id} [patch]
|
||||
func (ctrl *YxHistoryMajorEnrollController) UpdateFields(c *gin.Context) {
|
||||
var fields map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&fields); err != nil {
|
||||
common.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if err := ctrl.service.UpdateFields(c.Param("id"), fields); err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
common.Success(c, nil)
|
||||
}
|
||||
|
||||
// @Summary 删除历年招生记录
|
||||
// @Tags 历年招生
|
||||
// @Param id path string true "ID"
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /yx-history-enrolls/{id} [delete]
|
||||
func (ctrl *YxHistoryMajorEnrollController) Delete(c *gin.Context) {
|
||||
if err := ctrl.service.Delete(c.Param("id")); err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
common.Success(c, nil)
|
||||
}
|
||||
|
||||
// @Summary 批量创建历年招生记录
|
||||
// @Tags 历年招生
|
||||
// @Param items body []entity.YxHistoryMajorEnroll true "历年招生列表"
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /yx-history-enrolls/batch [post]
|
||||
func (ctrl *YxHistoryMajorEnrollController) BatchCreate(c *gin.Context) {
|
||||
var items []entity.YxHistoryMajorEnroll
|
||||
if err := c.ShouldBindJSON(&items); err != nil {
|
||||
common.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if err := ctrl.service.BatchCreate(items); err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
common.Success(c, nil)
|
||||
}
|
||||
|
||||
// @Summary 批量删除历年招生记录
|
||||
// @Tags 历年招生
|
||||
// @Param ids body []string true "ID列表"
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /yx-history-enrolls/batch [delete]
|
||||
func (ctrl *YxHistoryMajorEnrollController) BatchDelete(c *gin.Context) {
|
||||
var ids []string
|
||||
if err := c.ShouldBindJSON(&ids); err != nil {
|
||||
common.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if err := ctrl.service.BatchDelete(ids); err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
common.Success(c, nil)
|
||||
}
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
// Package controller 控制器层
|
||||
package controller
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"server/common"
|
||||
"server/modules/yx/entity"
|
||||
"server/modules/yx/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type YxSchoolMajorController struct {
|
||||
service *service.YxSchoolMajorService
|
||||
}
|
||||
|
||||
func NewYxSchoolMajorController() *YxSchoolMajorController {
|
||||
return &YxSchoolMajorController{service: service.NewYxSchoolMajorService()}
|
||||
}
|
||||
|
||||
func (ctrl *YxSchoolMajorController) RegisterRoutes(r *gin.RouterGroup) {
|
||||
r.GET("/yx-school-majors", ctrl.List)
|
||||
r.GET("/yx-school-majors/:id", ctrl.GetByID)
|
||||
r.POST("/yx-school-majors", ctrl.Create)
|
||||
r.PUT("/yx-school-majors/:id", ctrl.Update)
|
||||
r.PATCH("/yx-school-majors/:id", ctrl.UpdateFields)
|
||||
r.DELETE("/yx-school-majors/:id", ctrl.Delete)
|
||||
r.POST("/yx-school-majors/batch", ctrl.BatchCreate)
|
||||
r.DELETE("/yx-school-majors/batch", ctrl.BatchDelete)
|
||||
}
|
||||
|
||||
// List 获取院校专业列表
|
||||
// @Summary 获取院校专业列表
|
||||
// @Tags 院校专业
|
||||
// @Param page query int false "页码"
|
||||
// @Param size query int false "每页数量"
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /yx-school-majors [get]
|
||||
func (ctrl *YxSchoolMajorController) List(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
size, _ := strconv.Atoi(c.DefaultQuery("size", "10"))
|
||||
items, total, err := ctrl.service.List(page, size)
|
||||
if err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
common.SuccessPage(c, items, total, page, size)
|
||||
}
|
||||
|
||||
// GetByID 获取单个院校专业
|
||||
// @Summary 获取单个院校专业
|
||||
// @Tags 院校专业
|
||||
// @Param id path string true "ID"
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /yx-school-majors/{id} [get]
|
||||
func (ctrl *YxSchoolMajorController) GetByID(c *gin.Context) {
|
||||
item, err := ctrl.service.GetByID(c.Param("id"))
|
||||
if err != nil {
|
||||
common.Error(c, 404, "未找到")
|
||||
return
|
||||
}
|
||||
common.Success(c, item)
|
||||
}
|
||||
|
||||
// Create 创建院校专业
|
||||
// @Summary 创建院校专业
|
||||
// @Tags 院校专业
|
||||
// @Param item body entity.YxSchoolMajor true "院校专业信息"
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /yx-school-majors [post]
|
||||
func (ctrl *YxSchoolMajorController) Create(c *gin.Context) {
|
||||
var item entity.YxSchoolMajor
|
||||
if err := c.ShouldBindJSON(&item); err != nil {
|
||||
common.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if err := ctrl.service.Create(&item); err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
common.Success(c, item)
|
||||
}
|
||||
|
||||
// Update 更新院校专业
|
||||
// @Summary 更新院校专业
|
||||
// @Tags 院校专业
|
||||
// @Param id path string true "ID"
|
||||
// @Param item body entity.YxSchoolMajor true "院校专业信息"
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /yx-school-majors/{id} [put]
|
||||
func (ctrl *YxSchoolMajorController) Update(c *gin.Context) {
|
||||
var item entity.YxSchoolMajor
|
||||
if err := c.ShouldBindJSON(&item); err != nil {
|
||||
common.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
item.ID = c.Param("id")
|
||||
if err := ctrl.service.Update(&item); err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
common.Success(c, item)
|
||||
}
|
||||
|
||||
// UpdateFields 动态字段更新
|
||||
// @Summary 动态字段更新
|
||||
// @Tags 院校专业
|
||||
// @Param id path string true "ID"
|
||||
// @Param fields body map[string]interface{} true "要更新的字段"
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /yx-school-majors/{id} [patch]
|
||||
func (ctrl *YxSchoolMajorController) UpdateFields(c *gin.Context) {
|
||||
var fields map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&fields); err != nil {
|
||||
common.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if err := ctrl.service.UpdateFields(c.Param("id"), fields); err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
common.Success(c, nil)
|
||||
}
|
||||
|
||||
// Delete 删除院校专业
|
||||
// @Summary 删除院校专业
|
||||
// @Tags 院校专业
|
||||
// @Param id path string true "ID"
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /yx-school-majors/{id} [delete]
|
||||
func (ctrl *YxSchoolMajorController) Delete(c *gin.Context) {
|
||||
if err := ctrl.service.Delete(c.Param("id")); err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
common.Success(c, nil)
|
||||
}
|
||||
|
||||
// BatchCreate 批量创建
|
||||
// @Summary 批量创建院校专业
|
||||
// @Tags 院校专业
|
||||
// @Param items body []entity.YxSchoolMajor true "院校专业列表"
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /yx-school-majors/batch [post]
|
||||
func (ctrl *YxSchoolMajorController) BatchCreate(c *gin.Context) {
|
||||
var items []entity.YxSchoolMajor
|
||||
if err := c.ShouldBindJSON(&items); err != nil {
|
||||
common.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if err := ctrl.service.BatchCreate(items); err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
common.Success(c, nil)
|
||||
}
|
||||
|
||||
// BatchDelete 批量删除
|
||||
// @Summary 批量删除院校专业
|
||||
// @Tags 院校专业
|
||||
// @Param ids body []string true "ID列表"
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /yx-school-majors/batch [delete]
|
||||
func (ctrl *YxSchoolMajorController) BatchDelete(c *gin.Context) {
|
||||
var ids []string
|
||||
if err := c.ShouldBindJSON(&ids); err != nil {
|
||||
common.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if err := ctrl.service.BatchDelete(ids); err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
common.Success(c, nil)
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
// Package controller 控制层
|
||||
package controller
|
||||
|
||||
import (
|
||||
"server/common"
|
||||
"server/modules/yx/entity"
|
||||
"server/modules/yx/service"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type YxUserScoreController struct {
|
||||
service *service.YxUserScoreService
|
||||
}
|
||||
|
||||
func NewYxUserScoreController() *YxUserScoreController {
|
||||
return &YxUserScoreController{service: service.NewYxUserScoreService()}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (ctrl *YxUserScoreController) RegisterRoutes(rg *gin.RouterGroup) {
|
||||
group := rg.Group("/yx-user-scores")
|
||||
{
|
||||
group.GET("", ctrl.List)
|
||||
group.GET("/:id", ctrl.Get)
|
||||
group.POST("", ctrl.Create)
|
||||
group.PUT("/:id", ctrl.Update)
|
||||
group.DELETE("/:id", ctrl.Delete)
|
||||
}
|
||||
}
|
||||
|
||||
// List 获取用户分数列表
|
||||
// @Summary 获取用户分数列表
|
||||
// @Tags 用户分数
|
||||
// @Param page query int false "页码" default(1)
|
||||
// @Param size query int false "每页数量" default(10)
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /yx-user-scores [get]
|
||||
func (ctrl *YxUserScoreController) List(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
size, _ := strconv.Atoi(c.DefaultQuery("size", "10"))
|
||||
items, total, err := ctrl.service.List(page, size)
|
||||
if err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
common.Success(c, gin.H{
|
||||
"items": items,
|
||||
"total": total,
|
||||
})
|
||||
}
|
||||
|
||||
// Get 获取单个用户分数
|
||||
// @Summary 获取单个用户分数
|
||||
// @Tags 用户分数
|
||||
// @Param id path string true "ID"
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /yx-user-scores/{id} [get]
|
||||
func (ctrl *YxUserScoreController) Get(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
item, err := ctrl.service.GetByID(id)
|
||||
if err != nil {
|
||||
common.Error(c, 404, "未找到记录")
|
||||
return
|
||||
}
|
||||
common.Success(c, item)
|
||||
}
|
||||
|
||||
// Create 创建用户分数
|
||||
// @Summary 创建用户分数
|
||||
// @Tags 用户分数
|
||||
// @Param item body entity.YxUserScore true "用户分数信息"
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /yx-user-scores [post]
|
||||
func (ctrl *YxUserScoreController) Create(c *gin.Context) {
|
||||
var item entity.YxUserScore
|
||||
if err := c.ShouldBindJSON(&item); err != nil {
|
||||
common.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if err := ctrl.service.Create(&item); err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
common.Success(c, item)
|
||||
}
|
||||
|
||||
// Update 更新用户分数
|
||||
// @Summary 更新用户分数
|
||||
// @Tags 用户分数
|
||||
// @Param id path string true "ID"
|
||||
// @Param item body entity.YxUserScore true "用户分数信息"
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /yx-user-scores/{id} [put]
|
||||
func (ctrl *YxUserScoreController) Update(c *gin.Context) {
|
||||
var item entity.YxUserScore
|
||||
if err := c.ShouldBindJSON(&item); err != nil {
|
||||
common.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
item.ID = c.Param("id")
|
||||
if err := ctrl.service.Update(&item); err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
common.Success(c, item)
|
||||
}
|
||||
|
||||
// Delete 删除用户分数
|
||||
// @Summary 删除用户分数
|
||||
// @Tags 用户分数
|
||||
// @Param id path string true "ID"
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /yx-user-scores/{id} [delete]
|
||||
func (ctrl *YxUserScoreController) Delete(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if err := ctrl.service.Delete(id); err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
common.Success(c, nil)
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
// Package controller 控制层
|
||||
package controller
|
||||
|
||||
import (
|
||||
"server/common"
|
||||
"server/modules/yx/dto"
|
||||
"server/modules/yx/service"
|
||||
"server/modules/yx/vo"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type YxVolunteerController struct {
|
||||
service *service.YxVolunteerService
|
||||
}
|
||||
|
||||
func NewYxVolunteerController() *YxVolunteerController {
|
||||
return &YxVolunteerController{service: service.NewYxVolunteerService()}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (ctrl *YxVolunteerController) RegisterRoutes(rg *gin.RouterGroup) {
|
||||
group := rg.Group("/yx-volunteers")
|
||||
{
|
||||
group.GET("", ctrl.List)
|
||||
group.GET("/:id", ctrl.Get)
|
||||
group.POST("", ctrl.Create)
|
||||
group.PUT("/:id", ctrl.Update)
|
||||
group.DELETE("/:id", ctrl.Delete)
|
||||
}
|
||||
}
|
||||
|
||||
// List 获取志愿列表
|
||||
// @Summary 获取志愿列表
|
||||
// @Tags 志愿
|
||||
// @Param page query int false "页码" default(1)
|
||||
// @Param size query int false "每页数量" default(10)
|
||||
// @Success 200 {object} common.Response{data=[]vo.YxVolunteerVO}
|
||||
// @Router /yx-volunteers [get]
|
||||
func (ctrl *YxVolunteerController) List(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
size, _ := strconv.Atoi(c.DefaultQuery("size", "10"))
|
||||
items, total, err := ctrl.service.List(page, size)
|
||||
if err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
// 转换 Entity 到 VO
|
||||
vos := make([]vo.YxVolunteerVO, len(items))
|
||||
for i, item := range items {
|
||||
vos[i] = vo.YxVolunteerVO{
|
||||
ID: item.ID,
|
||||
VolunteerName: item.VolunteerName,
|
||||
ScoreId: item.ScoreId,
|
||||
CreateType: item.CreateType,
|
||||
State: item.State,
|
||||
CreateBy: item.CreateBy,
|
||||
CreateTime: item.CreateTime,
|
||||
UpdateBy: item.UpdateBy,
|
||||
UpdateTime: item.UpdateTime,
|
||||
SysOrgCode: item.SysOrgCode,
|
||||
}
|
||||
}
|
||||
common.SuccessPage(c, vos, total, page, size)
|
||||
}
|
||||
|
||||
// Get 获取单个志愿
|
||||
// @Summary 获取单个志愿
|
||||
// @Tags 志愿
|
||||
// @Param id path string true "ID"
|
||||
// @Success 200 {object} common.Response{data=vo.YxVolunteerVO}
|
||||
// @Router /yx-volunteers/{id} [get]
|
||||
func (ctrl *YxVolunteerController) Get(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
item, err := ctrl.service.GetByID(id)
|
||||
if err != nil {
|
||||
common.Error(c, 404, "未找到记录")
|
||||
return
|
||||
}
|
||||
voItem := vo.YxVolunteerVO{
|
||||
ID: item.ID,
|
||||
VolunteerName: item.VolunteerName,
|
||||
ScoreId: item.ScoreId,
|
||||
CreateType: item.CreateType,
|
||||
State: item.State,
|
||||
CreateBy: item.CreateBy,
|
||||
CreateTime: item.CreateTime,
|
||||
UpdateBy: item.UpdateBy,
|
||||
UpdateTime: item.UpdateTime,
|
||||
SysOrgCode: item.SysOrgCode,
|
||||
}
|
||||
common.Success(c, voItem)
|
||||
}
|
||||
|
||||
// Create 创建志愿
|
||||
// @Summary 创建志愿
|
||||
// @Tags 志愿
|
||||
// @Param request body dto.CreateVolunteerRequest true "志愿信息"
|
||||
// @Success 200 {object} common.Response{data=vo.YxVolunteerVO}
|
||||
// @Router /yx-volunteers [post]
|
||||
func (ctrl *YxVolunteerController) Create(c *gin.Context) {
|
||||
var req dto.CreateVolunteerRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.Error(c, 400, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
voItem, err := ctrl.service.CreateVolunteer(&req)
|
||||
if err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
common.Success(c, voItem)
|
||||
}
|
||||
|
||||
// Update 更新志愿
|
||||
// @Summary 更新志愿
|
||||
// @Tags 志愿
|
||||
// @Param id path string true "ID"
|
||||
// @Param request body dto.UpdateVolunteerRequest true "志愿信息"
|
||||
// @Success 200 {object} common.Response{data=vo.YxVolunteerVO}
|
||||
// @Router /yx-volunteers/{id} [put]
|
||||
func (ctrl *YxVolunteerController) Update(c *gin.Context) {
|
||||
var req dto.UpdateVolunteerRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.Error(c, 400, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
voItem, err := ctrl.service.UpdateVolunteer(c.Param("id"), &req)
|
||||
if err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
common.Success(c, voItem)
|
||||
}
|
||||
|
||||
// Delete 删除志愿
|
||||
// @Summary 删除志愿
|
||||
// @Tags 志愿
|
||||
// @Param id path string true "ID"
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /yx-volunteers/{id} [delete]
|
||||
func (ctrl *YxVolunteerController) Delete(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if err := ctrl.service.Delete(id); err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
common.Success(c, nil)
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
// Package controller 控制层
|
||||
package controller
|
||||
|
||||
import (
|
||||
"server/common"
|
||||
"server/modules/yx/entity"
|
||||
"server/modules/yx/service"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type YxVolunteerRecordController struct {
|
||||
service *service.YxVolunteerRecordService
|
||||
}
|
||||
|
||||
func NewYxVolunteerRecordController() *YxVolunteerRecordController {
|
||||
return &YxVolunteerRecordController{service: service.NewYxVolunteerRecordService()}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (ctrl *YxVolunteerRecordController) RegisterRoutes(rg *gin.RouterGroup) {
|
||||
group := rg.Group("/yx-volunteer-records")
|
||||
{
|
||||
group.GET("", ctrl.List)
|
||||
group.GET("/:id", ctrl.Get)
|
||||
group.POST("", ctrl.Create)
|
||||
group.PUT("/:id", ctrl.Update)
|
||||
group.DELETE("/:id", ctrl.Delete)
|
||||
}
|
||||
}
|
||||
|
||||
// List 获取志愿明细列表
|
||||
// @Summary 获取志愿明细列表
|
||||
// @Tags 志愿明细
|
||||
// @Param page query int false "页码" default(1)
|
||||
// @Param size query int false "每页数量" default(10)
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /yx-volunteer-records [get]
|
||||
func (ctrl *YxVolunteerRecordController) List(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
size, _ := strconv.Atoi(c.DefaultQuery("size", "10"))
|
||||
items, total, err := ctrl.service.List(page, size)
|
||||
if err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
common.Success(c, gin.H{
|
||||
"items": items,
|
||||
"total": total,
|
||||
})
|
||||
}
|
||||
|
||||
// Get 获取单个志愿明细
|
||||
// @Summary 获取单个志愿明细
|
||||
// @Tags 志愿明细
|
||||
// @Param id path string true "ID"
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /yx-volunteer-records/{id} [get]
|
||||
func (ctrl *YxVolunteerRecordController) Get(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
item, err := ctrl.service.GetByID(id)
|
||||
if err != nil {
|
||||
common.Error(c, 404, "未找到记录")
|
||||
return
|
||||
}
|
||||
common.Success(c, item)
|
||||
}
|
||||
|
||||
// Create 创建志愿明细
|
||||
// @Summary 创建志愿明细
|
||||
// @Tags 志愿明细
|
||||
// @Param item body entity.YxVolunteerRecord true "志愿明细信息"
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /yx-volunteer-records [post]
|
||||
func (ctrl *YxVolunteerRecordController) Create(c *gin.Context) {
|
||||
var item entity.YxVolunteerRecord
|
||||
if err := c.ShouldBindJSON(&item); err != nil {
|
||||
common.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if err := ctrl.service.Create(&item); err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
common.Success(c, item)
|
||||
}
|
||||
|
||||
// Update 更新志愿明细
|
||||
// @Summary 更新志愿明细
|
||||
// @Tags 志愿明细
|
||||
// @Param id path string true "ID"
|
||||
// @Param item body entity.YxVolunteerRecord true "志愿明细信息"
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /yx-volunteer-records/{id} [put]
|
||||
func (ctrl *YxVolunteerRecordController) Update(c *gin.Context) {
|
||||
var item entity.YxVolunteerRecord
|
||||
if err := c.ShouldBindJSON(&item); err != nil {
|
||||
common.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
item.ID = c.Param("id")
|
||||
if err := ctrl.service.Update(&item); err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
common.Success(c, item)
|
||||
}
|
||||
|
||||
// Delete 删除志愿明细
|
||||
// @Summary 删除志愿明细
|
||||
// @Tags 志愿明细
|
||||
// @Param id path string true "ID"
|
||||
// @Success 200 {object} common.Response
|
||||
// @Router /yx-volunteer-records/{id} [delete]
|
||||
func (ctrl *YxVolunteerRecordController) Delete(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if err := ctrl.service.Delete(id); err != nil {
|
||||
common.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
common.Success(c, nil)
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package dto
|
||||
|
||||
// 定义概率数量统计结果结构体,用于接收四种录取概率对应的各自数量
|
||||
type ProbabilityCountDTO struct {
|
||||
Hard int64 `json:"hard" gorm:"column:hard"` // 难录取(<60)
|
||||
Risky int64 `json:"risky" gorm:"column:risky"` // 可冲击(60<=x<73)
|
||||
Stable int64 `json:"stable" gorm:"column:stable"` // 较稳妥(73<=x<93)
|
||||
Safe int64 `json:"safe" gorm:"column:safe"` // 可保底(>=93)
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package dto
|
||||
|
||||
// CreateCalculationMajorRequest 创建计算专业请求
|
||||
type CreateCalculationMajorRequest struct {
|
||||
SchoolCode string `json:"schoolCode" binding:"required"` // 院校代码
|
||||
SchoolName string `json:"schoolName"` // 院校名称
|
||||
MajorCode string `json:"majorCode" binding:"required"` // 专业代码
|
||||
MajorName string `json:"majorName"` // 专业名称
|
||||
MajorType string `json:"majorType"` // 专业类型
|
||||
MajorTypeChild string `json:"majorTypeChild"` // 子专业类型
|
||||
PlanNum int `json:"planNum"` // 计划人数
|
||||
MainSubjects string `json:"mainSubjects"` // 主考科目
|
||||
Limitation string `json:"limitation"` // 限制条件
|
||||
ChineseScoreLimitation float64 `json:"chineseScoreLimitation"` // 语文分数限制
|
||||
EnglishScoreLimitation float64 `json:"englishScoreLimitation"` // 英语分数限制
|
||||
CulturalScoreLimitation float64 `json:"culturalScoreLimitation"` // 文化成绩限制
|
||||
ProfessionalScoreLimitation float64 `json:"professionalScoreLimitation"` // 专业分数限制
|
||||
EnrollmentCode string `json:"enrollmentCode"` // 招生代码
|
||||
Tuition string `json:"tuition"` // 学费
|
||||
Detail string `json:"detail"` // 详情
|
||||
Category string `json:"category"` // 类别
|
||||
Batch string `json:"batch"` // 批次
|
||||
RulesEnrollProbability string `json:"rulesEnrollProbability"` // 录取规则概率
|
||||
ProbabilityOperator string `json:"probabilityOperator"` // 概率操作符
|
||||
Kslx string `json:"kslx"` // 考试类型
|
||||
State string `json:"state"` // 状态
|
||||
Province string `json:"province"` // 省份
|
||||
SchoolNature string `json:"schoolNature"` // 院校性质
|
||||
InstitutionType string `json:"institutionType"` // 院校类型
|
||||
EnrollProbability float64 `json:"enrollProbability"` // 录取概率
|
||||
StudentScore float64 `json:"studentScore"` // 学生分数
|
||||
}
|
||||
|
||||
// UpdateCalculationMajorRequest 更新计算专业请求
|
||||
type UpdateCalculationMajorRequest struct {
|
||||
SchoolName string `json:"schoolName"` // 院校名称
|
||||
MajorName string `json:"majorName"` // 专业名称
|
||||
MajorType string `json:"majorType"` // 专业类型
|
||||
MajorTypeChild string `json:"majorTypeChild"` // 子专业类型
|
||||
PlanNum int `json:"planNum"` // 计划人数
|
||||
MainSubjects string `json:"mainSubjects"` // 主考科目
|
||||
Limitation string `json:"limitation"` // 限制条件
|
||||
ChineseScoreLimitation float64 `json:"chineseScoreLimitation"` // 语文分数限制
|
||||
EnglishScoreLimitation float64 `json:"englishScoreLimitation"` // 英语分数限制
|
||||
CulturalScoreLimitation float64 `json:"culturalScoreLimitation"` // 文化成绩限制
|
||||
ProfessionalScoreLimitation float64 `json:"professionalScoreLimitation"` // 专业分数限制
|
||||
EnrollmentCode string `json:"enrollmentCode"` // 招生代码
|
||||
Tuition string `json:"tuition"` // 学费
|
||||
Detail string `json:"detail"` // 详情
|
||||
Category string `json:"category"` // 类别
|
||||
Batch string `json:"batch"` // 批次
|
||||
RulesEnrollProbability string `json:"rulesEnrollProbability"` // 录取规则概率
|
||||
ProbabilityOperator string `json:"probabilityOperator"` // 概率操作符
|
||||
Kslx string `json:"kslx"` // 考试类型
|
||||
State string `json:"state"` // 状态
|
||||
Province string `json:"province"` // 省份
|
||||
SchoolNature string `json:"schoolNature"` // 院校性质
|
||||
InstitutionType string `json:"institutionType"` // 院校类型
|
||||
EnrollProbability float64 `json:"enrollProbability"` // 录取概率
|
||||
StudentScore float64 `json:"studentScore"` // 学生分数
|
||||
}
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
package dto
|
||||
|
||||
import (
|
||||
userVO "server/modules/user/vo"
|
||||
"server/modules/yx/vo"
|
||||
)
|
||||
|
||||
type UserMajorDTO struct {
|
||||
SchoolCode string `json:"schoolCode"`
|
||||
SchoolName string `json:"schoolName"`
|
||||
MajorCode string `json:"majorCode"`
|
||||
MajorName string `json:"majorName"`
|
||||
MajorType string `json:"majorType"`
|
||||
MajorTypeChild string `json:"majorTypeChild"`
|
||||
PlanNum int `json:"planNum"`
|
||||
MainSubjects string `json:"mainSubjects"`
|
||||
Limitation string `json:"limitation"`
|
||||
ChineseScoreLimitation float64 `json:"chineseScoreLimitation"`
|
||||
EnglishScoreLimitation float64 `json:"englishScoreLimitation"`
|
||||
CulturalScoreLimitation float64 `json:"culturalScoreLimitation"`
|
||||
ProfessionalScoreLimitation float64 `json:"professionalScoreLimitation"`
|
||||
EnrollmentCode string `json:"enrollmentCode"`
|
||||
Tuition string `json:"tuition"`
|
||||
Detail string `json:"detail"`
|
||||
Category string `json:"category"`
|
||||
Batch string `json:"batch"`
|
||||
RulesEnrollProbability string `json:"rulesEnrollProbability"`
|
||||
ProbabilityOperator string `json:"probabilityOperator"`
|
||||
// PrivateRulesEnrollProbability string `json:"privateRulesEnrollProbability"`
|
||||
// PrivateProbabilityOperator string `json:"privateProbabilityOperator"`
|
||||
RulesEnrollProbabilitySx string `json:"rulesEnrollProbabilitySx"`
|
||||
Kslx string `json:"kslx"`
|
||||
State string `json:"state"`
|
||||
HistoryMajorEnrollMap map[string]YxHistoryMajorEnrollDTO `json:"historyMajorEnrollMap"`
|
||||
// 计算相关字段 (非数据库直接映射)
|
||||
EnrollProbability float64 `json:"enrollProbability"` // 录取率
|
||||
StudentScore float64 `json:"studentScore" gorm:"column:studentScore"` // 学生折合分
|
||||
// PrivateStudentScore float64 `json:"privateStudentScore"` // 学生折合分(私有)
|
||||
// StudentConvertedScore float64 `json:"studentConvertedScore"` // 学生折合分(转换后)
|
||||
// FirstLevelDiscipline string `json:"firstLevelDiscipline"` // 一级学科 (需确认来源)
|
||||
|
||||
Province string `json:"province"` // 省份
|
||||
SchoolNature string `json:"schoolNature"` // 院校性质
|
||||
InstitutionType string `json:"institutionType"` // 院校类型
|
||||
}
|
||||
|
||||
// SchoolMajorDTO 院校专业查询结果 DTO
|
||||
type SchoolMajorDTO struct {
|
||||
SchoolCode string `json:"schoolCode"`
|
||||
SchoolName string `json:"schoolName"`
|
||||
MajorCode string `json:"majorCode"`
|
||||
MajorName string `json:"majorName"`
|
||||
MajorType string `json:"majorType"`
|
||||
MajorTypeChild string `json:"majorTypeChild"`
|
||||
PlanNum int `json:"planNum"`
|
||||
MainSubjects string `json:"mainSubjects"`
|
||||
Limitation string `json:"limitation"`
|
||||
ChineseScoreLimitation float64 `json:"chineseScoreLimitation"`
|
||||
EnglishScoreLimitation float64 `json:"englishScoreLimitation"`
|
||||
CulturalScoreLimitation float64 `json:"culturalScoreLimitation"`
|
||||
ProfessionalScoreLimitation float64 `json:"professionalScoreLimitation"`
|
||||
EnrollmentCode string `json:"enrollmentCode"`
|
||||
Tuition string `json:"tuition"`
|
||||
Detail string `json:"detail"`
|
||||
Category string `json:"category"`
|
||||
Batch string `json:"batch"`
|
||||
RulesEnrollProbability string `json:"rulesEnrollProbability"`
|
||||
ProbabilityOperator string `json:"probabilityOperator"`
|
||||
PrivateRulesEnrollProbability string `json:"privateRulesEnrollProbability"`
|
||||
PrivateProbabilityOperator string `json:"privateProbabilityOperator"`
|
||||
RulesEnrollProbabilitySx string `json:"rulesEnrollProbabilitySx"`
|
||||
Kslx string `json:"kslx"`
|
||||
State string `json:"state"`
|
||||
HistoryMajorEnrollMap map[string]YxHistoryMajorEnrollDTO `json:"historyMajorEnrollMap"`
|
||||
// 计算相关字段 (非数据库直接映射)
|
||||
HistoryMajorEnrollList []vo.YxHistoryMajorEnrollVO `json:"historyMajorEnrollList"`
|
||||
EnrollProbability float64 `json:"enrollProbability"` // 录取率
|
||||
StudentScore float64 `json:"studentScore"` // 学生折合分
|
||||
PrivateStudentScore float64 `json:"privateStudentScore"` // 学生折合分(私有)
|
||||
StudentConvertedScore float64 `json:"studentConvertedScore"` // 学生折合分(转换后)
|
||||
FirstLevelDiscipline string `json:"firstLevelDiscipline"` // 一级学科 (需确认来源)
|
||||
Province string `json:"province"` // 省份
|
||||
SchoolNature string `json:"schoolNature" gorm:"column:schoolNature"` // 院校性质
|
||||
InstitutionType string `json:"institutionType" gorm:"column:institutionType"` // 院校类型
|
||||
}
|
||||
|
||||
type YxHistoryMajorEnrollDTO struct {
|
||||
Year string `json:"year"`
|
||||
EnrollmentCode string `json:"enrollmentCode"`
|
||||
EnrollmentCount int `json:"enrollmentCount"`
|
||||
RulesEnrollProbability string `json:"rulesEnrollProbability"`
|
||||
ProbabilityOperator string `json:"probabilityOperator"`
|
||||
AdmissionLine float64 `json:"admissionLine"`
|
||||
ControlLine float64 `json:"controlLine"`
|
||||
// 其他字段...
|
||||
}
|
||||
|
||||
// SchoolMajorQuery 院校专业查询条件
|
||||
type SchoolMajorQuery struct {
|
||||
Page int `json:"page"`
|
||||
Size int `json:"size"`
|
||||
Keyword string `json:"keyword"` // 对应 keyword
|
||||
MajorType string `json:"majorType"` // 对应 major_type
|
||||
Category string `json:"category"` // 对应 category
|
||||
Batch string `json:"batch"` // 对应 批次
|
||||
Batch2 string `json:"batch2"` // 对应 批次2
|
||||
MajorTypeChildren []string `json:"majorTypeChildren"` // 对应 major_type_child in (...)
|
||||
MainSubjects string `json:"mainSubjects"` // 对应 main_subjects
|
||||
Probability string `json:"probability"` // 对应 录取概率类型
|
||||
ScoreId string `json:"scoreId"` // 对应 score_id
|
||||
TagList []string `json:"tagList"` // 对应 tags in (...)
|
||||
SchoolNatureList []string `json:"schoolNatureList"` // 对应 school_nature in (...)
|
||||
AddressList []string `json:"addressList"` // 对应 address in (...)
|
||||
KyjxList []string `json:"kyjxList"` // 对应 kyjx in (...)
|
||||
RulesEnrollProbabilityList []string `json:"rulesEnrollProbabilityList"` // 录取方式,对应 rules_enroll_probability in (...)
|
||||
LoginUserId string `json:"loginUserId"` // 登录用户 ID
|
||||
UserScoreVO userVO.UserScoreVO `json:"userScoreVO"` // 用户成绩 VO
|
||||
CalculationTableName string `json:"calculationTableName"` // 对应 calculation_table_name
|
||||
SchoolCode string `json:"schoolCode"` // 对应的 院校代码
|
||||
}
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
package dto
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
// SaveScoreRequest 保存成绩请求
|
||||
type SaveScoreRequest struct {
|
||||
CognitioPolyclinic string `json:"CognitioPolyclinic" binding:"required"`
|
||||
SubjectList []string `json:"SubjectList" binding:"required"`
|
||||
ProfessionalCategory string `json:"ProfessionalCategory" binding:"required"`
|
||||
ProfessionalCategoryChildren []string `json:"ProfessionalCategoryChildren" binding:"required"`
|
||||
ProfessionalCategoryChildrenScore map[string]float64 `json:"ProfessionalCategoryChildrenScore" binding:"required"`
|
||||
ProfessionalScore *float64 `json:"ProfessionalScore" binding:"required"`
|
||||
CulturalScore *float64 `json:"CulturalScore" binding:"required"`
|
||||
EnglishScore *float64 `json:"EnglishScore" binding:"required"`
|
||||
ChineseScore *float64 `json:"ChineseScore" binding:"required"`
|
||||
Province string `json:"Province" binding:"required"`
|
||||
CreateBy string
|
||||
}
|
||||
|
||||
func (req *SaveScoreRequest) Validate() error {
|
||||
if req.CognitioPolyclinic != "文科" && req.CognitioPolyclinic != "理科" {
|
||||
return errors.New("考试类型必须是'物理组'或'历史组'")
|
||||
}
|
||||
|
||||
if req.ProfessionalCategory == "表演类" || req.ProfessionalCategory == "音乐类" {
|
||||
if len(req.ProfessionalCategoryChildren) == 0 {
|
||||
return errors.New("表演类或音乐类必须至少选一个专业子级")
|
||||
}
|
||||
} else {
|
||||
req.ProfessionalCategoryChildren = []string{}
|
||||
}
|
||||
|
||||
if len(req.SubjectList) > 3 {
|
||||
return errors.New("选考科目有且最多只能传三个值")
|
||||
}
|
||||
|
||||
validSubjects := map[string]bool{"地理": true, "政治": true, "历史": true, "化学": true, "生物": true}
|
||||
for _, s := range req.SubjectList {
|
||||
if !validSubjects[s] {
|
||||
return errors.New("选考科目参数有误")
|
||||
}
|
||||
}
|
||||
if !isValidScore(*req.ProfessionalScore, 300) {
|
||||
return errors.New("统考成绩必须在0-300之间")
|
||||
}
|
||||
if !isValidScore(*req.CulturalScore, 750) {
|
||||
return errors.New("文化成绩必须在0-750之间")
|
||||
}
|
||||
if !isValidScore(*req.EnglishScore, 150) {
|
||||
return errors.New("英文成绩必须在0-150之间")
|
||||
}
|
||||
if !isValidScore(*req.ChineseScore, 150) {
|
||||
return errors.New("中文成绩必须在0-150之间")
|
||||
}
|
||||
// TODO 在这里判断一下 专业子级,如 表演类只有:"服装表演", "戏剧影视表演", "戏剧影视导演"。音乐类只有:音乐表演声乐、音乐表演器乐、音乐教育。
|
||||
validProfessionalChildren := map[string]string{ // 子级 -> 父级分类
|
||||
"服装表演": "表演类",
|
||||
"戏剧影视表演": "表演类",
|
||||
"戏剧影视导演": "表演类",
|
||||
"音乐表演声乐": "音乐类",
|
||||
"音乐表演器乐": "音乐类",
|
||||
"音乐教育": "音乐类",
|
||||
}
|
||||
// 统计各分类的数量
|
||||
categoryCount := make(map[string]int)
|
||||
for childName, score := range req.ProfessionalCategoryChildrenScore {
|
||||
// 验证子级名称是否合法
|
||||
parentCategory, exists := validProfessionalChildren[childName]
|
||||
if !exists {
|
||||
return errors.New("不支持的专业子级: " + childName)
|
||||
}
|
||||
// 统计分类数量
|
||||
categoryCount[parentCategory]++
|
||||
// 验证分数范围
|
||||
if !isValidScore(score, 300) {
|
||||
return errors.New(childName + "成绩必须在0-300之间")
|
||||
}
|
||||
}
|
||||
// 分类数量限制,比如每类最多几个
|
||||
for category, count := range categoryCount {
|
||||
if category == "音乐类" {
|
||||
// 判断 子级是否包含 xx
|
||||
if req.ProfessionalCategoryChildrenScore["音乐表演声乐"] > 0 && req.ProfessionalCategoryChildrenScore["音乐表演器乐"] > 0 {
|
||||
return errors.New("音乐类子级不可同时选择'音乐表演声乐'和'音乐表演器乐'")
|
||||
}
|
||||
}
|
||||
if count > 3 { // 假设每类最多3个
|
||||
return errors.New(category + "最多只能有3个子级")
|
||||
}
|
||||
for _, key := range req.ProfessionalCategoryChildren {
|
||||
if req.ProfessionalCategoryChildrenScore[key] == 0 {
|
||||
return errors.New(key + "成绩不能为空")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 专业子级和成绩数据一致性校验
|
||||
if err := req.validateProfessionalConsistency(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (req *SaveScoreRequest) validateProfessionalConsistency() error {
|
||||
// 特殊情况:如果不需要专业子级,则两个都应该为空
|
||||
if len(req.ProfessionalCategoryChildren) == 0 {
|
||||
if len(req.ProfessionalCategoryChildrenScore) != 0 {
|
||||
return errors.New("未选择专业子级时,成绩数据应为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// 创建映射用于双向验证
|
||||
childrenMap := make(map[string]bool)
|
||||
for _, child := range req.ProfessionalCategoryChildren {
|
||||
if childrenMap[child] {
|
||||
return errors.New("专业子级 '" + child + "' 重复")
|
||||
}
|
||||
childrenMap[child] = true
|
||||
}
|
||||
// 验证长度一致
|
||||
if len(childrenMap) != len(req.ProfessionalCategoryChildrenScore) {
|
||||
return errors.New("专业子级列表与成绩数据数量不匹配")
|
||||
}
|
||||
// 双向验证一致性
|
||||
for childName := range req.ProfessionalCategoryChildrenScore {
|
||||
if !childrenMap[childName] {
|
||||
return errors.New("成绩数据中的专业子级 '" + childName + "' 未在选中列表中")
|
||||
}
|
||||
}
|
||||
for _, child := range req.ProfessionalCategoryChildren {
|
||||
if _, exists := req.ProfessionalCategoryChildrenScore[child]; !exists {
|
||||
return errors.New("选中的专业子级 '" + child + "' 缺少成绩数据")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isValidScore(score float64, max float64) bool {
|
||||
return score >= 0 && score <= max
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package dto
|
||||
|
||||
// CreateVolunteerRequest 创建志愿请求
|
||||
type CreateVolunteerRequest struct {
|
||||
VolunteerName string `json:"volunteerName" binding:"required"` // 志愿单名称
|
||||
ScoreId string `json:"scoreId" binding:"required"` // 关联成绩ID
|
||||
CreateType string `json:"createType"` // 生成类型(1.手动生成,2.智能生成)
|
||||
}
|
||||
|
||||
// UpdateVolunteerRequest 更新志愿请求
|
||||
type UpdateVolunteerRequest struct {
|
||||
VolunteerName string `json:"volunteerName"` // 志愿单名称
|
||||
State string `json:"state"` // 志愿单状态(0-否,1-正在使用,2-历史)
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
// Package entity 实体层
|
||||
package entity
|
||||
|
||||
import "time"
|
||||
|
||||
// YxCalculationMajor 计算专业表实体
|
||||
type YxCalculationMajor struct {
|
||||
ID string `gorm:"column:id;primaryKey" json:"id"`
|
||||
ScoreID string `gorm:"column:score_id" json:"scoreId"`
|
||||
SchoolCode string `gorm:"column:school_code" json:"schoolCode"`
|
||||
MajorCode string `gorm:"column:major_code" json:"majorCode"`
|
||||
MajorName string `gorm:"column:major_name" json:"majorName"`
|
||||
EnrollmentCode string `gorm:"column:enrollment_code" json:"enrollmentCode"`
|
||||
Tuition string `gorm:"column:tuition" json:"tuition"`
|
||||
Detail string `gorm:"column:detail" json:"detail"`
|
||||
Category string `gorm:"column:category" json:"category"`
|
||||
RulesEnrollProbability string `gorm:"column:rules_enroll_probability" json:"rulesEnrollProbability"`
|
||||
Batch string `gorm:"column:batch" json:"batch"`
|
||||
StudentOldConvertedScore float64 `gorm:"column:student_old_converted_score" json:"studentOldConvertedScore"`
|
||||
StudentConvertedScore float64 `gorm:"column:student_converted_score" json:"studentConvertedScore"`
|
||||
EnrollProbability float64 `gorm:"column:enroll_probability" json:"enrollProbability"`
|
||||
ProbabilityOperator string `gorm:"column:probability_operator" json:"probabilityOperator"`
|
||||
CreateTime time.Time `gorm:"column:create_time" json:"createTime"`
|
||||
MajorType string `gorm:"column:major_type" json:"majorType"`
|
||||
MajorTypeChild string `gorm:"column:major_type_child" json:"majorTypeChild"`
|
||||
PlanNum int `gorm:"column:plan_num" json:"planNum"`
|
||||
MainSubjects string `gorm:"column:main_subjects" json:"mainSubjects"`
|
||||
Limitation string `gorm:"column:limitation" json:"limitation"`
|
||||
OtherScoreLimitation string `gorm:"column:other_score_limitation" json:"otherScoreLimitation"`
|
||||
RulesEnrollProbabilitySx string `gorm:"column:rules_enroll_probability_sx" json:"rulesEnrollProbabilitySx"`
|
||||
Kslx string `gorm:"column:kslx" json:"kslx"`
|
||||
PrivateStudentConvertedScore float64 `gorm:"column:private_student_converted_score" json:"privateStudentConvertedScore"`
|
||||
PrivateRulesEnrollProbability string `gorm:"column:private_rules_enroll_probability" json:"privateRulesEnrollProbability"`
|
||||
PrivateProbabilityOperator string `gorm:"column:private_probability_operator" json:"privateProbabilityOperator"`
|
||||
State string `gorm:"column:state" json:"state"`
|
||||
}
|
||||
|
||||
func (YxCalculationMajor) TableName() string {
|
||||
return "yx_calculation_major_2025_2"
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
// Package entity 实体层
|
||||
package entity
|
||||
|
||||
import "time"
|
||||
|
||||
// YxHistoryMajorEnroll 历年艺术类招生录取分数表实体
|
||||
type YxHistoryMajorEnroll struct {
|
||||
ID string `gorm:"column:id;primaryKey" json:"id"`
|
||||
SchoolCode string `gorm:"column:school_code" json:"schoolCode"`
|
||||
SchoolName string `gorm:"column:school_name" json:"schoolName"`
|
||||
InstitutionCode string `gorm:"column:institution_code" json:"institutionCode"`
|
||||
MajorCode string `gorm:"column:major_code" json:"majorCode"`
|
||||
MajorName string `gorm:"column:major_name" json:"majorName"`
|
||||
MajorType string `gorm:"column:major_type" json:"majorType"`
|
||||
EnrollmentCode string `gorm:"column:enrollment_code" json:"enrollmentCode"`
|
||||
Category string `gorm:"column:category" json:"category"`
|
||||
Year string `gorm:"column:year" json:"year"`
|
||||
EnrollNum int `gorm:"column:enroll_num" json:"enrollNum"`
|
||||
ScoreLineDifference float64 `gorm:"column:score_line_difference" json:"scoreLineDifference"`
|
||||
CreateBy string `gorm:"column:create_by" json:"createBy"`
|
||||
CreateTime time.Time `gorm:"column:create_time" json:"createTime"`
|
||||
UpdateBy string `gorm:"column:update_by" json:"updateBy"`
|
||||
UpdateTime time.Time `gorm:"column:update_time" json:"updateTime"`
|
||||
SysOrgCode string `gorm:"column:sys_org_code" json:"sysOrgCode"`
|
||||
Detail string `gorm:"column:detail" json:"detail"`
|
||||
RulesEnrollProbability string `gorm:"column:rules_enroll_probability" json:"rulesEnrollProbability"`
|
||||
ControlLine float64 `gorm:"column:control_line" json:"controlLine"`
|
||||
AdmissionLine float64 `gorm:"column:admission_line" json:"admissionLine"`
|
||||
ProbabilityOperator string `gorm:"column:probability_operator" json:"probabilityOperator"`
|
||||
Batch string `gorm:"column:batch" json:"batch"`
|
||||
OneVolunteerAdmissionNum int `gorm:"column:one_volunteer_admission_num" json:"oneVolunteerAdmissionNum"`
|
||||
AdmissionNum int `gorm:"column:admission_num" json:"admissionNum"`
|
||||
ActualPitcherNum int `gorm:"column:actual_pitcher_num" json:"actualPitcherNum"`
|
||||
CheckMaster string `gorm:"column:check_master" json:"checkMaster"`
|
||||
MajorTypeChild string `gorm:"column:major_type_child" json:"majorTypeChild"`
|
||||
MainSubjects string `gorm:"column:main_subjects" json:"mainSubjects"`
|
||||
Tuition string `gorm:"column:tuition" json:"tuition"`
|
||||
}
|
||||
|
||||
func (YxHistoryMajorEnroll) TableName() string {
|
||||
return "yx_history_major_enroll"
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package entity
|
||||
|
||||
import "time"
|
||||
|
||||
// YxHistoryScoreControlLine 历年各专业省控分数线实体
|
||||
type YxHistoryScoreControlLine struct {
|
||||
ID string `gorm:"column:id;primaryKey" json:"id"`
|
||||
Year string `gorm:"column:year" json:"year"` // 年份
|
||||
ProfessionalCategory string `gorm:"column:professional_category" json:"professionalCategory"` // 专业类别
|
||||
Category string `gorm:"column:category" json:"category"` // 文理科
|
||||
Batch string `gorm:"column:batch" json:"batch"` // 批次
|
||||
CulturalScore float64 `gorm:"column:cultural_score" json:"culturalScore"` // 文化分
|
||||
SpecialScore float64 `gorm:"column:special_score" json:"specialScore"` // 专业分
|
||||
CreateBy string `gorm:"column:create_by" json:"createBy"`
|
||||
CreateTime time.Time `gorm:"column:create_time" json:"createTime"`
|
||||
UpdateBy string `gorm:"column:update_by" json:"updateBy"`
|
||||
UpdateTime time.Time `gorm:"column:update_time" json:"updateTime"`
|
||||
}
|
||||
|
||||
func (YxHistoryScoreControlLine) TableName() string {
|
||||
return "yx_history_score_control_line_new"
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
// Package entity 实体层
|
||||
package entity
|
||||
|
||||
import "time"
|
||||
|
||||
// YxSchoolMajor 院校专业关联表实体
|
||||
type YxSchoolMajor struct {
|
||||
ID string `gorm:"column:id;primaryKey" json:"id"`
|
||||
SchoolCode string `gorm:"column:school_code" json:"schoolCode"`
|
||||
SchoolName string `gorm:"column:school_name" json:"schoolName"`
|
||||
MajorCode string `gorm:"column:major_code" json:"majorCode"`
|
||||
MajorName string `gorm:"column:major_name" json:"majorName"`
|
||||
MajorType string `gorm:"column:major_type" json:"majorType"`
|
||||
MajorTypeChild string `gorm:"column:major_type_child" json:"majorTypeChild"`
|
||||
MainSubjects string `gorm:"column:main_subjects" json:"mainSubjects"`
|
||||
EnrollmentCode string `gorm:"column:enrollment_code" json:"enrollmentCode"`
|
||||
Category string `gorm:"column:category" json:"category"`
|
||||
Batch string `gorm:"column:batch" json:"batch"`
|
||||
Tuition string `gorm:"column:tuition" json:"tuition"`
|
||||
PlanNum int `gorm:"column:plan_num" json:"planNum"`
|
||||
Detail string `gorm:"column:detail" json:"detail"`
|
||||
Semester string `gorm:"column:semester" json:"semester"`
|
||||
CreateBy string `gorm:"column:create_by" json:"createBy"`
|
||||
CreateTime time.Time `gorm:"column:create_time" json:"createTime"`
|
||||
UpdateBy string `gorm:"column:update_by" json:"updateBy"`
|
||||
UpdateTime time.Time `gorm:"column:update_time" json:"updateTime"`
|
||||
RulesEnrollProbabilitySx string `gorm:"column:rules_enroll_probability_sx" json:"rulesEnrollProbabilitySx"`
|
||||
RulesEnrollProbability string `gorm:"column:rules_enroll_probability" json:"rulesEnrollProbability"`
|
||||
ProbabilityOperator string `gorm:"column:probability_operator" json:"probabilityOperator"`
|
||||
CulturalControlLine float64 `gorm:"column:cultural_control_line" json:"culturalControlLine"`
|
||||
SpecialControlLine float64 `gorm:"column:special_control_line" json:"specialControlLine"`
|
||||
CheckMaster string `gorm:"column:check_master" json:"checkMaster"`
|
||||
Limitation string `gorm:"column:limitation" json:"limitation"`
|
||||
ProfessionalScoreLimitation float64 `gorm:"column:professional_score_limitation" json:"professionalScoreLimitation"`
|
||||
EnglishScoreLimitation float64 `gorm:"column:english_score_limitation" json:"englishScoreLimitation"`
|
||||
ChineseScoreLimitation float64 `gorm:"column:chinese_score_limitation" json:"chineseScoreLimitation"`
|
||||
CulturalScoreLimitation float64 `gorm:"column:cultural_score_limitation" json:"culturalScoreLimitation"`
|
||||
Kslx string `gorm:"column:kslx" json:"kslx"`
|
||||
PrivateProbabilityOperator string `gorm:"column:private_probability_operator" json:"privateProbabilityOperator"`
|
||||
PrivateRulesEnrollProbability string `gorm:"column:private_rules_enroll_probability" json:"privateRulesEnrollProbability"`
|
||||
State string `gorm:"column:state" json:"state"`
|
||||
}
|
||||
|
||||
func (YxSchoolMajor) TableName() string {
|
||||
return "yx_school_major"
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package entity
|
||||
|
||||
import "time"
|
||||
|
||||
// YxUserScore 用户分数信息表实体
|
||||
type YxUserScore struct {
|
||||
ID string `gorm:"column:id;primaryKey" json:"id"`
|
||||
Type string `gorm:"column:type;default:1" json:"type"` // 填报类型(1-普通类 2-艺术类)
|
||||
EducationalLevel string `gorm:"column:educational_level;default:1" json:"educationalLevel"` // 学历层次(1-本科,2-专科)
|
||||
ProfessionalCategory string `gorm:"column:professional_category;default:美术类" json:"professionalCategory"` // 专业类别(美术类/...)
|
||||
Subjects string `gorm:"column:subjects" json:"subjects"` // 选课
|
||||
ProfessionalScore float64 `gorm:"column:professional_score;default:0" json:"professionalScore"` // 专业成绩分
|
||||
CulturalScore float64 `gorm:"column:cultural_score;default:0" json:"culturalScore"` // 文化成绩分
|
||||
Ranking int `gorm:"column:ranking;default:0" json:"ranking"` // 位次
|
||||
CreateBy string `gorm:"column:create_by" json:"createBy"` // 创建人
|
||||
CreateTime time.Time `gorm:"column:create_time" json:"createTime"` // 创建时间
|
||||
UpdateBy string `gorm:"column:update_by" json:"updateBy"` // 修改人
|
||||
UpdateTime time.Time `gorm:"column:update_time" json:"updateTime"` // 修改时间
|
||||
State string `gorm:"column:state;default:1" json:"state"` // 状态(0-未使用,1-使用中)
|
||||
Province string `gorm:"column:province;default:北京" json:"province"` // 高考省份
|
||||
CognitioPolyclinic string `gorm:"column:cognitio_polyclinic" json:"cognitioPolyclinic"` // 文理分班(文科/理科)
|
||||
Batch string `gorm:"column:batch" json:"batch"` // 录取批次
|
||||
EnglishScore float64 `gorm:"column:english_score;default:0.00" json:"englishScore"` // 英语成绩
|
||||
ChineseScore float64 `gorm:"column:chinese_score;default:0.00" json:"chineseScore"` // 语文成绩
|
||||
Yybysy float64 `gorm:"column:yybysy;default:0.00" json:"yybysy"` // 音乐表演声乐
|
||||
Yybyqy float64 `gorm:"column:yybyqy;default:0.00" json:"yybyqy"` // 音乐表演器乐
|
||||
Yyjy float64 `gorm:"column:yyjy;default:0.00" json:"yyjy"` // 音乐教育
|
||||
Xjysdy float64 `gorm:"column:xjysdy;default:0.00" json:"xjysdy"` // 戏剧影视导演
|
||||
Xjysby float64 `gorm:"column:xjysby;default:0.00" json:"xjysby"` // 戏剧影视表演
|
||||
Fzby float64 `gorm:"column:fzby;default:0.00" json:"fzby"` // 服装表演
|
||||
ProfessionalCategoryChildren string `gorm:"column:professional_category_children" json:"professionalCategoryChildren"` // 子级专业类别
|
||||
KbdNum int `gorm:"column:kbd_num;default:0" json:"kbdNum"` // 可保底专业数量
|
||||
NlqNum int `gorm:"column:nlq_num;default:0" json:"nlqNum"` // 难录取专业数量
|
||||
KcjNum int `gorm:"column:kcj_num;default:0" json:"kcjNum"` // 可冲击专业数量
|
||||
JwtNum int `gorm:"column:jwt_num;default:0" json:"jwtNum"` // 较稳妥专业数量
|
||||
CalculationTableName string `gorm:"column:calculation_table_name" json:"calculationTableName"` // 记录结果表名
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (YxUserScore) TableName() string {
|
||||
return "yx_user_score"
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package entity
|
||||
|
||||
import "time"
|
||||
|
||||
// YxVolunteer 志愿表实体
|
||||
type YxVolunteer struct {
|
||||
ID string `gorm:"column:id;primaryKey" json:"id"`
|
||||
VolunteerName string `gorm:"column:volunteer_name" json:"volunteerName"` // 志愿单名称
|
||||
ScoreId string `gorm:"column:score_id" json:"scoreId"` // 使用成绩id
|
||||
CreateType string `gorm:"column:create_type;default:1" json:"createType"` // 生成类型(1.手动生成,2.智能生成)
|
||||
State string `gorm:"column:state;default:0" json:"state"` // 志愿单状态(0-否,1.正在使用,2-历史)
|
||||
CreateBy string `gorm:"column:create_by" json:"createBy"` // 创建人
|
||||
CreateTime time.Time `gorm:"column:create_time" json:"createTime"` // 创建日期
|
||||
UpdateBy string `gorm:"column:update_by" json:"updateBy"` // 更新人
|
||||
UpdateTime time.Time `gorm:"column:update_time" json:"updateTime"` // 更新日期
|
||||
SysOrgCode string `gorm:"column:sys_org_code" json:"sysOrgCode"` // 所属部门
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (YxVolunteer) TableName() string {
|
||||
return "yx_volunteer"
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package entity
|
||||
|
||||
import "time"
|
||||
|
||||
// YxVolunteerRecord 志愿明细表实体
|
||||
type YxVolunteerRecord struct {
|
||||
ID string `gorm:"column:id;primaryKey" json:"id"`
|
||||
VolunteerID string `gorm:"column:volunteer_id" json:"volunteerId"` // 志愿单id
|
||||
SchoolCode string `gorm:"column:school_code" json:"schoolCode"` // 学校编码
|
||||
MajorCode string `gorm:"column:major_code" json:"majorCode"` // 专业编码
|
||||
EnrollmentCode string `gorm:"column:enrollment_code" json:"enrollmentCode"` // 招生代码
|
||||
Indexs int `gorm:"column:indexs;default:1" json:"indexs"` // 志愿顺序
|
||||
CreateBy string `gorm:"column:create_by" json:"createBy"` // 创建人
|
||||
CreateTime time.Time `gorm:"column:create_time" json:"createTime"` // 创建日期
|
||||
Batch string `gorm:"column:batch" json:"batch"` // 录取批次
|
||||
EnrollProbability float64 `gorm:"column:enroll_probability;default:0.0000" json:"enrollProbability"` // 录取概率
|
||||
StudentConvertedScore float64 `gorm:"column:student_converted_score;default:0.0000" json:"studentConvertedScore"` // 折合分数
|
||||
Fctj int `gorm:"column:fctj;default:0" json:"fctj"` // 服从调剂
|
||||
CalculationMajorID string `gorm:"column:calculation_major_id" json:"calculationMajorId"` // 专业折算id
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (YxVolunteerRecord) TableName() string {
|
||||
return "yx_volunteer_record"
|
||||
}
|
||||
|
|
@ -0,0 +1,527 @@
|
|||
// Package mapper 数据访问层
|
||||
package mapper
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"server/common"
|
||||
"server/config"
|
||||
"server/modules/yx/dto"
|
||||
"server/modules/yx/entity"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type YxCalculationMajorMapper struct {
|
||||
*common.BaseMapper[entity.YxCalculationMajor]
|
||||
}
|
||||
|
||||
func NewYxCalculationMajorMapper() *YxCalculationMajorMapper {
|
||||
return &YxCalculationMajorMapper{
|
||||
BaseMapper: common.NewBaseMapper[entity.YxCalculationMajor](),
|
||||
}
|
||||
}
|
||||
|
||||
// QueryCostTime 存储各协程耗时的结构体
|
||||
type QueryCostTime struct {
|
||||
CountCost time.Duration // 总数量查询耗时
|
||||
ProbCountCost time.Duration // 四种概率数量查询耗时
|
||||
QueryCost time.Duration // 主列表查询耗时
|
||||
TotalCost time.Duration // 整体总耗时
|
||||
}
|
||||
|
||||
// FindRecommendList 查询推荐专业列表(优化版:并发查询总数量、概率数量、主列表)
|
||||
func (m *YxCalculationMajorMapper) FindRecommendList(query dto.SchoolMajorQuery) ([]dto.UserMajorDTO, int64, dto.ProbabilityCountDTO, error) {
|
||||
var items []dto.UserMajorDTO
|
||||
var total int64
|
||||
var probCount dto.ProbabilityCountDTO // 四种概率的数量统计结果
|
||||
|
||||
// 1. 表名合法性校验:非空 + 白名单
|
||||
tableName := query.UserScoreVO.CalculationTableName
|
||||
if tableName == "" {
|
||||
return nil, 0, dto.ProbabilityCountDTO{}, fmt.Errorf("CalculationTableName is empty")
|
||||
}
|
||||
|
||||
// 2. 基础条件SQL(共用过滤条件,排除概率筛选)
|
||||
baseSQL := " WHERE 1=1 AND cm.state > 0 "
|
||||
params := []interface{}{}
|
||||
|
||||
// 拼接共用过滤条件(与原有列表查询条件一致,保证统计结果准确性)
|
||||
if query.UserScoreVO.ID != "" {
|
||||
baseSQL += " AND cm.score_id = ?"
|
||||
params = append(params, query.UserScoreVO.ID)
|
||||
}
|
||||
if query.SchoolCode != "" {
|
||||
baseSQL += " AND cm.school_code = ?"
|
||||
params = append(params, query.SchoolCode)
|
||||
}
|
||||
if query.MajorType != "" {
|
||||
baseSQL += " AND cm.major_type = ?"
|
||||
params = append(params, query.MajorType)
|
||||
}
|
||||
if query.Category != "" {
|
||||
baseSQL += " AND cm.category = ?"
|
||||
params = append(params, query.Category)
|
||||
}
|
||||
if len(query.MajorTypeChildren) > 0 {
|
||||
placeholders := strings.Repeat("?,", len(query.MajorTypeChildren)-1) + "?"
|
||||
baseSQL += " AND cm.major_type_child IN (" + placeholders + ")"
|
||||
for _, v := range query.MajorTypeChildren {
|
||||
params = append(params, v)
|
||||
}
|
||||
}
|
||||
|
||||
if "" != query.Batch {
|
||||
baseSQL += " AND cm.batch = ?"
|
||||
params = append(params, query.Batch)
|
||||
}
|
||||
|
||||
if query.MainSubjects != "" {
|
||||
baseSQL += " AND cm.main_subjects = ?"
|
||||
params = append(params, query.MainSubjects)
|
||||
}
|
||||
|
||||
if query.Keyword != "" {
|
||||
baseSQL += " AND cm.major_name like ?"
|
||||
params = append(params, query.Keyword)
|
||||
}
|
||||
|
||||
// 3. 优化后的总数量COUNT SQL
|
||||
countSQL := fmt.Sprintf(`
|
||||
SELECT COUNT(cm.id) FROM %s cm
|
||||
%s
|
||||
`, tableName, baseSQL)
|
||||
|
||||
// 4. 四种概率批量统计SQL(使用CASE WHEN一次查询,性能最优)
|
||||
probCountSQL := fmt.Sprintf(`
|
||||
SELECT
|
||||
SUM(CASE WHEN cm.enroll_probability < 60 THEN 1 ELSE 0 END) AS hard,
|
||||
SUM(CASE WHEN cm.enroll_probability >= 60 AND cm.enroll_probability < 73 THEN 1 ELSE 0 END) AS risky,
|
||||
SUM(CASE WHEN cm.enroll_probability >= 73 AND cm.enroll_probability < 93 THEN 1 ELSE 0 END) AS stable,
|
||||
SUM(CASE WHEN cm.enroll_probability >= 93 THEN 1 ELSE 0 END) AS safe
|
||||
FROM %s cm
|
||||
%s
|
||||
`, tableName, baseSQL)
|
||||
|
||||
// 5. 主查询SQL(保留原有字段和JOIN)
|
||||
mainSQL := fmt.Sprintf(`
|
||||
SELECT
|
||||
cm.id,
|
||||
s.school_name,
|
||||
s.school_icon,
|
||||
cm.state,
|
||||
cm.school_code,
|
||||
cm.major_code,
|
||||
cm.major_name,
|
||||
cm.enrollment_code,
|
||||
cm.tuition,
|
||||
cm.detail as majorDetail,
|
||||
cm.category,
|
||||
cm.batch,
|
||||
cm.private_student_converted_score as privateStudentScore,
|
||||
cm.student_old_converted_score as studentScore,
|
||||
cm.student_converted_score,
|
||||
cm.enroll_probability,
|
||||
cm.rules_enroll_probability_sx,
|
||||
cm.rules_enroll_probability,
|
||||
cm.probability_operator,
|
||||
cm.major_type,
|
||||
cm.major_type_child,
|
||||
cm.plan_num,
|
||||
cm.main_subjects,
|
||||
cm.limitation,
|
||||
cm.other_score_limitation,
|
||||
s.province,
|
||||
s.school_nature,
|
||||
s.institution_type
|
||||
FROM %s cm
|
||||
LEFT JOIN yx_school_child sc ON sc.school_code = cm.school_code
|
||||
LEFT JOIN yx_school_research_teaching srt ON srt.school_id = sc.school_id
|
||||
LEFT JOIN yx_school s ON s.id = sc.school_id
|
||||
%s
|
||||
`, tableName, baseSQL)
|
||||
|
||||
// 拼接传入概率的筛选条件(兼容原有业务逻辑)
|
||||
switch query.Probability {
|
||||
case "难录取":
|
||||
mainSQL += " AND cm.enroll_probability < 60"
|
||||
case "可冲击":
|
||||
mainSQL += " AND (cm.enroll_probability >= 60 and cm.enroll_probability < 73)"
|
||||
case "较稳妥":
|
||||
mainSQL += " AND (cm.enroll_probability >= 73 and cm.enroll_probability < 93)"
|
||||
case "可保底":
|
||||
mainSQL += " AND (cm.enroll_probability >= 93)"
|
||||
}
|
||||
|
||||
mainSQL += " ORDER BY cm.enroll_probability DESC"
|
||||
// 6. 分页参数合法性校验
|
||||
page := query.Page
|
||||
size := query.Size
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if size < 1 {
|
||||
size = 10
|
||||
}
|
||||
if size > 100 {
|
||||
size = 100
|
||||
}
|
||||
offset := (page - 1) * size
|
||||
// 提前拼接分页条件,避免协程内操作共享变量
|
||||
mainSQL += fmt.Sprintf(" LIMIT %d OFFSET %d", size, offset)
|
||||
|
||||
// 7. 协程并发执行三个查询(总数量、概率数量、主列表),提升性能
|
||||
var wg sync.WaitGroup
|
||||
var countErr, probCountErr, queryErr error
|
||||
var queryCost QueryCostTime // 存储各协程耗时
|
||||
var mu sync.Mutex // 互斥锁:防止多协程同时修改queryCost引发竞态问题
|
||||
|
||||
// 整体开始时间
|
||||
totalStartTime := time.Now()
|
||||
|
||||
wg.Add(3)
|
||||
|
||||
// 协程1:总数量查询(单独记录耗时)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
// 记录该协程单独的开始时间
|
||||
start := time.Now()
|
||||
countErr = config.DB.Raw(countSQL, params...).Count(&total).Error
|
||||
// 计算该协程耗时,通过互斥锁安全写入共享变量
|
||||
mu.Lock()
|
||||
queryCost.CountCost = time.Since(start)
|
||||
mu.Unlock()
|
||||
}()
|
||||
|
||||
// 协程2:四种概率数量批量查询(单独记录耗时)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
// 记录该协程单独的开始时间
|
||||
start := time.Now()
|
||||
probCountErr = config.DB.Raw(probCountSQL, params...).Scan(&probCount).Error
|
||||
// 计算该协程耗时,通过互斥锁安全写入共享变量
|
||||
mu.Lock()
|
||||
queryCost.ProbCountCost = time.Since(start)
|
||||
mu.Unlock()
|
||||
}()
|
||||
|
||||
// 协程3:主列表查询(单独记录耗时)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
// 记录该协程单独的开始时间
|
||||
start := time.Now()
|
||||
queryErr = config.DB.Raw(mainSQL, params...).Scan(&items).Error
|
||||
// 计算该协程耗时,通过互斥锁安全写入共享变量
|
||||
mu.Lock()
|
||||
queryCost.QueryCost = time.Since(start)
|
||||
mu.Unlock()
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// 计算整体总耗时
|
||||
queryCost.TotalCost = time.Since(totalStartTime)
|
||||
|
||||
// 打印各协程耗时和总耗时(按需输出,可注释或删除)
|
||||
fmt.Printf("各查询耗时统计:\n")
|
||||
fmt.Printf(" 总数量查询耗时:%v\n", queryCost.CountCost)
|
||||
fmt.Printf(" 概率数量查询耗时:%v\n", queryCost.ProbCountCost)
|
||||
fmt.Printf(" 主列表查询耗时:%v\n", queryCost.QueryCost)
|
||||
fmt.Printf(" 整体总耗时:%v\n", queryCost.TotalCost)
|
||||
|
||||
// 8. 错误处理
|
||||
if countErr != nil {
|
||||
return nil, 0, dto.ProbabilityCountDTO{}, fmt.Errorf("failed to query total count: %w", countErr)
|
||||
}
|
||||
if probCountErr != nil {
|
||||
return nil, 0, dto.ProbabilityCountDTO{}, fmt.Errorf("failed to query probability count: %w", probCountErr)
|
||||
}
|
||||
if queryErr != nil {
|
||||
return nil, 0, dto.ProbabilityCountDTO{}, fmt.Errorf("failed to query recommend major list: %w", queryErr)
|
||||
}
|
||||
if items == nil {
|
||||
items = []dto.UserMajorDTO{}
|
||||
}
|
||||
return items, total, probCount, nil
|
||||
}
|
||||
|
||||
// FindRecommendList1 查询推荐专业列表(原版)
|
||||
func (m *YxCalculationMajorMapper) FindRecommendList1(query dto.SchoolMajorQuery) ([]dto.UserMajorDTO, int64, error) {
|
||||
var items []dto.UserMajorDTO
|
||||
var total int64
|
||||
|
||||
// 确保表名存在,防止 SQL 注入或空表名
|
||||
tableName := query.UserScoreVO.CalculationTableName
|
||||
if tableName == "" {
|
||||
return nil, 0, fmt.Errorf("CalculationTableName is empty")
|
||||
}
|
||||
|
||||
// 使用 Sprintf 动态插入表名
|
||||
countSQL := fmt.Sprintf(`
|
||||
SELECT COUNT(cm.id) FROM %s cm
|
||||
LEFT JOIN yx_school_child sc ON sc.school_code = cm.school_code
|
||||
LEFT JOIN yx_school_research_teaching srt ON srt.school_id = sc.school_id
|
||||
LEFT JOIN yx_school s ON s.id = sc.school_id
|
||||
WHERE 1=1 AND cm.state > 0
|
||||
`, tableName)
|
||||
|
||||
sql := fmt.Sprintf(`
|
||||
SELECT
|
||||
cm.id,
|
||||
s.school_name,
|
||||
s.school_icon,
|
||||
cm.state,
|
||||
cm.school_code,
|
||||
cm.major_code,
|
||||
cm.major_name,
|
||||
cm.enrollment_code,
|
||||
cm.tuition,
|
||||
cm.detail as majorDetail,
|
||||
cm.category,
|
||||
cm.batch,
|
||||
cm.private_student_converted_score as privateStudentScore,
|
||||
cm.student_old_converted_score as studentScore,
|
||||
cm.student_converted_score,
|
||||
cm.enroll_probability,
|
||||
cm.rules_enroll_probability_sx,
|
||||
cm.rules_enroll_probability,
|
||||
cm.probability_operator,
|
||||
cm.major_type,
|
||||
cm.major_type_child,
|
||||
cm.plan_num,
|
||||
cm.main_subjects,
|
||||
cm.limitation,
|
||||
cm.other_score_limitation,
|
||||
s.province as province,
|
||||
s.school_nature as schoolNature,
|
||||
s.institution_type as institutionType
|
||||
FROM %s cm
|
||||
LEFT JOIN yx_school_child sc ON sc.school_code = cm.school_code
|
||||
LEFT JOIN yx_school_research_teaching srt ON srt.school_id = sc.school_id
|
||||
LEFT JOIN yx_school s ON s.id = sc.school_id
|
||||
WHERE 1=1 AND cm.state > 0
|
||||
`, tableName)
|
||||
|
||||
params := []interface{}{}
|
||||
|
||||
// 注意:移除了 params = append(params, query.UserScoreVO.CalculationTableName) 因为表名已经通过 Sprintf 插入
|
||||
|
||||
if query.UserScoreVO.ID != "" {
|
||||
countSQL += " AND cm.score_id = ?"
|
||||
sql += " AND cm.score_id = ?"
|
||||
params = append(params, query.UserScoreVO.ID)
|
||||
}
|
||||
|
||||
if query.MajorType != "" {
|
||||
countSQL += " AND cm.major_type = ?"
|
||||
sql += " AND cm.major_type = ?"
|
||||
params = append(params, query.MajorType)
|
||||
}
|
||||
if query.Category != "" {
|
||||
countSQL += " AND cm.category = ?"
|
||||
sql += " AND cm.category = ?"
|
||||
params = append(params, query.Category)
|
||||
}
|
||||
if len(query.MajorTypeChildren) > 0 {
|
||||
placeholders := strings.Repeat("?,", len(query.MajorTypeChildren)-1) + "?"
|
||||
countSQL += " AND cm.major_type_child IN (" + placeholders + ")"
|
||||
sql += " AND cm.major_type_child IN (" + placeholders + ")"
|
||||
for _, v := range query.MajorTypeChildren {
|
||||
params = append(params, v)
|
||||
}
|
||||
}
|
||||
|
||||
if query.MainSubjects != "" {
|
||||
countSQL += " AND cm.main_subjects = ?"
|
||||
sql += " AND cm.main_subjects = ?"
|
||||
params = append(params, query.MainSubjects)
|
||||
}
|
||||
|
||||
// 录取概率
|
||||
switch query.Probability {
|
||||
case "难录取":
|
||||
countSQL += " AND cm.enroll_probability < 60"
|
||||
sql += " AND cm.enroll_probability < 60"
|
||||
case "可冲击":
|
||||
countSQL += " AND (cm.enroll_probability >= 60 and cm.enroll_probability < 73)"
|
||||
sql += " AND (cm.enroll_probability >= 60 and cm.enroll_probability < 73)"
|
||||
case "较稳妥":
|
||||
countSQL += " AND (cm.enroll_probability >= 73 and cm.enroll_probability < 93)"
|
||||
sql += " AND (cm.enroll_probability >= 73 and cm.enroll_probability < 93)"
|
||||
case "可保底":
|
||||
countSQL += " AND (cm.enroll_probability >= 93)"
|
||||
sql += " AND (cm.enroll_probability >= 93)"
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var countErr, queryErr error
|
||||
|
||||
wg.Add(2)
|
||||
// 协程1:COUNT 查询
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
countErr = config.DB.Raw(countSQL, params...).Count(&total).Error
|
||||
}()
|
||||
|
||||
// 协程2:主查询
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
sql += fmt.Sprintf(" LIMIT %d OFFSET %d", query.Size, (query.Page-1)*query.Size)
|
||||
queryErr = config.DB.Raw(sql, params...).Scan(&items).Error
|
||||
}()
|
||||
wg.Wait()
|
||||
if countErr != nil || queryErr != nil {
|
||||
return nil, 0, fmt.Errorf("countErr: %v, queryErr: %v", countErr, queryErr)
|
||||
}
|
||||
return items, total, queryErr
|
||||
}
|
||||
|
||||
// FindByScoreID 根据 scoreID 查找计算专业列表
|
||||
func (m *YxCalculationMajorMapper) FindByScoreID(scoreID string) ([]entity.YxCalculationMajor, error) {
|
||||
var items []entity.YxCalculationMajor
|
||||
err := m.GetDB().Where("score_id = ?", scoreID).Find(&items).Error
|
||||
return items, err
|
||||
}
|
||||
|
||||
// FindListByCompositeKeys 根据复合键查找计算专业列表
|
||||
func (m *YxCalculationMajorMapper) FindListByCompositeKeys(tableName string, keys []string, scoreId string) ([]entity.YxCalculationMajor, error) {
|
||||
if len(keys) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// 验证表名格式(防止表名注入)
|
||||
if !common.IsValidTableName(tableName) {
|
||||
return nil, fmt.Errorf("无效的表名: %s", tableName)
|
||||
}
|
||||
|
||||
// 验证和转义 score_id
|
||||
if scoreId == "" {
|
||||
return nil, fmt.Errorf("score_id 不能为空")
|
||||
}
|
||||
|
||||
var items []entity.YxCalculationMajor
|
||||
db := m.GetDB()
|
||||
if tableName != "" {
|
||||
db = db.Table(tableName)
|
||||
}
|
||||
|
||||
sql := "SELECT * FROM " + tableName + " WHERE score_id = ? AND (school_code, major_code, enrollment_code) IN ("
|
||||
var params []interface{}
|
||||
|
||||
// 将 score_id 作为第一个参数
|
||||
params = append(params, scoreId)
|
||||
|
||||
for i, key := range keys {
|
||||
parts := strings.Split(key, "_")
|
||||
if len(parts) != 3 {
|
||||
continue
|
||||
}
|
||||
if i > 0 {
|
||||
sql += ","
|
||||
}
|
||||
sql += "(?, ?, ?)"
|
||||
params = append(params, parts[0], parts[1], parts[2])
|
||||
}
|
||||
sql += ")"
|
||||
|
||||
err := db.Raw(sql, params...).Scan(&items).Error
|
||||
return items, err
|
||||
}
|
||||
|
||||
// FindDtoListByCompositeKeys 根据复合键查找 DTO 列表
|
||||
func (m *YxCalculationMajorMapper) FindDtoListByCompositeKeys(tableName string, keys []string, scoreId string) ([]dto.SchoolMajorDTO, error) {
|
||||
if len(keys) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if !common.IsValidTableName(tableName) {
|
||||
return nil, fmt.Errorf("无效的表名: %s", tableName)
|
||||
}
|
||||
if scoreId == "" {
|
||||
return nil, fmt.Errorf("score_id 不能为空")
|
||||
}
|
||||
|
||||
var items []dto.SchoolMajorDTO
|
||||
|
||||
// SQL with joins to get school info
|
||||
// Base Query similar to FindRecommendList but filtered by composite keys
|
||||
sqlStr := fmt.Sprintf(`
|
||||
SELECT
|
||||
cm.id,
|
||||
s.school_name,
|
||||
s.school_icon,
|
||||
cm.state,
|
||||
cm.school_code,
|
||||
cm.major_code,
|
||||
cm.major_name,
|
||||
cm.enrollment_code,
|
||||
cm.tuition,
|
||||
cm.detail as majorDetail,
|
||||
cm.category,
|
||||
cm.batch,
|
||||
cm.private_student_converted_score as privateStudentScore,
|
||||
cm.student_old_converted_score as studentScore,
|
||||
cm.student_converted_score,
|
||||
cm.enroll_probability,
|
||||
cm.rules_enroll_probability_sx,
|
||||
cm.rules_enroll_probability,
|
||||
cm.probability_operator,
|
||||
cm.major_type,
|
||||
cm.major_type_child,
|
||||
cm.plan_num,
|
||||
cm.main_subjects,
|
||||
cm.limitation,
|
||||
cm.other_score_limitation,
|
||||
s.province as province,
|
||||
s.school_nature as schoolNature,
|
||||
s.institution_type as institutionType
|
||||
FROM %s cm
|
||||
LEFT JOIN yx_school_child sc ON sc.school_code = cm.school_code
|
||||
LEFT JOIN yx_school_research_teaching srt ON srt.school_id = sc.school_id
|
||||
LEFT JOIN yx_school s ON s.id = sc.school_id
|
||||
WHERE cm.score_id = ? AND (cm.school_code, cm.major_code, cm.enrollment_code) IN (
|
||||
`, tableName)
|
||||
|
||||
var params []interface{}
|
||||
params = append(params, scoreId)
|
||||
|
||||
// Build IN clause
|
||||
var tuples []string
|
||||
for _, key := range keys {
|
||||
parts := strings.Split(key, "_")
|
||||
if len(parts) != 3 {
|
||||
continue
|
||||
}
|
||||
tuples = append(tuples, "(?, ?, ?)")
|
||||
params = append(params, parts[0], parts[1], parts[2])
|
||||
}
|
||||
|
||||
if len(tuples) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
sqlStr += strings.Join(tuples, ",") + ")"
|
||||
|
||||
err := m.GetDB().Raw(sqlStr, params...).Scan(&items).Error
|
||||
return items, err
|
||||
}
|
||||
|
||||
// BatchCreate 批量创建(支持动态表名)
|
||||
func (m *YxCalculationMajorMapper) BatchCreate(tableName string, items []entity.YxCalculationMajor, batchSize int) error {
|
||||
if tableName != "" {
|
||||
return m.GetDB().Table(tableName).CreateInBatches(items, batchSize).Error
|
||||
}
|
||||
return m.GetDB().CreateInBatches(items, batchSize).Error
|
||||
}
|
||||
|
||||
// DeleteByScoreID 根据 scoreID 删除
|
||||
func (m *YxCalculationMajorMapper) DeleteByScoreID(scoreID string) error {
|
||||
return m.GetDB().Delete(&entity.YxCalculationMajor{}, "score_id = ?", scoreID).Error
|
||||
}
|
||||
|
||||
// DeleteByScoreIDFromTable 从指定表删除 scoreID 对应的数据
|
||||
func (m *YxCalculationMajorMapper) DeleteByScoreIDFromTable(tableName, scoreID string) error {
|
||||
if tableName == "" {
|
||||
return nil
|
||||
}
|
||||
return m.GetDB().Table(tableName).Where("score_id = ?", scoreID).Delete(map[string]interface{}{}).Error
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue