Files
user-system/docs/archive/IMPLEMENTATION_PLAN.md

4453 lines
119 KiB
Markdown
Raw Normal View History

# 实施计划 (IMPLEMENTATION_PLAN.md)
## 概述
本文档详细描述用户管理系统的实施计划确保100%还原PRD、数据模型、技术架构、API、安全设计和部署文档的所有设计。
**目标**
- 100%还原PRD所有功能需求
- 100%还原数据模型设计
- 100%还原技术架构设计
- 100%还原API接口设计
- 100%还原安全设计
- 100%还原部署和运维方案
**技术指标**
- 支持10亿用户规模
- 支持10万级并发访问
- P50响应时间 < 100ms
- P99响应时间 < 500ms
- 系统可用性 99.99%
---
## 实施阶段划分
### 阶段1项目初始化与环境搭建第1-2周
**目标**:完成项目基础架构搭建
#### 1.1 技术栈确定
| 技术领域 | 选择 | 版本 | 说明 |
|---------|------|------|------|
| 开发语言 | Go | 1.23+ | 高性能、并发能力强 |
| Web框架 | Gin | 1.10+ | 轻量级、高性能 |
| ORM | GORM | 1.25+ | 功能强大、支持多数据库 |
| 数据库 | SQLite默认 | 3.40+ | 单机部署默认 |
| 数据库 | PostgreSQL | 14+ | 生产环境可选 |
| 数据库 | MySQL | 8.0+ | 生产环境可选 |
| 缓存 | Redis | 7.0+ | 可选用于L2缓存 |
| 配置管理 | Viper | 1.18+ | 支持多格式配置 |
| 日志 | Zap | 1.27+ | 高性能日志库 |
| 监控 | Prometheus | 2.50+ | 指标收集 |
| 链路追踪 | OpenTelemetry | 1.20+ | 分布式追踪 |
| 测试框架 | Testify | 1.9+ | 断言和mock |
| API文档 | Swagger | 0.30+ | 自动生成API文档 |
#### 1.2 项目结构初始化
```
user-management-system/
├── cmd/ # 主程序入口
│ └── server/
│ └── main.go # 服务启动入口
├── internal/ # 内部应用代码
│ ├── api/ # API层
│ │ ├── handler/ # HTTP处理器
│ │ ├── middleware/ # 中间件
│ │ └── router/ # 路由定义
│ ├── service/ # 业务逻辑层
│ │ ├── user.go # 用户服务
│ │ ├── role.go # 角色服务
│ │ ├── permission.go # 权限服务
│ │ ├── auth.go # 认证服务
│ │ └── device.go # 设备服务
│ ├── repository/ # 数据访问层
│ │ ├── user.go
│ │ ├── role.go
│ │ ├── permission.go
│ │ └── device.go
│ ├── domain/ # 领域模型
│ │ ├── user.go
│ │ ├── role.go
│ │ ├── permission.go
│ │ └── device.go
│ ├── cache/ # 缓存层
│ │ ├── l1.go # 本地缓存
│ │ ├── l2.go # Redis缓存
│ │ └── cache_manager.go # 缓存管理器
│ ├── config/ # 配置管理
│ │ └── config.go
│ ├── database/ # 数据库管理
│ │ ├── db.go # 数据库连接
│ │ ├── migration/ # 数据库迁移
│ │ └── transaction.go # 事务管理
│ ├── auth/ # 认证授权
│ │ ├── jwt.go # JWT工具
│ │ ├── password.go # 密码工具
│ │ └── oauth2.go # OAuth2工具
│ ├── security/ # 安全组件
│ │ ├── encryption.go # 加密工具
│ │ ├── ratelimit.go # 限流工具
│ │ └── validator.go # 验证工具
│ ├── monitoring/ # 监控组件
│ │ ├── metrics.go # Prometheus指标
│ │ ├── tracing.go # 链路追踪
│ │ └── health.go # 健康检查
│ └── utils/ # 工具函数
│ ├── logger.go
│ ├── validator.go
│ └── utils.go
├── pkg/ # 公共包
│ ├── errors/ # 错误定义
│ ├── response/ # 统一响应
│ └── constants/ # 常量定义
├── configs/ # 配置文件
│ ├── config.yaml # 默认配置
│ ├── config.dev.yaml # 开发环境
│ ├── config.prod.yaml # 生产环境
│ └── config.sqlite.yaml # SQLite配置
├── scripts/ # 脚本文件
│ ├── install.sh # 安装脚本
│ ├── start.sh # 启动脚本
│ ├── stop.sh # 停止脚本
│ ├── restart.sh # 重启脚本
│ ├── health-check.sh # 健康检查
│ ├── backup.sh # 备份脚本
│ └── self-check.sh # 自检脚本
├── deployments/ # 部署文件
│ ├── docker/
│ │ ├── Dockerfile
│ │ └── docker-compose.yml
│ ├── kubernetes/
│ │ ├── deployment.yaml
│ │ ├── service.yaml
│ │ ├── configmap.yaml
│ │ └── ingress.yaml
│ └── helm/
│ └── user-management/
│ ├── Chart.yaml
│ ├── values.yaml
│ └── templates/
├── migrations/ # 数据库迁移文件
│ ├── sqlite/
│ │ └── V1__init.sql
│ ├── postgresql/
│ │ └── V1__init.sql
│ └── mysql/
│ └── V1__init.sql
├── tests/ # 测试文件
│ ├── unit/ # 单元测试
│ ├── integration/ # 集成测试
│ └── e2e/ # 端到端测试
├── docs/ # 文档
│ ├── README.md
│ ├── PRD.md
│ ├── DATA_MODEL.md
│ ├── ARCHITECTURE.md
│ ├── API.md
│ ├── SECURITY.md
│ ├── DEPLOYMENT.md
│ └── IMPLEMENTATION_PLAN.md
├── data/ # 数据目录SQLite
│ └── .gitkeep
├── logs/ # 日志目录
│ └── .gitkeep
├── .gitignore
├── go.mod
├── go.sum
├── Makefile
├── README.md
└── LICENSE
```
#### 1.3 初始化任务清单
- [ ] 创建Go模块`go mod init github.com/user-management-system`
- [ ] 安装依赖:`go get -u github.com/gin-gonic/gin`
- [ ] 安装依赖:`go get -u gorm.io/gorm`
- [ ] 安装依赖:`go get -u gorm.io/driver/sqlite`
- [ ] 安装依赖:`go get -u gorm.io/driver/postgres`
- [ ] 安装依赖:`go get -u gorm.io/driver/mysql`
- [ ] 安装依赖:`go get -u github.com/redis/go-redis/v9`
- [ ] 安装依赖:`go get -u github.com/spf13/viper`
- [ ] 安装依赖:`go get -u go.uber.org/zap`
- [ ] 安装依赖:`go get -u github.com/prometheus/client_golang`
- [ ] 安装依赖:`go get -u go.opentelemetry.io/otel`
- [ ] 安装依赖:`go get -u github.com/swaggo/gin-swagger`
- [ ] 安装依赖:`go get -u github.com/stretchr/testify`
- [ ] 创建目录结构
- [ ] 配置`.gitignore`
- [ ] 创建`Makefile`
#### 1.4 配置文件示例
**config.yaml**
```yaml
server:
port: 8080
mode: release # debug, release
read_timeout: 30s
write_timeout: 30s
database:
type: sqlite # sqlite, postgresql, mysql
sqlite:
path: ./data/user_management.db
postgresql:
host: localhost
port: 5432
database: user_management
username: postgres
password: password
ssl_mode: disable
max_open_conns: 100
max_idle_conns: 10
mysql:
host: localhost
port: 3306
database: user_management
username: root
password: password
charset: utf8mb4
max_open_conns: 100
max_idle_conns: 10
cache:
l1:
enabled: true
max_size: 10000
ttl: 5m
l2:
enabled: true
type: redis
redis:
addr: localhost:6379
password: ""
db: 0
pool_size: 50
ttl: 30m
redis:
enabled: true
addr: localhost:6379
password: ""
db: 0
jwt:
secret: your-secret-key-change-in-production
access_token_expire: 2h
refresh_token_expire: 7d
security:
password_min_length: 8
password_require_special: true
password_require_number: true
login_max_attempts: 5
login_lock_duration: 30m
ratelimit:
enabled: true
login:
enabled: true
algorithm: token_bucket
capacity: 5
rate: 1 # 1 token per minute
window: 1m
register:
enabled: true
algorithm: leaky_bucket
capacity: 3
rate: 1
window: 1h
api:
enabled: true
algorithm: sliding_window
capacity: 1000
window: 1m
monitoring:
prometheus:
enabled: true
path: /metrics
tracing:
enabled: true
endpoint: http://localhost:4318
service_name: user-management-system
logging:
level: info # debug, info, warn, error
format: json # json, text
output:
- stdout
- ./logs/app.log
rotation:
max_size: 100 # MB
max_age: 30 # days
max_backups: 10
admin:
username: admin
password: <set-via-UMS_ADMIN_PASSWORD> # 历史方案,当前仓库已改为显式初始化
email: admin@example.com
cors:
enabled: true
allowed_origins:
- "*"
allowed_methods:
- GET
- POST
- PUT
- DELETE
- OPTIONS
allowed_headers:
- "*"
max_age: 3600
```
**Makefile**
```makefile
.PHONY: help build run test clean install deps migrate-up migrate-down backup lint fmt vet
help: ## 显示帮助信息
@echo "可用命令:"
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}'
deps: ## 安装依赖
@echo "安装依赖..."
go mod download
go mod tidy
build: ## 构建应用
@echo "构建应用..."
go build -o bin/user-management-system cmd/server/main.go
run: ## 运行应用
@echo "运行应用..."
go run cmd/server/main.go
test: ## 运行测试
@echo "运行测试..."
go test -v ./... -cover
test-coverage: ## 运行测试并生成覆盖率报告
@echo "运行测试并生成覆盖率..."
go test -v ./... -coverprofile=coverage.out
go tool cover -html=coverage.out -o coverage.html
lint: ## 运行代码检查
@echo "运行代码检查..."
golangci-lint run
fmt: ## 格式化代码
@echo "格式化代码..."
go fmt ./...
vet: ## 运行go vet
@echo "运行go vet..."
go vet ./...
migrate-up: ## 执行数据库迁移(向上)
@echo "执行数据库迁移..."
go run cmd/migrate/main.go up
migrate-down: ## 执行数据库迁移(向下)
@echo "回滚数据库迁移..."
go run cmd/migrate/main.go down
backup: ## 备份数据库
@echo "备份数据库..."
./scripts/backup.sh
clean: ## 清理构建文件
@echo "清理构建文件..."
rm -rf bin/
rm -f coverage.out coverage.html
install: ## 安装脚本权限
@echo "设置脚本权限..."
chmod +x scripts/*.sh
swagger: ## 生成Swagger文档
@echo "生成Swagger文档..."
swag init -g cmd/server/main.go
docker-build: ## 构建Docker镜像
@echo "构建Docker镜像..."
docker build -t user-management-system:latest .
docker-run: ## 运行Docker容器
@echo "运行Docker容器..."
docker-compose -f deployments/docker/docker-compose.yml up -d
docker-stop: ## 停止Docker容器
@echo "停止Docker容器..."
docker-compose -f deployments/docker/docker-compose.yml down
```
---
### 阶段2核心数据模型实现第3-4周
**目标**:完成所有数据模型的定义和数据库迁移
#### 2.1 数据模型实现
**2.1.1 用户模型 (users)**
```go
// internal/domain/user.go
package domain
import (
"time"
)
// Gender 性别
type Gender int
const (
GenderUnknown Gender = iota // 未知
GenderMale // 男
GenderFemale // 女
)
// UserStatus 用户状态
type UserStatus int
const (
UserStatusInactive UserStatus = 0 // 未激活
UserStatusActive UserStatus = 1 // 已激活
UserStatusLocked UserStatus = 2 // 已锁定
UserStatusDisabled UserStatus = 3 // 已禁用
)
// User 用户模型
type User struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
Username string `gorm:"type:varchar(50);uniqueIndex;not null" json:"username"`
Email string `gorm:"type:varchar(100);uniqueIndex" json:"email"`
Phone string `gorm:"type:varchar(20);uniqueIndex" json:"phone"`
Nickname string `gorm:"type:varchar(50)" json:"nickname"`
Avatar string `gorm:"type:varchar(255)" json:"avatar"`
Password string `gorm:"type:varchar(255)" json:"-"`
Gender Gender `gorm:"type:int;default:0" json:"gender"`
Birthday *time.Time `gorm:"type:date" json:"birthday,omitempty"`
Region string `gorm:"type:varchar(50)" json:"region"`
Bio string `gorm:"type:varchar(500)" json:"bio"`
Status UserStatus `gorm:"type:int;default:0;index" json:"status"`
LastLoginTime *time.Time `json:"last_login_time,omitempty"`
LastLoginIP string `gorm:"type:varchar(50)" json:"last_login_ip"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
DeletedAt *time.Time `gorm:"index" json:"deleted_at,omitempty"`
}
// TableName 指定表名
func (User) TableName() string {
return "users"
}
```
**2.1.2 角色模型 (roles)**
```go
// internal/domain/role.go
package domain
import "time"
// RoleStatus 角色状态
type RoleStatus int
const (
RoleStatusDisabled RoleStatus = 0 // 禁用
RoleStatusEnabled RoleStatus = 1 // 启用
)
// Role 角色模型
type Role struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
Name string `gorm:"type:varchar(50);uniqueIndex;not null" json:"name"`
Code string `gorm:"type:varchar(50);uniqueIndex;not null" json:"code"`
Description string `gorm:"type:varchar(200)" json:"description"`
ParentID *int64 `gorm:"index" json:"parent_id,omitempty"`
Level int `gorm:"default:1;index" json:"level"`
IsSystem bool `gorm:"default:false" json:"is_system"` // 是否系统角色
IsDefault bool `gorm:"default:false;index" json:"is_default"` // 是否默认角色
Status RoleStatus `gorm:"type:int;default:1" json:"status"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}
// TableName 指定表名
func (Role) TableName() string {
return "roles"
}
// PredefinedRoles 预定义角色
var PredefinedRoles = []Role{
{
ID: 1,
Name: "管理员",
Code: "admin",
Description: "系统管理员角色,拥有所有权限",
ParentID: nil,
Level: 1,
IsSystem: true,
IsDefault: false,
Status: RoleStatusEnabled,
},
{
ID: 2,
Name: "普通用户",
Code: "user",
Description: "普通用户角色,基本权限",
ParentID: nil,
Level: 1,
IsSystem: true,
IsDefault: true,
Status: RoleStatusEnabled,
},
}
```
**2.1.3 权限模型 (permissions)**
```go
// internal/domain/permission.go
package domain
import "time"
// PermissionType 权限类型
type PermissionType int
const (
PermissionTypeMenu PermissionType = iota // 菜单
PermissionTypeButton // 按钮
PermissionTypeAPI // 接口
)
// PermissionStatus 权限状态
type PermissionStatus int
const (
PermissionStatusDisabled PermissionStatus = 0 // 禁用
PermissionStatusEnabled PermissionStatus = 1 // 启用
)
// Permission 权限模型
type Permission struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
Name string `gorm:"type:varchar(50);not null" json:"name"`
Code string `gorm:"type:varchar(100);uniqueIndex;not null" json:"code"`
Type PermissionType `gorm:"type:int;not null" json:"type"`
Description string `gorm:"type:varchar(200)" json:"description"`
ParentID *int64 `gorm:"index" json:"parent_id,omitempty"`
Level int `gorm:"default:1" json:"level"`
Path string `gorm:"type:varchar(200)" json:"path,omitempty"`
Method string `gorm:"type:varchar(10)" json:"method,omitempty"`
Sort int `gorm:"default:0" json:"sort"`
Icon string `gorm:"type:varchar(50)" json:"icon,omitempty"`
Status PermissionStatus `gorm:"type:int;default:1" json:"status"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}
// TableName 指定表名
func (Permission) TableName() string {
return "permissions"
}
```
**2.1.4 其他核心模型**
```go
// internal/domain/user_role.go
package domain
import "time"
// UserRole 用户-角色关联
type UserRole struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
UserID int64 `gorm:"not null;index:idx_user_role;index:idx_user" json:"user_id"`
RoleID int64 `gorm:"not null;index:idx_user_role;index:idx_role" json:"role_id"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
}
// TableName 指定表名
func (UserRole) TableName() string {
return "user_roles"
}
// internal/domain/role_permission.go
package domain
import "time"
// RolePermission 角色-权限关联
type RolePermission struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
RoleID int64 `gorm:"not null;index:idx_role_perm;index:idx_role" json:"role_id"`
PermissionID int64 `gorm:"not null;index:idx_role_perm;index:idx_perm" json:"permission_id"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
}
// TableName 指定表名
func (RolePermission) TableName() string {
return "role_permissions"
}
// internal/domain/device.go
package domain
import "time"
// DeviceType 设备类型
type DeviceType int
const (
DeviceTypeUnknown DeviceType = iota
DeviceTypeWeb
DeviceTypeMobile
DeviceTypeDesktop
)
// DeviceStatus 设备状态
type DeviceStatus int
const (
DeviceStatusInactive DeviceStatus = 0
DeviceStatusActive DeviceStatus = 1
)
// Device 设备模型
type Device struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
UserID int64 `gorm:"not null;index" json:"user_id"`
DeviceID string `gorm:"type:varchar(100);uniqueIndex;not null" json:"device_id"`
DeviceName string `gorm:"type:varchar(100)" json:"device_name"`
DeviceType DeviceType `gorm:"type:int;default:0" json:"device_type"`
DeviceOS string `gorm:"type:varchar(50)" json:"device_os"`
DeviceBrowser string `gorm:"type:varchar(50)" json:"device_browser"`
IP string `gorm:"type:varchar(50)" json:"ip"`
Location string `gorm:"type:varchar(100)" json:"location"`
Status DeviceStatus `gorm:"type:int;default:1" json:"status"`
LastActiveTime time.Time `json:"last_active_time"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}
// TableName 指定表名
func (Device) TableName() string {
return "devices"
}
```
#### 2.2 数据库迁移脚本
**migrations/sqlite/V1__init.sql**
```sql
-- 创建用户表
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE,
phone VARCHAR(20) UNIQUE,
nickname VARCHAR(50),
avatar VARCHAR(255),
password VARCHAR(255),
gender INTEGER DEFAULT 0,
birthday DATE,
region VARCHAR(50),
bio VARCHAR(500),
status INTEGER DEFAULT 0,
last_login_time DATETIME,
last_login_ip VARCHAR(50),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
deleted_at DATETIME
);
-- 创建角色表
CREATE TABLE roles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(50) UNIQUE NOT NULL,
code VARCHAR(50) UNIQUE NOT NULL,
description VARCHAR(200),
parent_id INTEGER,
level INTEGER DEFAULT 1,
is_system INTEGER DEFAULT 0,
is_default INTEGER DEFAULT 0,
status INTEGER DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (parent_id) REFERENCES roles(id)
);
-- 创建权限表
CREATE TABLE permissions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(50) NOT NULL,
code VARCHAR(100) UNIQUE NOT NULL,
type INTEGER NOT NULL,
description VARCHAR(200),
parent_id INTEGER,
level INTEGER DEFAULT 1,
path VARCHAR(200),
method VARCHAR(10),
sort INTEGER DEFAULT 0,
icon VARCHAR(50),
status INTEGER DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (parent_id) REFERENCES permissions(id)
);
-- 创建用户-角色关联表
CREATE TABLE user_roles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
role_id INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (role_id) REFERENCES roles(id),
UNIQUE(user_id, role_id)
);
-- 创建角色-权限关联表
CREATE TABLE role_permissions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
role_id INTEGER NOT NULL,
permission_id INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (role_id) REFERENCES roles(id),
FOREIGN KEY (permission_id) REFERENCES permissions(id),
UNIQUE(role_id, permission_id)
);
-- 创建设备表
CREATE TABLE devices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
device_id VARCHAR(100) UNIQUE NOT NULL,
device_name VARCHAR(100),
device_type INTEGER DEFAULT 0,
device_os VARCHAR(50),
device_browser VARCHAR(50),
ip VARCHAR(50),
location VARCHAR(100),
status INTEGER DEFAULT 1,
last_active_time DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
-- 创建登录日志表
CREATE TABLE login_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
login_type INTEGER NOT NULL,
device_id VARCHAR(100),
ip VARCHAR(50),
location VARCHAR(100),
status INTEGER NOT NULL,
fail_reason VARCHAR(255),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
-- 创建操作日志表
CREATE TABLE operation_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
operation_type VARCHAR(50),
operation_name VARCHAR(100),
request_method VARCHAR(10),
request_path VARCHAR(200),
request_params TEXT,
response_status INTEGER,
ip VARCHAR(50),
user_agent VARCHAR(500),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
-- 创建索引
CREATE INDEX idx_users_status ON users(status);
CREATE INDEX idx_users_created_at ON users(created_at);
CREATE INDEX idx_roles_parent_id ON roles(parent_id);
CREATE INDEX idx_roles_is_default ON roles(is_default);
CREATE INDEX idx_permissions_parent_id ON permissions(parent_id);
CREATE INDEX idx_user_roles_user_id ON user_roles(user_id);
CREATE INDEX idx_user_roles_role_id ON user_roles(role_id);
CREATE INDEX idx_role_permissions_role_id ON role_permissions(role_id);
CREATE INDEX idx_role_permissions_permission_id ON role_permissions(permission_id);
CREATE INDEX idx_devices_user_id ON devices(user_id);
CREATE INDEX idx_devices_device_id ON devices(device_id);
CREATE INDEX idx_login_logs_user_id ON login_logs(user_id);
CREATE INDEX idx_login_logs_created_at ON login_logs(created_at);
CREATE INDEX idx_operation_logs_user_id ON operation_logs(user_id);
CREATE INDEX idx_operation_logs_created_at ON operation_logs(created_at);
-- 插入默认角色
INSERT INTO roles (name, code, description, is_system, is_default) VALUES
('管理员', 'admin', '系统管理员角色,拥有所有权限', 1, 0),
('普通用户', 'user', '普通用户角色,基本权限', 1, 1);
-- 默认管理员账号不再通过迁移直接写入
-- 当前方案改为使用显式初始化工具创建管理员账号
-- 分配管理员角色
-- 管理员角色绑定在显式初始化管理员时一并完成
```
**2.2.2 数据库迁移工具**
```go
// cmd/migrate/main.go
package main
import (
"fmt"
"log"
"os"
"github.com/user-management-system/internal/config"
"github.com/user-management-system/internal/database"
)
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage: migrate <up|down>")
os.Exit(1)
}
command := os.Args[1]
cfg, err := config.Load()
if err != nil {
log.Fatalf("加载配置失败: %v", err)
}
db, err := database.NewDB(cfg)
if err != nil {
log.Fatalf("连接数据库失败: %v", err)
}
switch command {
case "up":
if err := database.MigrateUp(db); err != nil {
log.Fatalf("执行迁移失败: %v", err)
}
log.Println("数据库迁移成功")
case "down":
if err := database.MigrateDown(db); err != nil {
log.Fatalf("回滚迁移失败: %v", err)
}
log.Println("数据库回滚成功")
default:
fmt.Println("未知命令: ", command)
os.Exit(1)
}
}
```
```go
// internal/database/migration.go
package database
import (
"embed"
"fmt"
"io/fs"
"strings"
"gorm.io/gorm"
)
//go:embed migrations/sqlite/*.sql
var sqlFiles embed.FS
// MigrateUp 执行向上迁移
func MigrateUp(db *gorm.DB) error {
// 读取迁移文件
files, err := fs.ReadDir(sqlFiles, "migrations/sqlite")
if err != nil {
return fmt.Errorf("读取迁移文件失败: %w", err)
}
for _, file := range files {
if !strings.HasSuffix(file.Name(), ".sql") {
continue
}
content, err := fs.ReadFile(sqlFiles, "migrations/sqlite/"+file.Name())
if err != nil {
return fmt.Errorf("读取文件 %s 失败: %w", file.Name(), err)
}
if err := db.Exec(string(content)).Error; err != nil {
return fmt.Errorf("执行迁移文件 %s 失败: %w", file.Name(), err)
}
fmt.Printf("执行迁移: %s\n", file.Name())
}
return nil
}
// MigrateDown 执行向下迁移
func MigrateDown(db *gorm.DB) error {
tables := []string{
"operation_logs", "login_logs", "devices",
"role_permissions", "user_roles",
"permissions", "roles", "users",
}
for _, table := range tables {
if err := db.Migrator().DropTable(table); err != nil {
return fmt.Errorf("删除表 %s 失败: %w", table, err)
}
fmt.Printf("删除表: %s\n", table)
}
return nil
}
```
#### 2.3 任务清单
- [ ] 定义用户模型 `User`
- [ ] 定义角色模型 `Role`
- [ ] 定义权限模型 `Permission`
- [ ] 定义用户-角色关联模型 `UserRole`
- [ ] 定义角色-权限关联模型 `RolePermission`
- [ ] 定义设备模型 `Device`
- [ ] 定义登录日志模型 `LoginLog`
- [ ] 定义操作日志模型 `OperationLog`
- [ ] 创建SQLite迁移脚本
- [ ] 创建PostgreSQL迁移脚本
- [ ] 创建MySQL迁移脚本
- [ ] 实现迁移工具
- [ ] 编写模型单元测试
---
### 阶段3缓存层实现第5周
**目标**实现L1本地缓存和L2 Redis缓存
#### 3.1 L1本地缓存实现
```go
// internal/cache/l1.go
package cache
import (
"sync"
"time"
)
// L1Cache L1本地缓存
type L1Cache struct {
data map[string]*cacheItem
mu sync.RWMutex
maxSize int
ttl time.Duration
}
type cacheItem struct {
value interface{}
expiration time.Time
}
// NewL1Cache 创建L1缓存
func NewL1Cache(maxSize int, ttl time.Duration) *L1Cache {
cache := &L1Cache{
data: make(map[string]*cacheItem),
maxSize: maxSize,
ttl: ttl,
}
// 启动清理过期数据的协程
go cache.cleanup()
return cache
}
// Set 设置缓存
func (c *L1Cache) Set(key string, value interface{}) {
c.mu.Lock()
defer c.mu.Unlock()
// 检查缓存大小
if len(c.data) >= c.maxSize {
// 简单的LRU删除最旧的
var oldestKey string
var oldestTime time.Time
for k, item := range c.data {
if oldestTime.IsZero() || item.expiration.Before(oldestTime) {
oldestKey = k
oldestTime = item.expiration
}
}
if oldestKey != "" {
delete(c.data, oldestKey)
}
}
c.data[key] = &cacheItem{
value: value,
expiration: time.Now().Add(c.ttl),
}
}
// Get 获取缓存
func (c *L1Cache) Get(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
item, ok := c.data[key]
if !ok {
return nil, false
}
// 检查是否过期
if time.Now().After(item.expiration) {
return nil, false
}
return item.value, true
}
// Delete 删除缓存
func (c *L1Cache) Delete(key string) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.data, key)
}
// Clear 清空缓存
func (c *L1Cache) Clear() {
c.mu.Lock()
defer c.mu.Unlock()
c.data = make(map[string]*cacheItem)
}
// Size 返回缓存大小
func (c *L1Cache) Size() int {
c.mu.RLock()
defer c.mu.RUnlock()
return len(c.data)
}
// cleanup 定期清理过期数据
func (c *L1Cache) cleanup() {
ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop()
for range ticker.C {
c.mu.Lock()
now := time.Now()
for key, item := range c.data {
if now.After(item.expiration) {
delete(c.data, key)
}
}
c.mu.Unlock()
}
}
```
#### 3.2 L2 Redis缓存实现
```go
// internal/cache/l2.go
package cache
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/redis/go-redis/v9"
)
// L2Cache L2 Redis缓存
type L2Cache struct {
client *redis.Client
ttl time.Duration
}
// NewL2Cache 创建L2缓存
func NewL2Cache(addr, password string, db int, ttl time.Duration) (*L2Cache, error) {
client := redis.NewClient(&redis.Options{
Addr: addr,
Password: password,
DB: db,
PoolSize: 50,
})
// 测试连接
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := client.Ping(ctx).Err(); err != nil {
return nil, fmt.Errorf("连接Redis失败: %w", err)
}
return &L2Cache{
client: client,
ttl: ttl,
}, nil
}
// Set 设置缓存
func (c *L2Cache) Set(ctx context.Context, key string, value interface{}) error {
data, err := json.Marshal(value)
if err != nil {
return fmt.Errorf("序列化失败: %w", err)
}
return c.client.Set(ctx, key, data, c.ttl).Err()
}
// Get 获取缓存
func (c *L2Cache) Get(ctx context.Context, key string, dest interface{}) error {
data, err := c.client.Get(ctx, key).Bytes()
if err != nil {
if err == redis.Nil {
return ErrCacheNotFound
}
return err
}
return json.Unmarshal(data, dest)
}
// Delete 删除缓存
func (c *L2Cache) Delete(ctx context.Context, keys ...string) error {
return c.client.Del(ctx, keys...).Err()
}
// SetWithCustomTTL 设置自定义TTL的缓存
func (c *L2Cache) SetWithCustomTTL(ctx context.Context, key string, value interface{}, ttl time.Duration) error {
data, err := json.Marshal(value)
if err != nil {
return fmt.Errorf("序列化失败: %w", err)
}
return c.client.Set(ctx, key, data, ttl).Err()
}
// MSet 批量设置
func (c *L2Cache) MSet(ctx context.Context, pairs map[string]interface{}) error {
pipe := c.client.Pipeline()
for key, value := range pairs {
data, err := json.Marshal(value)
if err != nil {
return fmt.Errorf("序列化失败: %w", err)
}
pipe.Set(ctx, key, data, c.ttl)
}
_, err := pipe.Exec(ctx)
return err
}
// MGet 批量获取
func (c *L2Cache) MGet(ctx context.Context, keys ...string) ([]interface{}, error) {
results, err := c.client.MGet(ctx, keys...).Result()
if err != nil {
return nil, err
}
var values []interface{}
for _, result := range results {
if result == nil {
values = append(values, nil)
} else {
var value interface{}
if err := json.Unmarshal([]byte(result.(string)), &value); err != nil {
return nil, err
}
values = append(values, value)
}
}
return values, nil
}
// Close 关闭连接
func (c *L2Cache) Close() error {
return c.client.Close()
}
```
#### 3.3 缓存管理器实现
```go
// internal/cache/cache_manager.go
package cache
import (
"context"
"fmt"
"sync"
"time"
"github.com/google/uuid"
)
var (
ErrCacheNotFound = fmt.Errorf("cache not found")
)
// CacheManager 缓存管理器
type CacheManager struct {
l1 *L1Cache
l2 *L2Cache
l1Enabled bool
l2Enabled bool
mu sync.RWMutex
hitCount int64
missCount int64
}
// NewCacheManager 创建缓存管理器
func NewCacheManager(l1 *L1Cache, l2 *L2Cache, l1Enabled, l2Enabled bool) *CacheManager {
return &CacheManager{
l1: l1,
l2: l2,
l1Enabled: l1Enabled,
l2Enabled: l2Enabled,
}
}
// Get 获取缓存(多级缓存)
func (cm *CacheManager) Get(ctx context.Context, key string, dest interface{}) error {
cm.mu.Lock()
cm.hitCount++
cm.mu.Unlock()
// 先查L1缓存
if cm.l1Enabled {
if value, ok := cm.l1.Get(key); ok {
return cm.decode(value, dest)
}
}
// 再查L2缓存
if cm.l2Enabled {
if err := cm.l2.Get(ctx, key, dest); err == nil {
// 回填L1缓存
if cm.l1Enabled {
var value interface{}
if err := cm.l2.Get(ctx, key, &value); err == nil {
cm.l1.Set(key, value)
}
}
return nil
} else if err != ErrCacheNotFound {
return err
}
}
cm.mu.Lock()
cm.hitCount--
cm.missCount++
cm.mu.Unlock()
return ErrCacheNotFound
}
// Set 设置缓存(多级缓存)
func (cm *CacheManager) Set(ctx context.Context, key string, value interface{}) error {
if cm.l1Enabled {
cm.l1.Set(key, value)
}
if cm.l2Enabled {
if err := cm.l2.Set(ctx, key, value); err != nil {
return err
}
}
return nil
}
// Delete 删除缓存
func (cm *CacheManager) Delete(ctx context.Context, key string) error {
if cm.l1Enabled {
cm.l1.Delete(key)
}
if cm.l2Enabled {
return cm.l2.Delete(ctx, key)
}
return nil
}
// Clear 清空所有缓存
func (cm *CacheManager) Clear(ctx context.Context) {
if cm.l1Enabled {
cm.l1.Clear()
}
if cm.l2Enabled {
// L2清理需要pattern这里简化处理
}
}
// GetOrLoad 缓存不存在时加载
func (cm *CacheManager) GetOrLoad(ctx context.Context, key string, dest interface{}, loader func() (interface{}, error)) error {
err := cm.Get(ctx, key, dest)
if err == nil {
return nil
}
if err != ErrCacheNotFound {
return err
}
// 缓存未命中,加载数据
value, err := loader()
if err != nil {
return err
}
// 设置缓存
if err := cm.Set(ctx, key, value); err != nil {
return err
}
return cm.decode(value, dest)
}
// GetStats 获取缓存统计
func (cm *CacheManager) GetStats() CacheStats {
cm.mu.RLock()
defer cm.mu.RUnlock()
total := cm.hitCount + cm.missCount
hitRate := 0.0
if total > 0 {
hitRate = float64(cm.hitCount) / float64(total) * 100
}
return CacheStats{
HitCount: cm.hitCount,
MissCount: cm.missCount,
HitRate: hitRate,
L1Size: cm.l1.Size(),
}
}
// CacheStats 缓存统计
type CacheStats struct {
HitCount int64 `json:"hit_count"`
MissCount int64 `json:"miss_count"`
HitRate float64 `json:"hit_rate"`
L1Size int `json:"l1_size"`
}
// decode 解码缓存值
func (cm *CacheManager) decode(value interface{}, dest interface{}) error {
// 这里需要根据实际类型进行解码
// 简化处理,直接赋值
return nil
}
// GenerateKey 生成缓存key
func GenerateKey(prefix string, parts ...string) string {
key := prefix
for _, part := range parts {
key += ":" + part
}
return key
}
// GenerateUniqueKey 生成唯一key
func GenerateUniqueKey() string {
return uuid.New().String()
}
```
#### 3.4 任务清单
- [ ] 实现L1本地缓存
- [ ] 实现L2 Redis缓存
- [ ] 实现缓存管理器
- [ ] 实现多级缓存策略
- [ ] 实现缓存统计
- [ ] 编写缓存单元测试
---
### 阶段4核心业务服务实现第6-8周
**目标**:实现用户、角色、权限、认证等核心业务服务
#### 4.1 用户服务实现
```go
// internal/service/user.go
package service
import (
"context"
"fmt"
"github.com/user-management-system/internal/cache"
"github.com/user-management-system/internal/domain"
"github.com/user-management-system/internal/repository"
)
// UserService 用户服务
type UserService struct {
userRepo repository.UserRepository
roleRepo repository.RoleRepository
cache *cache.CacheManager
auth AuthService
}
// NewUserService 创建用户服务
func NewUserService(
userRepo repository.UserRepository,
roleRepo repository.RoleRepository,
cache *cache.CacheManager,
auth AuthService,
) *UserService {
return &UserService{
userRepo: userRepo,
roleRepo: roleRepo,
cache: cache,
auth: auth,
}
}
// Register 用户注册
func (s *UserService) Register(ctx context.Context, req *RegisterRequest) (*domain.User, error) {
// 1. 验证用户名是否已存在
exists, err := s.userRepo.ExistsByUsername(ctx, req.Username)
if err != nil {
return nil, fmt.Errorf("检查用户名失败: %w", err)
}
if exists {
return nil, ErrUsernameExists
}
// 2. 验证邮箱是否已存在
if req.Email != "" {
exists, err := s.userRepo.ExistsByEmail(ctx, req.Email)
if err != nil {
return nil, fmt.Errorf("检查邮箱失败: %w", err)
}
if exists {
return nil, ErrEmailExists
}
}
// 3. 验证手机号是否已存在
if req.Phone != "" {
exists, err := s.userRepo.ExistsByPhone(ctx, req.Phone)
if err != nil {
return nil, fmt.Errorf("检查手机号失败: %w", err)
}
if exists {
return nil, ErrPhoneExists
}
}
// 4. 加密密码
hashedPassword, err := s.auth.HashPassword(req.Password)
if err != nil {
return nil, fmt.Errorf("加密密码失败: %w", err)
}
// 5. 创建用户
user := &domain.User{
Username: req.Username,
Email: req.Email,
Phone: req.Phone,
Nickname: req.Nickname,
Password: hashedPassword,
Status: domain.UserStatusInactive,
}
if err := s.userRepo.Create(ctx, user); err != nil {
return nil, fmt.Errorf("创建用户失败: %w", err)
}
// 6. 分配默认角色
defaultRole, err := s.roleRepo.FindDefault(ctx)
if err != nil {
return nil, fmt.Errorf("查找默认角色失败: %w", err)
}
if err := s.roleRepo.AssignToUser(ctx, user.ID, defaultRole.ID); err != nil {
return nil, fmt.Errorf("分配角色失败: %w", err)
}
// 7. 清除用户列表缓存
s.cache.Delete(ctx, cache.GenerateKey("users", "list"))
return user, nil
}
// Login 用户登录
func (s *UserService) Login(ctx context.Context, req *LoginRequest, device *DeviceInfo) (*LoginResponse, error) {
// 1. 查找用户
user, err := s.userRepo.FindByUsername(ctx, req.Username)
if err != nil {
return nil, ErrInvalidCredentials
}
// 2. 验证密码
if !s.auth.VerifyPassword(req.Password, user.Password) {
return nil, ErrInvalidCredentials
}
// 3. 检查用户状态
if user.Status == domain.UserStatusLocked {
return nil, ErrAccountLocked
}
if user.Status == domain.UserStatusDisabled {
return nil, ErrAccountDisabled
}
// 4. 生成Token
accessToken, err := s.auth.GenerateAccessToken(user.ID, user.Username)
if err != nil {
return nil, fmt.Errorf("生成访问令牌失败: %w", err)
}
refreshToken, err := s.auth.GenerateRefreshToken(user.ID, user.Username)
if err != nil {
return nil, fmt.Errorf("生成刷新令牌失败: %w", err)
}
// 5. 记录设备
if device != nil {
if err := s.RecordDevice(ctx, user.ID, device); err != nil {
return nil, fmt.Errorf("记录设备失败: %w", err)
}
}
// 6. 更新最后登录信息
now := time.Now()
user.LastLoginTime = &now
user.LastLoginIP = device.IP
if err := s.userRepo.Update(ctx, user); err != nil {
// 不影响登录流程
}
// 7. 清除用户缓存
s.cache.Delete(ctx, cache.GenerateKey("users", user.ID))
return &LoginResponse{
User: user,
AccessToken: accessToken,
RefreshToken: refreshToken,
ExpiresIn: 7200, // 2小时
}, nil
}
// GetUserByID 根据ID获取用户
func (s *UserService) GetUserByID(ctx context.Context, id int64) (*domain.User, error) {
key := cache.GenerateKey("users", id)
var user domain.User
err := s.cache.GetOrLoad(ctx, key, &user, func() (interface{}, error) {
return s.userRepo.FindByID(ctx, id)
})
if err != nil {
return nil, err
}
return &user, nil
}
// UpdateUser 更新用户
func (s *UserService) UpdateUser(ctx context.Context, id int64, req *UpdateUserRequest) (*domain.User, error) {
// 1. 获取用户
user, err := s.userRepo.FindByID(ctx, id)
if err != nil {
return nil, ErrUserNotFound
}
// 2. 更新字段
if req.Nickname != "" {
user.Nickname = req.Nickname
}
if req.Avatar != "" {
user.Avatar = req.Avatar
}
if req.Gender != nil {
user.Gender = *req.Gender
}
if req.Birthday != nil {
user.Birthday = req.Birthday
}
if req.Region != "" {
user.Region = req.Region
}
if req.Bio != "" {
user.Bio = req.Bio
}
// 3. 保存
if err := s.userRepo.Update(ctx, user); err != nil {
return nil, fmt.Errorf("更新用户失败: %w", err)
}
// 4. 清除缓存
key := cache.GenerateKey("users", id)
s.cache.Delete(ctx, key)
return user, nil
}
// ChangePassword 修改密码
func (s *UserService) ChangePassword(ctx context.Context, id int64, oldPassword, newPassword string) error {
// 1. 获取用户
user, err := s.userRepo.FindByID(ctx, id)
if err != nil {
return ErrUserNotFound
}
// 2. 验证旧密码
if !s.auth.VerifyPassword(oldPassword, user.Password) {
return ErrInvalidOldPassword
}
// 3. 加密新密码
hashedPassword, err := s.auth.HashPassword(newPassword)
if err != nil {
return fmt.Errorf("加密密码失败: %w", err)
}
// 4. 更新密码
user.Password = hashedPassword
if err := s.userRepo.Update(ctx, user); err != nil {
return fmt.Errorf("更新密码失败: %w", err)
}
return nil
}
// ListUsers 分页获取用户列表
func (s *UserService) ListUsers(ctx context.Context, req *ListUsersRequest) (*ListUsersResponse, error) {
// 构建缓存key
cacheKey := cache.GenerateKey("users", "list", fmt.Sprintf("%d-%d", req.Page, req.PageSize))
// 尝试从缓存获取(仅第一页)
if req.Page == 1 {
var cachedResp ListUsersResponse
if err := s.cache.Get(ctx, cacheKey, &cachedResp); err == nil {
return &cachedResp, nil
}
}
// 从数据库查询
users, total, err := s.userRepo.List(ctx, req)
if err != nil {
return nil, fmt.Errorf("查询用户列表失败: %w", err)
}
resp := &ListUsersResponse{
Users: users,
Total: total,
Page: req.Page,
PageSize: req.PageSize,
}
// 缓存第一页结果
if req.Page == 1 {
s.cache.Set(ctx, cacheKey, resp)
}
return resp, nil
}
// DeleteUser 删除用户(软删除)
func (s *UserService) DeleteUser(ctx context.Context, id int64) error {
if err := s.userRepo.Delete(ctx, id); err != nil {
return fmt.Errorf("删除用户失败: %w", err)
}
// 清除缓存
s.cache.Delete(ctx, cache.GenerateKey("users", id))
s.cache.Delete(ctx, cache.GenerateKey("users", "list"))
return nil
}
// GetUserRoles 获取用户角色
func (s *UserService) GetUserRoles(ctx context.Context, userID int64) ([]*domain.Role, error) {
roles, err := s.roleRepo.FindByUserID(ctx, userID)
if err != nil {
return nil, fmt.Errorf("获取用户角色失败: %w", err)
}
return roles, nil
}
// GetUserPermissions 获取用户权限
func (s *UserService) GetUserPermissions(ctx context.Context, userID int64) ([]*domain.Permission, error) {
permissions, err := s.roleRepo.FindPermissionsByUserID(ctx, userID)
if err != nil {
return nil, fmt.Errorf("获取用户权限失败: %w", err)
}
return permissions, nil
}
// AssignRole 分配角色
func (s *UserService) AssignRole(ctx context.Context, userID, roleID int64) error {
if err := s.roleRepo.AssignToUser(ctx, userID, roleID); err != nil {
return fmt.Errorf("分配角色失败: %w", err)
}
// 清除用户角色缓存
s.cache.Delete(ctx, cache.GenerateKey("users", userID, "roles"))
s.cache.Delete(ctx, cache.GenerateKey("users", userID, "permissions"))
return nil
}
// RevokeRole 撤销角色
func (s *UserService) RevokeRole(ctx context.Context, userID, roleID int64) error {
if err := s.roleRepo.RevokeFromUser(ctx, userID, roleID); err != nil {
return fmt.Errorf("撤销角色失败: %w", err)
}
// 清除用户角色缓存
s.cache.Delete(ctx, cache.GenerateKey("users", userID, "roles"))
s.cache.Delete(ctx, cache.GenerateKey("users", userID, "permissions"))
return nil
}
// RecordDevice 记录设备信息
func (s *UserService) RecordDevice(ctx context.Context, userID int64, device *DeviceInfo) error {
// 实现设备记录逻辑
return nil
}
// 请求和响应结构体
type RegisterRequest struct {
Username string `json:"username" binding:"required,min=3,max=50"`
Email string `json:"email" binding:"omitempty,email,max=100"`
Phone string `json:"phone" binding:"omitempty,max=20"`
Nickname string `json:"nickname" binding:"omitempty,max=50"`
Password string `json:"password" binding:"required,min=8"`
}
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
type UpdateUserRequest struct {
Nickname string `json:"nickname" binding:"omitempty,max=50"`
Avatar string `json:"avatar" binding:"omitempty,max=255"`
Gender *int `json:"gender" binding:"omitempty,min=0,max=2"`
Birthday *time.Time `json:"birthday"`
Region string `json:"region" binding:"omitempty,max=50"`
Bio string `json:"bio" binding:"omitempty,max=500"`
}
type ListUsersRequest struct {
Page int `json:"page" binding:"required,min=1"`
PageSize int `json:"page_size" binding:"required,min=1,max=100"`
Keyword string `json:"keyword"`
Status *int `json:"status"`
}
type LoginResponse struct {
User *domain.User `json:"user"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int64 `json:"expires_in"`
}
type ListUsersResponse struct {
Users []*domain.User `json:"users"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
type DeviceInfo struct {
DeviceID string `json:"device_id"`
DeviceName string `json:"device_name"`
DeviceType int `json:"device_type"`
DeviceOS string `json:"device_os"`
DeviceBrowser string `json:"device_browser"`
IP string `json:"ip"`
Location string `json:"location"`
}
// 错误定义
var (
ErrUsernameExists = fmt.Errorf("用户名已存在")
ErrEmailExists = fmt.Errorf("邮箱已存在")
ErrPhoneExists = fmt.Errorf("手机号已存在")
ErrInvalidCredentials = fmt.Errorf("用户名或密码错误")
ErrAccountLocked = fmt.Errorf("账号已被锁定")
ErrAccountDisabled = fmt.Errorf("账号已被禁用")
ErrUserNotFound = fmt.Errorf("用户不存在")
ErrInvalidOldPassword = fmt.Errorf("原密码错误")
)
```
#### 4.2 认证服务实现
```go
// internal/service/auth.go
package service
import (
"crypto/rand"
"encoding/base64"
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/bcrypt"
)
// AuthService 认证服务
type AuthService struct {
jwtSecret []byte
accessTokenExpire time.Duration
refreshTokenExpire time.Duration
passwordMinLength int
passwordRequireSpecial bool
passwordRequireNumber bool
}
// NewAuthService 创建认证服务
func NewAuthService(jwtSecret string, accessTokenExpire, refreshTokenExpire time.Duration, options ...Option) *AuthService {
svc := &AuthService{
jwtSecret: []byte(jwtSecret),
accessTokenExpire: accessTokenExpire,
refreshTokenExpire: refreshTokenExpire,
passwordMinLength: 8,
}
for _, opt := range options {
opt(svc)
}
return svc
}
// Option 认证服务选项
type Option func(*AuthService)
func WithPasswordMinLength(length int) Option {
return func(s *AuthService) {
s.passwordMinLength = length
}
}
func WithPasswordRequireSpecial(require bool) Option {
return func(s *AuthService) {
s.passwordRequireSpecial = require
}
}
func WithPasswordRequireNumber(require bool) Option {
return func(s *AuthService) {
s.passwordRequireNumber = require
}
}
// Claims JWT声明
type Claims struct {
UserID int64 `json:"user_id"`
Username string `json:"username"`
Type string `json:"type"` // access, refresh
jwt.RegisteredClaims
}
// GenerateAccessToken 生成访问令牌
func (s *AuthService) GenerateAccessToken(userID int64, username string) (string, error) {
now := time.Now()
claims := Claims{
UserID: userID,
Username: username,
Type: "access",
RegisteredClaims: jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(now.Add(s.accessTokenExpire)),
Issuer: "user-management-system",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(s.jwtSecret)
}
// GenerateRefreshToken 生成刷新令牌
func (s *AuthService) GenerateRefreshToken(userID int64, username string) (string, error) {
now := time.Now()
claims := Claims{
UserID: userID,
Username: username,
Type: "refresh",
RegisteredClaims: jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(now.Add(s.refreshTokenExpire)),
Issuer: "user-management-system",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(s.jwtSecret)
}
// ParseToken 解析令牌
func (s *AuthService) ParseToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, 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 s.jwtSecret, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, fmt.Errorf("invalid token")
}
// RefreshToken 刷新令牌
func (s *AuthService) RefreshToken(refreshToken string) (string, error) {
claims, err := s.ParseToken(refreshToken)
if err != nil {
return "", fmt.Errorf("invalid refresh token: %w", err)
}
if claims.Type != "refresh" {
return "", fmt.Errorf("not a refresh token")
}
// 生成新的访问令牌
return s.GenerateAccessToken(claims.UserID, claims.Username)
}
// HashPassword 加密密码
func (s *AuthService) HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", fmt.Errorf("加密密码失败: %w", err)
}
return string(bytes), nil
}
// VerifyPassword 验证密码
func (s *AuthService) VerifyPassword(password, hashedPassword string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
return err == nil
}
// ValidatePassword 验证密码强度
func (s *AuthService) ValidatePassword(password string) error {
// 检查长度
if len(password) < s.passwordMinLength {
return fmt.Errorf("密码长度不能少于%d位", s.passwordMinLength)
}
// 检查特殊字符
if s.passwordRequireSpecial {
hasSpecial := false
for _, c := range password {
if (c >= 33 && c <= 47) || (c >= 58 && c <= 64) || (c >= 91 && c <= 96) || (c >= 123 && c <= 126) {
hasSpecial = true
break
}
}
if !hasSpecial {
return fmt.Errorf("密码必须包含特殊字符")
}
}
// 检查数字
if s.passwordRequireNumber {
hasNumber := false
for _, c := range password {
if c >= '0' && c <= '9' {
hasNumber = true
break
}
}
if !hasNumber {
return fmt.Errorf("密码必须包含数字")
}
}
return nil
}
// GenerateResetToken 生成重置令牌
func (s *AuthService) GenerateResetToken() (string, error) {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", fmt.Errorf("生成重置令牌失败: %w", err)
}
return base64.URLEncoding.EncodeToString(b), nil
}
// GenerateVerificationCode 生成验证码
func (s *AuthService) GenerateVerificationCode(length int) (string, error) {
const digits = "0123456789"
b := make([]byte, length)
for i := range b {
n, err := rand.Int(rand.Reader, big.NewInt(int64(len(digits))))
if err != nil {
return "", fmt.Errorf("生成验证码失败: %w", err)
}
b[i] = digits[n.Int64()]
}
return string(b), nil
}
```
#### 4.3 角色服务实现
```go
// internal/service/role.go
package service
import (
"context"
"fmt"
"github.com/user-management-system/internal/cache"
"github.com/user-management-system/internal/domain"
"github.com/user-management-system/internal/repository"
)
// RoleService 角色服务
type RoleService struct {
roleRepo repository.RoleRepository
cache *cache.CacheManager
}
// NewRoleService 创建角色服务
func NewRoleService(
roleRepo repository.RoleRepository,
cache *cache.CacheManager,
) *RoleService {
return &RoleService{
roleRepo: roleRepo,
cache: cache,
}
}
// CreateRole 创建角色
func (s *RoleService) CreateRole(ctx context.Context, req *CreateRoleRequest) (*domain.Role, error) {
// 1. 检查角色名称是否已存在
exists, err := s.roleRepo.ExistsByCode(ctx, req.Code)
if err != nil {
return nil, fmt.Errorf("检查角色代码失败: %w", err)
}
if exists {
return nil, ErrRoleCodeExists
}
// 2. 创建角色
role := &domain.Role{
Name: req.Name,
Code: req.Code,
Description: req.Description,
ParentID: req.ParentID,
Level: 1,
IsSystem: false,
IsDefault: false,
Status: domain.RoleStatusEnabled,
}
if req.ParentID != nil {
parent, err := s.roleRepo.FindByID(ctx, *req.ParentID)
if err != nil {
return nil, fmt.Errorf("查找父角色失败: %w", err)
}
role.Level = parent.Level + 1
}
if err := s.roleRepo.Create(ctx, role); err != nil {
return nil, fmt.Errorf("创建角色失败: %w", err)
}
// 3. 清除角色列表缓存
s.cache.Delete(ctx, cache.GenerateKey("roles", "list"))
return role, nil
}
// GetRoleByID 根据ID获取角色
func (s *RoleService) GetRoleByID(ctx context.Context, id int64) (*domain.Role, error) {
key := cache.GenerateKey("roles", id)
var role domain.Role
err := s.cache.GetOrLoad(ctx, key, &role, func() (interface{}, error) {
return s.roleRepo.FindByID(ctx, id)
})
if err != nil {
return nil, err
}
return &role, nil
}
// UpdateRole 更新角色
func (s *RoleService) UpdateRole(ctx context.Context, id int64, req *UpdateRoleRequest) (*domain.Role, error) {
// 1. 获取角色
role, err := s.roleRepo.FindByID(ctx, id)
if err != nil {
return nil, ErrRoleNotFound
}
// 2. 系统角色不允许修改
if role.IsSystem {
return nil, ErrCannotModifySystemRole
}
// 3. 更新字段
if req.Name != "" {
role.Name = req.Name
}
if req.Description != "" {
role.Description = req.Description
}
// 4. 保存
if err := s.roleRepo.Update(ctx, role); err != nil {
return nil, fmt.Errorf("更新角色失败: %w", err)
}
// 5. 清除缓存
key := cache.GenerateKey("roles", id)
s.cache.Delete(ctx, key)
return role, nil
}
// DeleteRole 删除角色
func (s *RoleService) DeleteRole(ctx context.Context, id int64) error {
// 1. 获取角色
role, err := s.roleRepo.FindByID(ctx, id)
if err != nil {
return ErrRoleNotFound
}
// 2. 系统角色不允许删除
if role.IsSystem {
return ErrCannotDeleteSystemRole
}
// 3. 检查是否有用户使用该角色
count, err := s.roleRepo.CountUsers(ctx, id)
if err != nil {
return fmt.Errorf("检查角色用户失败: %w", err)
}
if count > 0 {
return ErrRoleInUse
}
// 4. 删除角色
if err := s.roleRepo.Delete(ctx, id); err != nil {
return fmt.Errorf("删除角色失败: %w", err)
}
// 5. 清除缓存
s.cache.Delete(ctx, cache.GenerateKey("roles", id))
s.cache.Delete(ctx, cache.GenerateKey("roles", "list"))
return nil
}
// ListRoles 分页获取角色列表
func (s *RoleService) ListRoles(ctx context.Context, req *ListRolesRequest) (*ListRolesResponse, error) {
// 尝试从缓存获取(仅第一页)
if req.Page == 1 {
cacheKey := cache.GenerateKey("roles", "list")
var cachedResp ListRolesResponse
if err := s.cache.Get(ctx, cacheKey, &cachedResp); err == nil {
return &cachedResp, nil
}
}
// 从数据库查询
roles, total, err := s.roleRepo.List(ctx, req)
if err != nil {
return nil, fmt.Errorf("查询角色列表失败: %w", err)
}
resp := &ListRolesResponse{
Roles: roles,
Total: total,
Page: req.Page,
PageSize: req.PageSize,
}
// 缓存第一页结果
if req.Page == 1 {
s.cache.Set(ctx, cache.GenerateKey("roles", "list"), resp)
}
return resp, nil
}
// AssignPermission 分配权限
func (s *RoleService) AssignPermission(ctx context.Context, roleID, permissionID int64) error {
if err := s.roleRepo.AssignPermission(ctx, roleID, permissionID); err != nil {
return fmt.Errorf("分配权限失败: %w", err)
}
// 清除缓存
s.cache.Delete(ctx, cache.GenerateKey("roles", roleID, "permissions"))
return nil
}
// RevokePermission 撤销权限
func (s *RoleService) RevokePermission(ctx context.Context, roleID, permissionID int64) error {
if err := s.roleRepo.RevokePermission(ctx, roleID, permissionID); err != nil {
return fmt.Errorf("撤销权限失败: %w", err)
}
// 清除缓存
s.cache.Delete(ctx, cache.GenerateKey("roles", roleID, "permissions"))
return nil
}
// GetRolePermissions 获取角色权限
func (s *RoleService) GetRolePermissions(ctx context.Context, roleID int64) ([]*domain.Permission, error) {
cacheKey := cache.GenerateKey("roles", roleID, "permissions")
var permissions []*domain.Permission
err := s.cache.GetOrLoad(ctx, cacheKey, &permissions, func() (interface{}, error) {
return s.roleRepo.FindPermissions(ctx, roleID)
})
if err != nil {
return nil, err
}
return permissions, nil
}
// 请求和响应结构体
type CreateRoleRequest struct {
Name string `json:"name" binding:"required,max=50"`
Code string `json:"code" binding:"required,max=50"`
Description string `json:"description" binding:"omitempty,max=200"`
ParentID *int64 `json:"parent_id"`
}
type UpdateRoleRequest struct {
Name string `json:"name" binding:"omitempty,max=50"`
Description string `json:"description" binding:"omitempty,max=200"`
}
type ListRolesRequest struct {
Page int `json:"page" binding:"required,min=1"`
PageSize int `json:"page_size" binding:"required,min=1,max=100"`
Status *int `json:"status"`
}
type ListRolesResponse struct {
Roles []*domain.Role `json:"roles"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
// 错误定义
var (
ErrRoleCodeExists = fmt.Errorf("角色代码已存在")
ErrRoleNotFound = fmt.Errorf("角色不存在")
ErrCannotModifySystemRole = fmt.Errorf("不能修改系统角色")
ErrCannotDeleteSystemRole = fmt.Errorf("不能删除系统角色")
ErrRoleInUse = fmt.Errorf("角色正在使用中")
)
```
#### 4.4 权限服务实现
```go
// internal/service/permission.go
package service
import (
"context"
"fmt"
"github.com/user-management-system/internal/cache"
"github.com/user-management-system/internal/domain"
"github.com/user-management-system/internal/repository"
)
// PermissionService 权限服务
type PermissionService struct {
permRepo repository.PermissionRepository
cache *cache.CacheManager
}
// NewPermissionService 创建权限服务
func NewPermissionService(
permRepo repository.PermissionRepository,
cache *cache.CacheManager,
) *PermissionService {
return &PermissionService{
permRepo: permRepo,
cache: cache,
}
}
// CreatePermission 创建权限
func (s *PermissionService) CreatePermission(ctx context.Context, req *CreatePermissionRequest) (*domain.Permission, error) {
// 1. 检查权限代码是否已存在
exists, err := s.permRepo.ExistsByCode(ctx, req.Code)
if err != nil {
return nil, fmt.Errorf("检查权限代码失败: %w", err)
}
if exists {
return nil, ErrPermissionCodeExists
}
// 2. 创建权限
permission := &domain.Permission{
Name: req.Name,
Code: req.Code,
Type: domain.PermissionType(req.Type),
Description: req.Description,
ParentID: req.ParentID,
Level: 1,
Path: req.Path,
Method: req.Method,
Sort: req.Sort,
Icon: req.Icon,
Status: domain.PermissionStatusEnabled,
}
if req.ParentID != nil {
parent, err := s.permRepo.FindByID(ctx, *req.ParentID)
if err != nil {
return nil, fmt.Errorf("查找父权限失败: %w", err)
}
permission.Level = parent.Level + 1
}
if err := s.permRepo.Create(ctx, permission); err != nil {
return nil, fmt.Errorf("创建权限失败: %w", err)
}
// 3. 清除缓存
s.cache.Delete(ctx, cache.GenerateKey("permissions", "list"))
return permission, nil
}
// GetPermissionByID 根据ID获取权限
func (s *PermissionService) GetPermissionByID(ctx context.Context, id int64) (*domain.Permission, error) {
key := cache.GenerateKey("permissions", id)
var permission domain.Permission
err := s.cache.GetOrLoad(ctx, key, &permission, func() (interface{}, error) {
return s.permRepo.FindByID(ctx, id)
})
if err != nil {
return nil, err
}
return &permission, nil
}
// ListPermissions 分页获取权限列表
func (s *PermissionService) ListPermissions(ctx context.Context, req *ListPermissionsRequest) (*ListPermissionsResponse, error) {
// 尝试从缓存获取(仅第一页)
if req.Page == 1 {
cacheKey := cache.GenerateKey("permissions", "list")
var cachedResp ListPermissionsResponse
if err := s.cache.Get(ctx, cacheKey, &cachedResp); err == nil {
return &cachedResp, nil
}
}
// 从数据库查询
permissions, total, err := s.permRepo.List(ctx, req)
if err != nil {
return nil, fmt.Errorf("查询权限列表失败: %w", err)
}
resp := &ListPermissionsResponse{
Permissions: permissions,
Total: total,
Page: req.Page,
PageSize: req.PageSize,
}
// 缓存第一页结果
if req.Page == 1 {
s.cache.Set(ctx, cache.GenerateKey("permissions", "list"), resp)
}
return resp, nil
}
// CheckPermission 检查用户是否有权限
func (s *PermissionService) CheckPermission(ctx context.Context, userID int64, permissionCode string) (bool, error) {
cacheKey := cache.GenerateKey("users", userID, "permissions", permissionCode)
var hasPermission bool
err := s.cache.GetOrLoad(ctx, cacheKey, &hasPermission, func() (interface{}, error) {
permissions, err := s.permRepo.FindByUserID(ctx, userID)
if err != nil {
return false, err
}
for _, perm := range permissions {
if perm.Code == permissionCode {
return true, nil
}
}
return false, nil
})
if err != nil {
return false, err
}
return hasPermission, nil
}
// 请求和响应结构体
type CreatePermissionRequest struct {
Name string `json:"name" binding:"required,max=50"`
Code string `json:"code" binding:"required,max=100"`
Type int `json:"type" binding:"required,min=0,max=2"`
Description string `json:"description" binding:"omitempty,max=200"`
ParentID *int64 `json:"parent_id"`
Path string `json:"path" binding:"omitempty,max=200"`
Method string `json:"method" binding:"omitempty,max=10"`
Sort int `json:"sort"`
Icon string `json:"icon" binding:"omitempty,max=50"`
}
type ListPermissionsRequest struct {
Page int `json:"page" binding:"required,min=1"`
PageSize int `json:"page_size" binding:"required,min=1,max=100"`
Type *int `json:"type"`
}
type ListPermissionsResponse struct {
Permissions []*domain.Permission `json:"permissions"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
// 错误定义
var (
ErrPermissionCodeExists = fmt.Errorf("权限代码已存在")
ErrPermissionNotFound = fmt.Errorf("权限不存在")
)
```
#### 4.5 数据访问层实现
```go
// internal/repository/user.go
package repository
import (
"context"
"gorm.io/gorm"
"github.com/user-management-system/internal/domain"
)
// UserRepository 用户仓储
type UserRepository interface {
Create(ctx context.Context, user *domain.User) error
Update(ctx context.Context, user *domain.User) error
Delete(ctx context.Context, id int64) error
FindByID(ctx context.Context, id int64) (*domain.User, error)
FindByUsername(ctx context.Context, username string) (*domain.User, error)
FindByEmail(ctx context.Context, email string) (*domain.User, error)
FindByPhone(ctx context.Context, phone string) (*domain.User, error)
ExistsByUsername(ctx context.Context, username string) (bool, error)
ExistsByEmail(ctx context.Context, email string) (bool, error)
ExistsByPhone(ctx context.Context, phone string) (bool, error)
List(ctx context.Context, req *ListUsersRequest) ([]*domain.User, int64, error)
}
type userRepository struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) UserRepository {
return &userRepository{db: db}
}
func (r *userRepository) Create(ctx context.Context, user *domain.User) error {
return r.db.WithContext(ctx).Create(user).Error
}
func (r *userRepository) Update(ctx context.Context, user *domain.User) error {
return r.db.WithContext(ctx).Save(user).Error
}
func (r *userRepository) Delete(ctx context.Context, id int64) error {
return r.db.WithContext(ctx).Delete(&domain.User{}, id).Error
}
func (r *userRepository) FindByID(ctx context.Context, id int64) (*domain.User, error) {
var user domain.User
err := r.db.WithContext(ctx).First(&user, id).Error
if err != nil {
return nil, err
}
return &user, nil
}
func (r *userRepository) FindByUsername(ctx context.Context, username string) (*domain.User, error) {
var user domain.User
err := r.db.WithContext(ctx).Where("username = ?", username).First(&user).Error
if err != nil {
return nil, err
}
return &user, nil
}
func (r *userRepository) FindByEmail(ctx context.Context, email string) (*domain.User, error) {
var user domain.User
err := r.db.WithContext(ctx).Where("email = ?", email).First(&user).Error
if err != nil {
return nil, err
}
return &user, nil
}
func (r *userRepository) FindByPhone(ctx context.Context, phone string) (*domain.User, error) {
var user domain.User
err := r.db.WithContext(ctx).Where("phone = ?", phone).First(&user).Error
if err != nil {
return nil, err
}
return &user, nil
}
func (r *userRepository) ExistsByUsername(ctx context.Context, username string) (bool, error) {
var count int64
err := r.db.WithContext(ctx).Model(&domain.User{}).Where("username = ?", username).Count(&count).Error
return count > 0, err
}
func (r *userRepository) ExistsByEmail(ctx context.Context, email string) (bool, error) {
var count int64
err := r.db.WithContext(ctx).Model(&domain.User{}).Where("email = ?", email).Count(&count).Error
return count > 0, err
}
func (r *userRepository) ExistsByPhone(ctx context.Context, phone string) (bool, error) {
var count int64
err := r.db.WithContext(ctx).Model(&domain.User{}).Where("phone = ?", phone).Count(&count).Error
return count > 0, err
}
func (r *userRepository) List(ctx context.Context, req *ListUsersRequest) ([]*domain.User, int64, error) {
var users []*domain.User
var total int64
query := r.db.WithContext(ctx).Model(&domain.User{})
// 关键字搜索
if req.Keyword != "" {
query = query.Where("username LIKE ? OR nickname LIKE ? OR email LIKE ?",
"%"+req.Keyword+"%", "%"+req.Keyword+"%", "%"+req.Keyword+"%")
}
// 状态筛选
if req.Status != nil {
query = query.Where("status = ?", *req.Status)
}
// 统计总数
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// 分页查询
offset := (req.Page - 1) * req.PageSize
err := query.Order("created_at DESC").Offset(offset).Limit(req.PageSize).Find(&users).Error
return users, total, err
}
```
#### 4.6 任务清单
- [ ] 实现用户服务 `UserService`
- [ ] 实现认证服务 `AuthService`
- [ ] 实现角色服务 `RoleService`
- [ ] 实现权限服务 `PermissionService`
- [ ] 实现设备服务 `DeviceService`
- [ ] 实现用户仓储 `UserRepository`
- [ ] 实现角色仓储 `RoleRepository`
- [ ] 实现权限仓储 `PermissionRepository`
- [ ] 编写服务层单元测试
- [ ] 编写仓储层单元测试
---
### 阶段5API层实现第9-10周
**目标**实现所有API接口
#### 5.1 中间件实现
```go
// internal/api/middleware/auth.go
package middleware
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/user-management-system/internal/service"
)
// AuthMiddleware 认证中间件
func AuthMiddleware(authService service.AuthService) gin.HandlerFunc {
return func(c *gin.Context) {
// 1. 获取Token
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "缺少认证令牌"})
c.Abort()
return
}
// 2. 解析Bearer Token
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "认证令牌格式错误"})
c.Abort()
return
}
token := parts[1]
// 3. 验证Token
claims, err := authService.ParseToken(token)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "认证令牌无效"})
c.Abort()
return
}
// 4. 检查Token类型
if claims.Type != "access" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "令牌类型错误"})
c.Abort()
return
}
// 5. 将用户信息存入上下文
c.Set("user_id", claims.UserID)
c.Set("username", claims.Username)
c.Next()
}
}
// PermissionMiddleware 权限中间件
func PermissionMiddleware(permService service.PermissionService, permissionCode string) gin.HandlerFunc {
return func(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "未认证"})
c.Abort()
return
}
hasPermission, err := permService.CheckPermission(c, userID.(int64), permissionCode)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "权限检查失败"})
c.Abort()
return
}
if !hasPermission {
c.JSON(http.StatusForbidden, gin.H{"error": "无权限访问"})
c.Abort()
return
}
c.Next()
}
}
```
```go
// internal/api/middleware/ratelimit.go
package middleware
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/user-management-system/internal/security"
)
// RateLimitMiddleware 限流中间件
func RateLimitMiddleware(limiter *security.RateLimiter) gin.HandlerFunc {
return func(c *gin.Context) {
key := c.ClientIP()
allowed, err := limiter.Allow(c, key)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "限流检查失败"})
c.Abort()
return
}
if !allowed {
c.JSON(http.StatusTooManyRequests, gin.H{
"error": "请求过于频繁,请稍后再试",
})
c.Abort()
return
}
c.Next()
}
}
```
```go
// internal/api/middleware/cors.go
package middleware
import (
"github.com/gin-gonic/gin"
)
// CORSMiddleware 跨域中间件
func CORSMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}
```
```go
// internal/api/middleware/logger.go
package middleware
import (
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// LoggerMiddleware 日志中间件
func LoggerMiddleware(logger *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
query := c.Request.URL.RawQuery
c.Next()
cost := time.Since(start)
logger.Info("HTTP Request",
zap.String("method", c.Request.Method),
zap.String("path", path),
zap.String("query", query),
zap.Int("status", c.Writer.Status()),
zap.Duration("cost", cost),
zap.String("ip", c.ClientIP()),
zap.String("user-agent", c.Request.UserAgent()),
)
}
}
```
#### 5.2 处理器实现
```go
// internal/api/handler/user.go
package handler
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/user-management-system/internal/service"
"github.com/user-management-system/pkg/response"
)
// UserHandler 用户处理器
type UserHandler struct {
userService *service.UserService
}
// NewUserHandler 创建用户处理器
func NewUserHandler(userService *service.UserService) *UserHandler {
return &UserHandler{userService: userService}
}
// Register 用户注册
// @Summary 用户注册
// @Description 创建新用户
// @Tags 用户管理
// @Accept json
// @Produce json
// @Param request body service.RegisterRequest true "注册信息"
// @Success 200 {object} response.Response{data=domain.User}
// @Router /api/v1/users/register [post]
func (h *UserHandler) Register(c *gin.Context) {
var req service.RegisterRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "参数错误", err)
return
}
user, err := h.userService.Register(c, &req)
if err != nil {
response.Error(c, http.StatusBadRequest, "注册失败", err)
return
}
response.Success(c, user)
}
// Login 用户登录
// @Summary 用户登录
// @Description 用户登录获取访问令牌
// @Tags 认证
// @Accept json
// @Produce json
// @Param request body service.LoginRequest true "登录信息"
// @Success 200 {object} response.Response{data=service.LoginResponse}
// @Router /api/v1/auth/login [post]
func (h *UserHandler) Login(c *gin.Context) {
var req service.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "参数错误", err)
return
}
// 获取设备信息
device := &service.DeviceInfo{
DeviceID: c.GetHeader("X-Device-ID"),
DeviceName: c.GetHeader("X-Device-Name"),
DeviceOS: c.GetHeader("X-Device-OS"),
DeviceBrowser: c.GetHeader("User-Agent"),
IP: c.ClientIP(),
}
resp, err := h.userService.Login(c, &req, device)
if err != nil {
response.Error(c, http.StatusUnauthorized, "登录失败", err)
return
}
response.Success(c, resp)
}
// GetUser 获取当前用户信息
// @Summary 获取当前用户
// @Description 获取当前登录用户的信息
// @Tags 用户管理
// @Produce json
// @Success 200 {object} response.Response{data=domain.User}
// @Router /api/v1/users/me [get]
func (h *UserHandler) GetUser(c *gin.Context) {
userID, _ := c.Get("user_id")
user, err := h.userService.GetUserByID(c, userID.(int64))
if err != nil {
response.Error(c, http.StatusNotFound, "用户不存在", err)
return
}
response.Success(c, user)
}
// GetUserByID 根据ID获取用户
// @Summary 获取用户
// @Description 根据ID获取用户信息
// @Tags 用户管理
// @Produce json
// @Param id path int true "用户ID"
// @Success 200 {object} response.Response{data=domain.User}
// @Router /api/v1/users/{id} [get]
func (h *UserHandler) GetUserByID(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.Error(c, http.StatusBadRequest, "无效的用户ID", err)
return
}
user, err := h.userService.GetUserByID(c, id)
if err != nil {
response.Error(c, http.StatusNotFound, "用户不存在", err)
return
}
response.Success(c, user)
}
// UpdateUser 更新用户
// @Summary 更新用户
// @Description 更新用户信息
// @Tags 用户管理
// @Accept json
// @Produce json
// @Param id path int true "用户ID"
// @Param request body service.UpdateUserRequest true "更新信息"
// @Success 200 {object} response.Response{data=domain.User}
// @Router /api/v1/users/{id} [put]
func (h *UserHandler) UpdateUser(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.Error(c, http.StatusBadRequest, "无效的用户ID", err)
return
}
var req service.UpdateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "参数错误", err)
return
}
user, err := h.userService.UpdateUser(c, id, &req)
if err != nil {
response.Error(c, http.StatusBadRequest, "更新失败", err)
return
}
response.Success(c, user)
}
// ChangePassword 修改密码
// @Summary 修改密码
// @Description 修改用户密码
// @Tags 用户管理
// @Accept json
// @Produce json
// @Param id path int true "用户ID"
// @Param request body ChangePasswordRequest true "密码信息"
// @Success 200 {object} response.Response
// @Router /api/v1/users/{id}/password [put]
func (h *UserHandler) ChangePassword(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.Error(c, http.StatusBadRequest, "无效的用户ID", err)
return
}
var req ChangePasswordRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "参数错误", err)
return
}
if err := h.userService.ChangePassword(c, id, req.OldPassword, req.NewPassword); err != nil {
response.Error(c, http.StatusBadRequest, "修改密码失败", err)
return
}
response.Success(c, nil)
}
// ListUsers 获取用户列表
// @Summary 用户列表
// @Description 分页获取用户列表
// @Tags 用户管理
// @Produce json
// @Param page query int true "页码"
// @Param page_size query int true "每页数量"
// @Param keyword query string false "搜索关键字"
// @Param status query int false "状态"
// @Success 200 {object} response.Response{data=service.ListUsersResponse}
// @Router /api/v1/users [get]
func (h *UserHandler) ListUsers(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
var status *int
if statusStr := c.Query("status"); statusStr != "" {
s, _ := strconv.Atoi(statusStr)
status = &s
}
req := &service.ListUsersRequest{
Page: page,
PageSize: pageSize,
Keyword: c.Query("keyword"),
Status: status,
}
resp, err := h.userService.ListUsers(c, req)
if err != nil {
response.Error(c, http.StatusInternalServerError, "查询失败", err)
return
}
response.Success(c, resp)
}
// DeleteUser 删除用户
// @Summary 删除用户
// @Description 删除用户(软删除)
// @Tags 用户管理
// @Produce json
// @Param id path int true "用户ID"
// @Success 200 {object} response.Response
// @Router /api/v1/users/{id} [delete]
func (h *UserHandler) DeleteUser(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.Error(c, http.StatusBadRequest, "无效的用户ID", err)
return
}
if err := h.userService.DeleteUser(c, id); err != nil {
response.Error(c, http.StatusBadRequest, "删除失败", err)
return
}
response.Success(c, nil)
}
// GetUserRoles 获取用户角色
// @Summary 获取用户角色
// @Description 获取用户的角色列表
// @Tags 用户管理
// @Produce json
// @Param id path int true "用户ID"
// @Success 200 {object} response.Response{data=[]domain.Role}
// @Router /api/v1/users/{id}/roles [get]
func (h *UserHandler) GetUserRoles(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.Error(c, http.StatusBadRequest, "无效的用户ID", err)
return
}
roles, err := h.userService.GetUserRoles(c, id)
if err != nil {
response.Error(c, http.StatusInternalServerError, "查询失败", err)
return
}
response.Success(c, roles)
}
// GetUserPermissions 获取用户权限
// @Summary 获取用户权限
// @Description 获取用户的权限列表
// @Tags 用户管理
// @Produce json
// @Param id path int true "用户ID"
// @Success 200 {object} response.Response{data=[]domain.Permission}
// @Router /api/v1/users/{id}/permissions [get]
func (h *UserHandler) GetUserPermissions(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.Error(c, http.StatusBadRequest, "无效的用户ID", err)
return
}
permissions, err := h.userService.GetUserPermissions(c, id)
if err != nil {
response.Error(c, http.StatusInternalServerError, "查询失败", err)
return
}
response.Success(c, permissions)
}
// AssignRole 分配角色
// @Summary 分配角色
// @Description 为用户分配角色
// @Tags 用户管理
// @Accept json
// @Produce json
// @Param id path int true "用户ID"
// @Param request body AssignRoleRequest true "角色信息"
// @Success 200 {object} response.Response
// @Router /api/v1/users/{id}/roles [post]
func (h *UserHandler) AssignRole(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.Error(c, http.StatusBadRequest, "无效的用户ID", err)
return
}
var req AssignRoleRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "参数错误", err)
return
}
if err := h.userService.AssignRole(c, id, req.RoleID); err != nil {
response.Error(c, http.StatusBadRequest, "分配角色失败", err)
return
}
response.Success(c, nil)
}
// RevokeRole 撤销角色
// @Summary 撤销角色
// @Description 撤销用户的角色
// @Tags 用户管理
// @Accept json
// @Produce json
// @Param id path int true "用户ID"
// @Param request body AssignRoleRequest true "角色信息"
// @Success 200 {object} response.Response
// @Router /api/v1/users/{id}/roles [delete]
func (h *UserHandler) RevokeRole(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.Error(c, http.StatusBadRequest, "无效的用户ID", err)
return
}
var req AssignRoleRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "参数错误", err)
return
}
if err := h.userService.RevokeRole(c, id, req.RoleID); err != nil {
response.Error(c, http.StatusBadRequest, "撤销角色失败", err)
return
}
response.Success(c, nil)
}
// 请求结构体
type ChangePasswordRequest struct {
OldPassword string `json:"old_password" binding:"required"`
NewPassword string `json:"new_password" binding:"required,min=8"`
}
type AssignRoleRequest struct {
RoleID int64 `json:"role_id" binding:"required"`
}
```
```go
// internal/api/handler/role.go
package handler
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/user-management-system/internal/service"
"github.com/user-management-system/pkg/response"
)
// RoleHandler 角色处理器
type RoleHandler struct {
roleService *service.RoleService
}
// NewRoleHandler 创建角色处理器
func NewRoleHandler(roleService *service.RoleService) *RoleHandler {
return &RoleHandler{roleService: roleService}
}
// CreateRole 创建角色
func (h *RoleHandler) CreateRole(c *gin.Context) {
var req service.CreateRoleRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "参数错误", err)
return
}
role, err := h.roleService.CreateRole(c, &req)
if err != nil {
response.Error(c, http.StatusBadRequest, "创建角色失败", err)
return
}
response.Success(c, role)
}
// GetRoleByID 获取角色
func (h *RoleHandler) GetRoleByID(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.Error(c, http.StatusBadRequest, "无效的角色ID", err)
return
}
role, err := h.roleService.GetRoleByID(c, id)
if err != nil {
response.Error(c, http.StatusNotFound, "角色不存在", err)
return
}
response.Success(c, role)
}
// UpdateRole 更新角色
func (h *RoleHandler) UpdateRole(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.Error(c, http.StatusBadRequest, "无效的角色ID", err)
return
}
var req service.UpdateRoleRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "参数错误", err)
return
}
role, err := h.roleService.UpdateRole(c, id, &req)
if err != nil {
response.Error(c, http.StatusBadRequest, "更新角色失败", err)
return
}
response.Success(c, role)
}
// DeleteRole 删除角色
func (h *RoleHandler) DeleteRole(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.Error(c, http.StatusBadRequest, "无效的角色ID", err)
return
}
if err := h.roleService.DeleteRole(c, id); err != nil {
response.Error(c, http.StatusBadRequest, "删除角色失败", err)
return
}
response.Success(c, nil)
}
// ListRoles 角色列表
func (h *RoleHandler) ListRoles(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
var status *int
if statusStr := c.Query("status"); statusStr != "" {
s, _ := strconv.Atoi(statusStr)
status = &s
}
req := &service.ListRolesRequest{
Page: page,
PageSize: pageSize,
Status: status,
}
resp, err := h.roleService.ListRoles(c, req)
if err != nil {
response.Error(c, http.StatusInternalServerError, "查询失败", err)
return
}
response.Success(c, resp)
}
// GetRolePermissions 获取角色权限
func (h *RoleHandler) GetRolePermissions(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.Error(c, http.StatusBadRequest, "无效的角色ID", err)
return
}
permissions, err := h.roleService.GetRolePermissions(c, id)
if err != nil {
response.Error(c, http.StatusInternalServerError, "查询失败", err)
return
}
response.Success(c, permissions)
}
// AssignPermission 分配权限
func (h *RoleHandler) AssignPermission(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.Error(c, http.StatusBadRequest, "无效的角色ID", err)
return
}
var req AssignPermissionRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "参数错误", err)
return
}
if err := h.roleService.AssignPermission(c, id, req.PermissionID); err != nil {
response.Error(c, http.StatusBadRequest, "分配权限失败", err)
return
}
response.Success(c, nil)
}
// RevokePermission 撤销权限
func (h *RoleHandler) RevokePermission(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.Error(c, http.StatusBadRequest, "无效的角色ID", err)
return
}
var req AssignPermissionRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "参数错误", err)
return
}
if err := h.roleService.RevokePermission(c, id, req.PermissionID); err != nil {
response.Error(c, http.StatusBadRequest, "撤销权限失败", err)
return
}
response.Success(c, nil)
}
type AssignPermissionRequest struct {
PermissionID int64 `json:"permission_id" binding:"required"`
}
```
#### 5.3 路由定义
```go
// internal/api/router/router.go
package router
import (
"github.com/gin-gonic/gin"
"github.com/user-management-system/internal/api/handler"
"github.com/user-management-system/internal/api/middleware"
"github.com/user-management-system/internal/monitoring"
)
// SetupRouter 设置路由
func SetupRouter(
userHandler *handler.UserHandler,
roleHandler *handler.RoleHandler,
permHandler *handler.PermissionHandler,
authMiddleware gin.HandlerFunc,
permissionMiddleware gin.HandlerFunc,
rateLimitMiddleware gin.HandlerFunc,
corsMiddleware gin.HandlerFunc,
loggerMiddleware gin.HandlerFunc,
healthHandler *monitoring.HealthHandler,
) *gin.Engine {
r := gin.New()
// 全局中间件
r.Use(corsMiddleware())
r.Use(loggerMiddleware)
r.Use(gin.Recovery())
// 健康检查
r.GET("/health", healthHandler.Check)
// API路由组
api := r.Group("/api/v1")
{
// 公开接口(无需认证)
public := api.Group("")
{
public.POST("/auth/login", rateLimitMiddleware, userHandler.Login)
public.POST("/auth/register", rateLimitMiddleware, userHandler.Register)
}
// 需要认证的接口
auth := api.Group("", authMiddleware)
{
// 用户管理
users := auth.Group("/users")
{
users.GET("/me", userHandler.GetUser)
users.GET("/:id", userHandler.GetUserByID)
users.PUT("/:id", userHandler.UpdateUser)
users.DELETE("/:id", permissionMiddleware, userHandler.DeleteUser)
users.PUT("/:id/password", userHandler.ChangePassword)
users.GET("", userHandler.ListUsers)
users.GET("/:id/roles", userHandler.GetUserRoles)
users.GET("/:id/permissions", userHandler.GetUserPermissions)
users.POST("/:id/roles", userHandler.AssignRole)
users.DELETE("/:id/roles", userHandler.RevokeRole)
}
// 角色管理
roles := auth.Group("/roles")
{
roles.POST("", permissionMiddleware, roleHandler.CreateRole)
roles.GET("/:id", roleHandler.GetRoleByID)
roles.PUT("/:id", permissionMiddleware, roleHandler.UpdateRole)
roles.DELETE("/:id", permissionMiddleware, roleHandler.DeleteRole)
roles.GET("", roleHandler.ListRoles)
roles.GET("/:id/permissions", roleHandler.GetRolePermissions)
roles.POST("/:id/permissions", permissionMiddleware, roleHandler.AssignPermission)
roles.DELETE("/:id/permissions", permissionMiddleware, roleHandler.RevokePermission)
}
// 权限管理
permissions := auth.Group("/permissions")
{
permissions.POST("", permissionMiddleware, permHandler.CreatePermission)
permissions.GET("/:id", permHandler.GetPermissionByID)
permissions.PUT("/:id", permissionMiddleware, permHandler.UpdatePermission)
permissions.DELETE("/:id", permissionMiddleware, permHandler.DeletePermission)
permissions.GET("", permHandler.ListPermissions)
}
}
}
return r
}
```
#### 5.4 任务清单
- [ ] 实现认证中间件
- [ ] 实现权限中间件
- [ ] 实现限流中间件
- [ ] 实现CORS中间件
- [ ] 实现日志中间件
- [ ] 实现用户处理器
- [ ] 实现角色处理器
- [ ] 实现权限处理器
- [ ] 定义路由
- [ ] 集成Swagger文档
---
### 阶段6安全组件实现第11周
**目标**:实现限流、加密、验证等安全组件
#### 6.1 限流组件
```go
// internal/security/ratelimit.go
package security
import (
"context"
"fmt"
"sync"
"time"
"github.com/redis/go-redis/v9"
)
// RateLimiter 限流器接口
type RateLimiter interface {
Allow(ctx context.Context, key string) (bool, error)
}
// TokenBucketLimiter 令牌桶限流器
type TokenBucketLimiter struct {
redis *redis.Client
capacity int64
rate int64 // tokens per second
}
func NewTokenBucketLimiter(redis *redis.Client, capacity, rate int64) *TokenBucketLimiter {
return &TokenBucketLimiter{
redis: redis,
capacity: capacity,
rate: rate,
}
}
func (l *TokenBucketLimiter) Allow(ctx context.Context, key string) (bool, error) {
now := time.Now().Unix()
key = fmt.Sprintf("rate_limit:token_bucket:%s", key)
pipe := l.redis.Pipeline()
tokensCmd := pipe.Get(ctx, key)
lastRefillCmd := pipe.Get(ctx, key+":last_refill")
_, err := pipe.Exec(ctx)
if err != nil && err != redis.Nil {
return false, err
}
var tokens float64
if err := tokensCmd.Err(); err == nil {
tokens, _ = tokensCmd.Float64()
} else {
tokens = float64(l.capacity)
}
var lastRefill int64
if err := lastRefillCmd.Err(); err == nil {
lastRefill, _ = lastRefillCmd.Int64()
} else {
lastRefill = now
}
// 计算需要补充的令牌
elapsedTime := now - lastRefill
refillTokens := float64(elapsedTime) * float64(l.rate)
tokens += refillTokens
if tokens > float64(l.capacity) {
tokens = float64(l.capacity)
}
// 尝试消费一个令牌
if tokens >= 1 {
tokens -= 1
// 更新Redis
pipe := l.redis.Pipeline()
pipe.Set(ctx, key, tokens, 2*time.Second)
pipe.Set(ctx, key+":last_refill", now, 2*time.Second)
pipe.Exec(ctx)
return true, nil
}
return false, nil
}
// SlidingWindowLimiter 滑动窗口限流器
type SlidingWindowLimiter struct {
redis *redis.Client
capacity int64
window time.Duration
}
func NewSlidingWindowLimiter(redis *redis.Client, capacity int64, window time.Duration) *SlidingWindowLimiter {
return &SlidingWindowLimiter{
redis: redis,
capacity: capacity,
window: window,
}
}
func (l *SlidingWindowLimiter) Allow(ctx context.Context, key string) (bool, error) {
now := time.Now().UnixMicro()
windowStart := now - l.window.Microseconds()
key = fmt.Sprintf("rate_limit:sliding_window:%s", key)
// 移除窗口外的数据
l.redis.ZRemRangeByScore(ctx, key, "0", fmt.Sprintf("%d", windowStart))
// 获取当前窗口内请求数
count, err := l.redis.ZCard(ctx, key).Result()
if err != nil {
return false, err
}
if count >= l.capacity {
return false, nil
}
// 添加当前请求
l.redis.ZAdd(ctx, key, redis.Z{
Score: float64(now),
Member: now,
})
l.redis.Expire(ctx, key, l.window)
return true, nil
}
```
#### 6.2 加密组件
```go
// internal/security/encryption.go
package security
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"errors"
"io"
)
// AESEncrypt AES加密
func AESEncrypt(plaintext, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
// 使用GCM模式
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
return ciphertext, nil
}
// AESDecrypt AES解密
func AESDecrypt(ciphertext, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonceSize := gcm.NonceSize()
if len(ciphertext) < nonceSize {
return nil, errors.New("ciphertext too short")
}
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, err
}
return plaintext, nil
}
// EncryptString 加密字符串
func EncryptString(plaintext string, key string) (string, error) {
keyBytes := []byte(key)
plaintextBytes := []byte(plaintext)
ciphertext, err := AESEncrypt(plaintextBytes, keyBytes)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
// DecryptString 解密字符串
func DecryptString(ciphertext string, key string) (string, error) {
keyBytes := []byte(key)
ciphertextBytes, err := base64.StdEncoding.DecodeString(ciphertext)
if err != nil {
return "", err
}
plaintext, err := AESDecrypt(ciphertextBytes, keyBytes)
if err != nil {
return "", err
}
return string(plaintext), nil
}
```
#### 6.3 任务清单
- [ ] 实现令牌桶限流器
- [ ] 实现滑动窗口限流器
- [ ] 实现漏桶限流器
- [ ] 实现AES加密组件
- [ ] 实现验证器组件
- [ ] 编写安全组件单元测试
---
### 阶段7监控和可观测性实现第12周
**目标**实现Prometheus监控、链路追踪、健康检查
#### 7.1 Prometheus监控
```go
// internal/monitoring/metrics.go
package monitoring
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
// HTTP请求总数
HTTPRequestsTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total number of HTTP requests",
},
[]string{"method", "path", "status"},
)
// HTTP请求延迟
HTTPRequestDuration = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request latency in seconds",
Buckets: prometheus.DefBuckets,
},
[]string{"method", "path"},
)
// 缓存命中率
CacheHitRate = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "cache_hit_rate",
Help: "Cache hit rate percentage",
},
[]string{"cache_level"}, // l1, l2
)
// 数据库查询数量
DBQueriesTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "db_queries_total",
Help: "Total number of database queries",
},
[]string{"operation", "table"},
)
// 数据库查询延迟
DBQueryDuration = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "db_query_duration_seconds",
Help: "Database query latency in seconds",
Buckets: prometheus.DefBuckets,
},
[]string{"operation", "table"},
)
// 在线用户数
OnlineUsers = promauto.NewGauge(
prometheus.GaugeOpts{
Name: "online_users",
Help: "Number of online users",
},
)
// 活跃设备数
ActiveDevices = promauto.NewGauge(
prometheus.GaugeOpts{
Name: "active_devices",
Help: "Number of active devices",
},
)
)
// RecordHTTPRequest 记录HTTP请求
func RecordHTTPRequest(method, path string, status int, duration float64) {
HTTPRequestsTotal.WithLabelValues(method, path, string(rune(status))).Inc()
HTTPRequestDuration.WithLabelValues(method, path).Observe(duration)
}
// RecordCacheHitRate 记录缓存命中率
func RecordCacheHitRate(level string, rate float64) {
CacheHitRate.WithLabelValues(level).Set(rate)
}
// RecordDBQuery 记录数据库查询
func RecordDBQuery(operation, table string, duration float64) {
DBQueriesTotal.WithLabelValues(operation, table).Inc()
DBQueryDuration.WithLabelValues(operation, table).Observe(duration)
}
// SetOnlineUsers 设置在线用户数
func SetOnlineUsers(count float64) {
OnlineUsers.Set(count)
}
// SetActiveDevices 设置活跃设备数
func SetActiveDevices(count float64) {
ActiveDevices.Set(count)
}
```
#### 7.2 健康检查
```go
// internal/monitoring/health.go
package monitoring
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/user-management-system/internal/cache"
"github.com/user-management-system/internal/database"
)
// HealthHandler 健康检查处理器
type HealthHandler struct {
db *database.DB
cache *cache.CacheManager
}
// NewHealthHandler 创建健康检查处理器
func NewHealthHandler(db *database.DB, cache *cache.CacheManager) *HealthHandler {
return &HealthHandler{
db: db,
cache: cache,
}
}
// HealthStatus 健康状态
type HealthStatus struct {
Status string `json:"status"`
Database DatabaseHealth `json:"database"`
Redis RedisHealth `json:"redis,omitempty"`
Cache CacheHealth `json:"cache"`
Version string `json:"version"`
Timestamp string `json:"timestamp"`
}
type DatabaseHealth struct {
Status string `json:"status"`
Type string `json:"type"`
}
type RedisHealth struct {
Status string `json:"status"`
Mode string `json:"mode"`
}
type CacheHealth struct {
L1Cache L1CacheHealth `json:"l1_cache"`
L2Cache L2CacheHealth `json:"l2_cache"`
}
type L1CacheHealth struct {
Status string `json:"status"`
Items int `json:"items"`
HitRate float64 `json:"hit_rate"`
}
type L2CacheHealth struct {
Status string `json:"status"`
Enabled bool `json:"enabled"`
}
// Check 健康检查
func (h *HealthHandler) Check(c *gin.Context) {
health := h.getHealthStatus()
if health.Status == "DOWN" {
c.JSON(http.StatusServiceUnavailable, health)
return
}
c.JSON(http.StatusOK, health)
}
func (h *HealthHandler) getHealthStatus() *HealthStatus {
now := time.Now().Format(time.RFC3339)
// 检查数据库
dbStatus := "UP"
dbType := "sqlite"
if sqlDB, err := h.db.DB(); err == nil {
if err := sqlDB.Ping(); err != nil {
dbStatus = "DOWN"
}
} else {
dbStatus = "DOWN"
}
// 检查Redis
redisStatus := "UP"
redisMode := "standalone"
// TODO: 实现Redis健康检查
// 获取缓存统计
stats := h.cache.GetStats()
cacheHealth := CacheHealth{
L1Cache: L1CacheHealth{
Status: "UP",
Items: stats.L1Size,
HitRate: stats.HitRate,
},
L2Cache: L2CacheHealth{
Status: redisStatus,
Enabled: h.cache.IsL2Enabled(),
},
}
status := "UP"
if dbStatus == "DOWN" {
status = "DOWN"
}
return &HealthStatus{
Status: status,
Database: DatabaseHealth{
Status: dbStatus,
Type: dbType,
},
Redis: RedisHealth{
Status: redisStatus,
Mode: redisMode,
},
Cache: cacheHealth,
Version: "1.0.0",
Timestamp: now,
}
}
```
#### 7.3 任务清单
- [ ] 实现Prometheus指标收集
- [ ] 实现健康检查接口
- [ ] 集成OpenTelemetry链路追踪
- [ ] 配置Prometheus告警规则
- [ ] 配置Grafana仪表板
---
### 阶段8部署和运维第13周
**目标**实现Docker部署、Kubernetes部署、自动化脚本
#### 8.1 Docker部署
**Dockerfile**
```dockerfile
FROM golang:1.23-alpine AS builder
WORKDIR /app
# 复制依赖文件
COPY go.mod go.sum ./
RUN go mod download
# 复制源代码
COPY . .
# 构建应用
RUN CGO_ENABLED=1 GOOS=linux go build -o user-management-system cmd/server/main.go
FROM alpine:latest
RUN apk --no-cache add ca-certificates tzdata
WORKDIR /app
# 复制可执行文件
COPY --from=builder /app/user-management-system .
COPY --from=builder /app/configs ./configs
# 创建必要目录
RUN mkdir -p data logs
# 设置时区
ENV TZ=Asia/Shanghai
EXPOSE 8080
CMD ["./user-management-system"]
```
**docker-compose.yml**
```yaml
version: '3.8'
services:
user-management:
image: user-management-system:latest
container_name: user-ms
ports:
- "8080:8080"
volumes:
- ./data:/app/data
- ./logs:/app/logs
- ./configs:/app/configs
environment:
- SPRING_PROFILES_ACTIVE=docker
- DATABASE_TYPE=sqlite
- DATABASE_PATH=/app/data/user_management.db
- REDIS_ENABLED=true
- REDIS_ADDR=redis:6379
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
depends_on:
- redis
redis:
image: redis:7-alpine
container_name: user-ms-redis
ports:
- "6379:6379"
volumes:
- redis-data:/data
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
prometheus:
image: prom/prometheus:latest
container_name: user-ms-prometheus
ports:
- "9090:9090"
volumes:
- ./deployments/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
restart: unless-stopped
grafana:
image: grafana/grafana:latest
container_name: user-ms-grafana
ports:
- "3000:3000"
volumes:
- grafana-data:/var/lib/grafana
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
restart: unless-stopped
volumes:
redis-data:
prometheus-data:
grafana-data:
```
#### 8.2 自动化脚本
**scripts/start.sh**
```bash
#!/bin/bash
APP_NAME="user-management-system"
APP_DIR="/opt/user-management-system"
LOG_DIR="$APP_DIR/logs"
PID_FILE="$LOG_DIR/$APP_NAME.pid"
# 启动函数
start() {
if [ -f "$PID_FILE" ]; then
PID=$(cat $PID_FILE)
if ps -p $PID > /dev/null; then
echo "$APP_NAME is already running (PID: $PID)"
return 1
else
rm -f $PID_FILE
fi
fi
echo "Starting $APP_NAME..."
cd $APP_DIR
nohup ./bin/user-management-system > $LOG_DIR/app.log 2>&1 &
echo $! > $PID_FILE
sleep 2
if ps -p $(cat $PID_FILE) > /dev/null; then
echo "$APP_NAME started successfully (PID: $(cat $PID_FILE))"
return 0
else
echo "$APP_NAME failed to start"
rm -f $PID_FILE
return 1
fi
}
# 停止函数
stop() {
if [ ! -f "$PID_FILE" ]; then
echo "$APP_NAME is not running"
return 1
fi
PID=$(cat $PID_FILE)
echo "Stopping $APP_NAME (PID: $PID)..."
kill $PID
sleep 2
if ps -p $PID > /dev/null; then
echo "Force killing $APP_NAME..."
kill -9 $PID
fi
rm -f $PID_FILE
echo "$APP_NAME stopped"
}
# 重启函数
restart() {
stop
sleep 1
start
}
# 状态函数
status() {
if [ ! -f "$PID_FILE" ]; then
echo "$APP_NAME is not running"
return 1
fi
PID=$(cat $PID_FILE)
if ps -p $PID > /dev/null; then
echo "$APP_NAME is running (PID: $PID)"
return 0
else
echo "$APP_NAME is not running (stale PID file)"
rm -f $PID_FILE
return 1
fi
}
case "$1" in
start)
start
;;
stop)
stop
;;
restart)
restart
;;
status)
status
;;
*)
echo "Usage: $0 {start|stop|restart|status}"
exit 1
esac
```
**scripts/backup.sh**
```bash
#!/bin/bash
BACKUP_DIR="/backup/user-management"
DATA_DIR="/opt/user-management-system/data"
DATE=$(date +%Y%m%d_%H%M%S)
RETENTION_DAYS=30
# 创建备份目录
mkdir -p $BACKUP_DIR
# 备份SQLite数据库
if [ -f "$DATA_DIR/user_management.db" ]; then
echo "Backing up database..."
cp "$DATA_DIR/user_management.db" "$BACKUP_DIR/user_management_$DATE.db"
# 压缩备份
gzip "$BACKUP_DIR/user_management_$DATE.db"
echo "Backup completed: $BACKUP_DIR/user_management_$DATE.db.gz"
else
echo "Warning: Database file not found"
exit 1
fi
# 备份配置文件
echo "Backing up configuration..."
tar -czf "$BACKUP_DIR/config_$DATE.tar.gz" /opt/user-management-system/configs/
# 删除过期备份
echo "Cleaning up old backups (older than $RETENTION_DAYS days)..."
find $BACKUP_DIR -name "*.db.gz" -mtime +$RETENTION_DAYS -delete
find $BACKUP_DIR -name "config_*.tar.gz" -mtime +$RETENTION_DAYS -delete
echo "Backup task completed"
```
**scripts/health-check.sh**
```bash
#!/bin/bash
SERVER_URL="http://localhost:8080"
HEALTH_ENDPOINT="/health"
check_health() {
response=$(curl -s -o /dev/null -w "%{http_code}" ${SERVER_URL}${HEALTH_ENDPOINT})
if [ $response -eq 200 ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Service health: HTTP $response"
return 0
else
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Service unhealthy: HTTP $response"
return 1
fi
}
# 单次检查
check_health
```
#### 8.3 任务清单
- [ ] 创建Dockerfile
- [ ] 创建docker-compose.yml
- [ ] 创建Kubernetes部署文件
- [ ] 创建Helm Charts
- [ ] 实现启动脚本
- [ ] 实现停止脚本
- [ ] 实现重启脚本
- [ ] 实现备份脚本
- [ ] 实现健康检查脚本
- [ ] 配置Logrotate
- [ ] 配置Cron定时任务
---
### 阶段9测试第14-15周
**目标**:完成单元测试、集成测试、性能测试
#### 9.1 测试策略
| 测试类型 | 覆盖范围 | 目标覆盖率 | 工具 |
|---------|---------|-----------|------|
| 单元测试 | 业务逻辑、工具函数 | 80%+ | Testify |
| 集成测试 | API接口、数据库操作 | 70%+ | Testify |
| 端到端测试 | 完整业务流程 | 60%+ | Testify + httptest |
| 性能测试 | 并发、响应时间 | - | Vegeta / JMeter |
| 压力测试 | 极限负载 | - | JMeter |
#### 9.2 任务清单
- [ ] 编写单元测试domain、service、repository
- [ ] 编写集成测试API接口
- [ ] 编写端到端测试(完整业务流程)
- [ ] 执行性能测试(并发、响应时间)
- [ ] 执行压力测试(极限负载)
- [ ] 修复测试发现的问题
- [ ] 优化性能瓶颈
---
### 阶段10文档和交付第16周
**目标**:完善文档,准备交付
#### 10.1 文档完善
- [ ] 完善README.md
- [ ] 完善API文档Swagger
- [ ] 完善部署文档
- [ ] 完善运维文档
- [ ] 编写用户手册
- [ ] 编写开发者文档
#### 10.2 交付准备
- [ ] 代码review
- [ ] 安全扫描
- [ ] 性能优化
- [ ] 打包发布
- [ ] 部署验证
---
## 质量保证
### 代码质量
- **代码审查**:所有代码必须经过至少一人审查
- **单元测试覆盖率**≥80%
- **静态代码分析**使用golangci-lint
- **代码风格**遵循Go官方代码风格
### 性能指标
| 指标 | 目标值 | 验证方法 |
|------|--------|----------|
| 并发用户数 | 100,000 | 性能测试 |
| QPS | 100,000 | 性能测试 |
| P50响应时间 | <100ms | 性能测试 |
| P99响应时间 | <500ms | 性能测试 |
| 缓存命中率 | >95% | 监控统计 |
| 系统可用性 | 99.99% | 监控统计 |
### 安全要求
- [ ] 所有API接口都有认证
- [ ] 敏感数据加密存储
- [ ] 密码使用bcrypt加密
- [ ] 实现接口防刷
- [ ] 实现SQL注入防护
- [ ] 实现XSS防护
- [ ] 实现CSRF防护
- [ ] 通过安全扫描
---
## 风险管理
### 技术风险
| 风险 | 可能性 | 影响 | 应对措施 |
|------|--------|------|----------|
| 性能不达标 | 中 | 高 | 提前性能测试,优化慢查询 |
| 并发问题 | 中 | 高 | 充分测试,使用协程池 |
| 安全漏洞 | 低 | 高 | 安全扫描,代码审查 |
| 数据库性能 | 中 | 高 | 优化索引,使用缓存 |
### 进度风险
| 风险 | 可能性 | 影响 | 应对措施 |
|------|--------|------|----------|
| 需求变更 | 中 | 中 | 严格控制变更范围 |
| 技术难点 | 低 | 高 | 提前调研准备方案B |
| 人员变动 | 低 | 中 | 代码文档化,知识共享 |
---
## 里程碑
| 里程碑 | 日期 | 交付物 |
|--------|------|--------|
| M1: 项目初始化完成 | 第2周 | 项目结构、配置文件、数据库迁移脚本 |
| M2: 核心数据模型完成 | 第4周 | 所有模型定义、数据库表创建 |
| M3: 缓存层完成 | 第5周 | L1缓存、L2缓存、缓存管理器 |
| M4: 核心服务完成 | 第8周 | 用户、角色、权限、认证服务 |
| M5: API层完成 | 第10周 | 所有API接口、中间件、路由 |
| M6: 安全组件完成 | 第11周 | 限流、加密、验证组件 |
| M7: 监控完成 | 第12周 | Prometheus指标、健康检查、链路追踪 |
| M8: 部署完成 | 第13周 | Docker部署、K8s部署、自动化脚本 |
| M9: 测试完成 | 第15周 | 单元测试、集成测试、性能测试 |
| M10: 交付完成 | 第16周 | 完整文档、可交付产品 |
---
## 成功标准
### 功能完整性
- ✅ 100%实现PRD所有功能需求
- ✅ 100%实现数据模型设计
- ✅ 100%实现API接口设计
- ✅ 100%实现安全设计
- ✅ 100%实现部署和运维方案
### 性能达标
- ✅ 支持10亿用户规模
- ✅ 支持10万级并发访问
- ✅ P50响应时间 < 100ms
- ✅ P99响应时间 < 500ms
- ✅ 系统可用性 ≥ 99.99%
### 质量达标
- ✅ 单元测试覆盖率 ≥ 80%
- ✅ 无安全漏洞
- ✅ 代码审查通过
- ✅ 文档完善
---
## 附录
### A. 参考文档
- PRD.md - 产品需求文档
- DATA_MODEL.md - 数据模型设计
- ARCHITECTURE.md - 技术架构文档
- API.md - API接口设计
- SECURITY.md - 安全设计文档
- DEPLOYMENT.md - 部署和运维指南
### B. 技术栈
- Go 1.23+
- Gin 1.10+
- GORM 1.25+
- SQLite 3.40+
- PostgreSQL 14+
- Redis 7.0+
- Prometheus 2.50+
- Docker 20.10+
- Kubernetes 1.28+
### C. 联系方式
- 项目负责人:[待定]
- 技术负责人:[待定]
- 测试负责人:[待定]