Skip to content

数据库设计

天机爻采用 混合数据库架构,结合关系型数据库(PostgreSQL)和文档数据库(MongoDB)的优势,实现灵活高效的数据存储方案。

数据库架构

┌─────────────────────────────────────────────────────┐
│              应用层 (Application)                    │
└────────────────┬────────────────────────────────────┘

        ┌────────┼────────┐
        │        │        │
        ▼        ▼        ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Supabase │ │ MongoDB  │ │  Redis   │
│(主数据库) │ │(文档库)  │ │ (缓存)   │
└──────────┘ └──────────┘ └──────────┘

技术选型

数据库用途优势
Supabase (PostgreSQL)用户、订单、权限强一致性、事务支持、关系查询
MongoDB Atlas占卜历史、日志灵活 Schema、高吞吐量
Redis缓存、Session极速读写、过期控制

一、Supabase 数据库设计

1. 用户表 (users)

sql
CREATE TABLE users (
  -- 主键
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  
  -- 基本信息
  email VARCHAR(255) UNIQUE NOT NULL,
  phone VARCHAR(20) UNIQUE,
  username VARCHAR(50) UNIQUE,
  avatar_url TEXT,
  
  -- 认证
  password_hash TEXT,
  email_verified BOOLEAN DEFAULT FALSE,
  phone_verified BOOLEAN DEFAULT FALSE,
  
  -- 个人资料
  birth_date DATE,
  birth_time TIME,
  timezone VARCHAR(50) DEFAULT 'Asia/Shanghai',
  gender VARCHAR(10) CHECK (gender IN ('male', 'female', 'other')),
  
  -- 订阅信息
  subscription_tier VARCHAR(20) DEFAULT 'free' 
    CHECK (subscription_tier IN ('free', 'basic', 'premium', 'vip')),
  subscription_expires_at TIMESTAMP WITH TIME ZONE,
  
  -- 状态
  is_active BOOLEAN DEFAULT TRUE,
  last_login_at TIMESTAMP WITH TIME ZONE,
  
  -- 元数据
  metadata JSONB DEFAULT '{}',
  
  -- 时间戳
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- 索引
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_subscription ON users(subscription_tier, subscription_expires_at);
CREATE INDEX idx_users_created_at ON users(created_at);

2. 订单表 (orders)

sql
CREATE TABLE orders (
  -- 主键
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  
  -- 用户关联
  user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  
  -- 订单信息
  order_number VARCHAR(50) UNIQUE NOT NULL,
  product_type VARCHAR(50) NOT NULL 
    CHECK (product_type IN ('bazi_basic', 'bazi_premium', 'ziwei', 'compatibility', 'subscription')),
  
  -- 金额
  amount DECIMAL(10, 2) NOT NULL,
  currency VARCHAR(3) DEFAULT 'CNY',
  
  -- 支付
  payment_method VARCHAR(20) 
    CHECK (payment_method IN ('alipay', 'wechat', 'stripe', 'paypal')),
  payment_status VARCHAR(20) DEFAULT 'pending'
    CHECK (payment_status IN ('pending', 'paid', 'failed', 'refunded')),
  paid_at TIMESTAMP WITH TIME ZONE,
  
  -- 交易信息
  transaction_id VARCHAR(100),
  
  -- 状态
  status VARCHAR(20) DEFAULT 'active'
    CHECK (status IN ('active', 'expired', 'cancelled')),
  
  -- 时间戳
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- 索引
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_order_number ON orders(order_number);
CREATE INDEX idx_orders_payment_status ON orders(payment_status);
CREATE INDEX idx_orders_created_at ON orders(created_at);

3. API 密钥表 (api_keys)

sql
CREATE TABLE api_keys (
  -- 主键
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  
  -- 用户关联
  user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  
  -- 密钥信息
  key_hash TEXT UNIQUE NOT NULL,
  key_prefix VARCHAR(10) NOT NULL, -- 显示前缀,如 "tj_abc..."
  name VARCHAR(100) NOT NULL,
  
  -- 权限
  scopes JSONB DEFAULT '["read"]', -- ['read', 'write', 'admin']
  
  -- 限流
  rate_limit_per_minute INTEGER DEFAULT 100,
  rate_limit_per_day INTEGER DEFAULT 1000,
  
  -- 使用统计
  usage_count INTEGER DEFAULT 0,
  last_used_at TIMESTAMP WITH TIME ZONE,
  
  -- 状态
  is_active BOOLEAN DEFAULT TRUE,
  expires_at TIMESTAMP WITH TIME ZONE,
  
  -- 时间戳
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- 索引
CREATE INDEX idx_api_keys_user_id ON api_keys(user_id);
CREATE INDEX idx_api_keys_key_hash ON api_keys(key_hash);
CREATE INDEX idx_api_keys_is_active ON api_keys(is_active);

4. 用户配额表 (user_quotas)

sql
CREATE TABLE user_quotas (
  -- 主键
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  
  -- 用户关联
  user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  
  -- 配额类型
  quota_type VARCHAR(50) NOT NULL 
    CHECK (quota_type IN ('bazi_calculate', 'ai_interpret', 'api_call')),
  
  -- 配额
  total_quota INTEGER NOT NULL,
  used_quota INTEGER DEFAULT 0,
  
  -- 重置周期
  reset_period VARCHAR(20) DEFAULT 'monthly'
    CHECK (reset_period IN ('daily', 'weekly', 'monthly', 'yearly', 'never')),
  last_reset_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  next_reset_at TIMESTAMP WITH TIME ZONE,
  
  -- 时间戳
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  
  -- 唯一约束
  UNIQUE(user_id, quota_type)
);

-- 索引
CREATE INDEX idx_user_quotas_user_id ON user_quotas(user_id);

二、MongoDB 数据库设计

1. 占卜历史集合 (divinations)

javascript
{
  _id: ObjectId("..."),
  
  // 用户信息
  userId: "uuid-string",
  
  // 占卜类型
  type: "bazi" | "ziwei" | "compatibility",
  
  // 输入数据
  input: {
    birthDate: "1990-01-15",
    birthTime: "14:30",
    timezone: "Asia/Shanghai",
    gender: "male",
    name: "张三" // 可选
  },
  
  // 计算结果
  chart: {
    yearPillar: { heavenlyStem: "庚", earthlyBranch: "午" },
    monthPillar: { heavenlyStem: "己", earthlyBranch: "丑" },
    dayPillar: { heavenlyStem: "甲", earthlyBranch: "寅" },
    hourPillar: { heavenlyStem: "辛", earthlyBranch: "未" }
  },
  
  // AI 解读
  interpretation: {
    personality: "你的性格如春天的树木...",
    career: "适合从事创意、教育类工作...",
    relationship: "感情细腻,重视精神交流...",
    health: "注意肝胆、筋骨健康...",
    advice: ["多接触水元素", "保持规律作息", "发挥创造力"]
  },
  
  // 元数据
  metadata: {
    aiModel: "gpt-4-turbo",
    processingTime: 2340, // ms
    cacheHit: false,
    language: "zh"
  },
  
  // 用户反馈
  feedback: {
    rating: 5, // 1-5 星
    comment: "很准确!",
    helpful: true
  },
  
  // 状态
  status: "completed" | "processing" | "failed",
  
  // 时间戳
  createdAt: ISODate("2025-11-06T10:30:00Z"),
  updatedAt: ISODate("2025-11-06T10:30:05Z")
}

索引:

javascript
db.divinations.createIndex({ userId: 1, createdAt: -1 });
db.divinations.createIndex({ type: 1 });
db.divinations.createIndex({ status: 1 });
db.divinations.createIndex({ "metadata.cacheHit": 1 });

2. 系统日志集合 (logs)

javascript
{
  _id: ObjectId("..."),
  
  // 日志级别
  level: "info" | "warn" | "error",
  
  // 日志类型
  category: "api" | "ai" | "database" | "payment",
  
  // 日志内容
  message: "User login successful",
  
  // 上下文
  context: {
    userId: "uuid-string",
    requestId: "req-123",
    ip: "192.168.1.1",
    userAgent: "Mozilla/5.0...",
    endpoint: "/api/bazi/calculate"
  },
  
  // 详细信息
  details: {
    // 任意 JSON 数据
  },
  
  // 错误信息(如果是错误日志)
  error: {
    message: "Database connection timeout",
    stack: "Error: ...\n at ...",
    code: "DB_TIMEOUT"
  },
  
  // 时间戳
  timestamp: ISODate("2025-11-06T10:30:00Z")
}

索引:

javascript
db.logs.createIndex({ timestamp: -1 });
db.logs.createIndex({ level: 1, timestamp: -1 });
db.logs.createIndex({ category: 1, timestamp: -1 });
db.logs.createIndex({ "context.userId": 1 });

// TTL 索引:30 天后自动删除日志
db.logs.createIndex({ timestamp: 1 }, { expireAfterSeconds: 2592000 });

3. 分析统计集合 (analytics)

javascript
{
  _id: ObjectId("..."),
  
  // 统计维度
  dimension: "daily" | "weekly" | "monthly",
  date: ISODate("2025-11-06"),
  
  // 用户统计
  users: {
    total: 10000,
    new: 234,
    active: 5678,
    premium: 890
  },
  
  // 占卜统计
  divinations: {
    total: 50000,
    byType: {
      bazi: 30000,
      ziwei: 15000,
      compatibility: 5000
    },
    aiInterpreted: 45000
  },
  
  // 收入统计
  revenue: {
    total: 123456.78,
    byCurrency: {
      CNY: 100000,
      USD: 3456.78
    },
    byProduct: {
      subscription: 80000,
      oneTime: 43456.78
    }
  },
  
  // 性能统计
  performance: {
    avgResponseTime: 234, // ms
    p95ResponseTime: 567,
    errorRate: 0.05, // %
    cacheHitRate: 72.3 // %
  },
  
  // 时间戳
  createdAt: ISODate("2025-11-06T00:00:00Z")
}

索引:

javascript
db.analytics.createIndex({ dimension: 1, date: -1 });

三、Redis 缓存设计

1. 缓存键命名规范

格式:<namespace>:<entity>:<id>:<field>

示例:
- tj:user:123:profile           # 用户资料
- tj:bazi:abc123:chart          # 八字命盘
- tj:ai:hash123:interpretation  # AI 解读
- tj:session:xyz789             # 用户会话
- tj:ratelimit:user:123:minute  # 限流计数

2. 缓存数据结构

typescript
// 用户资料缓存
redis.setex(
  'tj:user:123:profile',
  3600, // 1 小时
  JSON.stringify({
    id: '123',
    username: 'user123',
    tier: 'premium',
    // ...
  })
);

// 八字命盘缓存(长期缓存)
redis.setex(
  'tj:bazi:abc123:chart',
  86400, // 24 小时
  JSON.stringify({ /* 命盘数据 */ })
);

// 限流计数
redis.incr('tj:ratelimit:user:123:minute');
redis.expire('tj:ratelimit:user:123:minute', 60);

// Session 存储
redis.setex(
  'tj:session:xyz789',
  1800, // 30 分钟
  JSON.stringify({
    userId: '123',
    loginAt: Date.now(),
    // ...
  })
);

3. 缓存策略

数据类型TTL策略
用户资料1 小时LRU
命盘数据24 小时LRU
AI 解读24 小时LRU
Session30 分钟TTL
限流计数1 分钟TTL

数据一致性

1. 写入策略

Cache-Aside Pattern (旁路缓存):

typescript
async function getUser(userId: string) {
  // 1. 先查缓存
  const cached = await redis.get(`tj:user:${userId}:profile`);
  if (cached) {
    return JSON.parse(cached);
  }

  // 2. 查数据库
  const user = await supabase
    .from('users')
    .select('*')
    .eq('id', userId)
    .single();

  // 3. 写入缓存
  await redis.setex(
    `tj:user:${userId}:profile`,
    3600,
    JSON.stringify(user)
  );

  return user;
}

async function updateUser(userId: string, data: any) {
  // 1. 更新数据库
  await supabase
    .from('users')
    .update(data)
    .eq('id', userId);

  // 2. 删除缓存(下次读取时重新加载)
  await redis.del(`tj:user:${userId}:profile`);
}

2. 事务处理

sql
-- PostgreSQL 事务示例
BEGIN;

-- 1. 扣除配额
UPDATE user_quotas
SET used_quota = used_quota + 1
WHERE user_id = 'xxx' AND quota_type = 'ai_interpret'
  AND used_quota < total_quota;

-- 2. 创建订单
INSERT INTO orders (user_id, product_type, amount, status)
VALUES ('xxx', 'ai_interpret', 0.00, 'completed');

-- 3. 检查是否成功
IF NOT FOUND THEN
  ROLLBACK;
ELSE
  COMMIT;
END IF;

数据备份与恢复

1. 自动备份

Supabase (PostgreSQL):

  • 每日自动备份
  • 保留 7 天
  • Point-in-time Recovery (PITR)

MongoDB Atlas:

  • 连续备份(Continuous Backup)
  • 保留 30 天
  • Oplog 增量备份

2. 恢复策略

bash
# PostgreSQL 恢复
pg_restore -d tianjiyao backup_file.dump

# MongoDB 恢复
mongorestore --uri="mongodb+srv://..." /backup/path

性能优化

1. 查询优化

sql
-- 使用索引
EXPLAIN ANALYZE
SELECT * FROM orders
WHERE user_id = 'xxx' AND created_at > NOW() - INTERVAL '30 days';

-- 批量查询
SELECT * FROM users WHERE id IN ('id1', 'id2', 'id3');

-- 分页查询
SELECT * FROM divinations
WHERE user_id = 'xxx'
ORDER BY created_at DESC
LIMIT 20 OFFSET 0;

2. 连接池配置

typescript
// Supabase 连接池
const supabase = createClient(url, key, {
  db: {
    poolSize: 20,
  },
});

// MongoDB 连接池
const client = new MongoClient(uri, {
  maxPoolSize: 50,
  minPoolSize: 10,
});

数据迁移

示例:添加新字段

sql
-- 1. 添加字段
ALTER TABLE users ADD COLUMN referral_code VARCHAR(20);

-- 2. 创建索引
CREATE INDEX idx_users_referral_code ON users(referral_code);

-- 3. 回填数据(可选)
UPDATE users SET referral_code = generate_referral_code(id);

监控指标

指标目标监控工具
查询响应时间< 50msApplication Insights
连接池使用率< 80%Supabase Dashboard
缓存命中率> 70%Redis Insights
数据库大小监控增长Prometheus
慢查询数量< 10/小时pg_stat_statements

下一步


💡 数据库最佳实践

  • 使用索引优化查询
  • 定期清理过期数据
  • 监控慢查询
  • 定期备份数据

基于 MIT 许可发布 | 技术文档由 VitePress 驱动