From e9bd4dd204ed977c77137e9c97f3c799dce4ec24 Mon Sep 17 00:00:00 2001 From: bujinyuan Date: Sat, 28 Feb 2026 17:04:47 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=97=B6=E9=97=B4=E7=B2=BE=E5=BA=A6?= =?UTF-8?q?=E7=B2=BE=E7=A1=AE=E5=88=B0=E7=A7=92=20+=20=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E5=85=A8=E9=83=A8=E6=96=87=E6=A1=A3=E5=88=B0=E6=9C=80=E6=96=B0?= =?UTF-8?q?=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 时间字段修正: - init.sql: 所有 TIMESTAMP 改为 TIMESTAMP(0),精确到秒 - GORM 模型: 所有时间字段添加 type:timestamp(0) tag - 已有数据库表通过 ALTER TABLE 修正列类型 文档同步(架构设计 + 实施步骤): - system-architecture.md: - auth 模块描述更新为"有状态 JWT Token 管理(Redis 存储)" - 开发环境端口 8080 → 8085 - phase1-foundation-and-auth.md: - Go 版本 1.22+ → 1.23+ - 常量命名更新为驼峰风格(UserStatusActive / RoleUser 等) - Task 4 完整重写:增加 token_store.go、TokenValidator 接口、有状态 JWT - Task 5 端口号 8080 → 8085,测试命令增加登出失效验证 - 依赖版本选择原则说明 Made-with: Cursor --- backend/go-service/app/auth/model/role.go | 16 +-- backend/go-service/app/auth/model/user.go | 26 ++-- deploy/docker/postgres/init.sql | 10 +- docs/architecture/system-architecture.md | 4 +- .../2026-02-27-phase1-foundation-and-auth.md | 117 ++++++++++-------- 5 files changed, 91 insertions(+), 82 deletions(-) diff --git a/backend/go-service/app/auth/model/role.go b/backend/go-service/app/auth/model/role.go index 63c8a5e..fa096f6 100644 --- a/backend/go-service/app/auth/model/role.go +++ b/backend/go-service/app/auth/model/role.go @@ -5,11 +5,11 @@ import "time" // Role 角色表模型,对应 auth_roles 表 // 系统预置三种角色:user(普通用户)、admin(管理员)、super_admin(超级管理员) type Role struct { - ID int `json:"id" gorm:"primaryKey;autoIncrement"` // 角色唯一标识,自增主键 - Code string `json:"code" gorm:"uniqueIndex;size:50;not null"` // 角色代码,全局唯一,如 user/admin/super_admin(对应 constants.RoleCode*) - Name string `json:"name" gorm:"size:50;not null"` // 角色中文显示名称,如「普通用户」「管理员」 - Description string `json:"description" gorm:"size:200;default:''"` // 角色描述说明,用于后台管理界面展示 - CreatedAt time.Time `json:"created_at" gorm:"not null;autoCreateTime"` // 创建时间,由 GORM 自动填充 + ID int `json:"id" gorm:"primaryKey;autoIncrement"` // 角色唯一标识,自增主键 + Code string `json:"code" gorm:"uniqueIndex;size:50;not null"` // 角色代码,全局唯一,如 user/admin/super_admin(对应 constants.RoleCode*) + Name string `json:"name" gorm:"size:50;not null"` // 角色中文显示名称,如「普通用户」「管理员」 + Description string `json:"description" gorm:"size:200;default:''"` // 角色描述说明,用于后台管理界面展示 + CreatedAt time.Time `json:"created_at" gorm:"not null;autoCreateTime;type:timestamp(0)"` // 创建时间,由 GORM 自动填充,精确到秒 } // TableName 指定数据库表名 @@ -20,9 +20,9 @@ func (Role) TableName() string { // UserRole 用户角色关联表模型,对应 auth_user_roles 表 // 采用联合主键(user_id + role_id),一个用户可拥有多个角色 type UserRole struct { - UserID int64 `json:"user_id" gorm:"primaryKey"` // 关联的用户 ID,外键指向 auth_users.id - RoleID int `json:"role_id" gorm:"primaryKey"` // 关联的角色 ID,外键指向 auth_roles.id - CreatedAt time.Time `json:"created_at" gorm:"not null;autoCreateTime"` // 角色分配时间,由 GORM 自动填充 + UserID int64 `json:"user_id" gorm:"primaryKey"` // 关联的用户 ID,外键指向 auth_users.id + RoleID int `json:"role_id" gorm:"primaryKey"` // 关联的角色 ID,外键指向 auth_roles.id + CreatedAt time.Time `json:"created_at" gorm:"not null;autoCreateTime;type:timestamp(0)"` // 角色分配时间,由 GORM 自动填充,精确到秒 } // TableName 指定数据库表名 diff --git a/backend/go-service/app/auth/model/user.go b/backend/go-service/app/auth/model/user.go index b309596..199c322 100644 --- a/backend/go-service/app/auth/model/user.go +++ b/backend/go-service/app/auth/model/user.go @@ -5,19 +5,19 @@ import "time" // User 用户主表模型,对应 auth_users 表 type User struct { - ID int64 `json:"id" gorm:"primaryKey;autoIncrement"` // 用户唯一标识,自增主键 - Username string `json:"username" gorm:"uniqueIndex;size:50;not null"` // 登录用户名,全局唯一 - Email string `json:"email" gorm:"uniqueIndex;size:100;not null"` // 邮箱地址,全局唯一,用于登录和找回密码 - PasswordHash string `json:"-" gorm:"column:password_hash;size:255;not null"` // bcrypt 加密后的密码哈希,JSON 序列化时隐藏 - Nickname string `json:"nickname" gorm:"size:50;not null;default:''"` // 用户昵称,用于页面展示,允许重复 - Avatar string `json:"avatar" gorm:"size:500;not null;default:''"` // 头像 URL 地址,为空则使用默认头像 - Gender int `json:"gender" gorm:"not null;default:0"` // 性别:0=未知, 1=男, 2=女(对应 constants.Gender*) - Phone *string `json:"phone" gorm:"size:20"` // 手机号,可选字段,指针类型允许 NULL - Status int `json:"status" gorm:"not null;default:1"` // 账号状态:1=正常, 2=禁用, 3=注销(对应 constants.UserStatus*) - LastLoginAt *time.Time `json:"last_login_at"` // 最后登录时间,首次注册时为 NULL - LastLoginIP *string `json:"last_login_ip" gorm:"column:last_login_ip;size:50"` // 最后登录 IP 地址,用于安全审计 - CreatedAt time.Time `json:"created_at" gorm:"not null;autoCreateTime"` // 账号创建时间,由 GORM 自动填充 - UpdatedAt time.Time `json:"updated_at" gorm:"not null;autoUpdateTime"` // 最后更新时间,由 GORM 自动更新 + ID int64 `json:"id" gorm:"primaryKey;autoIncrement"` // 用户唯一标识,自增主键 + Username string `json:"username" gorm:"uniqueIndex;size:50;not null"` // 登录用户名,全局唯一 + Email string `json:"email" gorm:"uniqueIndex;size:100;not null"` // 邮箱地址,全局唯一,用于登录和找回密码 + PasswordHash string `json:"-" gorm:"column:password_hash;size:255;not null"` // bcrypt 加密后的密码哈希,JSON 序列化时隐藏 + Nickname string `json:"nickname" gorm:"size:50;not null;default:''"` // 用户昵称,用于页面展示,允许重复 + Avatar string `json:"avatar" gorm:"size:500;not null;default:''"` // 头像 URL 地址,为空则使用默认头像 + Gender int `json:"gender" gorm:"not null;default:0"` // 性别:0=未知, 1=男, 2=女(对应 constants.Gender*) + Phone *string `json:"phone" gorm:"size:20"` // 手机号,可选字段,指针类型允许 NULL + Status int `json:"status" gorm:"not null;default:1"` // 账号状态:1=正常, 2=禁用, 3=注销(对应 constants.UserStatus*) + LastLoginAt *time.Time `json:"last_login_at" gorm:"type:timestamp(0)"` // 最后登录时间,首次注册时为 NULL + LastLoginIP *string `json:"last_login_ip" gorm:"column:last_login_ip;size:50"` // 最后登录 IP 地址,用于安全审计 + CreatedAt time.Time `json:"created_at" gorm:"not null;autoCreateTime;type:timestamp(0)"` // 账号创建时间,由 GORM 自动填充,精确到秒 + UpdatedAt time.Time `json:"updated_at" gorm:"not null;autoUpdateTime;type:timestamp(0)"` // 最后更新时间,由 GORM 自动更新,精确到秒 } // TableName 指定数据库表名 diff --git a/deploy/docker/postgres/init.sql b/deploy/docker/postgres/init.sql index 3b3e98b..8e424f1 100644 --- a/deploy/docker/postgres/init.sql +++ b/deploy/docker/postgres/init.sql @@ -17,10 +17,10 @@ CREATE TABLE auth_users ( gender SMALLINT NOT NULL DEFAULT 0, phone VARCHAR(20) DEFAULT NULL, status SMALLINT NOT NULL DEFAULT 1, - last_login_at TIMESTAMP DEFAULT NULL, + last_login_at TIMESTAMP(0) DEFAULT NULL, last_login_ip VARCHAR(50) DEFAULT NULL, - created_at TIMESTAMP NOT NULL DEFAULT NOW(), - updated_at TIMESTAMP NOT NULL DEFAULT NOW() + created_at TIMESTAMP(0) NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP(0) NOT NULL DEFAULT NOW() ); COMMENT ON TABLE auth_users IS '用户主表,存储所有用户信息(普通用户与管理员共用)'; @@ -47,7 +47,7 @@ CREATE TABLE auth_roles ( code VARCHAR(50) UNIQUE NOT NULL, name VARCHAR(50) NOT NULL, description VARCHAR(200) DEFAULT '', - created_at TIMESTAMP NOT NULL DEFAULT NOW() + created_at TIMESTAMP(0) NOT NULL DEFAULT NOW() ); COMMENT ON TABLE auth_roles IS '角色表,定义系统中所有角色类型'; @@ -70,7 +70,7 @@ INSERT INTO auth_roles (code, name, description) VALUES CREATE TABLE auth_user_roles ( user_id BIGINT NOT NULL REFERENCES auth_users(id), role_id INT NOT NULL REFERENCES auth_roles(id), - created_at TIMESTAMP NOT NULL DEFAULT NOW(), + created_at TIMESTAMP(0) NOT NULL DEFAULT NOW(), PRIMARY KEY (user_id, role_id) ); diff --git a/docs/architecture/system-architecture.md b/docs/architecture/system-architecture.md index d1f17da..fa4134d 100644 --- a/docs/architecture/system-architecture.md +++ b/docs/architecture/system-architecture.md @@ -88,7 +88,7 @@ EchoChat 采用 **「精简单体 + 媒体微服务」** 架构,核心思想 | 模块 | 职责 | |------|------| -| auth | 用户注册/登录、JWT Token 管理、RBAC 角色权限(当前粗粒度 3 角色,预留细粒度权限点扩展) | +| auth | 用户注册/登录、有状态 JWT Token 管理(Redis 存储)、RBAC 角色权限(当前粗粒度 3 角色,预留细粒度权限点扩展) | | im | 即时消息收发、会话管理、消息存储 | | contact | 好友关系管理、好友分组 | | meeting | 会议创建/管理、信令转发、mediasoup 资源编排 | @@ -236,7 +236,7 @@ mediasoup C++ SFU 的 **"遥控器"**,不懂业务、不懂用户、只懂媒 ```yaml services: - go-service: # Go 后端 → :8080 + go-service: # Go 后端 → :8085 media-server: # mediasoup Node → :3000 + :40000-40100/udp postgres: # PostgreSQL 17 → :5432 redis: # Redis 7 → :6379 diff --git a/docs/plans/2026-02-27-phase1-foundation-and-auth.md b/docs/plans/2026-02-27-phase1-foundation-and-auth.md index 50987ca..030b08a 100644 --- a/docs/plans/2026-02-27-phase1-foundation-and-auth.md +++ b/docs/plans/2026-02-27-phase1-foundation-and-auth.md @@ -6,7 +6,7 @@ **架构:** Go 单体模块化服务 + uniapp 前台 + Vue3 管理端 + PostgreSQL + Redis,Docker Compose 编排。 -**技术栈:** Go 1.22+ (Gin + GORM + Wire + zap) / Vue 3.4+ / uniapp 3.0 / Element Plus / PostgreSQL 17 / Redis 7 / Docker Compose +**技术栈:** Go 1.23+ (Gin + GORM + Wire + zap) / Vue 3.4+ / uniapp 3.0 / Element Plus / PostgreSQL 17 / Redis 7 / Docker Compose **设计文档:** `docs/plans/2026-02-27-echochat-system-design.md` @@ -319,7 +319,7 @@ Run: `cd backend/go-service && go mod tidy` **Step 10: 运行验证** Run: `cd backend/go-service && go run cmd/server/main.go` -Expected: 服务启动在 :8080,访问 /health 返回 200 +Expected: 服务启动在 :8085,访问 /health 返回 200 **Step 11: Commit** @@ -344,18 +344,18 @@ git commit -m "feat(backend): Go 服务骨架(配置、日志、数据库、 **Step 1: 创建用户状态和角色常量** ```go -// app/constants/user_status.go +// app/constants/user_status.go — 驼峰命名,符合 Go 社区规范 const ( - USER_STATUS_ACTIVE = 1 // 正常 - USER_STATUS_DISABLED = 2 // 禁用 - USER_STATUS_DELETED = 3 // 注销 + UserStatusActive = 1 // 正常 + UserStatusDisabled = 2 // 禁用 + UserStatusDeleted = 3 // 注销 ) // app/constants/role_code.go const ( - ROLE_USER = "user" - ROLE_ADMIN = "admin" - ROLE_SUPER_ADMIN = "super_admin" + RoleUser = "user" + RoleAdmin = "admin" + RoleSuperAdmin = "super_admin" ) ``` @@ -419,6 +419,7 @@ git commit -m "feat(auth): 用户/角色模型与数据访问层" **Files:** - Create: `backend/go-service/app/auth/service/auth_service.go` +- Create: `backend/go-service/app/auth/service/token_store.go` - Create: `backend/go-service/app/dto/auth_dto.go` - Create: `backend/go-service/pkg/utils/jwt.go` - Create: `backend/go-service/pkg/utils/password.go` @@ -427,85 +428,87 @@ git commit -m "feat(auth): 用户/角色模型与数据访问层" **Step 1: 创建密码工具** 创建 `pkg/utils/password.go`: -- `HashPassword(password)` — bcrypt 加密 +- `HashPassword(password)` — bcrypt 加密(`golang.org/x/crypto/bcrypt`,复用 go.mod 已有的兼容版本) - `CheckPassword(password, hash)` — 密码校验 **Step 2: 创建 JWT 工具** -创建 `pkg/utils/jwt.go`: -- `GenerateToken(userID, username)` — 生成 Access Token -- `GenerateRefreshToken(userID)` — 生成 Refresh Token -- `ParseToken(tokenStr)` — 解析验证 Token +创建 `pkg/utils/jwt.go`(使用 `github.com/golang-jwt/jwt/v5@v5.2.1`,兼容 Go 1.23): +- `GenerateToken(cfg, userID, username, roles)` — 生成 Access Token +- `GenerateRefreshToken(cfg, userID)` — 生成 Refresh Token +- `ParseToken(cfg, tokenStr)` — 解析验证 Token - Claims 结构体包含 UserID, Username, Roles +> **依赖版本选择原则:** 优先选择兼容当前 Go 版本的包版本,不盲目拉取最新版,避免触发工具链升级。 + **Step 3: 创建认证 DTO** 创建 `app/dto/auth_dto.go`: ```go -type RegisterRequest struct { - Username string `json:"username" binding:"required,min=3,max=50"` - Email string `json:"email" binding:"required,email"` - Password string `json:"password" binding:"required,min=6,max=50"` - Nickname string `json:"nickname" binding:"max=50"` -} - -type LoginRequest struct { - Account string `json:"account" binding:"required"` // 邮箱或用户名 - Password string `json:"password" binding:"required"` -} - -type LoginResponse struct { - Token string `json:"token"` - RefreshToken string `json:"refresh_token"` - ExpiresIn int64 `json:"expires_in"` - User UserInfo `json:"user"` -} - type UserInfo struct { - ID int64 `json:"id"` - Username string `json:"username"` - Email string `json:"email"` - Nickname string `json:"nickname"` - Avatar string `json:"avatar"` - Roles []string `json:"roles"` + ID int64 `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + Nickname string `json:"nickname"` + Avatar string `json:"avatar"` + Gender int `json:"gender"` + Phone string `json:"phone,omitempty"` + Roles []string `json:"roles"` + CreatedAt string `json:"created_at,omitempty"` } ``` -**Step 4: 创建认证服务** +**Step 4: 创建 Token Redis 存储管理** + +创建 `app/auth/service/token_store.go`: +- 遵循设计方案中的 Redis Key 规范:`echo:auth:token:{user_id}` / `echo:auth:refresh:{user_id}` +- `SaveTokens(ctx, userID, accessToken, refreshToken)` — 存入 Redis(覆盖旧 Token,实现单设备登录) +- `ValidateAccessToken(ctx, userID, token)` — 校验 Access Token 与 Redis 一致 +- `ValidateRefreshToken(ctx, userID, token)` — 校验 Refresh Token 与 Redis 一致 +- `RemoveTokens(ctx, userID)` — 登出时删除 Redis 中的 Token + +**Step 5: 创建认证服务** 创建 `app/auth/service/auth_service.go`: -- `Register(ctx, req)` — 注册逻辑(检查重复、加密密码、创建用户、分配默认角色、生成 Token) -- `Login(ctx, req)` — 登录逻辑(查找用户、校验密码、检查状态、更新登录信息、生成 Token) -- `RefreshToken(ctx, refreshToken)` — 刷新 Token +- `Register(ctx, req)` — 注册逻辑(检查重复、加密密码、创建用户、分配默认角色、生成 Token + 存 Redis) +- `Login(ctx, req, clientIP)` — 登录逻辑(查找用户、校验密码、检查状态、更新登录信息、生成 Token + 存 Redis) +- `AdminLogin(ctx, req, clientIP)` — 管理员登录(额外检查 admin/super_admin 角色) +- `RefreshToken(ctx, refreshToken)` — 刷新 Token(校验 Redis + 生成新 Token + 覆盖 Redis) +- `Logout(ctx, userID)` — 登出(从 Redis 删除 Token) +- `ValidateAccessToken(ctx, userID, token)` — 供中间件调用,校验 Redis - `GetProfile(ctx, userID)` — 获取用户信息 - `UpdateProfile(ctx, userID, req)` — 更新用户信息 - `ChangePassword(ctx, userID, req)` — 修改密码 -**Step 5: 创建认证中间件** +**Step 6: 创建认证中间件** 创建 `pkg/middleware/auth.go`: -- `JWTAuth()` — JWT 验证中间件,从 Authorization Header 提取 Token +- `TokenValidator` 接口 — 定义 Token 校验契约,由 AuthService 实现(解耦 pkg 与 app 层依赖) +- `JWTAuth(cfg, validator)` — 有状态 JWT 验证中间件(解析 Token + 校验 Redis 有效性) - `RequireRole(roles ...string)` — 角色检查中间件 -- 将 UserID 和 Roles 注入 Gin Context +- 将 UserID、Username 和 Roles 注入 Gin Context -**Step 6: 更新 Wire Provider** +**Step 7: 更新 Wire Provider** -更新 `app/auth/provider.go`,添加 Service 层 Provider: +更新 `app/auth/provider.go`: ```go var AuthSet = wire.NewSet( dao.NewUserDAO, dao.NewRoleDAO, + service.NewTokenStore, service.NewAuthService, + controller.NewAuthController, + controller.NewAdminAuthController, ) ``` -**Step 7: Commit** +**Step 8: Commit** ```bash git add backend/go-service/ -git commit -m "feat(auth): 认证服务层(注册、登录、JWT、密码加密、中间件)" +git commit -m "feat(auth): 认证服务层(注册、登录、有状态JWT、密码加密、中间件)" ``` --- @@ -585,21 +588,27 @@ func RegisterRoutes(r *gin.Engine, ctrl *AuthController, adminCtrl *AdminAuthCon **Step 5: 运行验证** Run: `cd backend/go-service && go run cmd/server/main.go` -Expected: 服务启动,可以通过 curl 测试注册和登录接口 +Expected: 服务启动在 :8085,可以通过 curl 测试注册和登录接口 ```bash # 测试注册 -curl -X POST http://localhost:8080/api/v1/auth/register \ +curl -X POST http://localhost:8085/api/v1/auth/register \ -H "Content-Type: application/json" \ -d '{"username":"testuser","email":"test@example.com","password":"123456","nickname":"测试用户"}' # 测试登录 -curl -X POST http://localhost:8080/api/v1/auth/login \ +curl -X POST http://localhost:8085/api/v1/auth/login \ -H "Content-Type: application/json" \ -d '{"account":"testuser","password":"123456"}' + +# 测试登出后 Token 立即失效 +TOKEN=$(curl -s ... | jq -r '.data.token') +curl -X POST http://localhost:8085/api/v1/auth/logout -H "Authorization: Bearer $TOKEN" +curl http://localhost:8085/api/v1/auth/profile -H "Authorization: Bearer $TOKEN" +# 预期返回 401 "认证已失效,请重新登录" ``` -Expected: 注册返回 Token,登录返回 Token + 用户信息 +Expected: 注册返回 Token(同时存入 Redis),登录返回 Token + 用户信息,登出后 Token 立即失效 **Step 6: Commit**