文档 11 / 28

数据库设计文档

PostgreSQL · Redis · S3 · Snowflake · 数据模型 · 索引策略

册别:第三册 · 技术与架构 版本:V1.0 日期:2026-08-15 状态:草稿待评审

一、数据库总览

存储用途数据保留部署
PostgreSQL (RDS)业务核心数据(用户/通话/支付/社交)持久(90 天归档)AWS ap-southeast-1
Redis Cluster热数据/缓存/匹配池/计数器TTL 自动过期ElastiCache
S3静态资源/头像/录音归档180 天(合规)AWS S3 + CloudFront
Snowflake数据分析/BI/模型训练无限(分区归档)Snowflake SG 区
Kafka (MSK)消息队列/事件流1-7 天AWS MSK

1.1 数据分类与合规

数据类别示例存储位置保留期加密
个人身份信息 (PII)手机号/邮箱/真实姓名PostgreSQL (RDS)账号存续期AES-256 KMS
语音数据通话音频不存储(仅实时流)0
文本数据字幕/消息/翻译PG + S3 归档90 天AES-256
支付数据订单/交易/订阅PG (PCI-DSS)7 年(税务)AES-256 + Tokenization
行为数据点击/浏览/事件Snowflake2 年脱敏后
审核数据举报/标记/裁决PG + S33 年AES-256

二、PostgreSQL 核心表设计

2.1 users — 用户主表

CREATE TABLE users ( user_id VARCHAR(32) PRIMARY KEY, -- usr_xxx phone VARCHAR(20) UNIQUE, email VARCHAR(100), nickname VARCHAR(30) NOT NULL, avatar_url TEXT, mother_tongue VARCHAR(10) NOT NULL, -- zh-CN / en-US target_language VARCHAR(10) NOT NULL, gender VARCHAR(10), -- male/female/other/prefer_not_say birth_year SMALLINT, country_code VARCHAR(5) DEFAULT 'SG', vip_status VARCHAR(20) DEFAULT 'none', -- none/active/expired vip_expire_at BIGINT, coins_balance INTEGER DEFAULT 0, free_calls_left SMALLINT DEFAULT 10, free_calls_reset_at BIGINT, rating_avg DECIMAL(3,1) DEFAULT 5.0, total_calls INTEGER DEFAULT 0, trust_score SMALLINT DEFAULT 100, is_active BOOLEAN DEFAULT TRUE, is_banned BOOLEAN DEFAULT FALSE, ban_reason VARCHAR(100), device_ids JSONB DEFAULT '[]', created_at BIGINT NOT NULL, updated_at BIGINT NOT NULL, deleted_at BIGINT -- 软删除时间戳 ); CREATE INDEX idx_users_lang ON users(mother_tongue, target_language); CREATE INDEX idx_users_online ON users(is_active) WHERE is_active = TRUE; CREATE INDEX idx_users_vip ON users(vip_status, vip_expire_at);

2.2 calls — 通话记录

CREATE TABLE calls ( call_id VARCHAR(32) PRIMARY KEY, -- call_xxx caller_id VARCHAR(32) NOT NULL REFERENCES users(user_id), callee_id VARCHAR(32) NOT NULL REFERENCES users(user_id), start_time BIGINT NOT NULL, end_time BIGINT, duration_seconds INTEGER DEFAULT 0, caller_language VARCHAR(10), callee_language VARCHAR(10), cost_coins INTEGER DEFAULT 0, earner_coins INTEGER DEFAULT 0, caller_rating SMALLINT, -- 1-5 callee_rating SMALLINT, end_reason VARCHAR(20), -- normal_hangup/timeout/error/report has_translation BOOLEAN DEFAULT TRUE, avg_latency_ms INTEGER, quality_score DECIMAL(3,1), created_at BIGINT NOT NULL ) PARTITION BY RANGE (start_time); -- 按月分区(自动创建) CREATE INDEX idx_calls_user_time ON calls(caller_id, start_time DESC); CREATE INDEX idx_calls_callee_time ON calls(callee_id, start_time DESC);

2.3 translations — 翻译记录

