first commit
This commit is contained in:
commit
85a73e1ae9
12
.env
Normal file
12
.env
Normal file
@ -0,0 +1,12 @@
|
||||
# .env file
|
||||
|
||||
# 数据库配置
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=Lzy0916.
|
||||
DB_NAME=gorm
|
||||
|
||||
# JWT 认证配置
|
||||
# 强烈建议在生产环境使用更复杂的密钥
|
||||
JWT_SECRET=mySuperSecretKey_todo_api_flash_v1
|
||||
50
README.md
Normal file
50
README.md
Normal file
@ -0,0 +1,50 @@
|
||||
# Go Gin API 项目总结文档
|
||||
|
||||
## 🚀 项目概述
|
||||
|
||||
本项目成功构建了一个基于 **Go 语言 (Gin 框架)** 的 RESTful API 服务,实现了用户认证、待办事项 (Todo) 的 CRUD 操作,以及受保护的文件上传与静态服务功能。整个项目严格遵循 **三层架构(Handler/Service/Repository)** 和 **依赖注入(DI)** 原则,确保了代码的高内聚、低耦合和高可维护性。
|
||||
|
||||
## ✅ 核心功能与架构成就
|
||||
|
||||
本项目最关键的成就体现在 **安全性和架构的健壮性** 上:
|
||||
|
||||
### 1. 认证与授权 (Authentication & Authorization)
|
||||
|
||||
- **JWT 机制**:实现了基于 JWT 的用户登录和认证,所有受保护的路由均通过 Auth Middleware 拦截和验证。
|
||||
- **权限控制**:将 `AuthService` 注入到中间件中,确保只有携带有效令牌的用户才能访问 `/todos`, `/upload` 等受保护的端点。
|
||||
|
||||
### 2. 严格的数据隔离 (Data Isolation)
|
||||
|
||||
- **核心安全实现**:通过在 Service 层和 Repository 层强制执行 `WHERE user_id = ?` 约束,确保了用户 B 无法执行任何针对用户 A 数据的操作(包括读取、更新和删除)。
|
||||
- **调试成功**:成功解决了分层架构中因 `userID` 传递和 Repository 约束不一致导致的复杂编译和运行时安全漏洞。
|
||||
|
||||
### 3. 文件服务与解压 (File Service)
|
||||
|
||||
- **受保护上传**:`/upload` 接口受 JWT 保护,仅限登录用户上传文件。
|
||||
- **解压和静态服务**:实现了 ZIP 文件的上传、解压,并使用 Gin 的 `router.Static` 路由将解压后的 HTML 文件安全地对外提供服务。
|
||||
|
||||
### 4. 生产环境准备 (Deployment Readiness)
|
||||
|
||||
- **配置安全**:所有敏感配置(数据库凭证、JWT Secret)已从代码中移除,迁移至 **`.env` 环境变量** 进行管理,极大地提高了项目部署的灵活性和安全性。
|
||||
- **专业配置**:项目设置了 **`gin.ReleaseMode`** 和 **`router.SetTrustedProxies`**,优化了生产环境下的性能并解决了关键的安全警告。
|
||||
- **统一响应格式**:所有 API 响应均采用 `StandardResponse` 结构体,包含 HTTP 状态码、自定义业务 code 字段和数据 payload,确保了客户端与服务端的通信一致性和可维护性。
|
||||
|
||||
- **Gin 生产模式配置和代理安全设置**:
|
||||
- 启用了 `gin.ReleaseMode`,关闭了调试信息,提升了性能。
|
||||
- 设置了 `router.SetTrustedProxies`,确保了在反向代理环境下的安全。
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 项目技术栈
|
||||
|
||||
| 类别 | 技术栈/库 | 作用 |
|
||||
| :----------- | :----------------------------------- | :------------------------------------------------ |
|
||||
| **框架** | Gin | 高性能 Web 框架,用于路由和中间件管理。 |
|
||||
| **数据库** | PostgreSQL (GORM) | 数据库连接与 ORM 操作,支持强大的关系映射和查询。 |
|
||||
| **认证** | JWT (Go `jwt` library) | 令牌生成、签名和验证。 |
|
||||
| **配置** | `github.com/joho/godotenv` | 本地 `.env` 文件加载与管理。 |
|
||||
| **文件操作** | `archive/zip`, `os`, `path/filepath` | 处理 ZIP 文件的上传、解压和存储。 |
|
||||
|
||||
## 📦 最终代码状态
|
||||
|
||||
项目代码已达到高水平的专业性和稳定性,可以随时进行容器化(如 Docker)并部署到生产环境。
|
||||
18
constants/codes.go
Normal file
18
constants/codes.go
Normal file
@ -0,0 +1,18 @@
|
||||
package constants
|
||||
|
||||
// 业务状态码常量
|
||||
const (
|
||||
CodeSuccess = 1000 // 成功
|
||||
CodeInvalidAuth = 2000 // 认证/授权失败 (Token无效或缺失)
|
||||
CodeResourceNotFound = 3000 // 资源未找到 (如 ID 不存在或不属于用户)
|
||||
CodeValidationError = 4000 // 输入校验失败
|
||||
CodeInternalError = 5000 // 服务器内部错误
|
||||
CodeConflictError = 6000 // 资源冲突 (如用户名已存在)
|
||||
)
|
||||
|
||||
// StandardResponse 定义了统一的 JSON 响应格式
|
||||
type StandardResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data,omitempty"` // omitempty: 如果为nil则不显示
|
||||
}
|
||||
5
constants/constants.go
Normal file
5
constants/constants.go
Normal file
@ -0,0 +1,5 @@
|
||||
// constants/constants.go
|
||||
package constants
|
||||
|
||||
// UserIDKey 是存储在 Gin Context 中的用户 ID 键名
|
||||
const UserIDKey = "userID"
|
||||
14
dto/README.md
Normal file
14
dto/README.md
Normal file
@ -0,0 +1,14 @@
|
||||
# 使用 DTO(数据传输对象)
|
||||
|
||||
你完全抓住了我们在处理 注册/登录 过程中遇到的一个常见的 Go Web 开发陷阱,这和我们在 models/user.go 中设置的 标签冲突 有关。
|
||||
|
||||
🚨 标签冲突的困境
|
||||
你的目标是:
|
||||
|
||||
输入时 (注册/登录): 必须读取 JSON 中的明文密码,所以需要 json:"password"。
|
||||
|
||||
输出时 (防止信息泄露): 必须阻止密码哈希值被 JSON 序列化并返回给客户端,所以需要 json:"-"。
|
||||
|
||||
不幸的是,一个字段不能同时拥有两个不同的 json 标签。
|
||||
|
||||
解决这个困境的最佳和最专业的做法是 为输入/输出定义不同的结构体,将 models.User 结构体专门用于数据库操作,而将输入/输出交给 DTO
|
||||
13
dto/user_dto.go
Normal file
13
dto/user_dto.go
Normal file
@ -0,0 +1,13 @@
|
||||
package dto
|
||||
|
||||
// LoginInput 用于接收 /login 请求体
|
||||
type LoginInput struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
// RegisterInput 用于接收 /register 请求体(如果需要更严格的验证,可以在这里添加)
|
||||
type RegisterInput struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required,min=6"`
|
||||
}
|
||||
55
go.mod
Normal file
55
go.mod
Normal file
@ -0,0 +1,55 @@
|
||||
module go-todo-api
|
||||
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.10
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
gorm.io/driver/sqlite v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.14.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // 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.27.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 // 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/joho/godotenv v1.5.1 // 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/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // 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.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.54.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
golang.org/x/arch v0.20.0 // indirect
|
||||
golang.org/x/crypto v0.45.0 // indirect
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
golang.org/x/tools v0.38.0 // indirect
|
||||
google.golang.org/protobuf v1.36.9 // indirect
|
||||
gorm.io/driver/postgres v1.6.0 // indirect
|
||||
)
|
||||
126
go.sum
Normal file
126
go.sum
Normal file
@ -0,0 +1,126 @@
|
||||
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
|
||||
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
|
||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
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/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
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.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||
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.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
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-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
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.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
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/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/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/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/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.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
||||
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
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.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
||||
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
1
handlers/README.md
Normal file
1
handlers/README.md
Normal file
@ -0,0 +1 @@
|
||||
# 存放 Gin 路由处理函数
|
||||
97
handlers/auth_handler.go
Normal file
97
handlers/auth_handler.go
Normal file
@ -0,0 +1,97 @@
|
||||
// handlers/auth_handler.go
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"go-todo-api/constants"
|
||||
"go-todo-api/dto"
|
||||
"go-todo-api/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// AuthHandler 依赖于 AuthService 接口
|
||||
type AuthHandler struct {
|
||||
Service services.AuthService
|
||||
}
|
||||
|
||||
// NewAuthHandler 创建 AuthHandler 的新实例
|
||||
func NewAuthHandler(service services.AuthService) *AuthHandler {
|
||||
return &AuthHandler{Service: service}
|
||||
}
|
||||
|
||||
// RegisterHandler 处理 POST /register 请求
|
||||
func (h *AuthHandler) RegisterHandler(c *gin.Context) {
|
||||
var input dto.RegisterInput
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, constants.StandardResponse{
|
||||
Code: constants.CodeValidationError,
|
||||
Message: "Invalid input format or missing fields",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
newUser, err := h.Service.Register(&input)
|
||||
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "username already taken") { // 检查 Service 返回的特定错误
|
||||
c.JSON(http.StatusConflict, constants.StandardResponse{
|
||||
Code: constants.CodeConflictError,
|
||||
Message: "Username already taken",
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, constants.StandardResponse{
|
||||
Code: constants.CodeInternalError,
|
||||
Message: "Failed to register user",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 注册成功,返回脱敏信息
|
||||
c.JSON(http.StatusCreated, constants.StandardResponse{
|
||||
Code: constants.CodeSuccess,
|
||||
Message: "User created successfully",
|
||||
Data: newUser,
|
||||
})
|
||||
}
|
||||
|
||||
// LoginHandler 处理 POST /login 请求
|
||||
func (h *AuthHandler) LoginHandler(c *gin.Context) {
|
||||
var input dto.LoginInput
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, constants.StandardResponse{
|
||||
Code: constants.CodeValidationError,
|
||||
Message: "Invalid input format or missing fields",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
token, err := h.Service.Login(&input)
|
||||
|
||||
if err != nil {
|
||||
// 统一处理登录失败,防止泄露细节
|
||||
if errors.Is(err, errors.New("invalid username or password")) {
|
||||
c.JSON(http.StatusUnauthorized, constants.StandardResponse{
|
||||
Code: constants.CodeInvalidAuth,
|
||||
Message: "Invalid username or password",
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, constants.StandardResponse{
|
||||
Code: constants.CodeInternalError,
|
||||
Message: "Failed to login",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 登录成功,返回令牌
|
||||
c.JSON(http.StatusOK, constants.StandardResponse{
|
||||
Code: constants.CodeSuccess,
|
||||
Message: "Login successful!",
|
||||
Data: token,
|
||||
})
|
||||
}
|
||||
269
handlers/todo_handler.go
Normal file
269
handlers/todo_handler.go
Normal file
@ -0,0 +1,269 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"go-todo-api/constants"
|
||||
"go-todo-api/models"
|
||||
"go-todo-api/services"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// 辅助函数:从 Gin Context 中获取用户 ID
|
||||
// ❗ 修正:将此函数放在包级别 (不在任何结构体内)
|
||||
func getUserIDFromContext(c *gin.Context) (uint, error) {
|
||||
// Note: UserIDKey must be imported from the constants package
|
||||
// ... (Your implementation using constants.UserIDKey)
|
||||
userIDStr, exists := c.Get(constants.UserIDKey)
|
||||
if !exists {
|
||||
return 0, errors.New("User ID not found in context")
|
||||
}
|
||||
|
||||
// 转换 string 到 uint
|
||||
userIDUint, err := strconv.ParseUint(userIDStr.(string), 10, 32)
|
||||
if err != nil {
|
||||
return 0, errors.New("Invalid user ID format in context")
|
||||
}
|
||||
return uint(userIDUint), nil
|
||||
}
|
||||
|
||||
// TodoHandler 结构体持有 Service 接口,用于处理 HTTP 请求
|
||||
type TodoHandler struct {
|
||||
// Handler 层依赖于 Service 接口
|
||||
Service services.TodoService
|
||||
}
|
||||
|
||||
// NewTodoHandler 实例化一个新的 TodoHandler
|
||||
func NewTodoHandler(service services.TodoService) *TodoHandler {
|
||||
return &TodoHandler{Service: service}
|
||||
}
|
||||
|
||||
// FindAllTodosHandler 处理 GET /todos 请求
|
||||
func (h *TodoHandler) FindAllTodosHandler(c *gin.Context) {
|
||||
userID, err := getUserIDFromContext(c) // 1. 获取 UserID
|
||||
if err != nil || userID == 0 {
|
||||
// 401 错误,使用自定义代码 CodeInvalidAuth
|
||||
c.JSON(http.StatusUnauthorized, constants.StandardResponse{
|
||||
Code: constants.CodeInvalidAuth,
|
||||
Message: "Unauthorized or Invalid User ID",
|
||||
})
|
||||
return
|
||||
}
|
||||
// 1. 调用 Service 层获取数据 (Service 层调用 Repository 层)
|
||||
todos, err := h.Service.FindAllTodos(userID)
|
||||
|
||||
if err != nil {
|
||||
// 500 内部错误,使用自定义代码 CodeInternalError
|
||||
c.JSON(http.StatusInternalServerError, constants.StandardResponse{
|
||||
Code: constants.CodeInternalError,
|
||||
Message: fmt.Sprintf("Failed to retrieve todos: %v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
// 200 成功响应,使用 CodeSuccess
|
||||
// 2. 返回 JSON 响应 (Handler 负责 HTTP 细节)
|
||||
c.JSON(http.StatusOK, constants.StandardResponse{
|
||||
Code: constants.CodeSuccess,
|
||||
Message: "Successfully retrieved todos",
|
||||
Data: todos,
|
||||
})
|
||||
}
|
||||
|
||||
// FindTodoByIDHandler 处理 GET /todos/:id 请求
|
||||
func (h *TodoHandler) FindTodoByIDHandler(c *gin.Context) {
|
||||
userID, err := getUserIDFromContext(c) // 1. 获取 UserID
|
||||
if err != nil || userID == 0 {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized or Invalid User ID"})
|
||||
return
|
||||
}
|
||||
// 1. 获取 URL 参数 ID
|
||||
idStr := c.Param("id")
|
||||
// 将字符串 ID 转换为无符号整数 (uint)
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid Todo ID format"})
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 调用 Service 层获取数据
|
||||
todo, err := h.Service.FindTodoByID(uint(id), userID)
|
||||
|
||||
if err != nil {
|
||||
// 检查是否是“未找到记录”错误
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// 404 资源未找到,使用 CodeResourceNotFound
|
||||
c.JSON(http.StatusNotFound, constants.StandardResponse{
|
||||
Code: constants.CodeResourceNotFound,
|
||||
Message: "Todo item not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
// 其他数据库错误
|
||||
c.JSON(http.StatusInternalServerError, constants.StandardResponse{
|
||||
Code: constants.CodeInternalError,
|
||||
Message: fmt.Sprintf("Failed to retrieve todo: %v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 3. 返回 JSON 响应
|
||||
c.JSON(http.StatusOK, constants.StandardResponse{
|
||||
Code: constants.CodeSuccess,
|
||||
Message: "Successfully retrieved todo",
|
||||
Data: todo,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteTodoByIDHandler 处理 DELETE /todos/:id 请求
|
||||
func (h *TodoHandler) DeleteTodoByIDHandler(c *gin.Context) {
|
||||
userID, err := getUserIDFromContext(c) // 1. 获取 UserID
|
||||
if err != nil || userID == 0 {
|
||||
// 401 错误,使用自定义代码 CodeInvalidAuth
|
||||
c.JSON(http.StatusUnauthorized, constants.StandardResponse{
|
||||
Code: constants.CodeInvalidAuth,
|
||||
Message: "Unauthorized or Invalid User ID",
|
||||
})
|
||||
return
|
||||
}
|
||||
// 1. 获取并解析 ID
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, constants.StandardResponse{
|
||||
Code: constants.CodeInvalidAuth,
|
||||
Message: "Invalid Todo ID format",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 调用 Service 层执行删除逻辑
|
||||
err = h.Service.DeleteTodoByID(uint(id), userID)
|
||||
|
||||
if err != nil {
|
||||
// 检查是否是“未找到记录”错误(Repository层返回的gorm.ErrRecordNotFound)
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// 404 资源未找到,使用 CodeResourceNotFound
|
||||
c.JSON(http.StatusNotFound, constants.StandardResponse{
|
||||
Code: constants.CodeResourceNotFound,
|
||||
Message: "Todo item not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
// 其他数据库错误
|
||||
c.JSON(http.StatusInternalServerError, constants.StandardResponse{
|
||||
Code: constants.CodeInternalError,
|
||||
Message: fmt.Sprintf("Failed to delete todo item: %v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 3. 返回成功响应
|
||||
c.JSON(http.StatusOK, constants.StandardResponse{
|
||||
Code: constants.CodeSuccess,
|
||||
Message: "Todo deleted successfully (soft deleted)",
|
||||
})
|
||||
}
|
||||
|
||||
// CreateTodoHandler 处理 POST /todos 请求,创建新的待办事项
|
||||
func (h *TodoHandler) CreateTodoHandler(c *gin.Context) {
|
||||
var newTodo models.Todo
|
||||
|
||||
// 1. 绑定前端 JSON 数据
|
||||
// Todo 结构体中应有 binding:"required" 标签来确保数据完整性
|
||||
if err := c.ShouldBindJSON(&newTodo); err != nil {
|
||||
c.JSON(http.StatusBadRequest, constants.StandardResponse{
|
||||
Code: constants.CodeValidationError,
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
userID, err := getUserIDFromContext(c) // 获取 UserID
|
||||
|
||||
if err != nil || userID == 0 { // ❗ 核心修正:新增检查 userID == 0
|
||||
// 如果获取失败或者 ID 为 0,视为未授权或无效令牌
|
||||
c.JSON(http.StatusUnauthorized, constants.StandardResponse{
|
||||
Code: constants.CodeInvalidAuth,
|
||||
Message: "Unauthorized or Invalid User ID",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 调用 Service 层创建数据 (Service 会调用 Repository)
|
||||
if err := h.Service.CreateTodo(&newTodo, userID); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, constants.StandardResponse{
|
||||
Code: constants.CodeInternalError,
|
||||
Message: fmt.Sprintf("Failed to save todo item: %v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 3. 返回成功响应
|
||||
// newTodo 现在包含了 GORM 自动生成的 ID 和时间戳
|
||||
c.JSON(http.StatusCreated, constants.StandardResponse{
|
||||
Code: constants.CodeSuccess,
|
||||
Message: "Todo created successfully (via Service)",
|
||||
Data: newTodo,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateTodoHandler 处理 PATCH /todos/:id 请求
|
||||
func (h *TodoHandler) UpdateTodoHandler(c *gin.Context) {
|
||||
userID, err := getUserIDFromContext(c) // 1. 获取 UserID
|
||||
if err != nil || userID == 0 {
|
||||
c.JSON(http.StatusUnauthorized, constants.StandardResponse{
|
||||
Code: constants.CodeInvalidAuth,
|
||||
Message: "Unauthorized or Invalid User ID",
|
||||
})
|
||||
return
|
||||
}
|
||||
// 1. 获取并解析 ID
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, constants.StandardResponse{
|
||||
Code: constants.CodeValidationError,
|
||||
Message: "Invalid Todo ID format",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 接收更新的 JSON 数据到 map 中(用于部分更新)
|
||||
var input map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, constants.StandardResponse{
|
||||
Code: constants.CodeValidationError,
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 3. 调用 Service 层执行更新逻辑
|
||||
// 注意:Service 层将负责查找记录,然后执行更新
|
||||
updatedTodo, err := h.Service.UpdateTodo(uint(id), userID, input)
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, constants.StandardResponse{
|
||||
Code: constants.CodeResourceNotFound,
|
||||
Message: "Todo item not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, constants.StandardResponse{
|
||||
Code: constants.CodeInternalError,
|
||||
Message: fmt.Sprintf("Failed to update todo item: %v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 4. 返回更新后的记录
|
||||
c.JSON(http.StatusOK, constants.StandardResponse{
|
||||
Code: constants.CodeSuccess,
|
||||
Message: "Todo updated successfully",
|
||||
Data: updatedTodo,
|
||||
})
|
||||
}
|
||||
266
main.go
Normal file
266
main.go
Normal file
@ -0,0 +1,266 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/zip" // 保持,因为 handleUpload 使用
|
||||
"fmt"
|
||||
"go-todo-api/constants"
|
||||
"go-todo-api/handlers"
|
||||
"go-todo-api/models"
|
||||
"go-todo-api/repositories"
|
||||
"go-todo-api/services"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/joho/godotenv" // 保持,用于加载配置
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
// Go 标准库
|
||||
)
|
||||
|
||||
// DB 是全局数据库连接变量
|
||||
var DB *gorm.DB
|
||||
|
||||
// ❗ 新增:在 init() 中加载 .env 文件
|
||||
func init() {
|
||||
if err := godotenv.Load(); err != nil {
|
||||
log.Println("Note: No .env file found, relying on environment variables.")
|
||||
}
|
||||
}
|
||||
|
||||
// 修正 connectDatabase 函数,使其读取环境变量
|
||||
func connectDatabase() {
|
||||
// ❗ 核心修正:使用 os.Getenv 获取配置
|
||||
dbHost := os.Getenv("DB_HOST")
|
||||
dbPort := os.Getenv("DB_PORT")
|
||||
dbUser := os.Getenv("DB_USER")
|
||||
dbPassword := os.Getenv("DB_PASSWORD")
|
||||
dbName := os.Getenv("DB_NAME")
|
||||
|
||||
// 检查关键配置(如果未设置,则使用 os.Getenv 的默认空字符串,这会导致连接失败,但这是期望的安全行为)
|
||||
if dbHost == "" || dbUser == "" || dbPassword == "" || dbName == "" {
|
||||
log.Fatal("FATAL: Database environment variables (DB_HOST, DB_USER, etc.) are not fully set.")
|
||||
}
|
||||
|
||||
dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=Asia/Shanghai",
|
||||
dbHost, dbUser, dbPassword, dbName, dbPort)
|
||||
|
||||
database, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("Failed to connect to PostgreSQL database! \n", err)
|
||||
}
|
||||
// ⭐ 核心步骤:自动迁移
|
||||
err = database.AutoMigrate(&models.Todo{}, &models.User{})
|
||||
if err != nil {
|
||||
log.Fatal("Failed to auto-migrate database schema! \n", err)
|
||||
}
|
||||
|
||||
DB = database
|
||||
log.Println("PostgreSQL connection successful!") // 保持成功提示
|
||||
}
|
||||
|
||||
// ❗ 修改 AuthRequired 函数签名,使其接受 AuthService 接口
|
||||
func AuthRequired(authService services.AuthService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 1. 从 Authorization 请求头中提取令牌
|
||||
tokenString := c.GetHeader("Authorization")
|
||||
if tokenString == "" || !strings.HasPrefix(tokenString, "Bearer ") {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Missing or invalid token format"})
|
||||
c.Abort() // 阻止请求继续执行
|
||||
return
|
||||
}
|
||||
|
||||
// 移除 "Bearer " 前缀
|
||||
tokenString = strings.TrimPrefix(tokenString, "Bearer ")
|
||||
|
||||
// 2. ⭐ 调用 Service 层验证令牌
|
||||
// Service 层会处理解析、验证签名和过期时间
|
||||
subject, err := authService.AuthenticateToken(tokenString) // ❗ 必须在这里声明 subject
|
||||
// 3. 处理解析错误 (如签名不匹配、令牌过期等)
|
||||
if err != nil {
|
||||
// Service 层的错误信息已经很通用了
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired token"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
// 4. ⭐ 核心修改:将用户 ID 存储到 Gin Context 中
|
||||
c.Set(constants.UserIDKey, subject) // ❗ 使用 constants.UserIDKey
|
||||
|
||||
// 5. 令牌有效,继续执行下一个 Handler
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// unzipFile 将 ZIP 文件解压到目标目录
|
||||
func unzipFile(src, dest string) error {
|
||||
r, err := zip.OpenReader(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
for _, f := range r.File {
|
||||
// 1. 构建目标路径,确保文件不会逃逸到目标目录之外(安全检查)
|
||||
fpath := filepath.Join(dest, f.Name)
|
||||
if !filepath.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) {
|
||||
return fmt.Errorf("非法文件路径: %s", fpath)
|
||||
}
|
||||
|
||||
// 2. 如果是目录,创建目录
|
||||
if f.FileInfo().IsDir() {
|
||||
os.MkdirAll(fpath, os.ModePerm)
|
||||
continue
|
||||
}
|
||||
|
||||
// 3. 打开文件进行读写
|
||||
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
outFile.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
// 4. 复制文件内容
|
||||
_, err = io.Copy(outFile, rc)
|
||||
|
||||
// 确保关闭文件
|
||||
outFile.Close()
|
||||
rc.Close()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleUpload 是处理文件上传的函数
|
||||
func handleUpload(c *gin.Context) {
|
||||
// 1. 获取文本字段 (html_path)
|
||||
htmlPath := c.PostForm("html_path")
|
||||
if htmlPath == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Missing html_path field"})
|
||||
return
|
||||
}
|
||||
|
||||
timeDict := c.PostForm("time_dict")
|
||||
tsNow := time.Now().Format("20060102150405")
|
||||
if timeDict == "" {
|
||||
timeDict = tsNow
|
||||
}
|
||||
|
||||
// 2. 获取上传的文件 (zip_file)
|
||||
file, err := c.FormFile("zip_file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Missing zip_file or failed to get file"})
|
||||
return
|
||||
}
|
||||
// 目标解压目录(以当前时间戳或传入 time_dict 作为子目录)
|
||||
destDir := filepath.Join("D:\\test-html-page", timeDict)
|
||||
if err := os.MkdirAll(destDir, os.ModePerm); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create dest dir: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 1. 将 ZIP 文件保存到服务器的临时路径 (注意:我们先保存到临时目录)
|
||||
tempZipPath := filepath.Join(os.TempDir(), file.Filename)
|
||||
if err := c.SaveUploadedFile(file, tempZipPath); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save temp zip file: " + err.Error()})
|
||||
return
|
||||
}
|
||||
// 确保函数结束时删除临时文件
|
||||
defer os.Remove(tempZipPath)
|
||||
|
||||
// 2. ⭐ 调用解压函数
|
||||
if err := unzipFile(tempZipPath, destDir); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to unzip file: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 3. 构建用户最终访问的 URL
|
||||
// 为了简化,我们只返回访问路径。完整的访问 URL 需要下一步的 Gin 配置
|
||||
accessPath := filepath.ToSlash(filepath.Join("/html", timeDict, htmlPath))
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "HTML files extracted and ready to serve.",
|
||||
"final_access_url": accessPath,
|
||||
})
|
||||
}
|
||||
|
||||
// main 函数是 Go 程序的入口
|
||||
func main() {
|
||||
// 1. 切换到 Release Mode (生产模式)
|
||||
// 这样做可以禁用调试输出,并优化性能。
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
|
||||
connectDatabase()
|
||||
|
||||
// 确保 JWT Secret 的检查和获取逻辑不变
|
||||
jwtSecret := os.Getenv("JWT_SECRET")
|
||||
if jwtSecret == "" {
|
||||
log.Fatal("FATAL: JWT_SECRET environment variable is not set. Please check your .env file.")
|
||||
}
|
||||
|
||||
router := gin.Default()
|
||||
|
||||
// 2. 解决 "You trusted all proxies" 安全警告
|
||||
// 当你的应用部署在 Nginx 或负载均衡器后面时,Gin 需要知道哪些 IP 是安全的代理。
|
||||
// 以下配置信任了 Loopback 和所有私有网络范围(RFC1918),这是云部署的常见安全实践。
|
||||
// 如果你知道你的代理 IP,使用更严格的配置更好。
|
||||
router.SetTrustedProxies([]string{
|
||||
"127.0.0.1",
|
||||
"::1",
|
||||
"10.0.0.0/8", // 私有网络 A 类
|
||||
"172.16.0.0/12", // 私有网络 B 类
|
||||
"192.168.0.0/16", // 私有网络 C 类
|
||||
})
|
||||
// --- 依赖注入 (DI) ---
|
||||
todoRepo := repositories.NewGormTodoRepository(DB)
|
||||
todoService := services.NewTodoService(todoRepo)
|
||||
todoHandler := handlers.NewTodoHandler(todoService)
|
||||
|
||||
userRepo := repositories.NewGormUserRepository(DB)
|
||||
authService := services.NewAuthService(userRepo, jwtSecret)
|
||||
authHandler := handlers.NewAuthHandler(authService)
|
||||
|
||||
// 注册公开路由
|
||||
router.GET("/hello", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Hello from Gin, ready for Vue!"})
|
||||
})
|
||||
|
||||
router.Static("/html", "D:/test-html-page")
|
||||
router.POST("/register", authHandler.RegisterHandler)
|
||||
router.POST("/login", authHandler.LoginHandler)
|
||||
|
||||
// 注册需要 AuthRequired 中间件的路由
|
||||
authMiddleware := AuthRequired(authService)
|
||||
|
||||
router.POST("/upload", authMiddleware, handleUpload)
|
||||
|
||||
// Todo 受保护的 CRUD 路由
|
||||
router.POST("/todos", authMiddleware, todoHandler.CreateTodoHandler)
|
||||
router.GET("/todos", authMiddleware, todoHandler.FindAllTodosHandler)
|
||||
router.GET("/todos/:id", authMiddleware, todoHandler.FindTodoByIDHandler)
|
||||
router.PATCH("/todos/:id", authMiddleware, todoHandler.UpdateTodoHandler)
|
||||
router.DELETE("/todos/:id", authMiddleware, todoHandler.DeleteTodoByIDHandler)
|
||||
|
||||
// 运行服务器
|
||||
log.Printf("Server starting on :%s...", "8090") // 格式化输出
|
||||
if err := router.Run(":8090"); err != nil {
|
||||
panic("Server failed to start: " + err.Error())
|
||||
}
|
||||
}
|
||||
1
models/README.md
Normal file
1
models/README.md
Normal file
@ -0,0 +1 @@
|
||||
# 存放所有 Go 结构体 (Todo, User)
|
||||
20
models/todo.go
Normal file
20
models/todo.go
Normal file
@ -0,0 +1,20 @@
|
||||
// models/todo.go
|
||||
package models
|
||||
|
||||
import (
|
||||
// ❗ 删除: "go-todo-api/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Todo struct {
|
||||
gorm.Model
|
||||
Title string `json:"title" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
Done bool `json:"done"`
|
||||
|
||||
// UserID 是外键
|
||||
UserID uint `json:"-" gorm:"index"`
|
||||
|
||||
// ❗ 核心修正:添加 binding:"-" 阻止 Gin 递归验证 User 字段
|
||||
User User `json:"-" binding:"-"`
|
||||
}
|
||||
10
models/user.go
Normal file
10
models/user.go
Normal file
@ -0,0 +1,10 @@
|
||||
package models
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type User struct {
|
||||
gorm.Model
|
||||
Username string `gorm:"uniqueIndex" json:"username" binding:"required"`
|
||||
// 恢复 json:"-" 以防泄露,因为我们现在用 DTO 来处理输入
|
||||
Password string `json:"-" binding:"required,min=6"`
|
||||
}
|
||||
1
repositories/README.md
Normal file
1
repositories/README.md
Normal file
@ -0,0 +1 @@
|
||||
# 存放数据库操作逻辑
|
||||
72
repositories/gorm_todo_repository.go
Normal file
72
repositories/gorm_todo_repository.go
Normal file
@ -0,0 +1,72 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"go-todo-api/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// GormTodoRepository 结构体持有数据库连接,用于实现 TodoRepository 接口
|
||||
type GormTodoRepository struct {
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
||||
// NewGormTodoRepository 实例化一个新的 GORM 仓库
|
||||
func NewGormTodoRepository(db *gorm.DB) *GormTodoRepository {
|
||||
return &GormTodoRepository{DB: db}
|
||||
}
|
||||
|
||||
// ❗ 修正 FindAll:接收 userID,并添加 WHERE 约束
|
||||
func (r *GormTodoRepository) FindAll(userID uint) ([]models.Todo, error) {
|
||||
var todos []models.Todo
|
||||
// 增加 WHERE UserID = ? 约束
|
||||
result := r.DB.Where("user_id = ?", userID).Find(&todos)
|
||||
return todos, result.Error
|
||||
}
|
||||
|
||||
// ❗ 修正 FindByID:接收 userID,并添加 WHERE 约束
|
||||
// ❗ 必须确保这个方法使用了 UserID 来查找和授权
|
||||
func (r *GormTodoRepository) FindByID(id uint, userID uint) (*models.Todo, error) {
|
||||
var todo models.Todo
|
||||
// 增加 WHERE ID = ? AND UserID = ? 约束
|
||||
// ⭐ 核心:WHERE id = ? AND user_id = ? 约束
|
||||
result := r.DB.Where("id = ? AND user_id = ?", id, userID).First(&todo)
|
||||
if result.Error != nil {
|
||||
return nil, result.Error
|
||||
}
|
||||
return &todo, nil
|
||||
}
|
||||
|
||||
// ❗ 修正 Create 方法签名
|
||||
func (r *GormTodoRepository) Create(todo *models.Todo) error {
|
||||
if result := r.DB.Create(todo); result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ❗ 修正 Delete:接收 userID,并添加 WHERE 约束
|
||||
func (r *GormTodoRepository) Delete(id uint, userID uint) error {
|
||||
var todo models.Todo
|
||||
// 增加 WHERE ID = ? AND UserID = ? 约束
|
||||
result := r.DB.Where("id = ? AND user_id = ?", id, userID).Delete(&todo)
|
||||
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
|
||||
// 如果没有行受到影响,且没有错误,说明记录未找到(或不属于该用户)
|
||||
if result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ❗ 修正 Update 方法签名
|
||||
func (r *GormTodoRepository) Update(todo *models.Todo, input map[string]interface{}) error {
|
||||
// GORM 的 Updates 方法可以更新 map 中的字段
|
||||
if result := r.DB.Model(todo).Updates(input); result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
37
repositories/gorm_user_repository.go
Normal file
37
repositories/gorm_user_repository.go
Normal file
@ -0,0 +1,37 @@
|
||||
// repositories/gorm_user_repository.go
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"go-todo-api/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// GormUserRepository 是 User Repository 接口的 GORM 实现
|
||||
type GormUserRepository struct {
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
||||
// NewGormUserRepository 创建 GormUserRepository 的新实例
|
||||
func NewGormUserRepository(db *gorm.DB) *GormUserRepository {
|
||||
return &GormUserRepository{DB: db}
|
||||
}
|
||||
|
||||
// FindByUsername 根据用户名查找用户
|
||||
func (r *GormUserRepository) FindByUsername(username string) (*models.User, error) {
|
||||
var user models.User
|
||||
result := r.DB.Where("username = ?", username).First(&user)
|
||||
if result.Error != nil {
|
||||
// 统一返回 GORM 错误,让上层 Service 处理
|
||||
return nil, result.Error
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// Create 创建新用户
|
||||
func (r *GormUserRepository) Create(user *models.User) error {
|
||||
if result := r.DB.Create(user); result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
18
repositories/todo_repository.go
Normal file
18
repositories/todo_repository.go
Normal file
@ -0,0 +1,18 @@
|
||||
package repositories
|
||||
|
||||
import "go-todo-api/models"
|
||||
|
||||
type TodoRepository interface {
|
||||
// ❗ 修正:FindAll, FindByID, Delete 需要 userID 进行查询约束
|
||||
FindAll(userID uint) ([]models.Todo, error)
|
||||
FindByID(id uint, userID uint) (*models.Todo, error)
|
||||
|
||||
// ❗ 修正:Create 不应包含 userID,因为 Service 已经设置了 todo.UserID
|
||||
Create(todo *models.Todo) error
|
||||
|
||||
// ❗ 修正:Update 不应包含 userID,因为 FindByID 已经完成了授权
|
||||
Update(todo *models.Todo, input map[string]interface{}) error
|
||||
|
||||
// 修正:Delete 需要 userID 进行查询约束
|
||||
Delete(id uint, userID uint) error
|
||||
}
|
||||
11
repositories/user_repository.go
Normal file
11
repositories/user_repository.go
Normal file
@ -0,0 +1,11 @@
|
||||
package repositories
|
||||
|
||||
import "go-todo-api/models"
|
||||
|
||||
// UserRepository 定义了与 User 模型相关的数据库操作
|
||||
type UserRepository interface {
|
||||
// FindByUsername 根据用户名查找用户
|
||||
FindByUsername(username string) (*models.User, error)
|
||||
// Create 创建新用户
|
||||
Create(user *models.User) error
|
||||
}
|
||||
1
services/README.md
Normal file
1
services/README.md
Normal file
@ -0,0 +1 @@
|
||||
# 存放业务逻辑
|
||||
17
services/auth_service.go
Normal file
17
services/auth_service.go
Normal file
@ -0,0 +1,17 @@
|
||||
// services/auth_service.go
|
||||
package services
|
||||
|
||||
import (
|
||||
"go-todo-api/dto"
|
||||
"go-todo-api/models"
|
||||
)
|
||||
|
||||
// AuthService 定义了用户认证和授权相关的业务逻辑
|
||||
type AuthService interface {
|
||||
// Register 处理用户注册,返回创建的用户和可能的错误
|
||||
Register(input *dto.RegisterInput) (*models.User, error)
|
||||
// Login 处理用户登录,返回 JWT 令牌和可能的错误
|
||||
Login(input *dto.LoginInput) (string, error)
|
||||
// AuthenticateToken 验证 JWT 令牌,返回用户ID (Subject)
|
||||
AuthenticateToken(tokenString string) (string, error)
|
||||
}
|
||||
123
services/auth_service_impl.go
Normal file
123
services/auth_service_impl.go
Normal file
@ -0,0 +1,123 @@
|
||||
// services/auth_service_impl.go
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go-todo-api/dto"
|
||||
"go-todo-api/models"
|
||||
"go-todo-api/repositories"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// 定义一个秘密常量,你需要确保在整个应用中统一使用它!
|
||||
// 建议从配置文件或环境变量读取
|
||||
const jwtSecret = "aabbccdd66778899.."
|
||||
|
||||
// AuthServiceImpl 实现了 AuthService 接口
|
||||
type AuthServiceImpl struct {
|
||||
UserRepo repositories.UserRepository
|
||||
// ❗ 核心修正:添加 JWTSecret 字段
|
||||
JWTSecret string
|
||||
}
|
||||
|
||||
// NewAuthService 创建 AuthServiceImpl 的新实例
|
||||
func NewAuthService(userRepo repositories.UserRepository, secret string) *AuthServiceImpl {
|
||||
return &AuthServiceImpl{UserRepo: userRepo, JWTSecret: secret}
|
||||
}
|
||||
|
||||
// Register 处理用户注册逻辑
|
||||
func (s *AuthServiceImpl) Register(input *dto.RegisterInput) (*models.User, error) {
|
||||
// 1. 检查用户是否已存在
|
||||
_, err := s.UserRepo.FindByUsername(input.Username)
|
||||
if err == nil {
|
||||
// 如果 FindByUsername 没有返回错误,说明用户已存在
|
||||
return nil, fmt.Errorf("username already taken")
|
||||
}
|
||||
// 如果错误不是 gorm.ErrRecordNotFound,则返回其他错误
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. 密码哈希
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to hash password: %w", err)
|
||||
}
|
||||
|
||||
// 3. 创建用户模型
|
||||
newUser := models.User{
|
||||
Username: input.Username,
|
||||
Password: string(hashedPassword),
|
||||
}
|
||||
|
||||
// 4. 保存到数据库
|
||||
if err := s.UserRepo.Create(&newUser); err != nil {
|
||||
return nil, fmt.Errorf("failed to create user: %w", err)
|
||||
}
|
||||
|
||||
return &newUser, nil
|
||||
}
|
||||
|
||||
// Login 处理用户登录逻辑,返回 JWT 令牌
|
||||
func (s *AuthServiceImpl) Login(input *dto.LoginInput) (string, error) {
|
||||
// 1. 查找用户
|
||||
foundUser, err := s.UserRepo.FindByUsername(input.Username)
|
||||
if err != nil {
|
||||
// 统一返回授权错误,避免泄露用户不存在的信息
|
||||
return "", fmt.Errorf("invalid username or password")
|
||||
}
|
||||
|
||||
// 2. 密码比较
|
||||
err = bcrypt.CompareHashAndPassword([]byte(foundUser.Password), []byte(input.Password))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid username or password")
|
||||
}
|
||||
|
||||
// 3. JWT 令牌生成
|
||||
claims := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{
|
||||
Subject: fmt.Sprintf("%d", foundUser.ID),
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
})
|
||||
|
||||
// 4. 签名令牌
|
||||
tokenString, err := claims.SignedString([]byte(jwtSecret))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate token: %w", err)
|
||||
}
|
||||
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
// AuthenticateToken 验证 JWT 令牌
|
||||
func (s *AuthServiceImpl) AuthenticateToken(tokenString string) (string, error) {
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return []byte(jwtSecret), nil
|
||||
})
|
||||
|
||||
if err != nil || !token.Valid {
|
||||
return "", fmt.Errorf("invalid or expired token")
|
||||
}
|
||||
|
||||
// 从 Claims 中提取 Sub (用户 ID)
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("invalid token claims")
|
||||
}
|
||||
|
||||
sub, err := claims.GetSubject()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("missing subject in token claims")
|
||||
}
|
||||
|
||||
return sub, nil // 返回用户 ID 字符串
|
||||
}
|
||||
64
services/todo_implementation.go
Normal file
64
services/todo_implementation.go
Normal file
@ -0,0 +1,64 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"go-todo-api/models"
|
||||
"go-todo-api/repositories"
|
||||
)
|
||||
|
||||
// TodoServiceImpl 结构体持有 TodoRepository 接口,实现业务逻辑
|
||||
type TodoServiceImpl struct {
|
||||
// Repo 字段是 Repository 接口类型,而不是具体实现
|
||||
Repo repositories.TodoRepository
|
||||
}
|
||||
|
||||
// NewTodoService 实例化一个新的 TodoService
|
||||
func NewTodoService(repo repositories.TodoRepository) TodoService {
|
||||
return &TodoServiceImpl{Repo: repo}
|
||||
}
|
||||
|
||||
// FindAllTodos 实现了 Service 接口,直接调用 Repository
|
||||
func (s *TodoServiceImpl) FindAllTodos(userID uint) ([]models.Todo, error) {
|
||||
// 业务逻辑(例如:权限检查、数据缓存等)会在这里添加
|
||||
// ❗ 修正:将 userID 传递给 Repo
|
||||
return s.Repo.FindAll(userID)
|
||||
}
|
||||
|
||||
// FindTodoByID 实现了 Service 接口,并处理 Repository 可能返回的错误
|
||||
func (s *TodoServiceImpl) FindTodoByID(id uint, userID uint) (*models.Todo, error) {
|
||||
// 调用 Repository 查找数据
|
||||
return s.Repo.FindByID(id, userID)
|
||||
}
|
||||
|
||||
// DeleteTodoByID 实现了 Service 接口的删除方法
|
||||
func (s *TodoServiceImpl) DeleteTodoByID(id uint, userID uint) error {
|
||||
// ❗ 调用 Repository 层执行删除
|
||||
return s.Repo.Delete(id, userID)
|
||||
}
|
||||
|
||||
// CreateTodo 实现了 Service 接口的创建方法
|
||||
func (s *TodoServiceImpl) CreateTodo(todo *models.Todo, userID uint) error {
|
||||
// 1. 设置 UserID (这是业务逻辑,在 Service 层完成)
|
||||
todo.UserID = userID
|
||||
// ❗ 调用 Repository 层执行创建
|
||||
return s.Repo.Create(todo)
|
||||
}
|
||||
|
||||
// UpdateTodo 实现了 Service 接口的更新方法
|
||||
func (s *TodoServiceImpl) UpdateTodo(id uint, userID uint, input map[string]interface{}) (*models.Todo, error) {
|
||||
// 1. 先查找记录(Repository层 FindByID 已经实现了)
|
||||
// ❗ 修正:将 userID 传递给 FindByID
|
||||
todo, err := s.Repo.FindByID(id, userID)
|
||||
if err != nil {
|
||||
// 如果未找到或有其他错误,直接返回
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. 使用 Repository 层执行更新
|
||||
// 2. Repository 层执行更新 (Repository 不需知道 UserID,因为它操作的是已找到的 todo 对象)
|
||||
if err := s.Repo.Update(todo, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 3. 返回更新后的对象
|
||||
return todo, nil
|
||||
}
|
||||
19
services/todo_service.go
Normal file
19
services/todo_service.go
Normal file
@ -0,0 +1,19 @@
|
||||
package services
|
||||
|
||||
import "go-todo-api/models"
|
||||
|
||||
type TodoService interface {
|
||||
// FindAllTodos 返回所有 Todo 记录
|
||||
FindAllTodos(userID uint) ([]models.Todo, error)
|
||||
|
||||
// FindTodoByID 根据 ID 返回单条 Todo 记录
|
||||
FindTodoByID(id uint, userID uint) (*models.Todo, error)
|
||||
|
||||
// ❗ 新增:用于删除待办事项的方法
|
||||
DeleteTodoByID(id uint, userID uint) error // ❗ 确保有此签名
|
||||
|
||||
// ❗ 新增:创建待办事项时,需要传递用户 ID
|
||||
CreateTodo(todo *models.Todo, userID uint) error // ❗ 确保有此签名
|
||||
|
||||
UpdateTodo(id uint, userID uint, input map[string]interface{}) (*models.Todo, error) // ❗ 确保有此签名
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user