# Data Model: 数据持久化与异步任务处理集成

**Feature**: 002-gorm-postgres-asynq  
**Date**: 2025-11-12  
**Purpose**: 定义数据模型、配置结构和系统实体

## 概述

本文档定义了数据持久化和异步任务处理功能的数据模型，包括配置结构、数据库实体示例和任务载荷结构。

---

## 1. 配置模型

### 1.1 数据库配置

```go
// pkg/config/config.go

// DatabaseConfig 数据库连接配置
type DatabaseConfig struct {
    // 连接参数
    Host     string `mapstructure:"host"`     // 数据库主机地址
    Port     int    `mapstructure:"port"`     // 数据库端口
    User     string `mapstructure:"user"`     // 数据库用户名
    Password string `mapstructure:"password"` // 数据库密码（明文存储）
    DBName   string `mapstructure:"dbname"`   // 数据库名称
    SSLMode  string `mapstructure:"sslmode"`  // SSL 模式：disable, require, verify-ca, verify-full
    
    // 连接池配置
    MaxOpenConns    int           `mapstructure:"max_open_conns"`    // 最大打开连接数（默认：25）
    MaxIdleConns    int           `mapstructure:"max_idle_conns"`    // 最大空闲连接数（默认：10）
    ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"` // 连接最大生命周期（默认：5m）
}
```

**字段说明**：

| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| Host | string | localhost | PostgreSQL 服务器地址 |
| Port | int | 5432 | PostgreSQL 服务器端口 |
| User | string | postgres | 数据库用户名 |
| Password | string | - | 数据库密码（明文存储在配置文件中） |
| DBName | string | junhong_cmp | 数据库名称 |
| SSLMode | string | disable | SSL 连接模式 |
| MaxOpenConns | int | 25 | 最大数据库连接数 |
| MaxIdleConns | int | 10 | 最大空闲连接数 |
| ConnMaxLifetime | duration | 5m | 连接最大存活时间 |

### 1.2 任务队列配置

```go
// pkg/config/config.go

// QueueConfig 任务队列配置
type QueueConfig struct {
    // 并发配置
    Concurrency int `mapstructure:"concurrency"` // Worker 并发数（默认：10）
    
    // 队列优先级配置（队列名 -> 权重）
    Queues map[string]int `mapstructure:"queues"` // 例如：{"critical": 6, "default": 3, "low": 1}
    
    // 重试配置
    RetryMax int           `mapstructure:"retry_max"` // 最大重试次数（默认：5）
    Timeout  time.Duration `mapstructure:"timeout"`   // 任务超时时间（默认：10m）
}
```

**队列优先级**：
- `critical`: 关键任务（权重 6，约 60% 处理时间）
- `default`: 普通任务（权重 3，约 30% 处理时间）
- `low`: 低优先级任务（权重 1，约 10% 处理时间）

### 1.3 完整配置结构

```go
// pkg/config/config.go

// Config 应用配置
type Config struct {
    Server     ServerConfig     `mapstructure:"server"`
    Logging    LoggingConfig    `mapstructure:"logging"`
    Redis      RedisConfig      `mapstructure:"redis"`
    Database   DatabaseConfig   `mapstructure:"database"`   // 新增
    Queue      QueueConfig      `mapstructure:"queue"`      // 新增
    Middleware MiddlewareConfig `mapstructure:"middleware"`
}
```

---

## 2. 数据库实体模型

### 2.1 基础模型（Base Model）

```go
// internal/model/base.go

import (
    "time"
    "gorm.io/gorm"
)