CREATE TABLE translations ( translation_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, call_id VARCHAR(32) NOT NULL REFERENCES calls(call_id), seq INTEGER NOT NULL, speaker_id VARCHAR(32) NOT NULL, source_lang VARCHAR(10) NOT NULL, target_lang VARCHAR(10) NOT NULL, source_text TEXT NOT NULL, translated_text TEXT, asr_confidence DECIMAL(4,3), translate_confidence DECIMAL(4,3), latency_ms INTEGER, engine VARCHAR(30), -- google_translate/azure/self_hosted was_cached BOOLEAN DEFAULT FALSE, is_flagged BOOLEAN DEFAULT FALSE, created_at BIGINT NOT NULL, UNIQUE(call_id, seq, speaker_id) ) PARTITION BY RANGE (created_at); CREATE INDEX idx_translations_call ON translations(call_id, seq); CREATE INDEX idx_translations_flagged ON translations(is_flagged) WHERE is_flagged = TRUE;

2.4 payments — 支付订单

CREATE TABLE payments ( payment_id VARCHAR(32) PRIMARY KEY, user_id VARCHAR(32) NOT NULL REFERENCES users(user_id), type VARCHAR(20) NOT NULL, -- coin_purchase/vip_subscription/gift amount_sgd DECIMAL(10,2) NOT NULL, coins_amount INTEGER, channel VARCHAR(20) NOT NULL, -- paynow/stripe/apple_iap/google_play channel_tx_id VARCHAR(100), status VARCHAR(20) NOT NULL, -- pending/completed/failed/refunded failure_reason VARCHAR(200), receipt_data TEXT, -- Apple/Google 回执(加密) metadata JSONB, created_at BIGINT NOT NULL, completed_at BIGINT, CHECK(amount_sgd >= 0) ); CREATE INDEX idx_payments_user ON payments(user_id, created_at DESC); CREATE INDEX idx_payments_status ON payments(status, created_at);

2.5 subscriptions — VIP 订阅

CREATE TABLE subscriptions ( subscription_id VARCHAR(32) PRIMARY KEY, user_id VARCHAR(32) NOT NULL REFERENCES users(user_id), plan VARCHAR(20) NOT NULL, -- vip_week/vip_month/vip_year status VARCHAR(20) NOT NULL, -- active/cancelled/expired/past_due start_date BIGINT NOT NULL, end_date BIGINT NOT NULL, auto_renew BOOLEAN DEFAULT TRUE, channel VARCHAR(20), original_transaction_id VARCHAR(100), created_at BIGINT NOT NULL, updated_at BIGINT NOT NULL ); CREATE INDEX idx_subs_user ON subscriptions(user_id, status); CREATE INDEX idx_subs_expire ON subscriptions(end_date) WHERE status = 'active';

2.6 buddies — 语伴关系

CREATE TABLE buddies ( buddy_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, user_a_id VARCHAR(32) NOT NULL REFERENCES users(user_id), user_b_id VARCHAR(32) NOT NULL REFERENCES users(user_id), status VARCHAR(20) DEFAULT 'active', -- active/blocked/muted is_favorite BOOLEAN DEFAULT FALSE, total_calls INTEGER DEFAULT 0, last_call_at BIGINT, created_at BIGINT NOT NULL, CHECK(user_a_id < user_b_id), -- 保证唯一对 UNIQUE(user_a_id, user_b_id) ); CREATE INDEX idx_buddies_user ON buddies(user_a_id, status);

2.7 messages — 私聊消息

CREATE TABLE messages ( message_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, sender_id VARCHAR(32) NOT NULL REFERENCES users(user_id), receiver_id VARCHAR(32) NOT NULL REFERENCES users(user_id), type VARCHAR(10) DEFAULT 'text', -- text/gift/system content TEXT, translated_content TEXT, is_read BOOLEAN DEFAULT FALSE, is_flagged BOOLEAN DEFAULT FALSE, created_at BIGINT NOT NULL ) PARTITION BY RANGE (created_at); CREATE INDEX idx_messages_conv ON messages(sender_id, receiver_id, created_at DESC); CREATE INDEX idx_messages_unread ON messages(receiver_id, is_read) WHERE is_read = FALSE;

2.8 reports — 举报记录

CREATE TABLE reports ( report_id VARCHAR(32) PRIMARY KEY, reporter_id VARCHAR(32) NOT NULL REFERENCES users(user_id), reported_user_id VARCHAR(32) NOT NULL REFERENCES users(user_id), call_id VARCHAR(32) REFERENCES calls(call_id), reason VARCHAR(30) NOT NULL, description TEXT, severity VARCHAR(10) DEFAULT 'medium', -- low/medium/high/critical status VARCHAR(20) DEFAULT 'pending', -- pending/reviewing/resolved/dismissed resolution VARCHAR(50), -- user_warned/user_banned/false_positive moderator_id VARCHAR(32), evidence JSONB, created_at BIGINT NOT NULL, resolved_at BIGINT ); CREATE INDEX idx_reports_pending ON reports(status, severity, created_at) WHERE status = 'pending'; CREATE INDEX idx_reports_user ON reports(reported_user_id, status);

