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

@@ -9,7 +9,7 @@ type Role struct {
Code string `json:"code" gorm:"uniqueIndex;size:50;not null"` // 角色代码,全局唯一,如 user/admin/super_admin对应 constants.RoleCode* 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"` // 角色中文显示名称,如「普通用户」「管理员」 Name string `json:"name" gorm:"size:50;not null"` // 角色中文显示名称,如「普通用户」「管理员」
Description string `json:"description" gorm:"size:200;default:''"` // 角色描述说明,用于后台管理界面展示 Description string `json:"description" gorm:"size:200;default:''"` // 角色描述说明,用于后台管理界面展示
CreatedAt time.Time `json:"created_at" gorm:"not null;autoCreateTime"` // 创建时间,由 GORM 自动填充 CreatedAt time.Time `json:"created_at" gorm:"not null;autoCreateTime;type:timestamp(0)"` // 创建时间,由 GORM 自动填充,精确到秒
} }
// TableName 指定数据库表名 // TableName 指定数据库表名
@@ -22,7 +22,7 @@ func (Role) TableName() string {
type UserRole struct { type UserRole struct {
UserID int64 `json:"user_id" gorm:"primaryKey"` // 关联的用户 ID外键指向 auth_users.id UserID int64 `json:"user_id" gorm:"primaryKey"` // 关联的用户 ID外键指向 auth_users.id
RoleID int `json:"role_id" gorm:"primaryKey"` // 关联的角色 ID外键指向 auth_roles.id RoleID int `json:"role_id" gorm:"primaryKey"` // 关联的角色 ID外键指向 auth_roles.id
CreatedAt time.Time `json:"created_at" gorm:"not null;autoCreateTime"` // 角色分配时间,由 GORM 自动填充 CreatedAt time.Time `json:"created_at" gorm:"not null;autoCreateTime;type:timestamp(0)"` // 角色分配时间,由 GORM 自动填充,精确到秒
} }
// TableName 指定数据库表名 // TableName 指定数据库表名

View File

@@ -14,10 +14,10 @@ type User struct {
Gender int `json:"gender" gorm:"not null;default:0"` // 性别0=未知, 1=男, 2=女(对应 constants.Gender* Gender int `json:"gender" gorm:"not null;default:0"` // 性别0=未知, 1=男, 2=女(对应 constants.Gender*
Phone *string `json:"phone" gorm:"size:20"` // 手机号,可选字段,指针类型允许 NULL Phone *string `json:"phone" gorm:"size:20"` // 手机号,可选字段,指针类型允许 NULL
Status int `json:"status" gorm:"not null;default:1"` // 账号状态1=正常, 2=禁用, 3=注销(对应 constants.UserStatus* Status int `json:"status" gorm:"not null;default:1"` // 账号状态1=正常, 2=禁用, 3=注销(对应 constants.UserStatus*
LastLoginAt *time.Time `json:"last_login_at"` // 最后登录时间,首次注册时为 NULL 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 地址,用于安全审计 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 自动填充 CreatedAt time.Time `json:"created_at" gorm:"not null;autoCreateTime;type:timestamp(0)"` // 账号创建时间,由 GORM 自动填充,精确到秒
UpdatedAt time.Time `json:"updated_at" gorm:"not null;autoUpdateTime"` // 最后更新时间,由 GORM 自动更新 UpdatedAt time.Time `json:"updated_at" gorm:"not null;autoUpdateTime;type:timestamp(0)"` // 最后更新时间,由 GORM 自动更新,精确到秒
} }
// TableName 指定数据库表名 // TableName 指定数据库表名

View File

