完成
This commit is contained in:
@@ -1,38 +1,165 @@
|
||||
# Web 服务器项目
|
||||
|
||||
一个简单的 HTTP Web 服务器,提供 RESTful API。
|
||||
这是一个简单的 HTTP Web 服务器项目,演示了 Go 语言在网络编程、并发处理和 RESTful API 开发方面的应用。
|
||||
|
||||
## 功能特性
|
||||
- HTTP 服务器
|
||||
- RESTful API 端点
|
||||
## 项目特性
|
||||
|
||||
- HTTP 服务器基础功能
|
||||
- RESTful API 设计
|
||||
- JSON 数据处理
|
||||
- 路由处理
|
||||
- 路由管理
|
||||
- 中间件支持
|
||||
- 静态文件服务
|
||||
- 并发请求处理
|
||||
- 错误处理和日志记录
|
||||
- 简单的用户管理系统
|
||||
|
||||
## API 端点
|
||||
- `GET /` - 首页
|
||||
- `GET /api/users` - 获取用户列表
|
||||
- `POST /api/users` - 创建新用户
|
||||
- `GET /api/users/{id}` - 获取特定用户
|
||||
- `PUT /api/users/{id}` - 更新用户
|
||||
- `DELETE /api/users/{id}` - 删除用户
|
||||
## 项目结构
|
||||
|
||||
## 运行方法
|
||||
```bash
|
||||
cd 03-web-server
|
||||
go run main.go
|
||||
```
|
||||
03-web-server/
|
||||
├── README.md # 项目说明文档
|
||||
├── main.go # 主程序入口
|
||||
├── server/ # 服务器核心包
|
||||
│ ├── server.go # HTTP 服务器
|
||||
│ ├── router.go # 路由管理
|
||||
│ ├── middleware.go # 中间件
|
||||
│ └── handlers.go # 请求处理器
|
||||
├── models/ # 数据模型
|
||||
│ └── user.go # 用户模型
|
||||
├── static/ # 静态文件
|
||||
│ ├── index.html # 首页
|
||||
│ ├── style.css # 样式文件
|
||||
│ └── script.js # JavaScript 文件
|
||||
├── data/ # 数据文件
|
||||
│ └── users.json # 用户数据
|
||||
└── server_test.go # 测试文件
|
||||
```
|
||||
|
||||
服务器将在 http://localhost:8080 启动
|
||||
## 运行方法
|
||||
|
||||
```bash
|
||||
# 进入项目目录
|
||||
cd 10-projects/03-web-server
|
||||
|
||||
# 运行程序
|
||||
go run main.go
|
||||
|
||||
# 或者编译后运行
|
||||
go build -o webserver main.go
|
||||
./webserver
|
||||
```
|
||||
|
||||
## API 接口
|
||||
|
||||
### 用户管理 API
|
||||
|
||||
- `GET /api/users` - 获取所有用户
|
||||
- `GET /api/users/{id}` - 获取指定用户
|
||||
- `POST /api/users` - 创建新用户
|
||||
- `PUT /api/users/{id}` - 更新用户信息
|
||||
- `DELETE /api/users/{id}` - 删除用户
|
||||
|
||||
### 其他接口
|
||||
|
||||
- `GET /` - 首页
|
||||
- `GET /health` - 健康检查
|
||||
- `GET /api/stats` - 服务器统计信息
|
||||
- `GET /static/*` - 静态文件服务
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 启动服务器
|
||||
|
||||
```bash
|
||||
# 获取用户列表
|
||||
$ go run main.go
|
||||
🚀 服务器启动成功
|
||||
📍 地址: http://localhost:8080
|
||||
📊 健康检查: http://localhost:8080/health
|
||||
📚 API文档: http://localhost:8080/api
|
||||
```
|
||||
|
||||
### API 调用示例
|
||||
|
||||
```bash
|
||||
# 获取所有用户
|
||||
curl http://localhost:8080/api/users
|
||||
|
||||
# 创建新用户
|
||||
curl -X POST http://localhost:8080/api/users \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name":"张三","email":"zhangsan@example.com"}'
|
||||
```
|
||||
-d '{"name":"张三","email":"zhangsan@example.com","age":25}'
|
||||
|
||||
# 获取指定用户
|
||||
curl http://localhost:8080/api/users/1
|
||||
|
||||
# 更新用户信息
|
||||
curl -X PUT http://localhost:8080/api/users/1 \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name":"张三","email":"zhangsan@gmail.com","age":26}'
|
||||
|
||||
# 删除用户
|
||||
curl -X DELETE http://localhost:8080/api/users/1
|
||||
|
||||
# 健康检查
|
||||
curl http://localhost:8080/health
|
||||
|
||||
# 服务器统计
|
||||
curl http://localhost:8080/api/stats
|
||||
```
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
// GET /api/users
|
||||
{
|
||||
"status": "success",
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "张三",
|
||||
"email": "zhangsan@example.com",
|
||||
"age": 25,
|
||||
"created_at": "2024-01-01T10:00:00Z",
|
||||
"updated_at": "2024-01-01T10:00:00Z"
|
||||
}
|
||||
],
|
||||
"count": 1
|
||||
}
|
||||
|
||||
// GET /health
|
||||
{
|
||||
"status": "healthy",
|
||||
"timestamp": "2024-01-01T10:00:00Z",
|
||||
"uptime": "1h30m45s",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
```
|
||||
|
||||
## 学习要点
|
||||
|
||||
这个项目综合运用了以下 Go 语言特性:
|
||||
|
||||
1. **HTTP 服务器**: 使用 `net/http` 包创建 Web 服务器
|
||||
2. **路由管理**: 实现 RESTful 路由和参数解析
|
||||
3. **JSON 处理**: 请求和响应的 JSON 序列化/反序列化
|
||||
4. **中间件模式**: 日志记录、CORS、认证等中间件
|
||||
5. **并发处理**: 利用 goroutine 处理并发请求
|
||||
6. **错误处理**: HTTP 错误响应和日志记录
|
||||
7. **文件操作**: 静态文件服务和数据持久化
|
||||
8. **结构体和接口**: 数据模型和服务接口设计
|
||||
9. **包管理**: 多包项目结构和依赖管理
|
||||
10. **测试**: HTTP 服务器和 API 的测试
|
||||
|
||||
## 扩展建议
|
||||
|
||||
1. 添加用户认证和授权(JWT)
|
||||
2. 实现数据库集成(MySQL、PostgreSQL)
|
||||
3. 添加缓存支持(Redis)
|
||||
4. 实现 WebSocket 支持
|
||||
5. 添加 API 限流和熔断
|
||||
6. 集成 Swagger API 文档
|
||||
7. 添加配置文件支持
|
||||
8. 实现优雅关闭
|
||||
9. 添加监控和指标收集
|
||||
10. 支持 HTTPS 和 HTTP/2
|
2
golang-learning/10-projects/03-web-server/data/.gitkeep
Normal file
2
golang-learning/10-projects/03-web-server/data/.gitkeep
Normal file
@@ -0,0 +1,2 @@
|
||||
# 这个文件用于保持 data 目录在 git 中被跟踪
|
||||
# 实际的 users.json 文件会在程序运行时自动创建
|
5
golang-learning/10-projects/03-web-server/go.mod
Normal file
5
golang-learning/10-projects/03-web-server/go.mod
Normal file
@@ -0,0 +1,5 @@
|
||||
module webserver
|
||||
|
||||
go 1.19
|
||||
|
||||
require github.com/gorilla/mux v1.8.0
|
54
golang-learning/10-projects/03-web-server/main.go
Normal file
54
golang-learning/10-projects/03-web-server/main.go
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
main.go - Web服务器主程序
|
||||
这是一个简单的HTTP Web服务器,演示了Go语言的网络编程应用
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"webserver/server"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 创建服务器实例
|
||||
srv := server.NewServer(":8080")
|
||||
|
||||
// 设置路由
|
||||
srv.SetupRoutes()
|
||||
|
||||
// 启动信息
|
||||
fmt.Println("🚀 Go Web服务器启动中...")
|
||||
fmt.Println("📍 地址: http://localhost:8080")
|
||||
fmt.Println("📊 健康检查: http://localhost:8080/health")
|
||||
fmt.Println("📚 API文档: http://localhost:8080/api")
|
||||
fmt.Println("按 Ctrl+C 停止服务器")
|
||||
fmt.Println()
|
||||
|
||||
// 启动服务器(在goroutine中)
|
||||
go func() {
|
||||
if err := srv.Start(); err != nil {
|
||||
log.Printf("❌ 服务器启动失败: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待中断信号
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
|
||||
fmt.Println("\n🛑 正在关闭服务器...")
|
||||
|
||||
// 优雅关闭服务器
|
||||
if err := srv.Shutdown(); err != nil {
|
||||
log.Printf("❌ 服务器关闭失败: %v", err)
|
||||
} else {
|
||||
fmt.Println("✅ 服务器已安全关闭")
|
||||
}
|
||||
}
|
293
golang-learning/10-projects/03-web-server/models/user.go
Normal file
293
golang-learning/10-projects/03-web-server/models/user.go
Normal file
@@ -0,0 +1,293 @@
|
||||
/*
|
||||
user.go - 用户数据模型
|
||||
定义了用户的数据结构和相关操作
|
||||
*/
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// User 用户结构体
|
||||
type User struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Age int `json:"age"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// UserStorage 用户存储
|
||||
type UserStorage struct {
|
||||
users []User
|
||||
nextID int
|
||||
filePath string
|
||||
}
|
||||
|
||||
var storage *UserStorage
|
||||
|
||||
// init 初始化用户存储
|
||||
func init() {
|
||||
storage = &UserStorage{
|
||||
users: make([]User, 0),
|
||||
nextID: 1,
|
||||
filePath: "data/users.json",
|
||||
}
|
||||
|
||||
// 创建数据目录
|
||||
os.MkdirAll("data", 0755)
|
||||
|
||||
// 加载现有数据
|
||||
storage.load()
|
||||
}
|
||||
|
||||
// Validate 验证用户数据
|
||||
func (u *User) Validate() error {
|
||||
if strings.TrimSpace(u.Name) == "" {
|
||||
return fmt.Errorf("用户名不能为空")
|
||||
}
|
||||
|
||||
if len(u.Name) > 50 {
|
||||
return fmt.Errorf("用户名长度不能超过50个字符")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(u.Email) == "" {
|
||||
return fmt.Errorf("邮箱不能为空")
|
||||
}
|
||||
|
||||
if !isValidEmail(u.Email) {
|
||||
return fmt.Errorf("邮箱格式无效")
|
||||
}
|
||||
|
||||
if u.Age < 0 || u.Age > 150 {
|
||||
return fmt.Errorf("年龄必须在0-150之间")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isValidEmail 简单的邮箱格式验证
|
||||
func isValidEmail(email string) bool {
|
||||
return strings.Contains(email, "@") && strings.Contains(email, ".")
|
||||
}
|
||||
|
||||
// GetAllUsers 获取所有用户
|
||||
func GetAllUsers() ([]User, error) {
|
||||
return storage.users, nil
|
||||
}
|
||||
|
||||
// GetUserByID 根据ID获取用户
|
||||
func GetUserByID(id int) (*User, error) {
|
||||
for _, user := range storage.users {
|
||||
if user.ID == id {
|
||||
return &user, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("用户不存在")
|
||||
}
|
||||
|
||||
// CreateUser 创建新用户
|
||||
func CreateUser(user User) (*User, error) {
|
||||
// 检查邮箱是否已存在
|
||||
for _, existingUser := range storage.users {
|
||||
if existingUser.Email == user.Email {
|
||||
return nil, fmt.Errorf("邮箱已存在")
|
||||
}
|
||||
}
|
||||
|
||||
// 设置用户信息
|
||||
user.ID = storage.nextID
|
||||
user.CreatedAt = time.Now()
|
||||
user.UpdatedAt = time.Now()
|
||||
|
||||
// 添加到存储
|
||||
storage.users = append(storage.users, user)
|
||||
storage.nextID++
|
||||
|
||||
// 保存到文件
|
||||
if err := storage.save(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// UpdateUser 更新用户信息
|
||||
func UpdateUser(user User) (*User, error) {
|
||||
// 查找用户
|
||||
for i, existingUser := range storage.users {
|
||||
if existingUser.ID == user.ID {
|
||||
// 检查邮箱是否被其他用户使用
|
||||
for _, otherUser := range storage.users {
|
||||
if otherUser.ID != user.ID && otherUser.Email == user.Email {
|
||||
return nil, fmt.Errorf("邮箱已被其他用户使用")
|
||||
}
|
||||
}
|
||||
|
||||
// 保留创建时间,更新其他信息
|
||||
user.CreatedAt = existingUser.CreatedAt
|
||||
user.UpdatedAt = time.Now()
|
||||
|
||||
// 更新用户
|
||||
storage.users[i] = user
|
||||
|
||||
// 保存到文件
|
||||
if err := storage.save(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("用户不存在")
|
||||
}
|
||||
|
||||
// DeleteUser 删除用户
|
||||
func DeleteUser(id int) error {
|
||||
// 查找并删除用户
|
||||
for i, user := range storage.users {
|
||||
if user.ID == id {
|
||||
// 从切片中删除用户
|
||||
storage.users = append(storage.users[:i], storage.users[i+1:]...)
|
||||
|
||||
// 保存到文件
|
||||
return storage.save()
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("用户不存在")
|
||||
}
|
||||
|
||||
// load 从文件加载用户数据
|
||||
func (s *UserStorage) load() error {
|
||||
// 检查文件是否存在
|
||||
if _, err := os.Stat(s.filePath); os.IsNotExist(err) {
|
||||
// 文件不存在,创建示例数据
|
||||
s.createSampleData()
|
||||
return s.save()
|
||||
}
|
||||
|
||||
// 读取文件
|
||||
data, err := ioutil.ReadFile(s.filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 解析JSON
|
||||
if err := json.Unmarshal(data, &s.users); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新下一个ID
|
||||
maxID := 0
|
||||
for _, user := range s.users {
|
||||
if user.ID > maxID {
|
||||
maxID = user.ID
|
||||
}
|
||||
}
|
||||
s.nextID = maxID + 1
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// save 保存用户数据到文件
|
||||
func (s *UserStorage) save() error {
|
||||
data, err := json.MarshalIndent(s.users, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(s.filePath, data, 0644)
|
||||
}
|
||||
|
||||
// createSampleData 创建示例数据
|
||||
func (s *UserStorage) createSampleData() {
|
||||
now := time.Now()
|
||||
|
||||
sampleUsers := []User{
|
||||
{
|
||||
ID: 1,
|
||||
Name: "张三",
|
||||
Email: "zhangsan@example.com",
|
||||
Age: 25,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
Name: "李四",
|
||||
Email: "lisi@example.com",
|
||||
Age: 30,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
},
|
||||
{
|
||||
ID: 3,
|
||||
Name: "王五",
|
||||
Email: "wangwu@example.com",
|
||||
Age: 28,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
},
|
||||
}
|
||||
|
||||
s.users = sampleUsers
|
||||
s.nextID = 4
|
||||
}
|
||||
|
||||
// GetUserCount 获取用户总数
|
||||
func GetUserCount() int {
|
||||
return len(storage.users)
|
||||
}
|
||||
|
||||
// SearchUsers 搜索用户
|
||||
func SearchUsers(keyword string) []User {
|
||||
var results []User
|
||||
keyword = strings.ToLower(keyword)
|
||||
|
||||
for _, user := range storage.users {
|
||||
if strings.Contains(strings.ToLower(user.Name), keyword) ||
|
||||
strings.Contains(strings.ToLower(user.Email), keyword) {
|
||||
results = append(results, user)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// GetUsersByAge 根据年龄范围获取用户
|
||||
func GetUsersByAge(minAge, maxAge int) []User {
|
||||
var results []User
|
||||
|
||||
for _, user := range storage.users {
|
||||
if user.Age >= minAge && user.Age <= maxAge {
|
||||
results = append(results, user)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// GetRecentUsers 获取最近创建的用户
|
||||
func GetRecentUsers(limit int) []User {
|
||||
if limit <= 0 || limit > len(storage.users) {
|
||||
limit = len(storage.users)
|
||||
}
|
||||
|
||||
// 简单实现:返回最后创建的用户
|
||||
// 实际应用中应该按创建时间排序
|
||||
start := len(storage.users) - limit
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
|
||||
return storage.users[start:]
|
||||
}
|
369
golang-learning/10-projects/03-web-server/server/handlers.go
Normal file
369
golang-learning/10-projects/03-web-server/server/handlers.go
Normal file
@@ -0,0 +1,369 @@
|
||||
/*
|
||||
handlers.go - 请求处理器
|
||||
实现了各种HTTP请求的处理逻辑
|
||||
*/
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"webserver/models"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// Response 通用响应结构
|
||||
type Response struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Count int `json:"count,omitempty"`
|
||||
}
|
||||
|
||||
// HealthResponse 健康检查响应
|
||||
type HealthResponse struct {
|
||||
Status string `json:"status"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
Uptime string `json:"uptime"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// StatsResponse 统计信息响应
|
||||
type StatsResponse struct {
|
||||
Status string `json:"status"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
Uptime string `json:"uptime"`
|
||||
Version string `json:"version"`
|
||||
GoVersion string `json:"go_version"`
|
||||
NumCPU int `json:"num_cpu"`
|
||||
NumGoroutine int `json:"num_goroutine"`
|
||||
MemStats runtime.MemStats `json:"mem_stats"`
|
||||
}
|
||||
|
||||
var (
|
||||
startTime = time.Now()
|
||||
version = "1.0.0"
|
||||
)
|
||||
|
||||
// HomeHandler 首页处理器
|
||||
func HomeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
html := `
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Go Web服务器</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 40px; background-color: #f5f5f5; }
|
||||
.container { max-width: 800px; margin: 0 auto; background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
||||
h1 { color: #333; text-align: center; }
|
||||
.api-list { background: #f8f9fa; padding: 20px; border-radius: 5px; margin: 20px 0; }
|
||||
.api-item { margin: 10px 0; padding: 10px; background: white; border-left: 4px solid #007bff; }
|
||||
.method { font-weight: bold; color: #007bff; }
|
||||
.endpoint { font-family: monospace; background: #e9ecef; padding: 2px 6px; border-radius: 3px; }
|
||||
.description { color: #666; margin-top: 5px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🚀 Go Web服务器</h1>
|
||||
<p>欢迎使用Go语言编写的简单Web服务器!这个项目演示了HTTP服务器、RESTful API、JSON处理等功能。</p>
|
||||
|
||||
<h2>📚 API接口</h2>
|
||||
<div class="api-list">
|
||||
<div class="api-item">
|
||||
<span class="method">GET</span> <span class="endpoint">/health</span>
|
||||
<div class="description">健康检查</div>
|
||||
</div>
|
||||
<div class="api-item">
|
||||
<span class="method">GET</span> <span class="endpoint">/api/stats</span>
|
||||
<div class="description">服务器统计信息</div>
|
||||
</div>
|
||||
<div class="api-item">
|
||||
<span class="method">GET</span> <span class="endpoint">/api/users</span>
|
||||
<div class="description">获取所有用户</div>
|
||||
</div>
|
||||
<div class="api-item">
|
||||
<span class="method">POST</span> <span class="endpoint">/api/users</span>
|
||||
<div class="description">创建新用户</div>
|
||||
</div>
|
||||
<div class="api-item">
|
||||
<span class="method">GET</span> <span class="endpoint">/api/users/{id}</span>
|
||||
<div class="description">获取指定用户</div>
|
||||
</div>
|
||||
<div class="api-item">
|
||||
<span class="method">PUT</span> <span class="endpoint">/api/users/{id}</span>
|
||||
<div class="description">更新用户信息</div>
|
||||
</div>
|
||||
<div class="api-item">
|
||||
<span class="method">DELETE</span> <span class="endpoint">/api/users/{id}</span>
|
||||
<div class="description">删除用户</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>🛠️ 使用示例</h2>
|
||||
<pre style="background: #f8f9fa; padding: 15px; border-radius: 5px; overflow-x: auto;">
|
||||
# 获取所有用户
|
||||
curl http://localhost:8080/api/users
|
||||
|
||||
# 创建新用户
|
||||
curl -X POST http://localhost:8080/api/users \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name":"张三","email":"zhangsan@example.com","age":25}'
|
||||
|
||||
# 健康检查
|
||||
curl http://localhost:8080/health
|
||||
</pre>
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(html))
|
||||
}
|
||||
|
||||
// HealthHandler 健康检查处理器
|
||||
func HealthHandler(w http.ResponseWriter, r *http.Request) {
|
||||
uptime := time.Since(startTime)
|
||||
|
||||
response := HealthResponse{
|
||||
Status: "healthy",
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
Uptime: uptime.String(),
|
||||
Version: version,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// StatsHandler 统计信息处理器
|
||||
func StatsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
uptime := time.Since(startTime)
|
||||
var memStats runtime.MemStats
|
||||
runtime.ReadMemStats(&memStats)
|
||||
|
||||
response := StatsResponse{
|
||||
Status: "success",
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
Uptime: uptime.String(),
|
||||
Version: version,
|
||||
GoVersion: runtime.Version(),
|
||||
NumCPU: runtime.NumCPU(),
|
||||
NumGoroutine: runtime.NumGoroutine(),
|
||||
MemStats: memStats,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// GetUsersHandler 获取所有用户
|
||||
func GetUsersHandler(w http.ResponseWriter, r *http.Request) {
|
||||
users, err := models.GetAllUsers()
|
||||
if err != nil {
|
||||
sendErrorResponse(w, "获取用户列表失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := Response{
|
||||
Status: "success",
|
||||
Data: users,
|
||||
Count: len(users),
|
||||
}
|
||||
|
||||
sendJSONResponse(w, response, http.StatusOK)
|
||||
}
|
||||
|
||||
// GetUserHandler 获取指定用户
|
||||
func GetUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, err := strconv.Atoi(vars["id"])
|
||||
if err != nil {
|
||||
sendErrorResponse(w, "无效的用户ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := models.GetUserByID(id)
|
||||
if err != nil {
|
||||
sendErrorResponse(w, "用户不存在", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
response := Response{
|
||||
Status: "success",
|
||||
Data: user,
|
||||
}
|
||||
|
||||
sendJSONResponse(w, response, http.StatusOK)
|
||||
}
|
||||
|
||||
// CreateUserHandler 创建新用户
|
||||
func CreateUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var user models.User
|
||||
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
|
||||
sendErrorResponse(w, "无效的JSON数据", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证用户数据
|
||||
if err := user.Validate(); err != nil {
|
||||
sendErrorResponse(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
createdUser, err := models.CreateUser(user)
|
||||
if err != nil {
|
||||
sendErrorResponse(w, "创建用户失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := Response{
|
||||
Status: "success",
|
||||
Message: "用户创建成功",
|
||||
Data: createdUser,
|
||||
}
|
||||
|
||||
sendJSONResponse(w, response, http.StatusCreated)
|
||||
}
|
||||
|
||||
// UpdateUserHandler 更新用户信息
|
||||
func UpdateUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, err := strconv.Atoi(vars["id"])
|
||||
if err != nil {
|
||||
sendErrorResponse(w, "无效的用户ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
|
||||
sendErrorResponse(w, "无效的JSON数据", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 设置用户ID
|
||||
user.ID = id
|
||||
|
||||
// 验证用户数据
|
||||
if err := user.Validate(); err != nil {
|
||||
sendErrorResponse(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新用户
|
||||
updatedUser, err := models.UpdateUser(user)
|
||||
if err != nil {
|
||||
sendErrorResponse(w, "更新用户失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := Response{
|
||||
Status: "success",
|
||||
Message: "用户更新成功",
|
||||
Data: updatedUser,
|
||||
}
|
||||
|
||||
sendJSONResponse(w, response, http.StatusOK)
|
||||
}
|
||||
|
||||
// DeleteUserHandler 删除用户
|
||||
func DeleteUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, err := strconv.Atoi(vars["id"])
|
||||
if err != nil {
|
||||
sendErrorResponse(w, "无效的用户ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := models.DeleteUser(id); err != nil {
|
||||
sendErrorResponse(w, "删除用户失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := Response{
|
||||
Status: "success",
|
||||
Message: "用户删除成功",
|
||||
}
|
||||
|
||||
sendJSONResponse(w, response, http.StatusOK)
|
||||
}
|
||||
|
||||
// APIDocHandler API文档处理器
|
||||
func APIDocHandler(w http.ResponseWriter, r *http.Request) {
|
||||
doc := map[string]interface{}{
|
||||
"title": "Go Web服务器 API",
|
||||
"version": version,
|
||||
"description": "一个简单的RESTful API服务器",
|
||||
"endpoints": map[string]interface{}{
|
||||
"health": map[string]string{
|
||||
"method": "GET",
|
||||
"path": "/health",
|
||||
"description": "健康检查",
|
||||
},
|
||||
"stats": map[string]string{
|
||||
"method": "GET",
|
||||
"path": "/api/stats",
|
||||
"description": "服务器统计信息",
|
||||
},
|
||||
"users": map[string]interface{}{
|
||||
"list": map[string]string{
|
||||
"method": "GET",
|
||||
"path": "/api/users",
|
||||
"description": "获取所有用户",
|
||||
},
|
||||
"get": map[string]string{
|
||||
"method": "GET",
|
||||
"path": "/api/users/{id}",
|
||||
"description": "获取指定用户",
|
||||
},
|
||||
"create": map[string]string{
|
||||
"method": "POST",
|
||||
"path": "/api/users",
|
||||
"description": "创建新用户",
|
||||
},
|
||||
"update": map[string]string{
|
||||
"method": "PUT",
|
||||
"path": "/api/users/{id}",
|
||||
"description": "更新用户信息",
|
||||
},
|
||||
"delete": map[string]string{
|
||||
"method": "DELETE",
|
||||
"path": "/api/users/{id}",
|
||||
"description": "删除用户",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
sendJSONResponse(w, doc, http.StatusOK)
|
||||
}
|
||||
|
||||
// sendJSONResponse 发送JSON响应
|
||||
func sendJSONResponse(w http.ResponseWriter, data interface{}, statusCode int) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(statusCode)
|
||||
if err := json.NewEncoder(w).Encode(data); err != nil {
|
||||
http.Error(w, "JSON编码失败", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// sendErrorResponse 发送错误响应
|
||||
func sendErrorResponse(w http.ResponseWriter, message string, statusCode int) {
|
||||
response := Response{
|
||||
Status: "error",
|
||||
Error: message,
|
||||
}
|
||||
sendJSONResponse(w, response, statusCode)
|
||||
}
|
147
golang-learning/10-projects/03-web-server/server/middleware.go
Normal file
147
golang-learning/10-projects/03-web-server/server/middleware.go
Normal file
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
middleware.go - 中间件
|
||||
实现了各种HTTP中间件功能
|
||||
*/
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LoggingMiddleware 日志记录中间件
|
||||
func LoggingMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
|
||||
// 创建响应记录器来捕获状态码
|
||||
recorder := &responseRecorder{
|
||||
ResponseWriter: w,
|
||||
statusCode: http.StatusOK,
|
||||
}
|
||||
|
||||
// 调用下一个处理器
|
||||
next.ServeHTTP(recorder, r)
|
||||
|
||||
// 记录请求日志
|
||||
duration := time.Since(start)
|
||||
log.Printf("[%s] %s %s %d %v",
|
||||
r.Method,
|
||||
r.RequestURI,
|
||||
r.RemoteAddr,
|
||||
recorder.statusCode,
|
||||
duration,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// responseRecorder 响应记录器
|
||||
type responseRecorder struct {
|
||||
http.ResponseWriter
|
||||
statusCode int
|
||||
}
|
||||
|
||||
// WriteHeader 记录状态码
|
||||
func (rr *responseRecorder) WriteHeader(code int) {
|
||||
rr.statusCode = code
|
||||
rr.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
// CORSMiddleware CORS中间件
|
||||
func CORSMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// 设置CORS头
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
|
||||
// 处理预检请求
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// 调用下一个处理器
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// RecoveryMiddleware 恢复中间件(处理panic)
|
||||
func RecoveryMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
// 记录panic信息
|
||||
log.Printf("❌ Panic recovered: %v\n%s", err, debug.Stack())
|
||||
|
||||
// 返回500错误
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
}
|
||||
}()
|
||||
|
||||
// 调用下一个处理器
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// AuthMiddleware 认证中间件(示例)
|
||||
func AuthMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// 检查Authorization头
|
||||
token := r.Header.Get("Authorization")
|
||||
if token == "" {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// 这里可以添加实际的token验证逻辑
|
||||
// 为了演示,我们简单检查token是否为"Bearer valid-token"
|
||||
if token != "Bearer valid-token" {
|
||||
http.Error(w, "Invalid token", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// 调用下一个处理器
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// RateLimitMiddleware 限流中间件(简化版)
|
||||
func RateLimitMiddleware(next http.Handler) http.Handler {
|
||||
// 这里可以实现基于IP的限流逻辑
|
||||
// 为了简化,我们只是一个示例框架
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// 实际实现中,这里会检查请求频率
|
||||
// 如果超过限制,返回429状态码
|
||||
|
||||
// 调用下一个处理器
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// ContentTypeMiddleware 内容类型中间件
|
||||
func ContentTypeMiddleware(contentType string) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// SecurityMiddleware 安全头中间件
|
||||
func SecurityMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// 设置安全相关的HTTP头
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
w.Header().Set("X-XSS-Protection", "1; mode=block")
|
||||
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
|
||||
|
||||
// 调用下一个处理器
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
59
golang-learning/10-projects/03-web-server/server/router.go
Normal file
59
golang-learning/10-projects/03-web-server/server/router.go
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
router.go - 路由管理
|
||||
实现了HTTP路由和中间件管理功能
|
||||
*/
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// Router 路由器结构体
|
||||
type Router struct {
|
||||
*mux.Router
|
||||
}
|
||||
|
||||
// NewRouter 创建新的路由器
|
||||
func NewRouter() *Router {
|
||||
return &Router{
|
||||
Router: mux.NewRouter(),
|
||||
}
|
||||
}
|
||||
|
||||
// Use 添加中间件
|
||||
func (r *Router) Use(middleware func(http.Handler) http.Handler) {
|
||||
r.Router.Use(middleware)
|
||||
}
|
||||
|
||||
// Methods 设置HTTP方法(链式调用)
|
||||
func (r *Router) Methods(methods ...string) *mux.Route {
|
||||
return r.Router.Methods(methods...)
|
||||
}
|
||||
|
||||
// PathPrefix 路径前缀
|
||||
func (r *Router) PathPrefix(tpl string) *mux.Router {
|
||||
return r.Router.PathPrefix(tpl)
|
||||
}
|
||||
|
||||
// Subrouter 创建子路由
|
||||
func (r *Router) Subrouter() *mux.Router {
|
||||
return r.Router.NewRoute().Subrouter()
|
||||
}
|
||||
|
||||
// HandleFunc 处理函数路由
|
||||
func (r *Router) HandleFunc(path string, f func(http.ResponseWriter, *http.Request)) *mux.Route {
|
||||
return r.Router.HandleFunc(path, f)
|
||||
}
|
||||
|
||||
// Handle 处理器路由
|
||||
func (r *Router) Handle(path string, handler http.Handler) *mux.Route {
|
||||
return r.Router.Handle(path, handler)
|
||||
}
|
||||
|
||||
// ServeHTTP 实现http.Handler接口
|
||||
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
r.Router.ServeHTTP(w, req)
|
||||
}
|
94
golang-learning/10-projects/03-web-server/server/server.go
Normal file
94
golang-learning/10-projects/03-web-server/server/server.go
Normal file
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
server.go - HTTP服务器核心
|
||||
实现了HTTP服务器的基本功能和生命周期管理
|
||||
*/
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Server HTTP服务器结构体
|
||||
type Server struct {
|
||||
httpServer *http.Server
|
||||
router *Router
|
||||
addr string
|
||||
}
|
||||
|
||||
// NewServer 创建新的服务器实例
|
||||
func NewServer(addr string) *Server {
|
||||
router := NewRouter()
|
||||
|
||||
return &Server{
|
||||
httpServer: &http.Server{
|
||||
Addr: addr,
|
||||
Handler: router,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
WriteTimeout: 15 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
},
|
||||
router: router,
|
||||
addr: addr,
|
||||
}
|
||||
}
|
||||
|
||||
// SetupRoutes 设置路由
|
||||
func (s *Server) SetupRoutes() {
|
||||
// 添加中间件
|
||||
s.router.Use(LoggingMiddleware)
|
||||
s.router.Use(CORSMiddleware)
|
||||
s.router.Use(RecoveryMiddleware)
|
||||
|
||||
// 静态文件服务
|
||||
s.router.HandleFunc("/", HomeHandler).Methods("GET")
|
||||
s.router.PathPrefix("/static/").Handler(
|
||||
http.StripPrefix("/static/", http.FileServer(http.Dir("./static/"))),
|
||||
)
|
||||
|
||||
// 健康检查
|
||||
s.router.HandleFunc("/health", HealthHandler).Methods("GET")
|
||||
|
||||
// API路由
|
||||
apiRouter := s.router.PathPrefix("/api").Subrouter()
|
||||
|
||||
// 用户管理API
|
||||
apiRouter.HandleFunc("/users", GetUsersHandler).Methods("GET")
|
||||
apiRouter.HandleFunc("/users", CreateUserHandler).Methods("POST")
|
||||
apiRouter.HandleFunc("/users/{id:[0-9]+}", GetUserHandler).Methods("GET")
|
||||
apiRouter.HandleFunc("/users/{id:[0-9]+}", UpdateUserHandler).Methods("PUT")
|
||||
apiRouter.HandleFunc("/users/{id:[0-9]+}", DeleteUserHandler).Methods("DELETE")
|
||||
|
||||
// 服务器统计
|
||||
apiRouter.HandleFunc("/stats", StatsHandler).Methods("GET")
|
||||
|
||||
// API文档
|
||||
apiRouter.HandleFunc("", APIDocHandler).Methods("GET")
|
||||
}
|
||||
|
||||
// Start 启动服务器
|
||||
func (s *Server) Start() error {
|
||||
fmt.Printf("✅ 服务器启动成功,监听地址: %s\n", s.addr)
|
||||
return s.httpServer.ListenAndServe()
|
||||
}
|
||||
|
||||
// Shutdown 优雅关闭服务器
|
||||
func (s *Server) Shutdown() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
return s.httpServer.Shutdown(ctx)
|
||||
}
|
||||
|
||||
// GetRouter 获取路由器
|
||||
func (s *Server) GetRouter() *Router {
|
||||
return s.router
|
||||
}
|
||||
|
||||
// GetHTTPServer 获取HTTP服务器
|
||||
func (s *Server) GetHTTPServer() *http.Server {
|
||||
return s.httpServer
|
||||
}
|
358
golang-learning/10-projects/03-web-server/server_test.go
Normal file
358
golang-learning/10-projects/03-web-server/server_test.go
Normal file
@@ -0,0 +1,358 @@
|
||||
/*
|
||||
server_test.go - Web服务器测试文件
|
||||
测试HTTP服务器和API的各种功能
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"webserver/models"
|
||||
"webserver/server"
|
||||
)
|
||||
|
||||
// TestHealthHandler 测试健康检查处理器
|
||||
func TestHealthHandler(t *testing.T) {
|
||||
req, err := http.NewRequest("GET", "/health", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(server.HealthHandler)
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
// 检查状态码
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
t.Errorf("处理器返回错误状态码: got %v want %v", status, http.StatusOK)
|
||||
}
|
||||
|
||||
// 检查响应内容类型
|
||||
expected := "application/json"
|
||||
if ct := rr.Header().Get("Content-Type"); ct != expected {
|
||||
t.Errorf("处理器返回错误内容类型: got %v want %v", ct, expected)
|
||||
}
|
||||
|
||||
// 检查响应体
|
||||
var response server.HealthResponse
|
||||
if err := json.Unmarshal(rr.Body.Bytes(), &response); err != nil {
|
||||
t.Errorf("无法解析响应JSON: %v", err)
|
||||
}
|
||||
|
||||
if response.Status != "healthy" {
|
||||
t.Errorf("期望状态为 'healthy', 实际为 '%s'", response.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetUsersHandler 测试获取用户列表处理器
|
||||
func TestGetUsersHandler(t *testing.T) {
|
||||
req, err := http.NewRequest("GET", "/api/users", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(server.GetUsersHandler)
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
// 检查状态码
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
t.Errorf("处理器返回错误状态码: got %v want %v", status, http.StatusOK)
|
||||
}
|
||||
|
||||
// 检查响应内容类型
|
||||
expected := "application/json"
|
||||
if ct := rr.Header().Get("Content-Type"); ct != expected {
|
||||
t.Errorf("处理器返回错误内容类型: got %v want %v", ct, expected)
|
||||
}
|
||||
|
||||
// 检查响应体
|
||||
var response server.Response
|
||||
if err := json.Unmarshal(rr.Body.Bytes(), &response); err != nil {
|
||||
t.Errorf("无法解析响应JSON: %v", err)
|
||||
}
|
||||
|
||||
if response.Status != "success" {
|
||||
t.Errorf("期望状态为 'success', 实际为 '%s'", response.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateUserHandler 测试创建用户处理器
|
||||
func TestCreateUserHandler(t *testing.T) {
|
||||
user := models.User{
|
||||
Name: "测试用户",
|
||||
Email: "test@example.com",
|
||||
Age: 25,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(user)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", "/api/users", bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(server.CreateUserHandler)
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
// 检查状态码
|
||||
if status := rr.Code; status != http.StatusCreated {
|
||||
t.Errorf("处理器返回错误状态码: got %v want %v", status, http.StatusCreated)
|
||||
}
|
||||
|
||||
// 检查响应体
|
||||
var response server.Response
|
||||
if err := json.Unmarshal(rr.Body.Bytes(), &response); err != nil {
|
||||
t.Errorf("无法解析响应JSON: %v", err)
|
||||
}
|
||||
|
||||
if response.Status != "success" {
|
||||
t.Errorf("期望状态为 'success', 实际为 '%s'", response.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateUserHandlerInvalidData 测试创建用户处理器(无效数据)
|
||||
func TestCreateUserHandlerInvalidData(t *testing.T) {
|
||||
// 测试空用户名
|
||||
user := models.User{
|
||||
Name: "",
|
||||
Email: "test@example.com",
|
||||
Age: 25,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(user)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", "/api/users", bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(server.CreateUserHandler)
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
// 检查状态码
|
||||
if status := rr.Code; status != http.StatusBadRequest {
|
||||
t.Errorf("处理器返回错误状态码: got %v want %v", status, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// 检查响应体
|
||||
var response server.Response
|
||||
if err := json.Unmarshal(rr.Body.Bytes(), &response); err != nil {
|
||||
t.Errorf("无法解析响应JSON: %v", err)
|
||||
}
|
||||
|
||||
if response.Status != "error" {
|
||||
t.Errorf("期望状态为 'error', 实际为 '%s'", response.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateUserHandlerInvalidJSON 测试创建用户处理器(无效JSON)
|
||||
func TestCreateUserHandlerInvalidJSON(t *testing.T) {
|
||||
invalidJSON := []byte(`{"name": "测试用户", "email": "test@example.com", "age":}`)
|
||||
|
||||
req, err := http.NewRequest("POST", "/api/users", bytes.NewBuffer(invalidJSON))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(server.CreateUserHandler)
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
// 检查状态码
|
||||
if status := rr.Code; status != http.StatusBadRequest {
|
||||
t.Errorf("处理器返回错误状态码: got %v want %v", status, http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
// TestStatsHandler 测试统计信息处理器
|
||||
func TestStatsHandler(t *testing.T) {
|
||||
req, err := http.NewRequest("GET", "/api/stats", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(server.StatsHandler)
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
// 检查状态码
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
t.Errorf("处理器返回错误状态码: got %v want %v", status, http.StatusOK)
|
||||
}
|
||||
|
||||
// 检查响应体
|
||||
var response server.StatsResponse
|
||||
if err := json.Unmarshal(rr.Body.Bytes(), &response); err != nil {
|
||||
t.Errorf("无法解析响应JSON: %v", err)
|
||||
}
|
||||
|
||||
if response.Status != "success" {
|
||||
t.Errorf("期望状态为 'success', 实际为 '%s'", response.Status)
|
||||
}
|
||||
|
||||
if response.NumCPU <= 0 {
|
||||
t.Error("CPU数量应该大于0")
|
||||
}
|
||||
|
||||
if response.NumGoroutine <= 0 {
|
||||
t.Error("Goroutine数量应该大于0")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAPIDocHandler 测试API文档处理器
|
||||
func TestAPIDocHandler(t *testing.T) {
|
||||
req, err := http.NewRequest("GET", "/api", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(server.APIDocHandler)
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
// 检查状态码
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
t.Errorf("处理器返回错误状态码: got %v want %v", status, http.StatusOK)
|
||||
}
|
||||
|
||||
// 检查响应内容类型
|
||||
expected := "application/json"
|
||||
if ct := rr.Header().Get("Content-Type"); ct != expected {
|
||||
t.Errorf("处理器返回错误内容类型: got %v want %v", ct, expected)
|
||||
}
|
||||
|
||||
// 检查响应体包含API文档信息
|
||||
var doc map[string]interface{}
|
||||
if err := json.Unmarshal(rr.Body.Bytes(), &doc); err != nil {
|
||||
t.Errorf("无法解析响应JSON: %v", err)
|
||||
}
|
||||
|
||||
if _, exists := doc["title"]; !exists {
|
||||
t.Error("API文档应该包含title字段")
|
||||
}
|
||||
|
||||
if _, exists := doc["endpoints"]; !exists {
|
||||
t.Error("API文档应该包含endpoints字段")
|
||||
}
|
||||
}
|
||||
|
||||
// TestServer 测试完整的服务器
|
||||
func TestServer(t *testing.T) {
|
||||
// 创建服务器
|
||||
srv := server.NewServer(":0") // 使用随机端口
|
||||
srv.SetupRoutes()
|
||||
|
||||
// 创建测试服务器
|
||||
testServer := httptest.NewServer(srv.GetRouter())
|
||||
defer testServer.Close()
|
||||
|
||||
// 测试健康检查
|
||||
resp, err := http.Get(testServer.URL + "/health")
|
||||
if err != nil {
|
||||
t.Fatalf("健康检查请求失败: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("健康检查返回错误状态码: got %v want %v", resp.StatusCode, http.StatusOK)
|
||||
}
|
||||
|
||||
// 测试获取用户列表
|
||||
resp, err = http.Get(testServer.URL + "/api/users")
|
||||
if err != nil {
|
||||
t.Fatalf("获取用户列表请求失败: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("获取用户列表返回错误状态码: got %v want %v", resp.StatusCode, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMiddleware 测试中间件
|
||||
func TestMiddleware(t *testing.T) {
|
||||
// 测试CORS中间件
|
||||
req, err := http.NewRequest("OPTIONS", "/api/users", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
// 创建带中间件的处理器
|
||||
handler := server.CORSMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
// 检查CORS头
|
||||
if origin := rr.Header().Get("Access-Control-Allow-Origin"); origin != "*" {
|
||||
t.Errorf("期望CORS Origin为 '*', 实际为 '%s'", origin)
|
||||
}
|
||||
|
||||
if methods := rr.Header().Get("Access-Control-Allow-Methods"); methods == "" {
|
||||
t.Error("应该设置Access-Control-Allow-Methods头")
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkHealthHandler 健康检查处理器基准测试
|
||||
func BenchmarkHealthHandler(b *testing.B) {
|
||||
req, _ := http.NewRequest("GET", "/health", nil)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(server.HealthHandler)
|
||||
handler.ServeHTTP(rr, req)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkGetUsersHandler 获取用户列表处理器基准测试
|
||||
func BenchmarkGetUsersHandler(b *testing.B) {
|
||||
req, _ := http.NewRequest("GET", "/api/users", nil)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(server.GetUsersHandler)
|
||||
handler.ServeHTTP(rr, req)
|
||||
}
|
||||
}
|
||||
|
||||
// ExampleHealthHandler 健康检查处理器示例
|
||||
func ExampleHealthHandler() {
|
||||
req, _ := http.NewRequest("GET", "/health", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(server.HealthHandler)
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
fmt.Printf("Status: %d", rr.Code)
|
||||
// Output: Status: 200
|
||||
}
|
101
golang-learning/10-projects/03-web-server/static/index.html
Normal file
101
golang-learning/10-projects/03-web-server/static/index.html
Normal file
@@ -0,0 +1,101 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Go Web服务器</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>🚀 Go Web服务器</h1>
|
||||
<p>一个使用Go语言编写的简单HTTP服务器示例</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="api-section">
|
||||
<h2>📚 API接口测试</h2>
|
||||
|
||||
<div class="api-group">
|
||||
<h3>用户管理</h3>
|
||||
|
||||
<div class="api-item">
|
||||
<button onclick="getUsers()">获取所有用户</button>
|
||||
<span class="method get">GET</span>
|
||||
<span class="endpoint">/api/users</span>
|
||||
</div>
|
||||
|
||||
<div class="api-item">
|
||||
<button onclick="createUser()">创建用户</button>
|
||||
<span class="method post">POST</span>
|
||||
<span class="endpoint">/api/users</span>
|
||||
</div>
|
||||
|
||||
<div class="api-item">
|
||||
<input type="number" id="userId" placeholder="用户ID" min="1">
|
||||
<button onclick="getUser()">获取用户</button>
|
||||
<span class="method get">GET</span>
|
||||
<span class="endpoint">/api/users/{id}</span>
|
||||
</div>
|
||||
|
||||
<div class="api-item">
|
||||
<button onclick="deleteUser()">删除用户</button>
|
||||
<span class="method delete">DELETE</span>
|
||||
<span class="endpoint">/api/users/{id}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="api-group">
|
||||
<h3>系统信息</h3>
|
||||
|
||||
<div class="api-item">
|
||||
<button onclick="getHealth()">健康检查</button>
|
||||
<span class="method get">GET</span>
|
||||
<span class="endpoint">/health</span>
|
||||
</div>
|
||||
|
||||
<div class="api-item">
|
||||
<button onclick="getStats()">服务器统计</button>
|
||||
<span class="method get">GET</span>
|
||||
<span class="endpoint">/api/stats</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="form-section">
|
||||
<h2>📝 创建用户表单</h2>
|
||||
<form id="userForm">
|
||||
<div class="form-group">
|
||||
<label for="name">姓名:</label>
|
||||
<input type="text" id="name" name="name" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">邮箱:</label>
|
||||
<input type="email" id="email" name="email" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="age">年龄:</label>
|
||||
<input type="number" id="age" name="age" min="0" max="150" required>
|
||||
</div>
|
||||
|
||||
<button type="submit">创建用户</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="response-section">
|
||||
<h2>📄 响应结果</h2>
|
||||
<pre id="response"></pre>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>© 2024 Go Web服务器示例项目</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="/static/script.js"></script>
|
||||
</body>
|
||||
</html>
|
241
golang-learning/10-projects/03-web-server/static/script.js
Normal file
241
golang-learning/10-projects/03-web-server/static/script.js
Normal file
@@ -0,0 +1,241 @@
|
||||
// script.js - JavaScript 文件
|
||||
|
||||
// API 基础URL
|
||||
const API_BASE = '/api';
|
||||
|
||||
// 响应显示元素
|
||||
const responseElement = document.getElementById('response');
|
||||
|
||||
// 显示响应结果
|
||||
function showResponse(data, isError = false) {
|
||||
responseElement.textContent = JSON.stringify(data, null, 2);
|
||||
responseElement.className = isError ? 'error' : 'success';
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
function showLoading() {
|
||||
responseElement.textContent = '加载中...';
|
||||
responseElement.className = 'loading';
|
||||
}
|
||||
|
||||
// API 请求封装
|
||||
async function apiRequest(url, options = {}) {
|
||||
try {
|
||||
showLoading();
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers
|
||||
},
|
||||
...options
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
showResponse(data, true);
|
||||
return null;
|
||||
}
|
||||
|
||||
showResponse(data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
showResponse({
|
||||
error: '网络请求失败',
|
||||
message: error.message
|
||||
}, true);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有用户
|
||||
async function getUsers() {
|
||||
await apiRequest(`${API_BASE}/users`);
|
||||
}
|
||||
|
||||
// 获取指定用户
|
||||
async function getUser() {
|
||||
const userId = document.getElementById('userId').value;
|
||||
if (!userId) {
|
||||
showResponse({
|
||||
error: '请输入用户ID'
|
||||
}, true);
|
||||
return;
|
||||
}
|
||||
|
||||
await apiRequest(`${API_BASE}/users/${userId}`);
|
||||
}
|
||||
|
||||
// 创建用户(使用按钮)
|
||||
async function createUser() {
|
||||
const userData = {
|
||||
name: '测试用户',
|
||||
email: `test${Date.now()}@example.com`,
|
||||
age: Math.floor(Math.random() * 50) + 18
|
||||
};
|
||||
|
||||
await apiRequest(`${API_BASE}/users`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(userData)
|
||||
});
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
async function deleteUser() {
|
||||
const userId = document.getElementById('userId').value;
|
||||
if (!userId) {
|
||||
showResponse({
|
||||
error: '请输入用户ID'
|
||||
}, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(`确定要删除用户 ${userId} 吗?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await apiRequest(`${API_BASE}/users/${userId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
}
|
||||
|
||||
// 健康检查
|
||||
async function getHealth() {
|
||||
await apiRequest('/health');
|
||||
}
|
||||
|
||||
// 获取服务器统计
|
||||
async function getStats() {
|
||||
await apiRequest(`${API_BASE}/stats`);
|
||||
}
|
||||
|
||||
// 表单提交处理
|
||||
document.getElementById('userForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(this);
|
||||
const userData = {
|
||||
name: formData.get('name'),
|
||||
email: formData.get('email'),
|
||||
age: parseInt(formData.get('age'))
|
||||
};
|
||||
|
||||
// 验证数据
|
||||
if (!userData.name || !userData.email || !userData.age) {
|
||||
showResponse({
|
||||
error: '请填写所有必填字段'
|
||||
}, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (userData.age < 0 || userData.age > 150) {
|
||||
showResponse({
|
||||
error: '年龄必须在0-150之间'
|
||||
}, true);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await apiRequest(`${API_BASE}/users`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(userData)
|
||||
});
|
||||
|
||||
if (result) {
|
||||
// 清空表单
|
||||
this.reset();
|
||||
}
|
||||
});
|
||||
|
||||
// 页面加载完成后的初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 显示欢迎信息
|
||||
showResponse({
|
||||
message: '欢迎使用 Go Web服务器 API 测试页面!',
|
||||
instructions: [
|
||||
'点击上方按钮测试各种API接口',
|
||||
'使用表单创建新用户',
|
||||
'查看下方的响应结果'
|
||||
]
|
||||
});
|
||||
|
||||
// 自动获取用户列表
|
||||
setTimeout(getUsers, 1000);
|
||||
});
|
||||
|
||||
// 键盘快捷键
|
||||
document.addEventListener('keydown', function(e) {
|
||||
// Ctrl + Enter 快速创建用户
|
||||
if (e.ctrlKey && e.key === 'Enter') {
|
||||
createUser();
|
||||
}
|
||||
|
||||
// Ctrl + R 刷新用户列表
|
||||
if (e.ctrlKey && e.key === 'r') {
|
||||
e.preventDefault();
|
||||
getUsers();
|
||||
}
|
||||
});
|
||||
|
||||
// 工具函数:格式化时间
|
||||
function formatTime(timestamp) {
|
||||
return new Date(timestamp).toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
// 工具函数:格式化文件大小
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
// 工具函数:复制到剪贴板
|
||||
async function copyToClipboard(text) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
console.log('已复制到剪贴板');
|
||||
} catch (err) {
|
||||
console.error('复制失败:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加复制响应结果的功能
|
||||
responseElement.addEventListener('click', function() {
|
||||
if (this.textContent && this.textContent !== '加载中...') {
|
||||
copyToClipboard(this.textContent);
|
||||
}
|
||||
});
|
||||
|
||||
// 自动刷新功能(可选)
|
||||
let autoRefresh = false;
|
||||
let refreshInterval;
|
||||
|
||||
function toggleAutoRefresh() {
|
||||
autoRefresh = !autoRefresh;
|
||||
|
||||
if (autoRefresh) {
|
||||
refreshInterval = setInterval(getUsers, 5000);
|
||||
console.log('自动刷新已启用');
|
||||
} else {
|
||||
clearInterval(refreshInterval);
|
||||
console.log('自动刷新已禁用');
|
||||
}
|
||||
}
|
||||
|
||||
// 错误重试机制
|
||||
async function retryRequest(requestFunc, maxRetries = 3) {
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
return await requestFunc();
|
||||
} catch (error) {
|
||||
if (i === maxRetries - 1) {
|
||||
throw error;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
|
||||
}
|
||||
}
|
||||
}
|
271
golang-learning/10-projects/03-web-server/static/style.css
Normal file
271
golang-learning/10-projects/03-web-server/static/style.css
Normal file
@@ -0,0 +1,271 @@
|
||||
/* style.css - 样式文件 */
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
padding: 30px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
header p {
|
||||
font-size: 1.2em;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
main {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 30px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
section {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.response-section {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.5em;
|
||||
border-bottom: 2px solid #667eea;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #555;
|
||||
margin: 20px 0 15px 0;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.api-group {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.api-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
margin-bottom: 15px;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #667eea;
|
||||
}
|
||||
|
||||
.api-item button {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.3s;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.api-item button:hover {
|
||||
background: #5a6fd8;
|
||||
}
|
||||
|
||||
.api-item input {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
font-size: 14px;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.method {
|
||||
font-weight: bold;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
min-width: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.method.get {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.method.post {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.method.delete {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.endpoint {
|
||||
font-family: 'Courier New', monospace;
|
||||
background: #e9ecef;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
|
||||
#userForm button {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 30px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
transition: background-color 0.3s;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#userForm button:hover {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
#response {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 5px;
|
||||
padding: 20px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #666;
|
||||
border-top: 1px solid #eee;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
main {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.api-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.api-item button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
.loading {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.loading::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin: -10px 0 0 -10px;
|
||||
border: 2px solid #667eea;
|
||||
border-radius: 50%;
|
||||
border-top-color: transparent;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* 成功和错误状态 */
|
||||
.success {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #dc3545;
|
||||
}
|
Reference in New Issue
Block a user