2.9 gifts — 礼物记录

CREATE TABLE gifts ( gift_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, from_user_id VARCHAR(32) NOT NULL REFERENCES users(user_id), to_user_id VARCHAR(32) NOT NULL REFERENCES users(user_id), gift_type VARCHAR(20) NOT NULL, -- flower/applause/melody/crown/heart coins_cost INTEGER NOT NULL, call_id VARCHAR(32) REFERENCES calls(call_id), is_anonymous BOOLEAN DEFAULT FALSE, created_at BIGINT NOT NULL ); CREATE INDEX idx_gifts_receiver ON gifts(to_user_id, created_at DESC);

2.10 ad_events — 广告事件

CREATE TABLE ad_events ( event_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, user_id VARCHAR(32) NOT NULL REFERENCES users(user_id), ad_type VARCHAR(20) NOT NULL, -- rewarded_video/banner event_type VARCHAR(20) NOT NULL, -- impression/click/reward/dismiss/error provider VARCHAR(20), -- admob/ironsource ad_unit_id VARCHAR(50), reward_granted BOOLEAN DEFAULT FALSE, reward_coins INTEGER DEFAULT 0, error_code VARCHAR(20), created_at BIGINT NOT NULL ) PARTITION BY RANGE (created_at); CREATE INDEX idx_ads_user_daily ON ad_events(user_id, event_type, created_at DESC);

三、Redis 数据结构

Key 模式类型用途TTL
user:<uid>:sessionHash用户会话/Token7 天
match:pool:<lang>Sorted Set匹配池(score=rating)无(实时维护)
match:searching:<uid>String用户搜索状态60 秒
call:<cid>:participantsSet通话参与者通话结束清除
call:<cid>:qualityHash实时质量指标通话结束清除
translate:cache:<hash>String翻译结果缓存24 小时
user:<uid>:ads:todayString (counter)今日广告次数到次日 0 点
user:<uid>:rate_limitString (counter)API 限流计数60 秒
config:feature_flagsHashFeature Flag 全局配置无(手动刷新)
stats:online_countString全站在线人数无(实时更新)

3.1 匹配池 Sorted Set 设计

# 加入匹配池 ZADD match:pool:zh-CN <rating_score> <user_id> ZADD match:pool:en-US <rating_score> <user_id> # 查找匹配(按评分邻近) ZRANGEBYSCORE match:pool:en-US <min_score> <max_score> LIMIT 0 10 # 移除(匹配成功/取消/离线) ZREM match:pool:zh-CN <user_id> # 过期清理(定时任务每 30s) ZREMRANGEBYSCORE match:pool:zh-CN -inf <current_time - 60s>

四、分区与归档策略

4.1 表分区规则

表名分区键分区粒度保留期归档目标
callsstart_time按月12 个月热 + 无限冷S3 Parquet
translationscreated_at按月3 个月热 + 6 个月温S3 Parquet
messagescreated_at按月6 个月S3 (加密)
ad_eventscreated_at按月6 个月Snowflake
reportscreated_at按月3 年S3 (WORM)

4.2 自动归档脚本

-- 每月 1 号 03:00 执行(pg_cron) SELECT create_archive_partition( p_table_name := 'calls', p_month := DATE_TRUNC('month', NOW() - INTERVAL '3 months') ); -- 流程: -- 1. 将 3 个月前的分区数据导出为 Parquet → S3 -- 2. 验证 S3 数据完整性(行数 + checksum) -- 3. DROP 原分区 -- 4. 创建 FOREIGN TABLE 指向 S3(仍可查询)

4.3 数据生命周期

数据类型热存储温存储冷存储删除
通话记录PG 分区(0-3 月)S3 Parquet(3-12 月)Glacier(1-3 年)3 年后
翻译文本PG 分区(0-1 月)S3(1-6 月)Glacier(6-12 月)1 年后
私聊消息PG 分区(0-6 月)S3(6-12 月)1 年后
支付记录PG(永久)Glacier(7 年税务)7 年后
审核日志PG(0-3 月)S3 WORM(3-36 月)Glacier(3-10 年)10 年后

