# GEO 平台数据库设计文档

> **文档版本**: v1.0
> **创建日期**: 2026-05-01
> **依据文档**: [技术详细设计文档](../3-技术设计/01-GEO平台技术详细设计-v1.0.md)

---

## 目录

1. [数据库选型](#1-数据库选型)
2. [数据库设计](#2-数据库设计)
3. [表结构定义](#3-表结构定义)
4. [索引设计](#4-索引设计)
5. [初始化脚本](#5-初始化脚本)
6. [数据迁移](#6-数据迁移)
7. [备份策略](#7-备份策略)

---

## 1. 数据库选型

### 1.1 主数据库: PostgreSQL

| 属性 | 选择 | 理由 |
|------|------|------|
| **版本** | PostgreSQL 15+ | JSONB支持、异步能力、成熟稳定 |
| **部署** | Docker Compose | 简化部署 |
| **连接池** | asyncpg | 异步高并发 |

**选择理由**:
1. **JSONB支持**: 品牌别名、平台列表等半结构化数据存储
2. **异步驱动**: asyncpg + SQLAlchemy 2.0 完美支持异步
3. **全文搜索**: 支持中文分词 (可选扩展)
4. **可靠性**: ACID保证，数据一致性

### 1.2 缓存数据库: Redis

| 属性 | 选择 | 理由 |
|------|------|------|
| **版本** | Redis 7+ | 最新稳定版 |
| **用途** | 缓存/会话/队列 | 低延迟数据访问 |

**使用场景**:
- API响应缓存 (评分、统计)
- 会话存储
- 限流计数
- 任务队列

---

## 2. 数据库设计

### 2.1 命名规范

| 规范 | 示例 |
|------|------|
| 表名单词 | users, brands, citation_records |
| 字段名单词 | user_id, created_at |
| 索引命名 | idx_{table}_{column} |
| 主键 | id UUID |
| 时间戳 | created_at, updated_at |

### 2.2 ER图

```
┌─────────────────────────────────────────────────────────────────────────┐
│                              USERS                                      │
├─────────────────────────────────────────────────────────────────────────┤
│ id (PK, UUID)                                                          │
│ email (UNIQUE)                                                         │
│ password_hash                                                          │
│ name                                                                   │
│ plan                                                                   │
│ max_queries                                                            │
│ is_active                                                              │
│ email_verified                                                         │
│ verification_code                                                       │
│ verification_code_expires                                               │
│ reset_token                                                            │
│ reset_token_expires                                                    │
│ created_at                                                             │
│ updated_at                                                             │
└─────────────────────────────────────────────────────────────────────────┘
                                    │
                                    │ 1:N
                                    ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                              BRANDS                                     │
├─────────────────────────────────────────────────────────────────────────┤
│ id (PK, UUID)                                                          │
│ user_id (FK → users.id)                                                │
│ name (UNIQUE)                                                          │
│ aliases (JSONB)                                                        │
│ website                                                                │
│ industry                                                               │
│ platforms (JSONB)                                                      │
│ frequency                                                              │
│ status                                                                 │
│ last_queried_at                                                        │
│ next_query_at                                                          │
│ created_at                                                             │
│ updated_at                                                             │
└─────────────────────────────────────────────────────────────────────────┘
         │
         │ 1:N                    │ 1:N                  │ 1:N
         ▼                        ▼                      ▼
┌──────────────────┐    ┌──────────────────┐    ┌──────────────────┐
│   COMPETITORS   │    │ CITATION_RECORD │    │   QUERY_TASKS    │
├──────────────────┤    ├──────────────────┤    ├──────────────────┤
│ id (PK, UUID)   │    │ id (PK, UUID)   │    │ id (PK, UUID)   │
│ brand_id (FK)   │    │ brand_id (FK)   │    │ brand_id (FK)   │
│ name            │    │ platform        │    │ platform        │
│ aliases (JSONB)  │    │ cited           │    │ status         │
│ created_at      │    │ citation_text   │    │ error_message  │
└──────────────────┘    │ citation_pos    │    │ scheduled_at   │
                       │ sentiment       │    │ started_at     │
                       │ competitor_brands│    │ completed_at   │
                       │ raw_response    │    └──────────────────┘
                       │ confidence      │
                       │ match_type      │
                       │ queried_at       │
                       └──────────────────┘
```

---

## 3. 表结构定义

### 3.1 users 用户表

```sql
-- 用户表
CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email VARCHAR(255) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    name VARCHAR(100),
    plan VARCHAR(20) DEFAULT 'free' CHECK (plan IN ('free', 'pro', 'enterprise')),
    max_queries INTEGER DEFAULT 5,
    is_active BOOLEAN DEFAULT TRUE,
    email_verified BOOLEAN DEFAULT FALSE,
    verification_code VARCHAR(6),
    verification_code_expires TIMESTAMP,
    reset_token VARCHAR(255),
    reset_token_expires TIMESTAMP,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

-- 索引
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_plan ON users(plan);
CREATE INDEX idx_users_is_active ON users(is_active);

-- 注释
COMMENT ON TABLE users IS '用户表';
COMMENT ON COLUMN users.plan IS '套餐: free=免费, pro=专业版, enterprise=企业版';
COMMENT ON COLUMN users.max_queries IS '每日最大查询次数';
```

### 3.2 brands 品牌表

```sql
-- 品牌表
CREATE TABLE brands (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    name VARCHAR(50) NOT NULL,
    aliases JSONB DEFAULT '[]',
    website VARCHAR(500),
    industry VARCHAR(50),
    platforms JSONB NOT NULL DEFAULT '["wenxin", "kimi"]',
    frequency VARCHAR(20) DEFAULT 'weekly' CHECK (frequency IN ('daily', 'weekly', 'monthly')),
    status VARCHAR(20) DEFAULT 'active' CHECK (status IN ('active', 'paused', 'deleted')),
    last_queried_at TIMESTAMP,
    next_query_at TIMESTAMP,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    CONSTRAINT unique_brand_name_per_user UNIQUE (user_id, name)
);

-- 索引
CREATE INDEX idx_brands_user_id ON brands(user_id);
CREATE INDEX idx_brands_status ON brands(status);
CREATE INDEX idx_brands_next_query ON brands(next_query_at);
CREATE INDEX idx_brands_name ON brands(name);

-- 注释
COMMENT ON TABLE brands IS '品牌表';
COMMENT ON COLUMN brands.aliases IS '品牌别名列表 JSON数组';
COMMENT ON COLUMN brands.platforms IS '监控的AI平台列表 JSON数组';
COMMENT ON COLUMN brands.frequency IS '查询频率: daily=每日, weekly=每周, monthly=每月';
COMMENT ON COLUMN brands.status IS '状态: active=活跃, paused=暂停, deleted=已删除';
```

### 3.3 competitors 竞品表

```sql
-- 竞品表
CREATE TABLE competitors (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    brand_id UUID NOT NULL REFERENCES brands(id) ON DELETE CASCADE,
    name VARCHAR(50) NOT NULL,
    aliases JSONB DEFAULT '[]',
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

-- 索引
CREATE INDEX idx_competitors_brand_id ON competitors(brand_id);

-- 注释
COMMENT ON TABLE competitors IS '竞品表';
COMMENT ON COLUMN competitors.aliases IS '竞品别名列表 JSON数组';
```

### 3.4 citation_records 引用记录表

```sql
-- 引用记录表
CREATE TABLE citation_records (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    brand_id UUID NOT NULL REFERENCES brands(id) ON DELETE CASCADE,
    platform VARCHAR(50) NOT NULL,
    cited BOOLEAN NOT NULL DEFAULT FALSE,
    citation_text TEXT,
    citation_position INTEGER,
    sentiment VARCHAR(20) CHECK (sentiment IN ('positive', 'neutral', 'negative', NULL)),
    competitor_brands JSONB DEFAULT '[]',
    raw_response TEXT,
    confidence FLOAT CHECK (confidence >= 0 AND confidence <= 1),
    match_type VARCHAR(20) CHECK (match_type IN ('exact', 'alias', 'fuzzy', NULL)),
    queried_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

-- 索引
CREATE INDEX idx_citations_brand_id ON citation_records(brand_id);
CREATE INDEX idx_citations_platform ON citation_records(platform);
CREATE INDEX idx_citations_queried_at ON citation_records(queried_at);
CREATE INDEX idx_citations_brand_platform ON citation_records(brand_id, platform);
CREATE INDEX idx_citations_brand_queried ON citation_records(brand_id, queried_at);
CREATE INDEX idx_citations_cited ON citation_records(brand_id, cited);

-- 注释
COMMENT ON TABLE citation_records IS '引用记录表';
COMMENT ON COLUMN citation_records.cited IS '是否被引用';
COMMENT ON COLUMN citation_records.citation_position IS '引用位置(1-based)';
COMMENT ON COLUMN citation_records.sentiment IS '情感倾向: positive=正面, neutral=中性, negative=负面';
COMMENT ON COLUMN citation_records.match_type IS '匹配类型: exact=精确, alias=别名, fuzzy=模糊';
```

### 3.5 query_tasks 查询任务表

```sql
-- 查询任务表
CREATE TABLE query_tasks (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    brand_id UUID NOT NULL REFERENCES brands(id) ON DELETE CASCADE,
    platform VARCHAR(50) NOT NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'running', 'success', 'failed')),
    error_message TEXT,
    scheduled_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    started_at TIMESTAMP,
    completed_at TIMESTAMP
);

-- 索引
CREATE INDEX idx_query_tasks_brand_id ON query_tasks(brand_id);
CREATE INDEX idx_query_tasks_status ON query_tasks(status);
CREATE INDEX idx_query_tasks_scheduled ON query_tasks(scheduled_at);
CREATE INDEX idx_query_tasks_pending ON query_tasks(status, scheduled_at) WHERE status = 'pending';

-- 注释
COMMENT ON TABLE query_tasks IS '查询任务表';
COMMENT ON COLUMN query_tasks.status IS '任务状态: pending=等待中, running=执行中, success=成功, failed=失败';
```

---

## 4. 索引设计

### 4.1 索引清单

| 表名 | 索引名 | 列 | 类型 | 用途 |
|------|--------|-----|------|------|
| users | idx_users_email | email | B-Tree | 登录查询 |
| users | idx_users_plan | plan | B-Tree | 套餐统计 |
| brands | idx_brands_user_id | user_id | B-Tree | 用户品牌列表 |
| brands | idx_brands_status | status | B-Tree | 状态筛选 |
| brands | idx_brands_next_query | next_query_at | B-Tree | 调度查询 |
| competitors | idx_competitors_brand_id | brand_id | B-Tree | 竞品列表 |
| citation_records | idx_citations_brand_id | brand_id | B-Tree | 品牌记录 |
| citation_records | idx_citations_platform | platform | B-Tree | 平台筛选 |
| citation_records | idx_citations_queried_at | queried_at | B-Tree | 时间筛选 |
| citation_records | idx_citations_brand_platform | brand_id, platform | B-Tree | 组合查询 |
| query_tasks | idx_query_tasks_brand_id | brand_id | B-Tree | 任务查询 |
| query_tasks | idx_query_tasks_status | status | B-Tree | 状态筛选 |
| query_tasks | idx_query_tasks_pending | status, scheduled_at | B-Tree | 调度查询 |

### 4.2 复合索引设计

```sql
-- 品牌+平台+时间 复合索引 (高频查询)
CREATE INDEX idx_citations_brand_platform_time
ON citation_records(brand_id, platform, queried_at DESC);

-- 品牌+是否引用 索引 (统计查询)
CREATE INDEX idx_citations_cited_flag
ON citation_records(brand_id, cited) WHERE cited = TRUE;

-- 待执行任务索引 (调度器查询)
CREATE INDEX idx_query_tasks_ready
ON query_tasks(status, scheduled_at)
WHERE status IN ('pending', 'running');
```

---

## 5. 初始化脚本

### 5.1 完整建库建表脚本

```sql
-- GEO 平台数据库初始化脚本
-- 版本: v1.0
-- 日期: 2026-05-01

-- 创建数据库 (如需要)
-- CREATE DATABASE geo_platform;

-- 连接到目标数据库后执行以下脚本

-- ============================================
-- 1. 扩展启用
-- ============================================
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pgcrypto";

-- ============================================
-- 2. 用户表
-- ============================================
CREATE TABLE IF NOT EXISTS users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email VARCHAR(255) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    name VARCHAR(100),
    plan VARCHAR(20) DEFAULT 'free' CHECK (plan IN ('free', 'pro', 'enterprise')),
    max_queries INTEGER DEFAULT 5,
    is_active BOOLEAN DEFAULT TRUE,
    email_verified BOOLEAN DEFAULT FALSE,
    verification_code VARCHAR(6),
    verification_code_expires TIMESTAMP,
    reset_token VARCHAR(255),
    reset_token_expires TIMESTAMP,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_users_plan ON users(plan);
CREATE INDEX IF NOT EXISTS idx_users_is_active ON users(is_active);

-- ============================================
-- 3. 品牌表
-- ============================================
CREATE TABLE IF NOT EXISTS brands (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    name VARCHAR(50) NOT NULL,
    aliases JSONB DEFAULT '[]',
    website VARCHAR(500),
    industry VARCHAR(50),
    platforms JSONB NOT NULL DEFAULT '["wenxin", "kimi"]',
    frequency VARCHAR(20) DEFAULT 'weekly' CHECK (frequency IN ('daily', 'weekly', 'monthly')),
    status VARCHAR(20) DEFAULT 'active' CHECK (status IN ('active', 'paused', 'deleted')),
    last_queried_at TIMESTAMP,
    next_query_at TIMESTAMP,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    CONSTRAINT unique_brand_name_per_user UNIQUE (user_id, name)
);

CREATE INDEX IF NOT EXISTS idx_brands_user_id ON brands(user_id);
CREATE INDEX IF NOT EXISTS idx_brands_status ON brands(status);
CREATE INDEX IF NOT EXISTS idx_brands_next_query ON brands(next_query_at);
CREATE INDEX IF NOT EXISTS idx_brands_name ON brands(name);

-- ============================================
-- 4. 竞品表
-- ============================================
CREATE TABLE IF NOT EXISTS competitors (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    brand_id UUID NOT NULL REFERENCES brands(id) ON DELETE CASCADE,
    name VARCHAR(50) NOT NULL,
    aliases JSONB DEFAULT '[]',
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_competitors_brand_id ON competitors(brand_id);

-- ============================================
-- 5. 引用记录表
-- ============================================
CREATE TABLE IF NOT EXISTS citation_records (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    brand_id UUID NOT NULL REFERENCES brands(id) ON DELETE CASCADE,
    platform VARCHAR(50) NOT NULL,
    cited BOOLEAN NOT NULL DEFAULT FALSE,
    citation_text TEXT,
    citation_position INTEGER,
    sentiment VARCHAR(20) CHECK (sentiment IN ('positive', 'neutral', 'negative', NULL)),
    competitor_brands JSONB DEFAULT '[]',
    raw_response TEXT,
    confidence FLOAT CHECK (confidence >= 0 AND confidence <= 1),
    match_type VARCHAR(20) CHECK (match_type IN ('exact', 'alias', 'fuzzy', NULL)),
    queried_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_citations_brand_id ON citation_records(brand_id);
CREATE INDEX IF NOT EXISTS idx_citations_platform ON citation_records(platform);
CREATE INDEX IF NOT EXISTS idx_citations_queried_at ON citation_records(queried_at);
CREATE INDEX IF NOT EXISTS idx_citations_brand_platform ON citation_records(brand_id, platform);
CREATE INDEX IF NOT EXISTS idx_citations_brand_queried ON citation_records(brand_id, queried_at DESC);

-- ============================================
-- 6. 查询任务表
-- ============================================
CREATE TABLE IF NOT EXISTS query_tasks (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    brand_id UUID NOT NULL REFERENCES brands(id) ON DELETE CASCADE,
    platform VARCHAR(50) NOT NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'running', 'success', 'failed')),
    error_message TEXT,
    scheduled_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    started_at TIMESTAMP,
    completed_at TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_query_tasks_brand_id ON query_tasks(brand_id);
CREATE INDEX IF NOT EXISTS idx_query_tasks_status ON query_tasks(status);
CREATE INDEX IF NOT EXISTS idx_query_tasks_scheduled ON query_tasks(scheduled_at);
CREATE INDEX IF NOT EXISTS idx_query_tasks_pending ON query_tasks(status, scheduled_at) WHERE status = 'pending';

-- ============================================
-- 7. 触发器: updated_at 自动更新
-- ============================================
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = CURRENT_TIMESTAMP;
    RETURN NEW;
END;
$$ language 'plpgsql';

CREATE TRIGGER update_users_updated_at
    BEFORE UPDATE ON users
    FOR EACH ROW
    EXECUTE FUNCTION update_updated_at_column();

CREATE TRIGGER update_brands_updated_at
    BEFORE UPDATE ON brands
    FOR EACH ROW
    EXECUTE FUNCTION update_updated_at_column();

-- ============================================
-- 8. 视图: 品牌评分汇总
-- ============================================
CREATE OR REPLACE VIEW brand_score_summary AS
SELECT
    b.id AS brand_id,
    b.name AS brand_name,
    b.user_id,
    COUNT(cr.id) AS total_queries,
    SUM(CASE WHEN cr.cited THEN 1 ELSE 0 END) AS total_cited,
    ROUND(
        SUM(CASE WHEN cr.cited THEN 1 ELSE 0 END)::NUMERIC /
        NULLIF(COUNT(cr.id), 0) * 100,
        2
    ) AS citation_rate,
    AVG(cr.confidence) AS avg_confidence
FROM brands b
LEFT JOIN citation_records cr ON b.id = cr.brand_id
WHERE b.status = 'active'
GROUP BY b.id, b.name, b.user_id;

-- ============================================
-- 9. 视图: 平台统计
-- ============================================
CREATE OR REPLACE VIEW platform_stats AS
SELECT
    cr.platform,
    COUNT(*) AS total_queries,
    SUM(CASE WHEN cr.cited THEN 1 ELSE 0 END) AS total_cited,
    ROUND(
        SUM(CASE WHEN cr.cited THEN 1 ELSE 0 END)::NUMERIC /
        NULLIF(COUNT(*), 0) * 100,
        2
    ) AS citation_rate
FROM citation_records cr
GROUP BY cr.platform;

-- ============================================
-- 10. 初始化数据
-- ============================================

-- 创建管理员用户 (密码: Admin123!)
-- 实际部署时请更换密码
INSERT INTO users (
    id,
    email,
    password_hash,
    name,
    plan,
    max_queries,
    is_active,
    email_verified,
    is_admin
) VALUES (
    '00000000-0000-0000-0000-000000000001',
    'admin@geo-platform.com',
    '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.FQgL3aHf9gHqOK', -- Admin123!
    '系统管理员',
    'enterprise',
    999999,
    TRUE,
    TRUE,
    TRUE
) ON CONFLICT (email) DO NOTHING;

-- ============================================
-- 11. 权限设置 (如使用非postgres用户)
-- ============================================
-- GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO geo_app;
-- GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO geo_app;
-- GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO geo_app;

-- ============================================
-- 完成
-- ============================================
DO $$
BEGIN
    RAISE NOTICE 'GEO Platform database initialized successfully!';
END $$;
```

### 5.2 测试数据脚本

```sql
-- ============================================
-- 测试数据脚本
-- 仅用于开发/测试环境
-- ============================================

-- 清理现有测试数据
DELETE FROM query_tasks WHERE brand_id IN (SELECT id FROM brands WHERE name LIKE '测试%');
DELETE FROM citation_records WHERE brand_id IN (SELECT id FROM brands WHERE name LIKE '测试%');
DELETE FROM competitors WHERE brand_id IN (SELECT id FROM brands WHERE name LIKE '测试%');
DELETE FROM brands WHERE name LIKE '测试%';
DELETE FROM users WHERE email LIKE 'test%';

-- 创建测试用户
INSERT INTO users (id, email, password_hash, name, plan, max_queries, is_active, email_verified)
VALUES (
    '11111111-1111-1111-1111-111111111111',
    'test@example.com',
    '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.FQgL3aHf9gHqOK', -- Password123!
    '测试用户',
    'pro',
    100,
    TRUE,
    TRUE
);

-- 创建测试品牌
INSERT INTO brands (id, user_id, name, aliases, platforms, frequency, status)
VALUES (
    '22222222-2222-2222-2222-222222222222',
    '11111111-1111-1111-1111-111111111111',
    '测试品牌A',
    '["品牌A", "TestA"]'::jsonb,
    '["wenxin", "kimi", "tongyi"]'::jsonb,
    'daily',
    'active'
);

-- 创建竞品
INSERT INTO competitors (brand_id, name, aliases)
VALUES
    ('22222222-2222-2222-2222-222222222222', '竞品X', '["竞品X", "JX"]'::jsonb),
    ('22222222-2222-2222-2222-222222222222', '竞品Y', '["竞品Y"]'::jsonb);

-- 创建测试引用记录
INSERT INTO citation_records (brand_id, platform, cited, citation_text, citation_position, sentiment, confidence, match_type)
VALUES
    ('22222222-2222-2222-2222-222222222222', 'wenxin', TRUE, '测试品牌A是一家领先的企业...', 1, 'positive', 0.95, 'exact'),
    ('22222222-2222-2222-2222-222222222222', 'wenxin', TRUE, '在AI领域，TestA表现突出...', 2, 'positive', 0.88, 'alias'),
    ('22222222-2222-2222-2222-222222222222', 'kimi', FALSE, '目前暂无相关数据...', NULL, NULL, NULL, NULL),
    ('22222222-2222-2222-2222-222222222222', 'tongyi', TRUE, '测试品牌A在行业中...', 1, 'neutral', 0.92, 'exact');

DO $$
BEGIN
    RAISE NOTICE 'Test data inserted successfully!';
END $$;
```

---

## 6. 数据迁移

### 6.1 Alembic配置

**alembic.ini 配置**:
```ini
[alembic]
script_location = alembic
prepend_sys_path = .
version_path_separator = os

sqlalchemy.url = postgresql+asyncpg://postgres:postgres123@localhost:5432/geo_platform

[post_write_hooks]

[loggers]
keys = root,sqlalchemy,alembic

[handlers]
keys = console

[formatters]
keys = generic

[logger_root]
level = WARN
handlers = console
qualname =

[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine

[logger_alembic]
level = INFO
handlers =
qualname = alembic

[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic

[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
```

### 6.2 迁移命令

```bash
# 进入后端目录
cd backend

# 生成迁移
alembic revision --autogenerate -m "initial migration"

# 执行迁移
alembic upgrade head

# 查看当前版本
alembic current

# 回滚
alembic downgrade -1

# 查看历史
alembic history
```

---

## 7. 备份策略

### 7.1 备份类型

| 类型 | 频率 | 保留 | 说明 |
|------|------|------|------|
| 全量备份 | 每日 | 30天 | 完整数据库快照 |
| WAL归档 | 实时 | 7天 | 增量恢复 |
| 表导出 | 每周 | 12周 | 关键表CSV导出 |

### 7.2 备份脚本

```bash
#!/bin/bash
# backup.sh - 数据库备份脚本

# 配置
BACKUP_DIR="/backups/postgres"
DATE=$(date +%Y%m%d_%H%M%S)
DB_NAME="geo_platform"
DB_USER="postgres"
DB_HOST="localhost"

# 创建备份目录
mkdir -p $BACKUP_DIR

# 全量备份
pg_dump -h $DB_HOST -U $DB_USER -d $DB_NAME -F c -f "$BACKUP_DIR/full_backup_$DATE.dump"

# 删除30天前的备份
find $BACKUP_DIR -name "full_backup_*.dump" -mtime +30 -delete

# 记录备份日志
echo "[$(date)] Backup completed: full_backup_$DATE.dump" >> /var/log/backup.log
```

### 7.3 恢复流程

```bash
# 停止服务
docker-compose stop backend

# 恢复数据库
pg_restore -h localhost -U postgres -d geo_platform -c /backups/postgres/full_backup_20260501.dump

# 启动服务
docker-compose start backend
```

---

**文档状态**: 待评审
**审批人**: 技术负责人