// BaseModel 基础模型，包含通用字段
type BaseModel struct {
    ID        uint           `gorm:"primarykey" json:"id"`
    CreatedAt time.Time      `json:"created_at"`
    UpdatedAt time.Time      `json:"updated_at"`
    DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` // 软删除
}
```

**字段说明**：
- `ID`: 自增主键
- `CreatedAt`: 创建时间（GORM 自动管理）
- `UpdatedAt`: 更新时间（GORM 自动管理）
- `DeletedAt`: 删除时间（软删除，GORM 自动过滤已删除记录）

### 2.2 示例实体：用户模型

```go
// internal/model/user.go

// User 用户实体
type User struct {
    BaseModel
    
    // 基本信息
    Username string `gorm:"uniqueIndex;not null;size:50" json:"username"`
    Email    string `gorm:"uniqueIndex;not null;size:100" json:"email"`
    Password string `gorm:"not null;size:255" json:"-"` // 不返回给客户端
    
    // 状态字段
    Status string `gorm:"not null;size:20;default:'active';index" json:"status"`
    
    // 元数据
    LastLoginAt *time.Time `json:"last_login_at,omitempty"`
}

// TableName 指定表名
func (User) TableName() string {
    return "tb_user"
}
```

**索引策略**：
- `username`: 唯一索引（快速查找和去重）
- `email`: 唯一索引（快速查找和去重）
- `status`: 普通索引（状态过滤查询）
- `deleted_at`: 自动索引（软删除过滤）

**验证规则**：
- `username`: 长度 3-50 字符，字母数字下划线
- `email`: 标准邮箱格式
- `password`: 长度 >= 8 字符，bcrypt 哈希存储
- `status`: 枚举值（active, inactive, suspended）

### 2.3 示例实体：订单模型（演示手动关联关系）

```go
// internal/model/order.go

// Order 订单实体
type Order struct {
    BaseModel
    
    // 业务唯一键
    OrderID string `gorm:"uniqueIndex;not null;size:50" json:"order_id"`
    
    // 关联关系（仅存储 ID，不使用 GORM 关联）
    UserID uint `gorm:"not null;index" json:"user_id"`
    
    // 订单信息
    Amount int64  `gorm:"not null" json:"amount"`           // 金额（分）
    Status string `gorm:"not null;size:20;index" json:"status"`
    Remark string `gorm:"size:500" json:"remark,omitempty"`
    
    // 时间字段
    PaidAt      *time.Time `json:"paid_at,omitempty"`
    CompletedAt *time.Time `json:"completed_at,omitempty"`
}

// TableName 指定表名
func (Order) TableName() string {
    return "tb_order"
}
```

**关联关系说明**：
- `UserID`: 存储关联用户的 ID（普通字段，无数据库外键约束）
- **无 ORM 关联**：遵循 Constitution Principle IX，不使用 `foreignKey`、`belongsTo` 等标签
- 关联数据查询在 Service 层手动实现（见下方示例）

**手动查询关联数据示例**：
```go
// internal/service/order/service.go

// GetOrderWithUser 查询订单及关联的用户信息
func (s *Service) GetOrderWithUser(ctx context.Context, orderID uint) (*OrderDetail, error) {
    // 1. 查询订单
    order, err := s.store.Order.GetByID(ctx, orderID)
    if err != nil {
        return nil, fmt.Errorf("查询订单失败: %w", err)
    }
    
    // 2. 手动查询关联的用户
    user, err := s.store.User.GetByID(ctx, order.UserID)
    if err != nil {
        return nil, fmt.Errorf("查询用户失败: %w", err)
    }
    
    // 3. 组装返回数据
    return &OrderDetail{
        Order: order,
        User:  user,
    }, nil
}

// ListOrdersByUserID 查询指定用户的订单列表
func (s *Service) ListOrdersByUserID(ctx context.Context, userID uint, page, pageSize int) ([]*Order, int64, error) {
    return s.store.Order.ListByUserID(ctx, userID, page, pageSize)
}
```

**状态流转**：
```
pending → paid → processing → completed
                      ↓
                  cancelled
```

---

## 3. 数据传输对象（DTO）

### 3.1 用户 DTO

```go
// internal/model/user_dto.go

// CreateUserRequest 创建用户请求
type CreateUserRequest struct {
    Username string `json:"username" validate:"required,min=3,max=50,alphanum"`
    Email    string `json:"email" validate:"required,email"`
    Password string `json:"password" validate:"required,min=8"`
}

// UpdateUserRequest 更新用户请求
type UpdateUserRequest struct {
    Email  *string `json:"email" validate:"omitempty,email"`
    Status *string `json:"status" validate:"omitempty,oneof=active inactive suspended"`
}

// UserResponse 用户响应
type UserResponse struct {
    ID          uint       `json:"id"`
    Username    string     `json:"username"`
    Email       string     `json:"email"`
    Status      string     `json:"status"`
    CreatedAt   time.Time  `json:"created_at"`
    UpdatedAt   time.Time  `json:"updated_at"`
    LastLoginAt *time.Time `json:"last_login_at,omitempty"`
}

// ListUsersResponse 用户列表响应
type ListUsersResponse struct {
    Users      []UserResponse `json:"users"`
    Page       int            `json:"page"`
    PageSize   int            `json:"page_size"`
    Total      int64          `json:"total"`
    TotalPages int            `json:"total_pages"`
}
```

---

## 4. 任务载荷模型

### 4.1 任务类型常量

```go
// pkg/constants/constants.go

const (
    // 任务类型
    TaskTypeEmailSend     = "email:send"           // 发送邮件
    TaskTypeDataSync      = "data:sync"            // 数据同步
    TaskTypeSIMStatusSync = "sim:status:sync"      // SIM 卡状态同步
    TaskTypeCommission    = "commission:calculate" // 分佣计算
)
```

### 4.2 邮件任务载荷

```go
// internal/task/email.go

// EmailPayload 邮件任务载荷
type EmailPayload struct {
    RequestID string   `json:"request_id"` // 幂等性标识
    To        string   `json:"to"`         // 收件人
    Subject   string   `json:"subject"`    // 主题
    Body      string   `json:"body"`       // 正文
    CC        []string `json:"cc,omitempty"`  // 抄送
    Attachments []string `json:"attachments,omitempty"` // 附件路径
}
```

### 4.3 数据同步任务载荷

```go
// internal/task/sync.go

// DataSyncPayload 数据同步任务载荷
type DataSyncPayload struct {
    RequestID  string `json:"request_id"`  // 幂等性标识
    SyncType   string `json:"sync_type"`   // 同步类型：sim_status, flow_usage, real_name
    StartDate  string `json:"start_date"`  // 开始日期（YYYY-MM-DD）
    EndDate    string `json:"end_date"`    // 结束日期（YYYY-MM-DD）
    BatchSize  int    `json:"batch_size"`  // 批量大小（默认：100）
}
```

### 4.4 SIM 卡状态同步载荷

```go
// internal/task/sim.go

// SIMStatusSyncPayload SIM 卡状态同步任务载荷
type SIMStatusSyncPayload struct {
    RequestID string   `json:"request_id"` // 幂等性标识
    ICCIDs    []string `json:"iccids"`     // ICCID 列表
    ForceSync bool     `json:"force_sync"` // 强制同步（忽略缓存）
}
```

---

## 5. 数据库 Schema（SQL）

### 5.1 初始化 Schema

```sql
-- migrations/000001_init_schema.up.sql

-- 用户表
CREATE TABLE IF NOT EXISTS tb_user (
    id SERIAL PRIMARY KEY,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    deleted_at TIMESTAMP,
    
    -- 基本信息
    username VARCHAR(50) NOT NULL,
    email VARCHAR(100) NOT NULL,
    password VARCHAR(255) NOT NULL,
    
    -- 状态字段
    status VARCHAR(20) NOT NULL DEFAULT 'active',
    
    -- 元数据
    last_login_at TIMESTAMP,
    
    -- 唯一约束
    CONSTRAINT uk_user_username UNIQUE (username),
    CONSTRAINT uk_user_email UNIQUE (email)
);

-- 用户表索引
CREATE INDEX idx_user_deleted_at ON tb_user(deleted_at);
CREATE INDEX idx_user_status ON tb_user(status);
CREATE INDEX idx_user_created_at ON tb_user(created_at);

-- 订单表
CREATE TABLE IF NOT EXISTS tb_order (
    id SERIAL PRIMARY KEY,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    deleted_at TIMESTAMP,
    
    -- 业务唯一键
    order_id VARCHAR(50) NOT NULL,
    
    -- 关联关系（注意：无数据库外键约束，在代码中管理）
    user_id INTEGER NOT NULL,
    
    -- 订单信息
    amount BIGINT NOT NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'pending',
    remark VARCHAR(500),
    
    -- 时间字段
    paid_at TIMESTAMP,
    completed_at TIMESTAMP,
    
    -- 唯一约束
    CONSTRAINT uk_order_order_id UNIQUE (order_id)
);

-- 订单表索引
CREATE INDEX idx_order_deleted_at ON tb_order(deleted_at);
CREATE INDEX idx_order_user_id ON tb_order(user_id);
CREATE INDEX idx_order_status ON tb_order(status);
CREATE INDEX idx_order_created_at ON tb_order(created_at);

-- 添加注释
COMMENT ON TABLE tb_user IS '用户表';
COMMENT ON COLUMN tb_user.username IS '用户名（唯一）';
COMMENT ON COLUMN tb_user.email IS '邮箱（唯一）';
COMMENT ON COLUMN tb_user.password IS '密码（bcrypt 哈希）';
COMMENT ON COLUMN tb_user.status IS '用户状态：active, inactive, suspended';
COMMENT ON COLUMN tb_user.deleted_at IS '软删除时间';

COMMENT ON TABLE tb_order IS '订单表';
COMMENT ON COLUMN tb_order.order_id IS '订单号（业务唯一键）';
COMMENT ON COLUMN tb_order.user_id IS '用户 ID（在代码中维护关联，无数据库外键）';
COMMENT ON COLUMN tb_order.amount IS '金额（分）';
COMMENT ON COLUMN tb_order.status IS '订单状态：pending, paid, processing, completed, cancelled';
COMMENT ON COLUMN tb_order.deleted_at IS '软删除时间';
```

**重要说明**：
- ✅ **无外键约束**：`user_id` 仅作为普通字段存储，无 `REFERENCES` 约束
- ✅ **无触发器**：`created_at` 和 `updated_at` 由 GORM 自动管理，无需数据库触发器
- ✅ **遵循 Constitution Principle IX**：表关系在代码层面手动维护

### 5.2 回滚 Schema

```sql
-- migrations/000001_init_schema.down.sql

-- 删除表（按依赖顺序倒序删除）
DROP TABLE IF EXISTS tb_order;
DROP TABLE IF EXISTS tb_user;
```

---

## 6. Redis 键结构

### 6.1 任务锁键

```go
// pkg/constants/redis.go

// RedisTaskLockKey 生成任务锁键
// 格式: task:lock:{request_id}
// 用途: 幂等性控制
// 过期时间: 24 小时
func RedisTaskLockKey(requestID string) string {
    return fmt.Sprintf("task:lock:%s", requestID)
}
```

**使用示例**：
```go
key := constants.RedisTaskLockKey("req-123456")
// 结果: "task:lock:req-123456"
```

### 6.2 任务状态键

```go
// RedisTaskStatusKey 生成任务状态键
// 格式: task:status:{task_id}
// 用途: 存储任务执行状态
// 过期时间: 7 天
func RedisTaskStatusKey(taskID string) string {
    return fmt.Sprintf("task:status:%s", taskID)
}
```

---

## 7. 常量定义

### 7.1 用户状态常量

```go
// pkg/constants/constants.go

const (
    // 用户状态
    UserStatusActive    = "active"    // 激活
    UserStatusInactive  = "inactive"  // 未激活
    UserStatusSuspended = "suspended" // 暂停
)
```

### 7.2 订单状态常量

```go
const (
    // 订单状态
    OrderStatusPending    = "pending"    // 待支付
    OrderStatusPaid       = "paid"       // 已支付
    OrderStatusProcessing = "processing" // 处理中
    OrderStatusCompleted  = "completed"  // 已完成
    OrderStatusCancelled  = "cancelled"  // 已取消
)
```

### 7.3 数据库配置常量

```go
const (
    // 数据库连接池默认值
    DefaultMaxOpenConns    = 25
    DefaultMaxIdleConns    = 10
    DefaultConnMaxLifetime = 5 * time.Minute
    
    // 查询限制
    DefaultPageSize = 20
    MaxPageSize     = 100
    
    // 慢查询阈值
    SlowQueryThreshold = 100 * time.Millisecond
)
```

### 7.4 任务队列常量

```go
const (
    // 队列名称
    QueueCritical = "critical"
    QueueDefault  = "default"
    QueueLow      = "low"
    
    // 默认重试配置
    DefaultRetryMax = 5
    DefaultTimeout  = 10 * time.Minute
    
    // 默认并发数
    DefaultConcurrency = 10
)
```

---

## 8. 实体关系图（ER Diagram）

```
┌─────────────────┐
│    tb_user      │
├─────────────────┤
│ id (PK)         │
│ username (UQ)   │
│ email (UQ)      │
│ password        │
│ status          │
│ last_login_at   │
│ created_at      │
│ updated_at      │
│ deleted_at      │
└────────┬────────┘
         │
         │ 1:N (代码层面维护)
         │
┌────────▼────────┐
│    tb_order     │
├─────────────────┤
│ id (PK)         │
│ order_id (UQ)   │
│ user_id         │ ← 存储关联 ID（无数据库外键）
│ amount          │
│ status          │
│ remark          │
│ paid_at         │
│ completed_at    │
│ created_at      │
│ updated_at      │
│ deleted_at      │
└─────────────────┘
```

**关系说明**：
- 一个用户可以有多个订单（1:N 关系）
- 订单通过 `user_id` 字段存储用户 ID，**在代码层面维护关联**
- **无数据库外键约束**：遵循 Constitution Principle IX
- 关联查询在 Service 层手动实现（参见 2.3 节示例代码）

---

## 9. 数据验证规则

### 9.1 用户字段验证

| 字段 | 验证规则 | 错误消息 |
|------|----------|----------|
| username | required, min=3, max=50, alphanum | 用户名必填，3-50 个字母数字字符 |
| email | required, email | 邮箱必填且格式正确 |
| password | required, min=8 | 密码必填，至少 8 个字符 |
| status | oneof=active inactive suspended | 状态必须为 active, inactive, suspended 之一 |

### 9.2 订单字段验证

| 字段 | 验证规则 | 错误消息 |
|------|----------|----------|
| order_id | required, min=10, max=50 | 订单号必填，10-50 个字符 |
| user_id | required, gt=0 | 用户 ID 必填且大于 0 |
| amount | required, gte=0 | 金额必填且大于等于 0 |
| status | oneof=pending paid processing completed cancelled | 状态值无效 |

---

## 10. 数据迁移版本

| 版本 | 文件名 | 描述 | 日期 |
|------|--------|------|------|
| 1 | 000001_init_schema | 初始化用户表和订单表 | 2025-11-12 |

**添加新迁移**：
```bash
# 创建新迁移文件
migrate create -ext sql -dir migrations -seq add_sim_table

# 生成文件：
# migrations/000002_add_sim_table.up.sql
# migrations/000002_add_sim_table.down.sql
```

---

## 总结

本数据模型定义了：

1. **配置模型**：数据库连接配置、任务队列配置
2. **实体模型**：基础模型、用户模型、订单模型（示例）
3. **DTO 模型**：请求/响应数据传输对象
4. **任务载荷**：各类异步任务的载荷结构
5. **数据库 Schema**：SQL 迁移脚本
6. **Redis 键结构**：任务锁、任务状态等键生成函数
7. **常量定义**：状态枚举、默认配置值
8. **验证规则**：字段级别的数据验证规则

**设计原则**：
- 遵循 GORM 约定（BaseModel、软删除）
- 遵循 Constitution 命名规范（PascalCase 字段、snake_case 列名）
- 统一使用常量定义（避免硬编码）
- 支持软删除和审计字段（created_at, updated_at）
- 使用数据库约束保证数据完整性