@@ -17,10 +17,10 @@ CREATE TABLE auth_users (
gender SMALLINT NOT NULL DEFAULT 0, gender SMALLINT NOT NULL DEFAULT 0,
phone VARCHAR(20) DEFAULT NULL, phone VARCHAR(20) DEFAULT NULL,
status SMALLINT NOT NULL DEFAULT 1, 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, last_login_ip VARCHAR(50) DEFAULT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(), created_at TIMESTAMP(0) NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW() updated_at TIMESTAMP(0) NOT NULL DEFAULT NOW()
); );
COMMENT ON TABLE auth_users IS '用户主表,存储所有用户信息(普通用户与管理员共用)'; COMMENT ON TABLE auth_users IS '用户主表,存储所有用户信息(普通用户与管理员共用)';
@@ -47,7 +47,7 @@ CREATE TABLE auth_roles (
code VARCHAR(50) UNIQUE NOT NULL, code VARCHAR(50) UNIQUE NOT NULL,
name VARCHAR(50) NOT NULL, name VARCHAR(50) NOT NULL,
description VARCHAR(200) DEFAULT '', 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 '角色表,定义系统中所有角色类型'; COMMENT ON TABLE auth_roles IS '角色表,定义系统中所有角色类型';
@@ -70,7 +70,7 @@ INSERT INTO auth_roles (code, name, description) VALUES
CREATE TABLE auth_user_roles ( CREATE TABLE auth_user_roles (
user_id BIGINT NOT NULL REFERENCES auth_users(id), user_id BIGINT NOT NULL REFERENCES auth_users(id),
role_id INT NOT NULL REFERENCES auth_roles(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) 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 | 即时消息收发、会话管理、消息存储 | | im | 即时消息收发、会话管理、消息存储 |
| contact | 好友关系管理、好友分组 | | contact | 好友关系管理、好友分组 |
| meeting | 会议创建/管理、信令转发、mediasoup 资源编排 | | meeting | 会议创建/管理、信令转发、mediasoup 资源编排 |
@@ -236,7 +236,7 @@ mediasoup C++ SFU 的 **"遥控器"**,不懂业务、不懂用户、只懂媒
```yaml ```yaml
services: services:
go-service: # Go 后端 → :8080 go-service: # Go 后端 → :8085
media-server: # mediasoup Node → :3000 + :40000-40100/udp media-server: # mediasoup Node → :3000 + :40000-40100/udp
postgres: # PostgreSQL 17 → :5432 postgres: # PostgreSQL 17 → :5432
redis: # Redis 7 → :6379 redis: # Redis 7 → :6379

View File

@@ -6,7 +6,7 @@
**架构:** Go 单体模块化服务 + uniapp 前台 + Vue3 管理端 + PostgreSQL + RedisDocker Compose 编排。 **架构:** 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` **设计文档:** `docs/plans/2026-02-27-echochat-system-design.md`
@@ -319,7 +319,7 @@ Run: `cd backend/go-service && go mod tidy`
**Step 10: 运行验证** **Step 10: 运行验证**
Run: `cd backend/go-service && go run cmd/server/main.go` Run: `cd backend/go-service && go run cmd/server/main.go`
Expected: 服务启动在 :8080,访问 /health 返回 200 Expected: 服务启动在 :8085,访问 /health 返回 200
**Step 11: Commit** **Step 11: Commit**
@@ -344,18 +344,18 @@ git commit -m "feat(backend): Go 服务骨架(配置、日志、数据库、
**Step 1: 创建用户状态和角色常量** **Step 1: 创建用户状态和角色常量**
```go ```go
// app/constants/user_status.go // app/constants/user_status.go — 驼峰命名,符合 Go 社区规范
const ( const (
USER_STATUS_ACTIVE = 1 // 正常 UserStatusActive = 1 // 正常
USER_STATUS_DISABLED = 2 // 禁用 UserStatusDisabled = 2 // 禁用
USER_STATUS_DELETED = 3 // 注销 UserStatusDeleted = 3 // 注销
) )
// app/constants/role_code.go // app/constants/role_code.go
const ( const (
ROLE_USER = "user" RoleUser = "user"
ROLE_ADMIN = "admin" RoleAdmin = "admin"
ROLE_SUPER_ADMIN = "super_admin" RoleSuperAdmin = "super_admin"
) )
``` ```
@@ -419,6 +419,7 @@ git commit -m "feat(auth): 用户/角色模型与数据访问层"
**Files:** **Files:**
- Create: `backend/go-service/app/auth/service/auth_service.go` - 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/app/dto/auth_dto.go`
- Create: `backend/go-service/pkg/utils/jwt.go` - Create: `backend/go-service/pkg/utils/jwt.go`
- Create: `backend/go-service/pkg/utils/password.go` - Create: `backend/go-service/pkg/utils/password.go`
@@ -427,85 +428,87 @@ git commit -m "feat(auth): 用户/角色模型与数据访问层"
**Step 1: 创建密码工具** **Step 1: 创建密码工具**
创建 `pkg/utils/password.go` 创建 `pkg/utils/password.go`
- `HashPassword(password)` — bcrypt 加密 - `HashPassword(password)` — bcrypt 加密`golang.org/x/crypto/bcrypt`,复用 go.mod 已有的兼容版本)
- `CheckPassword(password, hash)` — 密码校验 - `CheckPassword(password, hash)` — 密码校验
**Step 2: 创建 JWT 工具** **Step 2: 创建 JWT 工具**
创建 `pkg/utils/jwt.go` 创建 `pkg/utils/jwt.go`(使用 `github.com/golang-jwt/jwt/v5@v5.2.1`,兼容 Go 1.23
- `GenerateToken(userID, username)` — 生成 Access Token - `GenerateToken(cfg, userID, username, roles)` — 生成 Access Token
- `GenerateRefreshToken(userID)` — 生成 Refresh Token - `GenerateRefreshToken(cfg, userID)` — 生成 Refresh Token
- `ParseToken(tokenStr)` — 解析验证 Token - `ParseToken(cfg, tokenStr)` — 解析验证 Token
- Claims 结构体包含 UserID, Username, Roles - Claims 结构体包含 UserID, Username, Roles
> **依赖版本选择原则:** 优先选择兼容当前 Go 版本的包版本,不盲目拉取最新版,避免触发工具链升级。
**Step 3: 创建认证 DTO** **Step 3: 创建认证 DTO**
创建 `app/dto/auth_dto.go` 创建 `app/dto/auth_dto.go`
```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 { type UserInfo struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Username string `json:"username"` Username string `json:"username"`
Email string `json:"email"` Email string `json:"email"`
Nickname string `json:"nickname"` Nickname string `json:"nickname"`
Avatar string `json:"avatar"` Avatar string `json:"avatar"`
Gender int `json:"gender"`
Phone string `json:"phone,omitempty"`
Roles []string `json:"roles"` 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` 创建 `app/auth/service/auth_service.go`
- `Register(ctx, req)` — 注册逻辑(检查重复、加密密码、创建用户、分配默认角色、生成 Token - `Register(ctx, req)` — 注册逻辑(检查重复、加密密码、创建用户、分配默认角色、生成 Token + 存 Redis
- `Login(ctx, req)` — 登录逻辑(查找用户、校验密码、检查状态、更新登录信息、生成 Token - `Login(ctx, req, clientIP)` — 登录逻辑(查找用户、校验密码、检查状态、更新登录信息、生成 Token + 存 Redis
- `RefreshToken(ctx, refreshToken)` — 刷新 Token - `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)` — 获取用户信息 - `GetProfile(ctx, userID)` — 获取用户信息
- `UpdateProfile(ctx, userID, req)` — 更新用户信息 - `UpdateProfile(ctx, userID, req)` — 更新用户信息
- `ChangePassword(ctx, userID, req)` — 修改密码 - `ChangePassword(ctx, userID, req)` — 修改密码
**Step 5: 创建认证中间件** **Step 6: 创建认证中间件**
创建 `pkg/middleware/auth.go` 创建 `pkg/middleware/auth.go`
- `JWTAuth()` — JWT 验证中间件,从 Authorization Header 提取 Token - `TokenValidator` 接口 — 定义 Token 校验契约,由 AuthService 实现(解耦 pkg 与 app 层依赖)
- `JWTAuth(cfg, validator)` — 有状态 JWT 验证中间件(解析 Token + 校验 Redis 有效性)
- `RequireRole(roles ...string)` — 角色检查中间件 - `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 ```go
var AuthSet = wire.NewSet( var AuthSet = wire.NewSet(
dao.NewUserDAO, dao.NewUserDAO,
dao.NewRoleDAO, dao.NewRoleDAO,
service.NewTokenStore,
service.NewAuthService, service.NewAuthService,
controller.NewAuthController,
controller.NewAdminAuthController,
) )
``` ```
**Step 7: Commit** **Step 8: Commit**
```bash ```bash
git add backend/go-service/ 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: 运行验证** **Step 5: 运行验证**
Run: `cd backend/go-service && go run cmd/server/main.go` Run: `cd backend/go-service && go run cmd/server/main.go`
Expected: 服务启动,可以通过 curl 测试注册和登录接口 Expected: 服务启动在 :8085,可以通过 curl 测试注册和登录接口
```bash ```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" \ -H "Content-Type: application/json" \
-d '{"username":"testuser","email":"test@example.com","password":"123456","nickname":"测试用户"}' -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" \ -H "Content-Type: application/json" \
-d '{"account":"testuser","password":"123456"}' -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** **Step 6: Commit**