Files
junhong_cmp_fiber/specs/002-gorm-postgres-asynq/data-model.md
huang 984ccccc63 docs(constitution): 新增数据库设计原则(v2.4.0)
在项目宪章中新增第九条原则"数据库设计原则",明确禁止使用数据库外键约束和ORM关联标签。

主要变更:
- 新增原则IX:数据库设计原则(Database Design Principles)
- 强制要求:数据库表不得使用外键约束
- 强制要求:GORM模型不得使用ORM关联标签(foreignKey、hasMany等)
- 强制要求:表关系必须通过ID字段手动维护
- 强制要求:关联数据查询必须显式编写,避免ORM魔法
- 强制要求:时间字段由GORM处理,不使用数据库触发器

设计理念:
- 提升业务逻辑灵活性(无数据库约束限制)
- 优化高并发性能(无外键检查开销)
- 增强代码可读性(显式查询,无隐式预加载)
- 简化数据库架构和迁移流程
- 支持分布式和微服务场景

版本升级:2.3.0 → 2.4.0(MINOR)
2025-11-13 13:40:19 +08:00

645 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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. 数据库 SchemaSQL
### 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
- 使用数据库约束保证数据完整性