做完了一部分,备份一下,防止以外删除

This commit is contained in:
2025-11-11 15:16:38 +08:00
parent 9600e5b6e0
commit e98dd4d725
39 changed files with 2423 additions and 183 deletions

View File

@@ -1,7 +1,7 @@
# Data Model: Fiber Middleware Integration
**Feature**: 001-fiber-middleware-integration
**Date**: 2025-11-10
**Feature**: 001-fiber-middleware-integration
**Date**: 2025-11-10
**Phase**: 1 - Design & Contracts
## Overview
@@ -162,9 +162,9 @@ middleware:
### AuthToken Entity
**Storage**: Redis key-value pair
**Key Format**: `auth:token:{token_string}` (generated via `constants.RedisAuthTokenKey()`)
**Value Format**: Plain string containing user ID
**Storage**: Redis key-value pair
**Key Format**: `auth:token:{token_string}` (generated via `constants.RedisAuthTokenKey()`)
**Value Format**: Plain string containing user ID
**TTL**: Managed by Redis (set when token is created)
```go
@@ -218,16 +218,16 @@ func RedisAuthTokenKey(token string) string {
// Access via: c.Locals("key")
// Request ID (set by requestid middleware)
requestID := c.Locals("requestid").(string) // UUID v4 string
requestID := c.Locals(constants.ContextKeyRequestID).(string) // UUID v4 string
// User ID (set by keyauth middleware after validation)
userID, ok := c.Locals("user_id").(string)
userID, ok := c.Locals(constants.ContextKeyUserID).(string)
if !ok {
// Not authenticated
}
// Start time (for duration calculation)
startTime := c.Locals("start_time").(time.Time)
startTime := c.Locals(constants.ContextKeyStartTime).(time.Time)
```
**Context Keys** (constants in `pkg/constants/constants.go`):
@@ -254,8 +254,8 @@ const (
### Application Log Entry
**Format**: JSON
**Output**: `logs/app.log`
**Format**: JSON
**Output**: `logs/app.log`
**Logger**: Zap (appLogger instance)
```json
@@ -287,17 +287,21 @@ const (
```go
appLogger.Info("User created successfully",
zap.String("request_id", requestID),
zap.String("user_id", userID),
zap.String(constants.ContextKeyUserID, userID),
zap.String("username", username),
zap.String("ip", ip),
)
```
### Access Log Entry
### Access Log Entry Format
**Format**: JSON
**Output**: `logs/access.log`
**Purpose**: Records all HTTP requests for audit trail, performance monitoring, and troubleshooting
**Format**: JSON (one entry per line)
**Output**: `logs/access.log`
**Logger**: Zap (accessLogger instance)
**Requirement**: Implements spec.md FR-011
**Complete JSON Schema**:
```json
{
@@ -309,22 +313,32 @@ appLogger.Info("User created successfully",
"duration_ms": 45.234,
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"ip": "192.168.1.100",
"user_agent": "Mozilla/5.0...",
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)...",
"user_id": "user-789"
}
```
**Fields**:
- `timestamp`: ISO 8601 format (RFC3339)
- `level`: Always "info" for access logs
- `method`: HTTP method (GET, POST, PUT, DELETE)
- `path`: Request path
- `status`: HTTP status code
- `duration_ms`: Request duration in milliseconds
- `request_id`: Request correlation ID
- `ip`: Client IP address
- `user_agent`: User-Agent header
- `user_id`: Authenticated user ID (if available, empty if not)
**Field Definitions**:
| Field | Type | Required | Description | Example |
|-------|------|----------|-------------|---------|
| `timestamp` | string | Yes | ISO 8601 timestamp (RFC3339 with milliseconds) | `2025-11-10T15:30:45.123Z` |
| `level` | string | Yes | Log level (always "info" for access logs) | `info` |
| `method` | string | Yes | HTTP method | `GET`, `POST`, `PUT`, `DELETE`, `PATCH` |
| `path` | string | Yes | Request path (including query params) | `/api/v1/users?page=1` |
| `status` | int | Yes | HTTP status code | `200`, `401`, `500` |
| `duration_ms` | float64 | Yes | Request processing duration in milliseconds | `45.234` |
| `request_id` | string | Yes | UUID v4 request identifier | `550e8400-e29b-41d4-a716-446655440000` |
| `ip` | string | Yes | Client IP address | `192.168.1.100` |
| `user_agent` | string | Yes | User-Agent header value | `Mozilla/5.0...` |
| `user_id` | string | No | Authenticated user ID (empty string if not authenticated) | `user-789` or `""` |
**Notes**:
- All access logs are written at "info" level
- `user_id` field is empty string (`""`) for unauthenticated requests
- `duration_ms` includes full middleware chain execution time
- `path` includes query parameters for complete request tracking
- Logged after response is sent (includes actual status code)
**Zap Usage**:
@@ -337,7 +351,7 @@ accessLogger.Info("",
zap.String("request_id", requestID),
zap.String("ip", c.IP()),
zap.String("user_agent", c.Get("User-Agent")),
zap.String("user_id", userID),
zap.String(constants.ContextKeyUserID, userID),
)
```
@@ -531,7 +545,7 @@ func Wrap(code int, message string, err error) *AppError {
### Rate Limit Tracking
**Storage**: In-memory (default) or Redis (for distributed)
**Storage**: In-memory (default) or Redis (for distributed)
**Managed by**: Fiber limiter middleware (internal storage)
```go
@@ -625,16 +639,16 @@ func RedisRateLimitKey(ip string) string {
2. Recover Middleware (catch panics)
3. RequestID Middleware (generate UUID v4)
→ Store in c.Locals("requestid")
→ Store in c.Locals(constants.ContextKeyRequestID)
4. Logger Middleware (start)
→ Store start_time in c.Locals("start_time")
→ Store start_time in c.Locals(constants.ContextKeyStartTime)
5. KeyAuth Middleware
→ Extract token from header
→ Call TokenValidator.Validate(token)
→ Validate with Redis: GET auth:token:{token}
→ If valid: Store user_id in c.Locals("user_id")
→ If valid: Store user_id in c.Locals(constants.ContextKeyUserID)
→ If invalid: Return 401 with error code
→ If Redis down: Return 503 with error code
@@ -643,7 +657,7 @@ func RedisRateLimitKey(ip string) string {
→ If exceeded: Return 429 with error code
7. Handler (business logic)
→ Access c.Locals("requestid"), c.Locals("user_id")
→ Access c.Locals(constants.ContextKeyRequestID), c.Locals(constants.ContextKeyUserID)
→ Process request
→ Return response via response.Success() or response.Error()
@@ -676,3 +690,142 @@ func RedisRateLimitKey(ip string) string {
- ✅ No Java-style patterns (no I-prefix, no Impl-suffix)
**Next**: Generate API contracts (OpenAPI specification)
---
## 8. Configuration Validation Rules
This section defines comprehensive validation constraints for all configuration fields, implementing the requirements in spec.md FR-003.
### Validation Error Format
All validation errors MUST follow this format:
```
"Invalid configuration: {field_path}: {error_reason} (current value: {value}, expected: {constraint})"
```
Example:
```
"Invalid configuration: server.read_timeout: duration out of range (current value: 1s, expected: 5s-300s)"
```
---
### Server Configuration Validation
| Field | Type | Required | Constraint | Default | Error Message |
|-------|------|----------|------------|---------|---------------|
| `server.address` | string | Yes | Non-empty, format `:PORT` or `HOST:PORT` | `:3000` | "server.address: must be non-empty and in format ':PORT' or 'HOST:PORT'" |
| `server.read_timeout` | duration | Yes | 5s - 300s | `10s` | "server.read_timeout: duration out of range (expected: 5s-300s)" |
| `server.write_timeout` | duration | Yes | 5s - 300s | `10s` | "server.write_timeout: duration out of range (expected: 5s-300s)" |
| `server.shutdown_timeout` | duration | Yes | 10s - 120s | `30s` | "server.shutdown_timeout: duration out of range (expected: 10s-120s)" |
| `server.prefork` | bool | No | true or false | `false` | "server.prefork: must be boolean (true/false)" |
---
### Redis Configuration Validation
| Field | Type | Required | Constraint | Default | Error Message |
|-------|------|----------|------------|---------|---------------|
| `redis.address` | string | Yes | Non-empty, format `HOST:PORT` | `localhost:6379` | "redis.address: must be non-empty and in format 'HOST:PORT'" |
| `redis.password` | string | No | Any string (empty allowed) | `""` | N/A |
| `redis.db` | int | No | 0 - 15 | `0` | "redis.db: database number out of range (expected: 0-15)" |
| `redis.pool_size` | int | No | 1 - 1000 | `10` | "redis.pool_size: pool size out of range (expected: 1-1000)" |
| `redis.min_idle_conns` | int | No | 0 - pool_size | `5` | "redis.min_idle_conns: must be 0 to pool_size" |
| `redis.dial_timeout` | duration | No | 1s - 30s | `5s` | "redis.dial_timeout: timeout out of range (expected: 1s-30s)" |
| `redis.read_timeout` | duration | No | 1s - 30s | `3s` | "redis.read_timeout: timeout out of range (expected: 1s-30s)" |
| `redis.write_timeout` | duration | No | 1s - 30s | `3s` | "redis.write_timeout: timeout out of range (expected: 1s-30s)" |
---
### Logging Configuration Validation
| Field | Type | Required | Constraint | Default | Error Message |
|-------|------|----------|------------|---------|---------------|
| `logging.level` | string | No | One of: debug, info, warn, error | `info` | "logging.level: invalid log level (expected: debug, info, warn, error)" |
| `logging.development` | bool | No | true or false | `false` | "logging.development: must be boolean (true/false)" |
#### App Log Validation
| Field | Type | Required | Constraint | Default | Error Message |
|-------|------|----------|------------|---------|---------------|
| `logging.app_log.filename` | string | Yes | Non-empty, valid file path | `logs/app.log` | "logging.app_log.filename: must be non-empty valid file path" |
| `logging.app_log.max_size` | int | No | 1 - 1000 (MB) | `100` | "logging.app_log.max_size: size out of range (expected: 1-1000 MB)" |
| `logging.app_log.max_backups` | int | No | 0 - 999 (0 = keep all) | `30` | "logging.app_log.max_backups: count out of range (expected: 0-999)" |
| `logging.app_log.max_age` | int | No | 1 - 365 (days) | `30` | "logging.app_log.max_age: retention period out of range (expected: 1-365 days)" |
| `logging.app_log.compress` | bool | No | true or false | `true` | "logging.app_log.compress: must be boolean (true/false)" |
#### Access Log Validation
| Field | Type | Required | Constraint | Default | Error Message |
|-------|------|----------|------------|---------|---------------|
| `logging.access_log.filename` | string | Yes | Non-empty, valid file path | `logs/access.log` | "logging.access_log.filename: must be non-empty valid file path" |
| `logging.access_log.max_size` | int | No | 1 - 1000 (MB) | `500` | "logging.access_log.max_size: size out of range (expected: 1-1000 MB)" |
| `logging.access_log.max_backups` | int | No | 0 - 999 (0 = keep all) | `90` | "logging.access_log.max_backups: count out of range (expected: 0-999)" |
| `logging.access_log.max_age` | int | No | 1 - 365 (days) | `90` | "logging.access_log.max_age: retention period out of range (expected: 1-365 days)" |
| `logging.access_log.compress` | bool | No | true or false | `true` | "logging.access_log.compress: must be boolean (true/false)" |
---
### Middleware Configuration Validation
| Field | Type | Required | Constraint | Default | Error Message |
|-------|------|----------|------------|---------|---------------|
| `middleware.enable_auth` | bool | No | true or false | `true` | "middleware.enable_auth: must be boolean (true/false)" |
| `middleware.enable_rate_limiter` | bool | No | true or false | `false` | "middleware.enable_rate_limiter: must be boolean (true/false)" |
#### Rate Limiter Validation
| Field | Type | Required | Constraint | Default | Error Message |
|-------|------|----------|------------|---------|---------------|
| `middleware.rate_limiter.max` | int | No | 1 - 10000 | `30` | "middleware.rate_limiter.max: request limit out of range (expected: 1-10000)" |
| `middleware.rate_limiter.expiration` | duration | No | 1s - 1h | `1m` | "middleware.rate_limiter.expiration: window duration out of range (expected: 1s-1h)" |
| `middleware.rate_limiter.storage` | string | No | "memory" or "redis" | `memory` | "middleware.rate_limiter.storage: invalid storage type (expected: memory or redis)" |
---
### Validation Implementation Guidelines
**Location**: `pkg/config/loader.go` - `Validate()` function
**Validation Order**:
1. **Required fields check** - Fail fast if critical fields missing
2. **Type validation** - Viper handles basic type conversion
3. **Range validation** - Check numeric bounds
4. **Format validation** - Validate string formats (HOST:PORT, file paths)
5. **Cross-field validation** - Check dependencies (e.g., min_idle_conns <= pool_size)
**Example Implementation Pattern**:
```go
func (c *Config) Validate() error {
// Required fields
if c.Server.Address == "" {
return fmt.Errorf("Invalid configuration: server.address: must be non-empty (current value: empty)")
}
// Range validation
if c.Server.ReadTimeout < 5*time.Second || c.Server.ReadTimeout > 300*time.Second {
return fmt.Errorf("Invalid configuration: server.read_timeout: duration out of range (current value: %s, expected: 5s-300s)", c.Server.ReadTimeout)
}
// Format validation
if !strings.Contains(c.Redis.Address, ":") {
return fmt.Errorf("Invalid configuration: redis.address: invalid format (current value: %s, expected: HOST:PORT)", c.Redis.Address)
}
// Cross-field validation
if c.Redis.MinIdleConns > c.Redis.PoolSize {
return fmt.Errorf("Invalid configuration: redis.min_idle_conns: must not exceed pool_size (current value: %d, pool_size: %d)", c.Redis.MinIdleConns, c.Redis.PoolSize)
}
return nil
}
```
**Testing Requirements**:
- Unit tests MUST cover all validation rules (T020 in tasks.md)
- Test valid configurations (should pass)
- Test each constraint violation (should fail with correct error message)
- Test edge cases (min/max boundaries)
- Test malformed YAML (should be caught by Viper)