五、性能与容量规划(Phase 1)

5.1 容量估算

日增量月增量年增量存储/年
users~500~1.5 万~18 万~50 MB
calls~3 万~90 万~1080 万~5 GB
translations~150 万~4500 万~5.4 亿~250 GB
messages~2 万~60 万~720 万~3 GB
ad_events~60 万~1800 万~2.16 亿~80 GB
reports~50~1500~1.8 万~10 MB
合计~340 GB/年

5.2 RDS 实例规格

环境实例类型存储CPU/RAM月费 (USD)
Devdb.t3.medium50 GB GP32 vCPU / 4 GB~$60
Stagingdb.t3.large100 GB GP32 vCPU / 8 GB~$120
Productiondb.r6i.large (主) + db.t3.medium (只读)500 GB GP32 vCPU / 16 GB + 2/4~$280
Production (P2)db.r6i.xlarge (主) + db.r6i.large ×21000 GB4 vCPU / 32 GB~$600

5.3 关键查询优化

查询场景索引策略预期耗时
用户通话历史idx_calls_user_time (caller_id, start_time DESC)< 50ms
匹配池查找Redis Sorted Set(非 PG)< 5ms
未读消息数Partial Index (is_read=false)< 10ms
待处理举报Partial Index (status=pending)< 20ms
翻译质量分析PG + Snowflake 外部表异步批处理
在线用户统计Redis HyperLogLog< 1ms

六、安全与合规

6.1 加密策略

数据加密方式密钥管理
手机号AES-256(列级加密)AWS KMS
邮箱AES-256(列级加密)AWS KMS
支付回执AES-256 + TokenizationAWS KMS + Stripe Vault
翻译文本TLS 传输 + AES-256 存储AWS KMS
备份AES-256(备份文件加密)AWS KMS

6.2 备份策略

类型频率保留恢复测试
自动快照每 6 小时7 天每周恢复演练
每日全量每天 03:0030 天每两周
每周归档每周日1 年每月
PITR持续(WAL)35 天

6.3 PDPA 合规映射

PDPA 条款数据库实现
第 14 条(同意)users 表 consent_version + consent_date 字段
第 16 条(最小化)仅收集必要字段;address/birthdate 可选
第 21 条(访问)GET /auth/data-export 接口 → 导出全部个人数据
第 21 条(删除)DELETE /auth/account → 30 天后 anonymize(GDPR 风格)
第 24 条(保留期限)分区 + 自动归档 + 生命周期策略
第 26 条(跨境传输)Phase 1 数据不出新加坡;API 调用海外需 DPA

七、数据库迁移管理

7.1 迁移工具

工具用途
pg_dump / pg_restore环境间数据迁移(dev→staging→prod)
Flyway版本化 SQL 迁移脚本(V1__init.sql, V2__add_index.sql)
pg_cron定时任务(归档/统计/清理)
pg_partman自动分区管理(按月创建/清理)

7.2 初始化脚本清单

migrations/ ├── V1__init_schema.sql -- 所有 CREATE TABLE ├── V2__create_indexes.sql -- 所有索引 ├── V3__seed_countries.sql -- 国家/语言种子数据 ├── V4__create_partitions.sql -- 分区表初始化 ├── V5__setup_pgcron.sql -- 定时任务 ├── V6__encrypt_existing.sql -- 列加密迁移 └── V7__add_feature_flags.sql -- 功能开关种子

7.3 数据脱敏规则

字段脱敏方式示例
手机号中间 4 位 → ***+65 9***4567
邮箱@ 前部分脱敏a***@gmail.com
昵称不脱敏(公开信息)Alex
翻译文本非 PII,不脱敏
IP 地址末段清零192.168.1.0

附录

附录 A:ER 图核心关系

users 1──┐ │ ├──< calls >──┐ │ │ users 2──┘ ├──< translations │ └──< gifts users 1──< buddies >── users 2 (self-referencing) users 1──< messages >── users 2 users ──< payments users ──< subscriptions users ──< reports (as reporter) users ──< reports (as reported) users ──< ad_events

附录 B:相关文档

文档关联内容
08 系统架构 HLD数据层在整体架构中的位置
09 实时翻译子系统 LLDtranslations 表的写入频率/格式
13 支付中台接入方案payments/subscriptions 详细设计

附录 C:修订记录

版本日期修改内容作者
V1.02026-08-15初始版本DBA 团队