fix: 时间精度精确到秒 + 同步全部文档到最新状态

时间字段修正:
- 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
This commit is contained in:
bujinyuan
2026-02-28 17:04:47 +08:00
parent b607434c57
commit e9bd4dd204
5 changed files with 91 additions and 82 deletions

View File

@@ -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 指定数据库表名

View File

@@ -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 指定数据库表名

View File

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

View File

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

View File

@@ -6,7 +6,7 @@
**架构:** Go 单体模块化服务 + uniapp 前台 + Vue3 管理端 + PostgreSQL + RedisDocker 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**