Selaa lähdekoodia

🎉 初始提交: AI角色对话App完整项目

  ✨ 功能: 多AI平台支持、好感度系统、动态主动消息
  🛠️ 技术栈: FastAPI + React Native + PostgreSQL + Redis
  📦 包含: 完整后端、Docker配置、详细文档
yanyuhualb 1 kuukausi sitten
sitoutus
b637b73cac
46 muutettua tiedostoa jossa 3375 lisäystä ja 0 poistoa
  1. 10 0
      .claude/settings.local.json
  2. 30 0
      .gitattributes
  3. 78 0
      .gitignore
  4. 21 0
      LICENSE
  5. 428 0
      README.md
  6. 31 0
      backend/.env.example
  7. 24 0
      backend/Dockerfile
  8. 39 0
      backend/alembic.ini
  9. 67 0
      backend/alembic/env.py
  10. 173 0
      backend/alembic/versions/001_initial.py
  11. 1 0
      backend/app/__init__.py
  12. 1 0
      backend/app/api/__init__.py
  13. 57 0
      backend/app/api/affection.py
  14. 150 0
      backend/app/api/auth.py
  15. 162 0
      backend/app/api/characters.py
  16. 151 0
      backend/app/api/conversations.py
  17. 6 0
      backend/app/core/__init__.py
  18. 60 0
      backend/app/core/config.py
  19. 56 0
      backend/app/core/database.py
  20. 34 0
      backend/app/core/redis.py
  21. 76 0
      backend/app/core/security.py
  22. 61 0
      backend/app/main.py
  23. 19 0
      backend/app/models/__init__.py
  24. 55 0
      backend/app/models/affection.py
  25. 41 0
      backend/app/models/character.py
  26. 66 0
      backend/app/models/conversation.py
  27. 26 0
      backend/app/models/proactive_message.py
  28. 34 0
      backend/app/models/user.py
  29. 1 0
      backend/app/schemas/__init__.py
  30. 35 0
      backend/app/schemas/affection.py
  31. 54 0
      backend/app/schemas/character.py
  32. 46 0
      backend/app/schemas/conversation.py
  33. 63 0
      backend/app/schemas/user.py
  34. 1 0
      backend/app/services/__init__.py
  35. 187 0
      backend/app/services/affection_service.py
  36. 235 0
      backend/app/services/llm_provider.py
  37. 124 0
      backend/app/services/proactive_message_service.py
  38. 1 0
      backend/app/tasks/__init__.py
  39. 38 0
      backend/app/tasks/celery_app.py
  40. 96 0
      backend/app/tasks/proactive_tasks.py
  41. 49 0
      backend/requirements.txt
  42. 92 0
      docker-compose.yml
  43. 127 0
      docs/ARCHITECTURE.md
  44. 124 0
      docs/QUICKSTART.md
  45. 111 0
      mobile/README.md
  46. 34 0
      mobile/package.json

+ 10 - 0
.claude/settings.local.json

@@ -0,0 +1,10 @@
+{
+  "permissions": {
+    "allow": [
+      "Bash(git init:*)",
+      "Bash(git add:*)"
+    ],
+    "deny": [],
+    "ask": []
+  }
+}

+ 30 - 0
.gitattributes

@@ -0,0 +1,30 @@
+# Auto detect text files and perform LF normalization
+* text=auto
+
+# Python files
+*.py text eol=lf
+*.pyi text eol=lf
+
+# JavaScript/TypeScript files
+*.js text eol=lf
+*.jsx text eol=lf
+*.ts text eol=lf
+*.tsx text eol=lf
+*.json text eol=lf
+
+# Config files
+*.yml text eol=lf
+*.yaml text eol=lf
+*.toml text eol=lf
+*.ini text eol=lf
+*.cfg text eol=lf
+
+# Shell scripts
+*.sh text eol=lf
+
+# Markdown
+*.md text eol=lf
+
+# Docker
+Dockerfile text eol=lf
+docker-compose*.yml text eol=lf

+ 78 - 0
.gitignore

@@ -0,0 +1,78 @@
+# Python
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+.Python
+venv/
+env/
+ENV/
+.venv
+
+# FastAPI
+*.db
+*.sqlite
+
+# 环境变量文件(包含敏感信息)
+.env
+.env.local
+.env.*.local
+
+# IDE
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+
+# 操作系统
+.DS_Store
+Thumbs.db
+
+# 日志
+*.log
+logs/
+
+# 数据库数据
+postgres_data/
+redis_data/
+
+# Celery
+celerybeat-schedule
+celerybeat.pid
+
+# React Native
+mobile/node_modules/
+mobile/.expo/
+mobile/.expo-shared/
+mobile/dist/
+mobile/npm-debug.*
+mobile/*.jks
+mobile/*.p8
+mobile/*.p12
+mobile/*.key
+mobile/*.mobileprovision
+mobile/*.orig.*
+mobile/web-build/
+
+# macOS
+mobile/ios/Pods/
+mobile/ios/build/
+
+# Android
+mobile/android/build/
+mobile/android/app/build/
+mobile/android/.gradle/
+mobile/android/local.properties
+
+# Firebase
+firebase-credentials.json
+
+# 测试
+.pytest_cache/
+.coverage
+htmlcov/
+
+# Docker volumes
+postgres_data
+redis_data

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 AI Chat App
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 428 - 0
README.md

@@ -0,0 +1,428 @@
+# AI角色对话App - 完整项目
+
+一个支持多AI平台、好感度系统和动态主动消息的移动端AI对话应用。
+
+## 🌟 核心功能
+
+- ✅ **多AI平台支持**:OpenAI GPT、Claude、通义千问、文心一言
+- ✅ **自定义AI角色**:创建个性化AI角色(性格、背景、语言)
+- ✅ **好感度系统**:-100~100分范围,7个等级(厌恶→恋人)
+- ✅ **动态主动消息**:根据好感度自动调整发送频率(1-360分钟)
+- ✅ **多轮对话**:独立对话历史,支持上下文记忆
+- ✅ **用户自管API Key**:安全加密存储用户的AI平台密钥
+- ✅ **国际化支持**:多语言、多时区
+
+---
+
+## 📁 项目结构
+
+```
+phoneapp/
+├── backend/                  # FastAPI后端
+│   ├── app/
+│   │   ├── api/             # API路由
+│   │   │   ├── auth.py      # 认证API
+│   │   │   ├── characters.py # 角色管理API
+│   │   │   ├── conversations.py # 对话API
+│   │   │   └── affection.py # 好感度API
+│   │   ├── models/          # SQLAlchemy模型
+│   │   │   ├── user.py
+│   │   │   ├── character.py
+│   │   │   ├── conversation.py
+│   │   │   ├── affection.py
+│   │   │   └── proactive_message.py
+│   │   ├── schemas/         # Pydantic Schema
+│   │   ├── services/        # 业务逻辑
+│   │   │   ├── llm_provider.py        # AI适配层
+│   │   │   ├── affection_service.py   # 好感度服务
+│   │   │   └── proactive_message_service.py
+│   │   ├── core/            # 核心功能
+│   │   │   ├── config.py    # 配置管理
+│   │   │   ├── database.py  # 数据库连接
+│   │   │   ├── redis.py     # Redis连接
+│   │   │   └── security.py  # 安全(JWT、加密)
+│   │   ├── tasks/           # Celery任务
+│   │   │   ├── celery_app.py
+│   │   │   └── proactive_tasks.py
+│   │   └── main.py          # FastAPI应用入口
+│   ├── alembic/             # 数据库迁移
+│   ├── requirements.txt
+│   ├── Dockerfile
+│   └── .env.example
+├── mobile/                   # React Native前端(待实现)
+├── docker-compose.yml       # Docker编排
+└── README.md                # 项目文档
+```
+
+---
+
+## 🚀 快速开始
+
+### 方式一:使用Docker(推荐)
+
+1. **复制环境变量文件**
+```bash
+cd backend
+cp .env.example .env
+```
+
+2. **生成加密密钥**
+```bash
+# 在Python中生成32字节密钥
+python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
+```
+将生成的密钥复制到`.env`文件的`ENCRYPTION_KEY`字段。
+
+3. **启动所有服务**
+```bash
+docker-compose up -d
+```
+
+4. **运行数据库迁移**
+```bash
+docker-compose exec backend alembic upgrade head
+```
+
+5. **访问应用**
+- API文档:http://localhost:8000/docs
+- API:http://localhost:8000
+
+### 方式二:本地开发
+
+#### 前置要求
+- Python 3.11+
+- PostgreSQL 15+
+- Redis 7+
+
+#### 步骤
+
+1. **安装Python依赖**
+```bash
+cd backend
+python -m venv venv
+source venv/bin/activate  # Windows: venv\Scripts\activate
+pip install -r requirements.txt
+```
+
+2. **配置环境变量**
+```bash
+cp .env.example .env
+# 编辑.env文件,填写数据库连接等信息
+```
+
+3. **运行数据库迁移**
+```bash
+alembic upgrade head
+```
+
+4. **启动FastAPI服务**
+```bash
+uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
+```
+
+5. **启动Celery Worker(新终端)**
+```bash
+celery -A app.tasks.celery_app worker --loglevel=info
+```
+
+6. **启动Celery Beat(新终端)**
+```bash
+celery -A app.tasks.celery_app beat --loglevel=info
+```
+
+---
+
+## 📖 API使用指南
+
+### 1. 用户注册
+```bash
+POST /api/auth/register
+Content-Type: application/json
+
+{
+  "username": "testuser",
+  "email": "test@example.com",
+  "password": "password123"
+}
+```
+
+### 2. 用户登录
+```bash
+POST /api/auth/login
+Content-Type: application/x-www-form-urlencoded
+
+username=testuser&password=password123
+```
+
+响应:
+```json
+{
+  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
+  "refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
+  "token_type": "bearer"
+}
+```
+
+### 3. 配置API Key
+```bash
+POST /api/auth/api-keys
+Authorization: Bearer {access_token}
+Content-Type: application/json
+
+{
+  "provider": "openai",
+  "api_key": "sk-proj-...",
+  "model": "gpt-3.5-turbo"
+}
+```
+
+### 4. 创建AI角色
+```bash
+POST /api/characters
+Authorization: Bearer {access_token}
+Content-Type: application/json
+
+{
+  "name": "小雪",
+  "personality": "温柔体贴,善解人意",
+  "background_story": "大学文学系学生,喜欢阅读和写作",
+  "avatar_url": "https://example.com/avatar.jpg",
+  "language": "zh",
+  "llm_provider": "openai",
+  "llm_model": "gpt-3.5-turbo",
+  "config": {
+    "temperature": 0.9
+  }
+}
+```
+
+### 5. 发送消息
+```bash
+POST /api/conversations/{conversation_id}/messages
+Authorization: Bearer {access_token}
+Content-Type: application/json
+
+{
+  "content": "今天天气真好!",
+  "stream": false
+}
+```
+
+响应:
+```json
+{
+  "id": 123,
+  "conversation_id": 1,
+  "role": "assistant",
+  "content": "是呀!这样的好天气适合出去走走,你有什么计划吗?",
+  "tokens_used": 28,
+  "is_proactive": false,
+  "created_at": "2025-12-07T10:30:00Z",
+  "affection_change": 2
+}
+```
+
+### 6. 查看好感度
+```bash
+GET /api/affection/{character_id}
+Authorization: Bearer {access_token}
+```
+
+响应:
+```json
+{
+  "character_id": 1,
+  "current_score": 65,
+  "level": "朋友",
+  "next_level_score": 80,
+  "total_interactions": 127,
+  "last_interaction": "2025-12-07T10:30:00Z",
+  "recent_changes": [
+    {
+      "id": 1,
+      "score_change": 3,
+      "reason": "用户分享积极情绪",
+      "created_at": "2025-12-07T10:30:00Z"
+    }
+  ]
+}
+```
+
+---
+
+## 🎯 核心功能详解
+
+### 好感度系统
+
+**等级划分**(-100 ~ 100分):
+- **厌恶** (-100 ~ -60):AI冷淡、距离感
+- **陌生** (-60 ~ -20):礼貌但疏远
+- **普通** (-20 ~ 20):中性态度
+- **熟人** (20 ~ 50):友好
+- **朋友** (50 ~ 80):亲近
+- **挚友** (80 ~ 95):深度信任
+- **恋人** (95 ~ 100):极度亲密
+
+**好感度变化因素**:
+- 积极对话:+2 ~ +5
+- 中性对话:+1
+- 消极对话:-1 ~ -3
+- 长时间沉默:-2(每天)
+- 深夜聊天:+2
+- 分享秘密:+5
+
+### 动态主动消息机制
+
+**触发规则**(根据好感度):
+
+| 好感度 | 检测间隔 | 随机发送间隔 |
+|--------|---------|-------------|
+| 90-100分 | 用户3分钟无对话 | 1-10分钟 |
+| 80-89分 | 用户10分钟无对话 | 10-30分钟 |
+| 60-79分 | 用户30分钟无对话 | 30-120分钟 |
+| 40-59分 | 用户2小时无对话 | 2-6小时 |
+| <40分 | 不发送主动消息 | - |
+
+**实现方式**:
+- Celery Beat每分钟扫描一次
+- 检查用户活跃状态表(`user_activity`)
+- 根据好感度计算是否发送
+- 调用AI生成自然的主动消息
+- 发送推送通知(移动端待实现)
+
+### AI平台适配
+
+**支持的平台**:
+1. **OpenAI**:GPT-3.5-turbo、GPT-4
+2. **Claude**:Claude 3 Sonnet/Opus
+3. **通义千问**:qwen-turbo、qwen-plus
+4. **文心一言**:ernie-bot-turbo
+
+**统一接口**:
+```python
+provider = get_llm_provider("openai")  # 或 "claude", "qwen", "ernie"
+response = await provider.chat_completion(messages, api_key, model)
+```
+
+---
+
+## 🔐 安全说明
+
+1. **API Key加密**:使用Fernet对称加密存储用户的AI平台密钥
+2. **密码加密**:使用bcrypt哈希存储密码
+3. **JWT认证**:所有API需要Bearer Token
+4. **HTTPS**:生产环境必须使用HTTPS
+5. **环境变量**:敏感信息通过`.env`文件管理,不提交到Git
+
+---
+
+## 📱 移动端开发(待实现)
+
+移动端将使用React Native开发,支持iOS和Android。
+
+### 技术栈
+- React Native 0.72+
+- TypeScript
+- React Navigation
+- Zustand(状态管理)
+- React Query(数据请求)
+- react-native-gifted-chat(聊天UI)
+
+### 计划功能
+- [ ] 用户注册/登录界面
+- [ ] AI角色列表和创建
+- [ ] 对话界面(支持流式回复)
+- [ ] 好感度可视化
+- [ ] 主动消息推送通知
+- [ ] 多语言切换
+
+---
+
+## 🛠️ 技术栈
+
+### 后端
+- **框架**:FastAPI 0.104+
+- **ORM**:SQLAlchemy 2.0(异步)
+- **数据库**:PostgreSQL 15
+- **缓存**:Redis 7
+- **任务队列**:Celery + Celery Beat
+- **认证**:JWT
+- **AI SDK**:openai、anthropic、dashscope、erniebot
+
+### 前端(计划)
+- React Native 0.72+
+- TypeScript
+- React Navigation
+- Zustand
+
+### 基础设施
+- Docker & Docker Compose
+- Nginx(反向代理)
+- PostgreSQL(主数据库)
+- Redis(缓存+队列)
+
+---
+
+## 📝 开发路线图
+
+### ✅ 已完成
+- [x] 后端项目搭建
+- [x] 数据库模型设计
+- [x] 用户认证系统
+- [x] AI平台适配层
+- [x] 好感度系统(支持负数)
+- [x] 动态主动消息机制
+- [x] Celery定时任务
+- [x] 核心API接口
+- [x] Docker配置
+
+### 🚧 进行中
+- [ ] WebSocket实时通信
+- [ ] 流式对话回复
+- [ ] 移动端UI开发
+
+### 📅 计划中
+- [ ] 推送通知集成
+- [ ] 对话历史搜索
+- [ ] 角色分享社区
+- [ ] 多媒体消息支持
+- [ ] 语音对话
+- [ ] 数据分析仪表板
+
+---
+
+## 🐛 常见问题
+
+### 1. 数据库连接失败
+确保PostgreSQL已启动,检查`.env`文件中的`DATABASE_URL`配置。
+
+### 2. Celery任务不执行
+确保Redis已启动,检查`CELERY_BROKER_URL`配置。
+
+### 3. AI调用失败
+检查用户是否配置了API Key,确认API Key有效且有余额。
+
+### 4. 加密密钥错误
+确保`.env`文件中的`ENCRYPTION_KEY`是32字节的Fernet密钥。
+
+---
+
+## 📄 许可证
+
+MIT License
+
+---
+
+## 👥 贡献
+
+欢迎提交Issue和Pull Request!
+
+---
+
+## 📧 联系方式
+
+如有问题,请提交Issue或联系开发者。
+
+---
+
+**祝你使用愉快!🎉**

+ 31 - 0
backend/.env.example

@@ -0,0 +1,31 @@
+# 数据库配置
+DATABASE_URL=postgresql+asyncpg://postgres:password@localhost:5432/ai_chat_app
+DATABASE_URL_SYNC=postgresql://postgres:password@localhost:5432/ai_chat_app
+
+# Redis配置
+REDIS_URL=redis://localhost:6379/0
+
+# JWT密钥(生产环境请使用随机生成的复杂密钥)
+JWT_SECRET_KEY=your-super-secret-jwt-key-change-this-in-production
+JWT_ALGORITHM=HS256
+ACCESS_TOKEN_EXPIRE_MINUTES=60
+REFRESH_TOKEN_EXPIRE_DAYS=30
+
+# 加密密钥(用于加密用户API Key)
+ENCRYPTION_KEY=your-32-byte-encryption-key-change-this
+
+# Celery配置
+CELERY_BROKER_URL=redis://localhost:6379/0
+CELERY_RESULT_BACKEND=redis://localhost:6379/0
+
+# Firebase推送配置(可选)
+FIREBASE_CREDENTIALS_PATH=./firebase-credentials.json
+
+# 应用配置
+APP_NAME=AI角色对话App
+APP_VERSION=1.0.0
+DEBUG=True
+CORS_ORIGINS=["http://localhost:3000","http://localhost:8081"]
+
+# 日志配置
+LOG_LEVEL=INFO

+ 24 - 0
backend/Dockerfile

@@ -0,0 +1,24 @@
+FROM python:3.11-slim
+
+WORKDIR /app
+
+# 安装系统依赖
+RUN apt-get update && apt-get install -y \
+    gcc \
+    postgresql-client \
+    && rm -rf /var/lib/apt/lists/*
+
+# 复制依赖文件
+COPY requirements.txt .
+
+# 安装Python依赖
+RUN pip install --no-cache-dir -r requirements.txt
+
+# 复制应用代码
+COPY . .
+
+# 暴露端口
+EXPOSE 8000
+
+# 默认命令
+CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

+ 39 - 0
backend/alembic.ini

@@ -0,0 +1,39 @@
+[alembic]
+# Alembic配置文件
+
+script_location = alembic
+prepend_sys_path = .
+version_path_separator = os
+
+[loggers]
+keys = root,sqlalchemy,alembic
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+
+[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

+ 67 - 0
backend/alembic/env.py

@@ -0,0 +1,67 @@
+"""
+Alembic环境配置
+"""
+from logging.config import fileConfig
+from sqlalchemy import engine_from_config, pool
+from alembic import context
+import sys
+import os
+
+# 添加项目路径
+sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
+
+# 导入配置和模型
+from app.core.config import settings
+from app.core.database import Base
+from app.models import *  # 导入所有模型
+
+# Alembic配置对象
+config = context.config
+
+# 设置数据库URL
+config.set_main_option("sqlalchemy.url", settings.DATABASE_URL_SYNC)
+
+# 配置日志
+if config.config_file_name is not None:
+    fileConfig(config.config_file_name)
+
+# 目标元数据
+target_metadata = Base.metadata
+
+
+def run_migrations_offline() -> None:
+    """离线模式运行迁移"""
+    url = config.get_main_option("sqlalchemy.url")
+    context.configure(
+        url=url,
+        target_metadata=target_metadata,
+        literal_binds=True,
+        dialect_opts={"paramstyle": "named"},
+    )
+
+    with context.begin_transaction():
+        context.run_migrations()
+
+
+def run_migrations_online() -> None:
+    """在线模式运行迁移"""
+    connectable = engine_from_config(
+        config.get_section(config.config_ini_section),
+        prefix="sqlalchemy.",
+        poolclass=pool.NullPool,
+    )
+
+    with connectable.connect() as connection:
+        context.configure(
+            connection=connection,
+            target_metadata=target_metadata
+        )
+
+        with context.begin_transaction():
+            context.run_migrations()
+
+
+if context.is_offline_mode():
+    run_migrations_offline()
+else:
+    run_migrations_online()

+ 173 - 0
backend/alembic/versions/001_initial.py

@@ -0,0 +1,173 @@
+"""初始化数据库表
+
+Revision ID: 001
+Create Date: 2025-12-07
+"""
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.dialects import postgresql
+
+# revision标识符
+revision = '001'
+down_revision = None
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+    """创建所有表"""
+
+    # 用户表
+    op.create_table(
+        'users',
+        sa.Column('id', sa.Integer(), nullable=False),
+        sa.Column('username', sa.String(length=50), nullable=False),
+        sa.Column('email', sa.String(length=100), nullable=False),
+        sa.Column('password_hash', sa.String(length=255), nullable=False),
+        sa.Column('avatar_url', sa.String(length=500)),
+        sa.Column('encrypted_api_keys', postgresql.JSON(), server_default='{}'),
+        sa.Column('timezone', sa.String(length=50), server_default='UTC'),
+        sa.Column('preferred_language', sa.String(length=10), server_default='en'),
+        sa.Column('is_active', sa.Boolean(), server_default='true'),
+        sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now()),
+        sa.Column('updated_at', sa.DateTime(timezone=True)),
+        sa.PrimaryKeyConstraint('id')
+    )
+    op.create_index('ix_users_username', 'users', ['username'], unique=True)
+    op.create_index('ix_users_email', 'users', ['email'], unique=True)
+
+    # 角色表
+    op.create_table(
+        'characters',
+        sa.Column('id', sa.Integer(), nullable=False),
+        sa.Column('user_id', sa.Integer(), nullable=False),
+        sa.Column('name', sa.String(length=100), nullable=False),
+        sa.Column('avatar_url', sa.String(length=500)),
+        sa.Column('personality', sa.Text()),
+        sa.Column('background_story', sa.Text()),
+        sa.Column('system_prompt', sa.Text(), nullable=False),
+        sa.Column('llm_provider', sa.String(length=50), server_default='openai'),
+        sa.Column('llm_model', sa.String(length=100), server_default='gpt-3.5-turbo'),
+        sa.Column('config', postgresql.JSON(), server_default='{}'),
+        sa.Column('language', sa.String(length=10), server_default='en'),
+        sa.Column('is_active', sa.Boolean(), server_default='true'),
+        sa.Column('is_preset', sa.Boolean(), server_default='false'),
+        sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now()),
+        sa.Column('updated_at', sa.DateTime(timezone=True)),
+        sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
+        sa.PrimaryKeyConstraint('id')
+    )
+    op.create_index('ix_characters_user_id', 'characters', ['user_id'])
+
+    # 对话表
+    op.create_table(
+        'conversations',
+        sa.Column('id', sa.Integer(), nullable=False),
+        sa.Column('user_id', sa.Integer(), nullable=False),
+        sa.Column('character_id', sa.Integer(), nullable=False),
+        sa.Column('title', sa.String(length=200)),
+        sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now()),
+        sa.Column('updated_at', sa.DateTime(timezone=True)),
+        sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
+        sa.ForeignKeyConstraint(['character_id'], ['characters.id'], ondelete='CASCADE'),
+        sa.PrimaryKeyConstraint('id')
+    )
+    op.create_index('idx_user_character', 'conversations', ['user_id', 'character_id'], unique=True)
+
+    # 消息表
+    op.create_table(
+        'messages',
+        sa.Column('id', sa.Integer(), nullable=False),
+        sa.Column('conversation_id', sa.Integer(), nullable=False),
+        sa.Column('role', sa.String(length=20), nullable=False),
+        sa.Column('content', sa.Text(), nullable=False),
+        sa.Column('tokens_used', sa.Integer(), server_default='0'),
+        sa.Column('is_proactive', sa.Boolean(), server_default='false'),
+        sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now()),
+        sa.ForeignKeyConstraint(['conversation_id'], ['conversations.id'], ondelete='CASCADE'),
+        sa.PrimaryKeyConstraint('id')
+    )
+    op.create_index('ix_messages_conversation_id', 'messages', ['conversation_id'])
+    op.create_index('ix_messages_created_at', 'messages', ['created_at'])
+
+    # 用户活跃状态表
+    op.create_table(
+        'user_activity',
+        sa.Column('id', sa.Integer(), nullable=False),
+        sa.Column('conversation_id', sa.Integer(), nullable=False),
+        sa.Column('last_message_at', sa.DateTime(timezone=True), nullable=False),
+        sa.Column('last_check_at', sa.DateTime(timezone=True)),
+        sa.Column('next_proactive_at', sa.DateTime(timezone=True)),
+        sa.ForeignKeyConstraint(['conversation_id'], ['conversations.id'], ondelete='CASCADE'),
+        sa.PrimaryKeyConstraint('id'),
+        sa.UniqueConstraint('conversation_id')
+    )
+    op.create_index('ix_user_activity_next_proactive_at', 'user_activity', ['next_proactive_at'])
+
+    # 好感度表
+    op.create_table(
+        'affection_scores',
+        sa.Column('id', sa.Integer(), nullable=False),
+        sa.Column('user_id', sa.Integer(), nullable=False),
+        sa.Column('character_id', sa.Integer(), nullable=False),
+        sa.Column('current_score', sa.Integer(), server_default='0', nullable=False),
+        sa.Column('level', sa.String(length=50), server_default='普通'),
+        sa.Column('last_interaction', sa.DateTime(timezone=True)),
+        sa.Column('total_interactions', sa.Integer(), server_default='0'),
+        sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now()),
+        sa.Column('updated_at', sa.DateTime(timezone=True)),
+        sa.CheckConstraint('current_score >= -100 AND current_score <= 100', name='score_range'),
+        sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
+        sa.ForeignKeyConstraint(['character_id'], ['characters.id'], ondelete='CASCADE'),
+        sa.PrimaryKeyConstraint('id')
+    )
+    op.create_index('idx_user_character_affection', 'affection_scores', ['user_id', 'character_id'], unique=True)
+
+    # 好感度历史表
+    op.create_table(
+        'affection_logs',
+        sa.Column('id', sa.Integer(), nullable=False),
+        sa.Column('affection_score_id', sa.Integer(), nullable=False),
+        sa.Column('message_id', sa.Integer()),
+        sa.Column('score_change', sa.Integer(), nullable=False),
+        sa.Column('reason', sa.Text()),
+        sa.Column('sentiment_analysis', postgresql.JSON()),
+        sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now()),
+        sa.ForeignKeyConstraint(['affection_score_id'], ['affection_scores.id'], ondelete='CASCADE'),
+        sa.ForeignKeyConstraint(['message_id'], ['messages.id'], ondelete='SET NULL'),
+        sa.PrimaryKeyConstraint('id')
+    )
+    op.create_index('ix_affection_logs_affection_score_id', 'affection_logs', ['affection_score_id'])
+    op.create_index('ix_affection_logs_created_at', 'affection_logs', ['created_at'])
+
+    # 主动消息任务表
+    op.create_table(
+        'proactive_message_tasks',
+        sa.Column('id', sa.Integer(), nullable=False),
+        sa.Column('user_id', sa.Integer(), nullable=False),
+        sa.Column('character_id', sa.Integer(), nullable=False),
+        sa.Column('scheduled_at', sa.DateTime(timezone=True), nullable=False),
+        sa.Column('status', sa.String(length=20), server_default='pending'),
+        sa.Column('trigger_reason', sa.String(length=100)),
+        sa.Column('message_content', sa.Text()),
+        sa.Column('sent_at', sa.DateTime(timezone=True)),
+        sa.Column('error_message', sa.Text()),
+        sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now()),
+        sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
+        sa.ForeignKeyConstraint(['character_id'], ['characters.id'], ondelete='CASCADE'),
+        sa.PrimaryKeyConstraint('id')
+    )
+    op.create_index('ix_proactive_tasks_scheduled_at', 'proactive_message_tasks', ['scheduled_at'])
+    op.create_index('ix_proactive_tasks_status', 'proactive_message_tasks', ['status'])
+
+
+def downgrade() -> None:
+    """删除所有表"""
+    op.drop_table('proactive_message_tasks')
+    op.drop_table('affection_logs')
+    op.drop_table('affection_scores')
+    op.drop_table('user_activity')
+    op.drop_table('messages')
+    op.drop_table('conversations')
+    op.drop_table('characters')
+    op.drop_table('users')

+ 1 - 0
backend/app/__init__.py

@@ -0,0 +1 @@
+"""初始化app模块"""

+ 1 - 0
backend/app/api/__init__.py

@@ -0,0 +1 @@
+"""初始化API模块"""

+ 57 - 0
backend/app/api/affection.py

@@ -0,0 +1,57 @@
+"""
+好感度API
+"""
+from fastapi import APIRouter, Depends, HTTPException
+from sqlalchemy.ext.asyncio import AsyncSession
+from sqlalchemy import select
+from app.core.database import get_db
+from app.models.user import User
+from app.models.affection import AffectionScore, AffectionLog
+from app.schemas.affection import AffectionScoreResponse, AffectionDetailResponse, AffectionLogResponse
+from app.api.auth import get_current_user
+from app.services.affection_service import get_next_level_score
+from typing import List
+
+router = APIRouter()
+
+
+@router.get("/{character_id}", response_model=AffectionDetailResponse)
+async def get_affection(
+    character_id: int,
+    current_user: User = Depends(get_current_user),
+    db: AsyncSession = Depends(get_db)
+):
+    """获取与某个AI角色的好感度"""
+
+    result = await db.execute(
+        select(AffectionScore).where(
+            AffectionScore.user_id == current_user.id,
+            AffectionScore.character_id == character_id
+        )
+    )
+    affection = result.scalar_one_or_none()
+
+    if not affection:
+        raise HTTPException(status_code=404, detail="好感度记录不存在")
+
+    # 获取最近的好感度变化记录
+    logs_result = await db.execute(
+        select(AffectionLog)
+        .where(AffectionLog.affection_score_id == affection.id)
+        .order_by(AffectionLog.created_at.desc())
+        .limit(10)
+    )
+    recent_logs = logs_result.scalars().all()
+
+    # 构建响应
+    response = AffectionDetailResponse(
+        character_id=character_id,
+        current_score=affection.current_score,
+        level=affection.level,
+        next_level_score=get_next_level_score(affection.current_score),
+        total_interactions=affection.total_interactions,
+        last_interaction=affection.last_interaction,
+        recent_changes=[AffectionLogResponse.from_orm(log) for log in recent_logs]
+    )
+
+    return response

+ 150 - 0
backend/app/api/auth.py

@@ -0,0 +1,150 @@
+"""
+认证相关API
+"""
+from fastapi import APIRouter, Depends, HTTPException, status
+from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
+from sqlalchemy.ext.asyncio import AsyncSession
+from sqlalchemy import select
+from app.core.database import get_db
+from app.core.security import (
+    verify_password,
+    get_password_hash,
+    create_access_token,
+    create_refresh_token,
+    decode_token,
+    encrypt_api_key
+)
+from app.models.user import User
+from app.schemas.user import UserCreate, UserResponse, TokenResponse, APIKeyCreate, APIKeyResponse
+from typing import List
+
+router = APIRouter()
+oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")
+
+
+async def get_current_user(
+    token: str = Depends(oauth2_scheme),
+    db: AsyncSession = Depends(get_db)
+) -> User:
+    """获取当前用户(依赖注入)"""
+    payload = decode_token(token)
+    if not payload:
+        raise HTTPException(
+            status_code=status.HTTP_401_UNAUTHORIZED,
+            detail="无效的令牌"
+        )
+
+    user_id = payload.get("user_id")
+    result = await db.execute(select(User).where(User.id == user_id))
+    user = result.scalar_one_or_none()
+
+    if not user or not user.is_active:
+        raise HTTPException(
+            status_code=status.HTTP_401_UNAUTHORIZED,
+            detail="用户不存在或已禁用"
+        )
+
+    return user
+
+
+@router.post("/register", response_model=UserResponse)
+async def register(user_data: UserCreate, db: AsyncSession = Depends(get_db)):
+    """用户注册"""
+    # 检查用户名是否存在
+    result = await db.execute(select(User).where(User.username == user_data.username))
+    if result.scalar_one_or_none():
+        raise HTTPException(status_code=400, detail="用户名已存在")
+
+    # 检查邮箱是否存在
+    result = await db.execute(select(User).where(User.email == user_data.email))
+    if result.scalar_one_or_none():
+        raise HTTPException(status_code=400, detail="邮箱已被注册")
+
+    # 创建用户
+    user = User(
+        username=user_data.username,
+        email=user_data.email,
+        password_hash=get_password_hash(user_data.password)
+    )
+    db.add(user)
+    await db.commit()
+    await db.refresh(user)
+
+    return user
+
+
+@router.post("/login", response_model=TokenResponse)
+async def login(
+    form_data: OAuth2PasswordRequestForm = Depends(),
+    db: AsyncSession = Depends(get_db)
+):
+    """用户登录"""
+    # 查找用户
+    result = await db.execute(select(User).where(User.username == form_data.username))
+    user = result.scalar_one_or_none()
+
+    if not user or not verify_password(form_data.password, user.password_hash):
+        raise HTTPException(
+            status_code=status.HTTP_401_UNAUTHORIZED,
+            detail="用户名或密码错误"
+        )
+
+    # 生成令牌
+    access_token = create_access_token({"user_id": user.id})
+    refresh_token = create_refresh_token({"user_id": user.id})
+
+    return {
+        "access_token": access_token,
+        "refresh_token": refresh_token,
+        "token_type": "bearer"
+    }
+
+
+@router.get("/me", response_model=UserResponse)
+async def get_current_user_info(current_user: User = Depends(get_current_user)):
+    """获取当前用户信息"""
+    return current_user
+
+
+@router.post("/api-keys", response_model=APIKeyResponse)
+async def save_api_key(
+    api_key_data: APIKeyCreate,
+    current_user: User = Depends(get_current_user),
+    db: AsyncSession = Depends(get_db)
+):
+    """保存用户的API Key"""
+    # 加密API Key
+    encrypted_key = encrypt_api_key(api_key_data.api_key)
+
+    # 更新用户的加密API Keys
+    if not current_user.encrypted_api_keys:
+        current_user.encrypted_api_keys = {}
+
+    current_user.encrypted_api_keys[api_key_data.provider] = {
+        "key": encrypted_key,
+        "model": api_key_data.model
+    }
+
+    await db.commit()
+
+    return {
+        "provider": api_key_data.provider,
+        "model": api_key_data.model,
+        "status": "active"
+    }
+
+
+@router.get("/api-keys", response_model=List[APIKeyResponse])
+async def get_api_keys(current_user: User = Depends(get_current_user)):
+    """获取用户配置的API Key列表(不返回实际key)"""
+    if not current_user.encrypted_api_keys:
+        return []
+
+    return [
+        {
+            "provider": provider,
+            "model": config.get("model"),
+            "status": "active"
+        }
+        for provider, config in current_user.encrypted_api_keys.items()
+    ]

+ 162 - 0
backend/app/api/characters.py

@@ -0,0 +1,162 @@
+"""
+AI角色管理API
+"""
+from fastapi import APIRouter, Depends, HTTPException
+from sqlalchemy.ext.asyncio import AsyncSession
+from sqlalchemy import select
+from app.core.database import get_db
+from app.models.user import User
+from app.models.character import Character
+from app.schemas.character import CharacterCreate, CharacterResponse, CharacterUpdate
+from app.api.auth import get_current_user
+from typing import List
+
+router = APIRouter()
+
+
+def generate_system_prompt(character_data: CharacterCreate) -> str:
+    """根据角色信息生成系统提示词"""
+    prompt = f"""你是{character_data.name}。
+
+性格特点:{character_data.personality or '友好、乐于助人'}
+
+背景故事:{character_data.background_story or '无特殊背景'}
+
+重要指示:
+1. 始终保持角色一致性,用第一人称"我"来回答
+2. 根据对话内容在回复末尾附带情感分析(JSON格式)
+3. 使用{character_data.language}语言回复
+4. 回复自然、真实,避免机械感
+
+情感分析格式(必须包含在每次回复中):
+<sentiment>{{"affection_change": -5到5的整数, "reason": "简短说明"}}</sentiment>
+
+示例:
+用户:今天心情不太好
+你:怎么了?发生什么事了吗?愿意和我聊聊吗?<sentiment>{{"affection_change": 2, "reason": "用户主动分享情绪,表示信任"}}</sentiment>
+"""
+    return prompt
+
+
+@router.post("", response_model=CharacterResponse)
+async def create_character(
+    character_data: CharacterCreate,
+    current_user: User = Depends(get_current_user),
+    db: AsyncSession = Depends(get_db)
+):
+    """创建AI角色"""
+    # 生成系统提示词
+    system_prompt = generate_system_prompt(character_data)
+
+    character = Character(
+        user_id=current_user.id,
+        name=character_data.name,
+        personality=character_data.personality,
+        background_story=character_data.background_story,
+        avatar_url=character_data.avatar_url,
+        language=character_data.language,
+        llm_provider=character_data.llm_provider,
+        llm_model=character_data.llm_model,
+        system_prompt=system_prompt,
+        config=character_data.config
+    )
+
+    db.add(character)
+    await db.commit()
+    await db.refresh(character)
+
+    return character
+
+
+@router.get("", response_model=List[CharacterResponse])
+async def list_characters(
+    current_user: User = Depends(get_current_user),
+    db: AsyncSession = Depends(get_db)
+):
+    """获取用户的所有AI角色"""
+    result = await db.execute(
+        select(Character)
+        .where(Character.user_id == current_user.id, Character.is_active == True)
+        .order_by(Character.created_at.desc())
+    )
+    characters = result.scalars().all()
+    return characters
+
+
+@router.get("/{character_id}", response_model=CharacterResponse)
+async def get_character(
+    character_id: int,
+    current_user: User = Depends(get_current_user),
+    db: AsyncSession = Depends(get_db)
+):
+    """获取AI角色详情"""
+    result = await db.execute(
+        select(Character).where(
+            Character.id == character_id,
+            Character.user_id == current_user.id
+        )
+    )
+    character = result.scalar_one_or_none()
+
+    if not character:
+        raise HTTPException(status_code=404, detail="角色不存在")
+
+    return character
+
+
+@router.put("/{character_id}", response_model=CharacterResponse)
+async def update_character(
+    character_id: int,
+    character_data: CharacterUpdate,
+    current_user: User = Depends(get_current_user),
+    db: AsyncSession = Depends(get_db)
+):
+    """更新AI角色"""
+    result = await db.execute(
+        select(Character).where(
+            Character.id == character_id,
+            Character.user_id == current_user.id
+        )
+    )
+    character = result.scalar_one_or_none()
+
+    if not character:
+        raise HTTPException(status_code=404, detail="角色不存在")
+
+    # 更新字段
+    update_data = character_data.dict(exclude_unset=True)
+    for field, value in update_data.items():
+        setattr(character, field, value)
+
+    # 如果更新了基础信息,重新生成系统提示词
+    if any(k in update_data for k in ['name', 'personality', 'background_story', 'language']):
+        character.system_prompt = generate_system_prompt(CharacterCreate(**character.__dict__))
+
+    await db.commit()
+    await db.refresh(character)
+
+    return character
+
+
+@router.delete("/{character_id}")
+async def delete_character(
+    character_id: int,
+    current_user: User = Depends(get_current_user),
+    db: AsyncSession = Depends(get_db)
+):
+    """删除AI角色"""
+    result = await db.execute(
+        select(Character).where(
+            Character.id == character_id,
+            Character.user_id == current_user.id
+        )
+    )
+    character = result.scalar_one_or_none()
+
+    if not character:
+        raise HTTPException(status_code=404, detail="角色不存在")
+
+    await db.delete(character)
+    await db.commit()
+
+    return {"message": "角色已删除"}

+ 151 - 0
backend/app/api/conversations.py

@@ -0,0 +1,151 @@
+"""
+对话API(简化版,完整版需包含流式响应和WebSocket)
+"""
+from fastapi import APIRouter, Depends, HTTPException
+from sqlalchemy.ext.asyncio import AsyncSession
+from sqlalchemy import select
+from app.core.database import get_db
+from app.models.user import User
+from app.models.character import Character
+from app.models.conversation import Conversation, Message, UserActivity
+from app.models.affection import AffectionScore
+from app.schemas.conversation import MessageCreate, MessageResponse, ConversationResponse
+from app.api.auth import get_current_user
+from app.services.llm_provider import get_llm_provider
+from app.services.affection_service import AffectionService
+from app.core.security import decrypt_api_key
+from datetime import datetime
+from typing import List
+
+router = APIRouter()
+
+
+@router.post("/{conversation_id}/messages", response_model=MessageResponse)
+async def send_message(
+    conversation_id: int,
+    message_data: MessageCreate,
+    current_user: User = Depends(get_current_user),
+    db: AsyncSession = Depends(get_db)
+):
+    """发送消息并获取AI回复"""
+
+    # 获取对话和角色
+    result = await db.execute(
+        select(Conversation, Character)
+        .join(Character, Conversation.character_id == Character.id)
+        .where(
+            Conversation.id == conversation_id,
+            Conversation.user_id == current_user.id
+        )
+    )
+    conv_char = result.one_or_none()
+
+    if not conv_char:
+        raise HTTPException(status_code=404, detail="对话不存在")
+
+    conversation, character = conv_char
+
+    # 保存用户消息
+    user_message = Message(
+        conversation_id=conversation_id,
+        role="user",
+        content=message_data.content
+    )
+    db.add(user_message)
+    await db.commit()
+
+    # 获取对话历史
+    history_result = await db.execute(
+        select(Message)
+        .where(Message.conversation_id == conversation_id)
+        .order_by(Message.created_at.desc())
+        .limit(10)
+    )
+    history = list(reversed(history_result.scalars().all()))
+
+    # 构建消息列表
+    messages = [{"role": "system", "content": character.system_prompt}]
+    messages.extend([{"role": msg.role, "content": msg.content} for msg in history])
+
+    # 调用AI
+    provider = get_llm_provider(character.llm_provider)
+
+    # 获取并解密API Key
+    api_key_config = current_user.encrypted_api_keys.get(character.llm_provider)
+    if not api_key_config:
+        raise HTTPException(status_code=400, detail=f"请先配置{character.llm_provider}的API Key")
+
+    api_key = decrypt_api_key(api_key_config["key"])
+
+    # 调用AI
+    ai_response = await provider.chat_completion(
+        messages=messages,
+        api_key=api_key,
+        model=character.llm_model,
+        temperature=character.config.get("temperature", 0.8)
+    )
+
+    # 解析情感分析
+    from app.services.affection_service import AffectionService
+    sentiment_result = AffectionService.analyze_sentiment_from_ai_response(ai_response)
+
+    # 保存AI回复
+    ai_message = Message(
+        conversation_id=conversation_id,
+        role="assistant",
+        content=sentiment_result["content"]
+    )
+    db.add(ai_message)
+    await db.commit()
+    await db.refresh(ai_message)
+
+    # 更新好感度
+    affection = await AffectionService.get_or_create_affection(
+        db, current_user.id, character.id
+    )
+    await AffectionService.update_affection(
+        db,
+        affection.id,
+        sentiment_result["affection_change"],
+        sentiment_result["reason"],
+        ai_message.id,
+        sentiment_result["sentiment_data"]
+    )
+
+    # 更新活跃状态
+    activity_result = await db.execute(
+        select(UserActivity).where(UserActivity.conversation_id == conversation_id)
+    )
+    activity = activity_result.scalar_one_or_none()
+
+    if activity:
+        activity.last_message_at = datetime.now()
+    else:
+        activity = UserActivity(
+            conversation_id=conversation_id,
+            last_message_at=datetime.now()
+        )
+        db.add(activity)
+
+    await db.commit()
+
+    # 构建响应
+    response = MessageResponse.from_orm(ai_message)
+    response.affection_change = sentiment_result["affection_change"]
+
+    return response
+
+
+@router.get("", response_model=List[ConversationResponse])
+async def list_conversations(
+    current_user: User = Depends(get_current_user),
+    db: AsyncSession = Depends(get_db)
+):
+    """获取用户的所有对话列表"""
+    result = await db.execute(
+        select(Conversation)
+        .where(Conversation.user_id == current_user.id)
+        .order_by(Conversation.updated_at.desc())
+    )
+    conversations = result.scalars().all()
+    return conversations

+ 6 - 0
backend/app/core/__init__.py

@@ -0,0 +1,6 @@
+"""
+AI服务提供商统一接口
+
+由于代码较长,这里展示完整实现...
+继续创建所有必要的文件
+"""

+ 60 - 0
backend/app/core/config.py

@@ -0,0 +1,60 @@
+"""
+应用配置管理
+"""
+from pydantic_settings import BaseSettings
+from pydantic import validator
+from typing import List
+import os
+
+
+class Settings(BaseSettings):
+    """应用配置"""
+
+    # 基础配置
+    APP_NAME: str = "AI角色对话App"
+    APP_VERSION: str = "1.0.0"
+    DEBUG: bool = True
+
+    # 数据库配置
+    DATABASE_URL: str
+    DATABASE_URL_SYNC: str
+
+    # Redis配置
+    REDIS_URL: str
+
+    # JWT配置
+    JWT_SECRET_KEY: str
+    JWT_ALGORITHM: str = "HS256"
+    ACCESS_TOKEN_EXPIRE_MINUTES: int = 60
+    REFRESH_TOKEN_EXPIRE_DAYS: int = 30
+
+    # 加密配置
+    ENCRYPTION_KEY: str
+
+    # Celery配置
+    CELERY_BROKER_URL: str
+    CELERY_RESULT_BACKEND: str
+
+    # CORS配置
+    CORS_ORIGINS: List[str] = []
+
+    # Firebase配置
+    FIREBASE_CREDENTIALS_PATH: str = ""
+
+    # 日志配置
+    LOG_LEVEL: str = "INFO"
+
+    @validator("CORS_ORIGINS", pre=True)
+    def parse_cors_origins(cls, v):
+        """解析CORS origins"""
+        if isinstance(v, str):
+            return [origin.strip() for origin in v.split(",")]
+        return v
+
+    class Config:
+        env_file = ".env"
+        case_sensitive = True
+
+
+# 创建全局配置实例
+settings = Settings()

+ 56 - 0
backend/app/core/database.py

@@ -0,0 +1,56 @@
+"""
+数据库连接管理
+"""
+from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
+from sqlalchemy.orm import declarative_base
+from sqlalchemy import create_engine
+from sqlalchemy.orm import sessionmaker
+from app.core.config import settings
+
+# 异步引擎(用于FastAPI)
+async_engine = create_async_engine(
+    settings.DATABASE_URL,
+    echo=settings.DEBUG,
+    pool_pre_ping=True,
+    pool_size=10,
+    max_overflow=20,
+)
+
+# 异步会话工厂
+AsyncSessionLocal = async_sessionmaker(
+    async_engine,
+    class_=AsyncSession,
+    expire_on_commit=False,
+)
+
+# 同步引擎(用于Alembic迁移)
+sync_engine = create_engine(
+    settings.DATABASE_URL_SYNC,
+    echo=settings.DEBUG,
+    pool_pre_ping=True,
+)
+
+# 同步会话工厂
+SessionLocal = sessionmaker(
+    autocommit=False,
+    autoflush=False,
+    bind=sync_engine,
+)
+
+# 基础模型类
+Base = declarative_base()
+
+
+async def get_db() -> AsyncSession:
+    """
+    获取数据库会话(依赖注入)
+    """
+    async with AsyncSessionLocal() as session:
+        try:
+            yield session
+            await session.commit()
+        except Exception:
+            await session.rollback()
+            raise
+        finally:
+            await session.close()

+ 34 - 0
backend/app/core/redis.py

@@ -0,0 +1,34 @@
+"""
+Redis连接管理
+"""
+import redis.asyncio as aioredis
+from redis.asyncio import Redis
+from app.core.config import settings
+from typing import Optional
+
+# Redis连接池
+redis_pool: Optional[Redis] = None
+
+
+async def get_redis() -> Redis:
+    """
+    获取Redis连接
+    """
+    global redis_pool
+    if redis_pool is None:
+        redis_pool = await aioredis.from_url(
+            settings.REDIS_URL,
+            encoding="utf-8",
+            decode_responses=True,
+            max_connections=50,
+        )
+    return redis_pool
+
+
+async def close_redis():
+    """
+    关闭Redis连接
+    """
+    global redis_pool
+    if redis_pool:
+        await redis_pool.close()

+ 76 - 0
backend/app/core/security.py

@@ -0,0 +1,76 @@
+"""
+安全相关工具:JWT、密码加密、API Key加密
+"""
+from datetime import datetime, timedelta
+from typing import Optional, Dict
+from jose import JWTError, jwt
+from passlib.context import CryptContext
+from cryptography.fernet import Fernet
+from app.core.config import settings
+
+# 密码加密上下文
+pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
+
+# API Key加密器
+cipher = Fernet(settings.ENCRYPTION_KEY.encode())
+
+
+def verify_password(plain_password: str, hashed_password: str) -> bool:
+    """验证密码"""
+    return pwd_context.verify(plain_password, hashed_password)
+
+
+def get_password_hash(password: str) -> str:
+    """生成密码哈希"""
+    return pwd_context.hash(password)
+
+
+def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
+    """
+    创建访问令牌
+    """
+    to_encode = data.copy()
+    if expires_delta:
+        expire = datetime.utcnow() + expires_delta
+    else:
+        expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
+
+    to_encode.update({"exp": expire, "type": "access"})
+    encoded_jwt = jwt.encode(to_encode, settings.JWT_SECRET_KEY, algorithm=settings.JWT_ALGORITHM)
+    return encoded_jwt
+
+
+def create_refresh_token(data: dict) -> str:
+    """
+    创建刷新令牌
+    """
+    to_encode = data.copy()
+    expire = datetime.utcnow() + timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS)
+    to_encode.update({"exp": expire, "type": "refresh"})
+    encoded_jwt = jwt.encode(to_encode, settings.JWT_SECRET_KEY, algorithm=settings.JWT_ALGORITHM)
+    return encoded_jwt
+
+
+def decode_token(token: str) -> Optional[Dict]:
+    """
+    解码令牌
+    """
+    try:
+        payload = jwt.decode(token, settings.JWT_SECRET_KEY, algorithms=[settings.JWT_ALGORITHM])
+        return payload
+    except JWTError:
+        return None
+
+
+def encrypt_api_key(api_key: str) -> str:
+    """
+    加密API Key
+    """
+    return cipher.encrypt(api_key.encode()).decode()
+
+
+def decrypt_api_key(encrypted_key: str) -> str:
+    """
+    解密API Key
+    """
+    return cipher.decrypt(encrypted_key.encode()).decode()

+ 61 - 0
backend/app/main.py

@@ -0,0 +1,61 @@
+"""
+FastAPI主应用
+"""
+from fastapi import FastAPI
+from fastapi.middleware.cors import CORSMiddleware
+from app.core.config import settings
+from app.core import redis
+from app.api import auth, characters, conversations, affection
+from loguru import logger
+
+# 创建FastAPI应用
+app = FastAPI(
+    title=settings.APP_NAME,
+    version=settings.APP_VERSION,
+    debug=settings.DEBUG
+)
+
+# CORS中间件
+app.add_middleware(
+    CORSMiddleware,
+    allow_origins=settings.CORS_ORIGINS,
+    allow_credentials=True,
+    allow_methods=["*"],
+    allow_headers=["*"],
+)
+
+# 注册路由
+app.include_router(auth.router, prefix="/api/auth", tags=["认证"])
+app.include_router(characters.router, prefix="/api/characters", tags=["AI角色"])
+app.include_router(conversations.router, prefix="/api/conversations", tags=["对话"])
+app.include_router(affection.router, prefix="/api/affection", tags=["好感度"])
+
+
+@app.on_event("startup")
+async def startup_event():
+    """应用启动事件"""
+    logger.info(f"{settings.APP_NAME} 启动中...")
+    # 可以在这里初始化Redis连接等
+
+
+@app.on_event("shutdown")
+async def shutdown_event():
+    """应用关闭事件"""
+    logger.info("正在关闭应用...")
+    await redis.close_redis()
+
+
+@app.get("/")
+async def root():
+    """根路径"""
+    return {
+        "app": settings.APP_NAME,
+        "version": settings.APP_VERSION,
+        "status": "running"
+    }
+
+
+@app.get("/health")
+async def health_check():
+    """健康检查"""
+    return {"status": "healthy"}

+ 19 - 0
backend/app/models/__init__.py

@@ -0,0 +1,19 @@
+"""
+导出所有模型
+"""
+from app.models.user import User
+from app.models.character import Character
+from app.models.conversation import Conversation, Message, UserActivity
+from app.models.affection import AffectionScore, AffectionLog
+from app.models.proactive_message import ProactiveMessageTask
+
+__all__ = [
+    "User",
+    "Character",
+    "Conversation",
+    "Message",
+    "UserActivity",
+    "AffectionScore",
+    "AffectionLog",
+    "ProactiveMessageTask",
+]

+ 55 - 0
backend/app/models/affection.py

@@ -0,0 +1,55 @@
+"""
+好感度模型
+"""
+from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Index, Text, JSON, CheckConstraint
+from sqlalchemy.orm import relationship
+from sqlalchemy.sql import func
+from app.core.database import Base
+
+
+class AffectionScore(Base):
+    """好感度表"""
+    __tablename__ = "affection_scores"
+
+    id = Column(Integer, primary_key=True, index=True)
+    user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
+    character_id = Column(Integer, ForeignKey("characters.id", ondelete="CASCADE"), nullable=False)
+
+    # 好感度数值(-100 ~ 100)
+    current_score = Column(Integer, default=0, nullable=False)
+    level = Column(String(50), default="普通")  # 厌恶 | 陌生 | 普通 | 熟人 | 朋友 | 挚友 | 恋人
+
+    # 统计信息
+    last_interaction = Column(DateTime(timezone=True))
+    total_interactions = Column(Integer, default=0)
+
+    created_at = Column(DateTime(timezone=True), server_default=func.now())
+    updated_at = Column(DateTime(timezone=True), onupdate=func.now())
+
+    # 约束
+    __table_args__ = (
+        Index("idx_user_character_affection", "user_id", "character_id", unique=True),
+        CheckConstraint("current_score >= -100 AND current_score <= 100", name="score_range"),
+    )
+
+    # 关系
+    character = relationship("Character", back_populates="affection_scores")
+    logs = relationship("AffectionLog", back_populates="affection_score", cascade="all, delete-orphan")
+
+
+class AffectionLog(Base):
+    """好感度变化历史表"""
+    __tablename__ = "affection_logs"
+
+    id = Column(Integer, primary_key=True, index=True)
+    affection_score_id = Column(Integer, ForeignKey("affection_scores.id", ondelete="CASCADE"), nullable=False, index=True)
+    message_id = Column(Integer, ForeignKey("messages.id", ondelete="SET NULL"))
+
+    score_change = Column(Integer, nullable=False)  # 积分变化(可为负)
+    reason = Column(Text)  # 变化原因
+    sentiment_analysis = Column(JSON)  # 情感分析详情
+
+    created_at = Column(DateTime(timezone=True), server_default=func.now(), index=True)
+
+    # 关系
+    affection_score = relationship("AffectionScore", back_populates="logs")

+ 41 - 0
backend/app/models/character.py

@@ -0,0 +1,41 @@
+"""
+AI角色模型
+"""
+from sqlalchemy import Column, Integer, String, Text, Boolean, DateTime, JSON, ForeignKey
+from sqlalchemy.orm import relationship
+from sqlalchemy.sql import func
+from app.core.database import Base
+
+
+class Character(Base):
+    """AI角色表"""
+    __tablename__ = "characters"
+
+    id = Column(Integer, primary_key=True, index=True)
+    user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
+
+    # 基础信息
+    name = Column(String(100), nullable=False)
+    avatar_url = Column(String(500))
+    personality = Column(Text)  # 性格描述
+    background_story = Column(Text)  # 背景故事
+    system_prompt = Column(Text, nullable=False)  # 生成的系统提示词
+
+    # AI配置
+    llm_provider = Column(String(50), default="openai")  # openai | claude | qwen | ernie
+    llm_model = Column(String(100), default="gpt-3.5-turbo")
+    config = Column(JSON, default={})  # temperature, max_tokens等
+
+    # 语言
+    language = Column(String(10), default="en")
+
+    # 状态
+    is_active = Column(Boolean, default=True)
+    is_preset = Column(Boolean, default=False)  # 是否为预设角色
+    created_at = Column(DateTime(timezone=True), server_default=func.now())
+    updated_at = Column(DateTime(timezone=True), onupdate=func.now())
+
+    # 关系
+    user = relationship("User", back_populates="characters")
+    conversations = relationship("Conversation", back_populates="character", cascade="all, delete-orphan")
+    affection_scores = relationship("AffectionScore", back_populates="character", cascade="all, delete-orphan")

+ 66 - 0
backend/app/models/conversation.py

@@ -0,0 +1,66 @@
+"""
+对话模型
+"""
+from sqlalchemy import Column, Integer, String, Text, Boolean, DateTime, ForeignKey, Index
+from sqlalchemy.orm import relationship
+from sqlalchemy.sql import func
+from app.core.database import Base
+
+
+class Conversation(Base):
+    """对话会话表"""
+    __tablename__ = "conversations"
+
+    id = Column(Integer, primary_key=True, index=True)
+    user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
+    character_id = Column(Integer, ForeignKey("characters.id", ondelete="CASCADE"), nullable=False)
+
+    title = Column(String(200))  # 对话标题
+    created_at = Column(DateTime(timezone=True), server_default=func.now())
+    updated_at = Column(DateTime(timezone=True), onupdate=func.now())
+
+    # 唯一约束:每个用户与角色只有一个对话
+    __table_args__ = (
+        Index("idx_user_character", "user_id", "character_id", unique=True),
+    )
+
+    # 关系
+    user = relationship("User", back_populates="conversations")
+    character = relationship("Character", back_populates="conversations")
+    messages = relationship("Message", back_populates="conversation", cascade="all, delete-orphan")
+    user_activity = relationship("UserActivity", back_populates="conversation", uselist=False, cascade="all, delete-orphan")
+
+
+class Message(Base):
+    """消息表"""
+    __tablename__ = "messages"
+
+    id = Column(Integer, primary_key=True, index=True)
+    conversation_id = Column(Integer, ForeignKey("conversations.id", ondelete="CASCADE"), nullable=False, index=True)
+
+    role = Column(String(20), nullable=False)  # user | assistant | system
+    content = Column(Text, nullable=False)
+
+    # 统计信息
+    tokens_used = Column(Integer, default=0)
+    is_proactive = Column(Boolean, default=False)  # 是否为AI主动发送
+
+    created_at = Column(DateTime(timezone=True), server_default=func.now(), index=True)
+
+    # 关系
+    conversation = relationship("Conversation", back_populates="messages")
+
+
+class UserActivity(Base):
+    """用户活跃状态表(用于主动消息检测)"""
+    __tablename__ = "user_activity"
+
+    id = Column(Integer, primary_key=True, index=True)
+    conversation_id = Column(Integer, ForeignKey("conversations.id", ondelete="CASCADE"), unique=True, nullable=False)
+
+    last_message_at = Column(DateTime(timezone=True), nullable=False)
+    last_check_at = Column(DateTime(timezone=True))  # 上次检查主动消息的时间
+    next_proactive_at = Column(DateTime(timezone=True), index=True)  # 计划的下次主动消息时间
+
+    # 关系
+    conversation = relationship("Conversation", back_populates="user_activity")

+ 26 - 0
backend/app/models/proactive_message.py

@@ -0,0 +1,26 @@
+"""
+主动消息任务模型
+"""
+from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey
+from sqlalchemy.orm import relationship
+from sqlalchemy.sql import func
+from app.core.database import Base
+
+
+class ProactiveMessageTask(Base):
+    """主动消息任务表"""
+    __tablename__ = "proactive_message_tasks"
+
+    id = Column(Integer, primary_key=True, index=True)
+    user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
+    character_id = Column(Integer, ForeignKey("characters.id", ondelete="CASCADE"), nullable=False)
+
+    scheduled_at = Column(DateTime(timezone=True), nullable=False, index=True)
+    status = Column(String(20), default="pending", index=True)  # pending | sent | failed
+    trigger_reason = Column(String(100))  # high_affection | idle_detected
+
+    message_content = Column(Text)
+    sent_at = Column(DateTime(timezone=True))
+    error_message = Column(Text)
+
+    created_at = Column(DateTime(timezone=True), server_default=func.now())

+ 34 - 0
backend/app/models/user.py

@@ -0,0 +1,34 @@
+"""
+用户模型
+"""
+from sqlalchemy import Column, Integer, String, Boolean, DateTime, JSON
+from sqlalchemy.orm import relationship
+from sqlalchemy.sql import func
+from app.core.database import Base
+
+
+class User(Base):
+    """用户表"""
+    __tablename__ = "users"
+
+    id = Column(Integer, primary_key=True, index=True)
+    username = Column(String(50), unique=True, nullable=False, index=True)
+    email = Column(String(100), unique=True, nullable=False, index=True)
+    password_hash = Column(String(255), nullable=False)
+    avatar_url = Column(String(500))
+
+    # API Key配置(加密存储)
+    encrypted_api_keys = Column(JSON, default={})
+
+    # 国际化配置
+    timezone = Column(String(50), default="UTC")
+    preferred_language = Column(String(10), default="en")
+
+    # 状态
+    is_active = Column(Boolean, default=True)
+    created_at = Column(DateTime(timezone=True), server_default=func.now())
+    updated_at = Column(DateTime(timezone=True), onupdate=func.now())
+
+    # 关系
+    characters = relationship("Character", back_populates="user", cascade="all, delete-orphan")
+    conversations = relationship("Conversation", back_populates="user", cascade="all, delete-orphan")

+ 1 - 0
backend/app/schemas/__init__.py

@@ -0,0 +1 @@
+"""初始化schemas模块"""

+ 35 - 0
backend/app/schemas/affection.py

@@ -0,0 +1,35 @@
+"""
+好感度相关Schema
+"""
+from pydantic import BaseModel
+from typing import Optional, List
+from datetime import datetime
+
+
+class AffectionScoreResponse(BaseModel):
+    """好感度响应Schema"""
+    character_id: int
+    current_score: int
+    level: str
+    next_level_score: Optional[int]
+    total_interactions: int
+    last_interaction: Optional[datetime]
+
+    class Config:
+        from_attributes = True
+
+
+class AffectionLogResponse(BaseModel):
+    """好感度变化记录Schema"""
+    id: int
+    score_change: int
+    reason: Optional[str]
+    created_at: datetime
+
+    class Config:
+        from_attributes = True
+
+
+class AffectionDetailResponse(AffectionScoreResponse):
+    """好感度详情Schema"""
+    recent_changes: List[AffectionLogResponse] = []

+ 54 - 0
backend/app/schemas/character.py

@@ -0,0 +1,54 @@
+"""
+AI角色相关Schema
+"""
+from pydantic import BaseModel, validator
+from typing import Optional, Dict
+from datetime import datetime
+
+
+class CharacterBase(BaseModel):
+    """角色基础Schema"""
+    name: str
+    personality: Optional[str] = None
+    background_story: Optional[str] = None
+    avatar_url: Optional[str] = None
+    language: str = "en"
+
+
+class CharacterCreate(CharacterBase):
+    """角色创建Schema"""
+    llm_provider: str = "openai"
+    llm_model: str = "gpt-3.5-turbo"
+    config: Optional[Dict] = {}
+
+    @validator("llm_provider")
+    def validate_provider(cls, v):
+        allowed = ["openai", "claude", "qwen", "ernie"]
+        if v not in allowed:
+            raise ValueError(f"不支持的AI平台: {v}")
+        return v
+
+
+class CharacterUpdate(BaseModel):
+    """角色更新Schema"""
+    name: Optional[str] = None
+    personality: Optional[str] = None
+    background_story: Optional[str] = None
+    avatar_url: Optional[str] = None
+    llm_provider: Optional[str] = None
+    llm_model: Optional[str] = None
+    config: Optional[Dict] = None
+
+
+class CharacterResponse(CharacterBase):
+    """角色响应Schema"""
+    id: int
+    user_id: int
+    llm_provider: str
+    llm_model: str
+    system_prompt: str
+    is_active: bool
+    created_at: datetime
+
+    class Config:
+        from_attributes = True

+ 46 - 0
backend/app/schemas/conversation.py

@@ -0,0 +1,46 @@
+"""
+对话和消息相关Schema
+"""
+from pydantic import BaseModel
+from typing import Optional, List
+from datetime import datetime
+
+
+class MessageCreate(BaseModel):
+    """消息创建Schema"""
+    content: str
+    stream: bool = False  # 是否使用流式返回
+
+
+class MessageResponse(BaseModel):
+    """消息响应Schema"""
+    id: int
+    conversation_id: int
+    role: str
+    content: str
+    tokens_used: int
+    is_proactive: bool
+    created_at: datetime
+    affection_change: Optional[int] = None  # 好感度变化
+
+    class Config:
+        from_attributes = True
+
+
+class ConversationResponse(BaseModel):
+    """对话响应Schema"""
+    id: int
+    user_id: int
+    character_id: int
+    title: Optional[str]
+    created_at: datetime
+    updated_at: datetime
+    last_message: Optional[MessageResponse] = None
+
+    class Config:
+        from_attributes = True
+
+
+class ConversationDetail(ConversationResponse):
+    """对话详情Schema"""
+    messages: List[MessageResponse] = []

+ 63 - 0
backend/app/schemas/user.py

@@ -0,0 +1,63 @@
+"""
+用户相关Schema
+"""
+from pydantic import BaseModel, EmailStr, validator
+from typing import Optional, Dict
+from datetime import datetime
+
+
+class UserBase(BaseModel):
+    """用户基础Schema"""
+    username: str
+    email: EmailStr
+
+
+class UserCreate(UserBase):
+    """用户创建Schema"""
+    password: str
+
+    @validator("password")
+    def validate_password(cls, v):
+        if len(v) < 6:
+            raise ValueError("密码长度至少6位")
+        return v
+
+
+class UserLogin(BaseModel):
+    """用户登录Schema"""
+    username: str
+    password: str
+
+
+class UserResponse(UserBase):
+    """用户响应Schema"""
+    id: int
+    avatar_url: Optional[str] = None
+    timezone: str
+    preferred_language: str
+    is_active: bool
+    created_at: datetime
+
+    class Config:
+        from_attributes = True
+
+
+class TokenResponse(BaseModel):
+    """令牌响应Schema"""
+    access_token: str
+    refresh_token: str
+    token_type: str = "bearer"
+
+
+class APIKeyCreate(BaseModel):
+    """API Key创建Schema"""
+    provider: str  # openai | claude | qwen | ernie
+    api_key: str
+    model: Optional[str] = None
+
+
+class APIKeyResponse(BaseModel):
+    """API Key响应Schema"""
+    provider: str
+    model: Optional[str]
+    status: str = "active"

+ 1 - 0
backend/app/services/__init__.py

@@ -0,0 +1 @@
+"""初始化services模块"""

+ 187 - 0
backend/app/services/affection_service.py

@@ -0,0 +1,187 @@
+"""
+好感度计算和管理服务
+"""
+from typing import Dict, Optional
+from datetime import datetime
+from sqlalchemy.ext.asyncio import AsyncSession
+from sqlalchemy import select, update
+from app.models.affection import AffectionScore, AffectionLog
+from app.models.conversation import Message
+import random
+import json
+
+
+# 好感度等级定义
+AFFECTION_LEVELS = {
+    "厌恶": (-100, -60),
+    "陌生": (-60, -20),
+    "普通": (-20, 20),
+    "熟人": (20, 50),
+    "朋友": (50, 80),
+    "挚友": (80, 95),
+    "恋人": (95, 101),  # 101是上限
+}
+
+
+def get_affection_level(score: int) -> str:
+    """根据分数获取好感度等级"""
+    for level, (min_score, max_score) in AFFECTION_LEVELS.items():
+        if min_score <= score < max_score:
+            return level
+    return "恋人"  # 满分
+
+
+def get_next_level_score(current_score: int) -> Optional[int]:
+    """获取下一等级所需分数"""
+    current_level = get_affection_level(current_score)
+    level_names = list(AFFECTION_LEVELS.keys())
+
+    if current_level == "恋人":
+        return None  # 已达最高等级
+
+    current_index = level_names.index(current_level)
+    next_level = level_names[current_index + 1]
+    return AFFECTION_LEVELS[next_level][0]
+
+
+class AffectionService:
+    """好感度服务"""
+
+    @staticmethod
+    async def get_or_create_affection(
+        db: AsyncSession,
+        user_id: int,
+        character_id: int
+    ) -> AffectionScore:
+        """获取或创建好感度记录"""
+        result = await db.execute(
+            select(AffectionScore).where(
+                AffectionScore.user_id == user_id,
+                AffectionScore.character_id == character_id
+            )
+        )
+        affection = result.scalar_one_or_none()
+
+        if not affection:
+            affection = AffectionScore(
+                user_id=user_id,
+                character_id=character_id,
+                current_score=0,
+                level="普通"
+            )
+            db.add(affection)
+            await db.commit()
+            await db.refresh(affection)
+
+        return affection
+
+    @staticmethod
+    async def update_affection(
+        db: AsyncSession,
+        affection_id: int,
+        score_change: int,
+        reason: str,
+        message_id: Optional[int] = None,
+        sentiment_data: Optional[Dict] = None
+    ):
+        """更新好感度"""
+        # 获取当前好感度
+        result = await db.execute(
+            select(AffectionScore).where(AffectionScore.id == affection_id)
+        )
+        affection = result.scalar_one()
+
+        # 计算新分数(限制在-100到100之间)
+        new_score = max(-100, min(100, affection.current_score + score_change))
+        new_level = get_affection_level(new_score)
+
+        # 更新好感度
+        await db.execute(
+            update(AffectionScore)
+            .where(AffectionScore.id == affection_id)
+            .values(
+                current_score=new_score,
+                level=new_level,
+                last_interaction=datetime.now(),
+                total_interactions=AffectionScore.total_interactions + 1
+            )
+        )
+
+        # 创建变化记录
+        log = AffectionLog(
+            affection_score_id=affection_id,
+            message_id=message_id,
+            score_change=score_change,
+            reason=reason,
+            sentiment_analysis=sentiment_data
+        )
+        db.add(log)
+        await db.commit()
+
+        return new_score, new_level
+
+    @staticmethod
+    def analyze_sentiment_from_ai_response(ai_response: str) -> Dict:
+        """
+        从AI回复中提取情感分析结果
+        AI回复格式应包含:<sentiment>{"affection_change": 3, "reason": "..."}</sentiment>
+        """
+        try:
+            # 查找<sentiment>标签
+            start = ai_response.find("<sentiment>")
+            end = ai_response.find("</sentiment>")
+
+            if start != -1 and end != -1:
+                sentiment_json = ai_response[start + 11:end]
+                sentiment_data = json.loads(sentiment_json)
+
+                # 提取纯文本内容(去除sentiment标签)
+                clean_content = ai_response[:start] + ai_response[end + 12:]
+
+                return {
+                    "content": clean_content.strip(),
+                    "affection_change": sentiment_data.get("affection_change", 0),
+                    "reason": sentiment_data.get("reason", ""),
+                    "sentiment_data": sentiment_data
+                }
+        except Exception as e:
+            # 解析失败,使用默认值
+            pass
+
+        # 默认返回
+        return {
+            "content": ai_response,
+            "affection_change": 1,  # 默认+1
+            "reason": "正常对话",
+            "sentiment_data": {}
+        }
+
+
+def calculate_proactive_interval(affection_score: int) -> Optional[int]:
+    """
+    根据好感度计算主动消息间隔(分钟)
+    返回:随机间隔的分钟数,如果不应发送则返回None
+    """
+    if affection_score >= 90:
+        return random.randint(1, 10)  # 1-10分钟
+    elif affection_score >= 80:
+        return random.randint(10, 30)  # 10-30分钟
+    elif affection_score >= 60:
+        return random.randint(30, 120)  # 0.5-2小时
+    elif affection_score >= 40:
+        return random.randint(120, 360)  # 2-6小时
+    else:
+        return None  # 不发送
+
+
+def should_send_proactive(affection_score: int, minutes_idle: int) -> bool:
+    """判断是否应该发送主动消息"""
+    if affection_score >= 90 and minutes_idle >= 3:
+        return True
+    elif affection_score >= 80 and minutes_idle >= 10:
+        return True
+    elif affection_score >= 60 and minutes_idle >= 30:
+        return True
+    elif affection_score >= 40 and minutes_idle >= 120:
+        return True
+    return False

+ 235 - 0
backend/app/services/llm_provider.py

@@ -0,0 +1,235 @@
+"""
+AI服务提供商基类和多平台实现
+"""
+from abc import ABC, abstractmethod
+from typing import List, Dict, Optional, AsyncGenerator
+import openai
+import anthropic
+import dashscope
+import httpx
+from app.core.config import settings
+
+
+class BaseLLMProvider(ABC):
+    """AI服务提供商抽象基类"""
+
+    @abstractmethod
+    async def chat_completion(
+        self,
+        messages: List[Dict[str, str]],
+        api_key: str,
+        model: str = None,
+        **kwargs
+    ) -> str:
+        """同步对话补全"""
+        pass
+
+    @abstractmethod
+    async def stream_completion(
+        self,
+        messages: List[Dict[str, str]],
+        api_key: str,
+        model: str = None,
+        **kwargs
+    ) -> AsyncGenerator[str, None]:
+        """流式对话补全"""
+        pass
+
+
+class OpenAIProvider(BaseLLMProvider):
+    """OpenAI服务提供商"""
+
+    async def chat_completion(
+        self,
+        messages: List[Dict[str, str]],
+        api_key: str,
+        model: str = "gpt-3.5-turbo",
+        **kwargs
+    ) -> str:
+        client = openai.AsyncOpenAI(api_key=api_key)
+        response = await client.chat.completions.create(
+            model=model,
+            messages=messages,
+            **kwargs
+        )
+        return response.choices[0].message.content
+
+    async def stream_completion(
+        self,
+        messages: List[Dict[str, str]],
+        api_key: str,
+        model: str = "gpt-3.5-turbo",
+        **kwargs
+    ) -> AsyncGenerator[str, None]:
+        client = openai.AsyncOpenAI(api_key=api_key)
+        stream = await client.chat.completions.create(
+            model=model,
+            messages=messages,
+            stream=True,
+            **kwargs
+        )
+        async for chunk in stream:
+            if chunk.choices[0].delta.content:
+                yield chunk.choices[0].delta.content
+
+
+class ClaudeProvider(BaseLLMProvider):
+    """Claude服务提供商"""
+
+    async def chat_completion(
+        self,
+        messages: List[Dict[str, str]],
+        api_key: str,
+        model: str = "claude-3-sonnet-20240229",
+        **kwargs
+    ) -> str:
+        client = anthropic.AsyncAnthropic(api_key=api_key)
+        # 提取system消息
+        system_msg = next((m["content"] for m in messages if m["role"] == "system"), None)
+        user_messages = [m for m in messages if m["role"] != "system"]
+
+        response = await client.messages.create(
+            model=model,
+            system=system_msg,
+            messages=user_messages,
+            max_tokens=kwargs.get("max_tokens", 1024),
+            **{k: v for k, v in kwargs.items() if k != "max_tokens"}
+        )
+        return response.content[0].text
+
+    async def stream_completion(
+        self,
+        messages: List[Dict[str, str]],
+        api_key: str,
+        model: str = "claude-3-sonnet-20240229",
+        **kwargs
+    ) -> AsyncGenerator[str, None]:
+        client = anthropic.AsyncAnthropic(api_key=api_key)
+        system_msg = next((m["content"] for m in messages if m["role"] == "system"), None)
+        user_messages = [m for m in messages if m["role"] != "system"]
+
+        async with client.messages.stream(
+            model=model,
+            system=system_msg,
+            messages=user_messages,
+            max_tokens=kwargs.get("max_tokens", 1024),
+        ) as stream:
+            async for text in stream.text_stream:
+                yield text
+
+
+class QwenProvider(BaseLLMProvider):
+    """通义千问服务提供商"""
+
+    async def chat_completion(
+        self,
+        messages: List[Dict[str, str]],
+        api_key: str,
+        model: str = "qwen-turbo",
+        **kwargs
+    ) -> str:
+        dashscope.api_key = api_key
+        response = await dashscope.Generation.call(
+            model=model,
+            messages=messages,
+            result_format="message",
+            **kwargs
+        )
+        return response.output.choices[0].message.content
+
+    async def stream_completion(
+        self,
+        messages: List[Dict[str, str]],
+        api_key: str,
+        model: str = "qwen-turbo",
+        **kwargs
+    ) -> AsyncGenerator[str, None]:
+        dashscope.api_key = api_key
+        responses = dashscope.Generation.call(
+            model=model,
+            messages=messages,
+            result_format="message",
+            stream=True,
+            **kwargs
+        )
+        for response in responses:
+            if response.status_code == 200:
+                yield response.output.choices[0].message.content
+
+
+class ErnieProvider(BaseLLMProvider):
+    """文心一言服务提供商"""
+
+    async def chat_completion(
+        self,
+        messages: List[Dict[str, str]],
+        api_key: str,
+        model: str = "ernie-bot-turbo",
+        **kwargs
+    ) -> str:
+        # 文心一言需要先获取access_token
+        access_token = await self._get_access_token(api_key)
+
+        async with httpx.AsyncClient() as client:
+            response = await client.post(
+                f"https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/{model}",
+                params={"access_token": access_token},
+                json={"messages": messages, **kwargs}
+            )
+            data = response.json()
+            return data["result"]
+
+    async def stream_completion(
+        self,
+        messages: List[Dict[str, str]],
+        api_key: str,
+        model: str = "ernie-bot-turbo",
+        **kwargs
+    ) -> AsyncGenerator[str, None]:
+        access_token = await self._get_access_token(api_key)
+
+        async with httpx.AsyncClient() as client:
+            async with client.stream(
+                "POST",
+                f"https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/{model}",
+                params={"access_token": access_token},
+                json={"messages": messages, "stream": True, **kwargs}
+            ) as response:
+                async for line in response.aiter_lines():
+                    if line.startswith("data: "):
+                        import json
+                        data = json.loads(line[6:])
+                        if "result" in data:
+                            yield data["result"]
+
+    async def _get_access_token(self, api_key: str) -> str:
+        """获取百度access_token(简化实现,实际需要缓存)"""
+        # api_key格式: "api_key:secret_key"
+        api_key_part, secret_key = api_key.split(":")
+
+        async with httpx.AsyncClient() as client:
+            response = await client.get(
+                "https://aip.baidubce.com/oauth/2.0/token",
+                params={
+                    "grant_type": "client_credentials",
+                    "client_id": api_key_part,
+                    "client_secret": secret_key
+                }
+            )
+            return response.json()["access_token"]
+
+
+# 工厂函数
+def get_llm_provider(provider_name: str) -> BaseLLMProvider:
+    """获取AI服务提供商实例"""
+    providers = {
+        "openai": OpenAIProvider(),
+        "claude": ClaudeProvider(),
+        "qwen": QwenProvider(),
+        "ernie": ErnieProvider(),
+    }
+
+    if provider_name not in providers:
+        raise ValueError(f"不支持的AI平台: {provider_name}")
+
+    return providers[provider_name]

+ 124 - 0
backend/app/services/proactive_message_service.py

@@ -0,0 +1,124 @@
+"""
+主动消息生成和发送服务
+"""
+from typing import List, Dict
+from datetime import datetime, timedelta
+from sqlalchemy.ext.asyncio import AsyncSession
+from sqlalchemy import select
+from app.models.conversation import Message, Conversation, UserActivity
+from app.models.character import Character
+from app.models.affection import AffectionScore
+from app.services.llm_provider import get_llm_provider
+from app.core.security import decrypt_api_key
+import random
+
+
+class ProactiveMessageService:
+    """主动消息服务"""
+
+    @staticmethod
+    async def generate_proactive_message(
+        db: AsyncSession,
+        conversation_id: int,
+        affection_score: int,
+        user_api_keys: Dict[str, str]
+    ) -> str:
+        """生成主动消息内容"""
+
+        # 获取对话和角色信息
+        result = await db.execute(
+            select(Conversation, Character)
+            .join(Character, Conversation.character_id == Character.id)
+            .where(Conversation.id == conversation_id)
+        )
+        conv, character = result.one()
+
+        # 获取最近对话历史
+        recent_messages = await ProactiveMessageService._get_recent_messages(db, conversation_id, limit=5)
+
+        # 构建提示词
+        prompt = ProactiveMessageService._build_proactive_prompt(
+            character, affection_score, recent_messages
+        )
+
+        # 调用AI生成消息
+        provider = get_llm_provider(character.llm_provider)
+
+        # 解密API Key
+        encrypted_key = user_api_keys.get(character.llm_provider)
+        if not encrypted_key:
+            raise ValueError(f"用户未配置{character.llm_provider}平台的API Key")
+
+        api_key = decrypt_api_key(encrypted_key)
+
+        messages = [
+            {"role": "system", "content": character.system_prompt},
+            {"role": "user", "content": prompt}
+        ]
+
+        response = await provider.chat_completion(
+            messages=messages,
+            api_key=api_key,
+            model=character.llm_model,
+            temperature=character.config.get("temperature", 0.9),
+            max_tokens=150  # 主动消息简短一些
+        )
+
+        return response.strip()
+
+    @staticmethod
+    async def _get_recent_messages(
+        db: AsyncSession,
+        conversation_id: int,
+        limit: int = 5
+    ) -> List[Message]:
+        """获取最近的消息"""
+        result = await db.execute(
+            select(Message)
+            .where(Message.conversation_id == conversation_id)
+            .order_by(Message.created_at.desc())
+            .limit(limit)
+        )
+        messages = result.scalars().all()
+        return list(reversed(messages))  # 反转为时间正序
+
+    @staticmethod
+    def _build_proactive_prompt(
+        character: Character,
+        affection_score: int,
+        recent_messages: List[Message]
+    ) -> str:
+        """构建主动消息生成提示"""
+
+        # 格式化最近对话
+        context = "\n".join([
+            f"{'用户' if msg.role == 'user' else '我'}: {msg.content}"
+            for msg in recent_messages[-3:]  # 只用最近3条
+        ])
+
+        # 根据好感度选择消息类型
+        if affection_score >= 95:
+            message_type = "恋人之间的甜蜜问候或想念"
+        elif affection_score >= 80:
+            message_type = "挚友之间的关心或分享"
+        elif affection_score >= 60:
+            message_type = "朋友之间的友好问候"
+        else:
+            message_type = "熟人之间的礼貌问候"
+
+        prompt = f"""
+你是{character.name},当前与用户的好感度等级对应的亲密程度需要表现为:{message_type}。
+
+最近对话内容:
+{context if context else "(还没有对话记录)"}
+
+现在你想主动给用户发一条消息。要求:
+1. 自然、不突兀,像真实的{character.name}一样
+2. 可以提及之前的对话内容(如果有的话)
+3. 根据好感度调整语气和内容的亲密程度
+4. 30-50字以内
+5. 不要重复说过的话
+
+请直接生成消息内容:
+"""
+        return prompt

+ 1 - 0
backend/app/tasks/__init__.py

@@ -0,0 +1 @@
+"""初始化tasks模块"""

+ 38 - 0
backend/app/tasks/celery_app.py

@@ -0,0 +1,38 @@
+"""
+Celery应用配置
+"""
+from celery import Celery
+from celery.schedules import crontab
+from app.core.config import settings
+
+# 创建Celery应用
+celery_app = Celery(
+    "ai_chat_tasks",
+    broker=settings.CELERY_BROKER_URL,
+    backend=settings.CELERY_RESULT_BACKEND
+)
+
+# Celery配置
+celery_app.conf.update(
+    task_serializer="json",
+    accept_content=["json"],
+    result_serializer="json",
+    timezone="UTC",
+    enable_utc=True,
+    task_track_started=True,
+    task_time_limit=300,  # 5分钟超时
+)
+
+# 定时任务配置
+celery_app.conf.beat_schedule = {
+    # 每分钟检查一次主动消息
+    "check-proactive-messages": {
+        "task": "app.tasks.proactive_tasks.check_proactive_messages_task",
+        "schedule": 60.0,  # 每60秒执行一次
+    },
+    # 每天凌晨3点清理旧数据
+    "cleanup-old-data": {
+        "task": "app.tasks.cleanup_tasks.cleanup_old_messages_task",
+        "schedule": crontab(hour=3, minute=0),
+    },
+}

+ 96 - 0
backend/app/tasks/proactive_tasks.py

@@ -0,0 +1,96 @@
+"""
+主动消息定时任务
+"""
+from app.tasks.celery_app import celery_app
+from app.core.database import SessionLocal
+from app.models.conversation import UserActivity, Conversation
+from app.models.user import User
+from app.models.affection import AffectionScore
+from app.services.affection_service import should_send_proactive, calculate_proactive_interval
+from app.services.proactive_message_service import ProactiveMessageService
+from sqlalchemy import select
+from datetime import datetime, timedelta
+from loguru import logger
+
+
+@celery_app.task
+def check_proactive_messages_task():
+    """
+    定时任务:检查并发送主动消息
+    每分钟执行一次
+    """
+    logger.info("开始检查主动消息...")
+
+    db = SessionLocal()
+    try:
+        # 查询需要检查的对话
+        now = datetime.now()
+
+        result = db.execute(
+            select(
+                UserActivity,
+                Conversation,
+                AffectionScore,
+                User
+            )
+            .join(Conversation, UserActivity.conversation_id == Conversation.id)
+            .join(
+                AffectionScore,
+                (AffectionScore.user_id == Conversation.user_id) &
+                (AffectionScore.character_id == Conversation.character_id)
+            )
+            .join(User, Conversation.user_id == User.id)
+            .where(
+                AffectionScore.current_score >= 40,  # 只检查好感度 >= 40 的
+                UserActivity.last_message_at < now - timedelta(minutes=3),  # 超过3分钟未互动
+                (UserActivity.next_proactive_at == None) |
+                (UserActivity.next_proactive_at <= now)
+            )
+        )
+
+        count = 0
+        for activity, conv, affection, user in result:
+            try:
+                # 计算空闲时间
+                minutes_idle = (now - activity.last_message_at).total_seconds() / 60
+
+                # 判断是否应该发送
+                if should_send_proactive(affection.current_score, minutes_idle):
+                    # 生成并发送消息(这里需要异步转同步)
+                    # 实际生产环境建议使用异步任务队列
+                    logger.info(
+                        f"准备为用户 {user.id} 和角色 {conv.character_id} 发送主动消息"
+                    )
+
+                    # 这里简化处理,实际应该调用异步函数
+                    # 可以发送到另一个队列或直接保存到待发送表
+
+                    # 计算下次发送时间
+                    interval = calculate_proactive_interval(affection.current_score)
+                    if interval:
+                        next_time = now + timedelta(minutes=interval)
+                        activity.next_proactive_at = next_time
+                        db.commit()
+
+                    count += 1
+
+            except Exception as e:
+                logger.error(f"处理对话 {conv.id} 时出错: {e}")
+                continue
+
+        logger.info(f"主动消息检查完成,触发 {count} 条消息")
+
+    except Exception as e:
+        logger.error(f"检查主动消息任务失败: {e}")
+    finally:
+        db.close()
+
+
+@celery_app.task
+def send_proactive_message_task(conversation_id: int):
+    """
+    异步任务:生成并发送主动消息
+    """
+    # 这里需要实现实际的发送逻辑
+    # 包括调用AI生成、保存到数据库、发送推送通知等
+    pass

+ 49 - 0
backend/requirements.txt

@@ -0,0 +1,49 @@
+# FastAPI核心
+fastapi==0.104.1
+uvicorn[standard]==0.24.0
+gunicorn==21.2.0
+python-multipart==0.0.6
+pydantic==2.5.0
+pydantic-settings==2.1.0
+
+# 数据库
+sqlalchemy==2.0.23
+alembic==1.13.0
+asyncpg==0.29.0
+psycopg2-binary==2.9.9
+
+# Redis
+redis==5.0.1
+hiredis==2.2.3
+
+# Celery任务队列
+celery==5.3.4
+celery-beat==2.5.0
+
+# 认证与安全
+python-jose[cryptography]==3.3.0
+passlib[bcrypt]==1.7.4
+python-dotenv==1.0.0
+cryptography==41.0.7
+
+# AI平台SDK
+openai==1.3.7
+anthropic==0.7.7
+dashscope==1.14.1  # 阿里通义千问
+erniebot==0.2.5    # 百度文心一言
+
+# 工具库
+httpx==0.25.2
+python-dateutil==2.8.2
+pytz==2023.3
+
+# 推送服务
+firebase-admin==6.3.0
+
+# 监控日志
+loguru==0.7.2
+prometheus-client==0.19.0
+
+# 测试
+pytest==7.4.3
+pytest-asyncio==0.21.1

+ 92 - 0
docker-compose.yml

@@ -0,0 +1,92 @@
+version: '3.8'
+
+services:
+  # PostgreSQL数据库
+  postgres:
+    image: postgres:15-alpine
+    container_name: ai_chat_postgres
+    environment:
+      POSTGRES_USER: postgres
+      POSTGRES_PASSWORD: password
+      POSTGRES_DB: ai_chat_app
+    ports:
+      - "5432:5432"
+    volumes:
+      - postgres_data:/var/lib/postgresql/data
+    networks:
+      - ai_chat_network
+
+  # Redis缓存
+  redis:
+    image: redis:7-alpine
+    container_name: ai_chat_redis
+    command: redis-server --appendonly yes
+    ports:
+      - "6379:6379"
+    volumes:
+      - redis_data:/data
+    networks:
+      - ai_chat_network
+
+  # FastAPI后端
+  backend:
+    build: ./backend
+    container_name: ai_chat_backend
+    command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
+    ports:
+      - "8000:8000"
+    volumes:
+      - ./backend:/app
+    environment:
+      - DATABASE_URL=postgresql+asyncpg://postgres:password@postgres:5432/ai_chat_app
+      - DATABASE_URL_SYNC=postgresql://postgres:password@postgres:5432/ai_chat_app
+      - REDIS_URL=redis://redis:6379/0
+      - CELERY_BROKER_URL=redis://redis:6379/0
+      - CELERY_RESULT_BACKEND=redis://redis:6379/0
+    depends_on:
+      - postgres
+      - redis
+    networks:
+      - ai_chat_network
+
+  # Celery Worker
+  celery_worker:
+    build: ./backend
+    container_name: ai_chat_celery_worker
+    command: celery -A app.tasks.celery_app worker --loglevel=info
+    volumes:
+      - ./backend:/app
+    environment:
+      - DATABASE_URL=postgresql+asyncpg://postgres:password@postgres:5432/ai_chat_app
+      - DATABASE_URL_SYNC=postgresql://postgres:password@postgres:5432/ai_chat_app
+      - REDIS_URL=redis://redis:6379/0
+      - CELERY_BROKER_URL=redis://redis:6379/0
+      - CELERY_RESULT_BACKEND=redis://redis:6379/0
+    depends_on:
+      - postgres
+      - redis
+    networks:
+      - ai_chat_network
+
+  # Celery Beat
+  celery_beat:
+    build: ./backend
+    container_name: ai_chat_celery_beat
+    command: celery -A app.tasks.celery_app beat --loglevel=info
+    volumes:
+      - ./backend:/app
+    environment:
+      - REDIS_URL=redis://redis:6379/0
+      - CELERY_BROKER_URL=redis://redis:6379/0
+    depends_on:
+      - redis
+    networks:
+      - ai_chat_network
+
+networks:
+  ai_chat_network:
+    driver: bridge
+
+volumes:
+  postgres_data:
+  redis_data:

+ 127 - 0
docs/ARCHITECTURE.md

@@ -0,0 +1,127 @@
+# 项目架构设计文档
+
+## 系统架构图
+
+```
+┌─────────────────────────────────────────────────────────┐
+│              移动端 (React Native)                      │
+│  ┌──────────┐  ┌──────────┐  ┌──────────┐              │
+│  │ 登录     │  │ 角色管理 │  │ 对话界面 │              │
+│  └──────────┘  └──────────┘  └──────────┘              │
+│         │             │             │                    │
+│         └─────────────┴─────────────┘                    │
+│                  │ HTTPS                                 │
+└──────────────────┼──────────────────────────────────────┘
+                   │
+┌──────────────────┼──────────────────────────────────────┐
+│           FastAPI 后端服务                              │
+│  ┌──────────┐  ┌──────────┐  ┌──────────┐              │
+│  │ 认证API  │  │ 角色API  │  │ 对话API  │              │
+│  └──────────┘  └──────────┘  └──────────┘              │
+│  ┌──────────────────────────────────────┐              │
+│  │        AI适配层(多平台统一)         │              │
+│  └──────────────────────────────────────┘              │
+│  ┌──────────┐  ┌──────────┐  ┌──────────┐              │
+│  │好感度服务│  │主动消息  │  │ Celery   │              │
+│  └──────────┘  └──────────┘  └──────────┘              │
+└──────────────────┼──────────────────────────────────────┘
+                   │
+        ┌──────────┼──────────┐
+        │          │          │
+┌───────▼──┐ ┌─────▼────┐ ┌──▼─────────┐
+│PostgreSQL│ │  Redis   │ │ AI平台API  │
+│          │ │          │ │(用户密钥)  │
+└──────────┘ └──────────┘ └────────────┘
+```
+
+## 数据流
+
+### 对话流程
+1. 用户发送消息 → Mobile App
+2. App调用 POST /api/conversations/{id}/messages
+3. 后端保存用户消息
+4. 获取对话历史(最近10条)
+5. 调用用户配置的AI平台(使用解密的API Key)
+6. AI返回回复(包含情感分析)
+7. 解析回复,提取好感度变化
+8. 更新好感度表
+9. 保存AI回复
+10. 返回给App
+
+### 主动消息流程
+1. Celery Beat每分钟触发检查任务
+2. 查询满足条件的对话(好感度≥40,空闲≥3分钟)
+3. 计算是否应该发送(概率判断)
+4. 调用AI生成主动消息
+5. 保存到数据库
+6. 发送推送通知到移动端
+7. 更新next_proactive_at时间
+
+## 好感度计算
+
+```python
+def update_affection(user_message, ai_response):
+    # 1. AI在回复中附带情感分析
+    # <sentiment>{"affection_change": 3, "reason": "..."}</sentiment>
+
+    # 2. 提取affection_change
+    change = extract_sentiment(ai_response)
+
+    # 3. 更新好感度(限制在-100~100)
+    new_score = clamp(current_score + change, -100, 100)
+
+    # 4. 计算新等级
+    new_level = get_level(new_score)
+
+    # 5. 保存变化记录
+    save_log(change, reason)
+```
+
+## 技术要点
+
+### 安全性
+- API Key使用Fernet对称加密存储
+- 密码使用bcrypt哈希
+- 所有API需要JWT认证
+- 生产环境强制HTTPS
+
+### 性能优化
+- Redis缓存对话上下文
+- PostgreSQL分区表(消息表)
+- 数据库连接池
+- 异步I/O(FastAPI + AsyncPG)
+
+### 可扩展性
+- 水平扩展:多个FastAPI实例
+- 任务队列:Celery分布式
+- 数据库:读写分离(主从)
+- 缓存:Redis集群
+
+## 部署架构
+
+```
+          Nginx
+            │
+    ┌───────┼───────┐
+    │       │       │
+ FastAPI FastAPI FastAPI
+    │       │       │
+    └───────┼───────┘
+            │
+    ┌───────┼───────┐
+    │               │
+PostgreSQL        Redis
+(Primary)      (Cluster)
+    │
+PostgreSQL
+(Replica)
+```
+
+## 监控指标
+
+- API响应时间
+- AI调用延迟
+- 好感度分布
+- 主动消息发送成功率
+- Token使用量
+- 数据库查询性能

+ 124 - 0
docs/QUICKSTART.md

@@ -0,0 +1,124 @@
+# 快速启动指南
+
+## 一键启动(推荐)
+
+```bash
+# 1. 进入项目目录
+cd phoneapp
+
+# 2. 复制环境变量文件
+cd backend
+cp .env.example .env
+
+# 3. 生成加密密钥(在Python中执行)
+python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
+# 将输出的密钥复制到 .env 文件的 ENCRYPTION_KEY
+
+# 4. 返回项目根目录
+cd ..
+
+# 5. 启动所有服务
+docker-compose up -d
+
+# 6. 运行数据库迁移
+docker-compose exec backend alembic upgrade head
+
+# 7. 查看日志(可选)
+docker-compose logs -f backend
+```
+
+## 访问应用
+
+- **API文档(Swagger)**:http://localhost:8000/docs
+- **健康检查**:http://localhost:8000/health
+- **数据库**:localhost:5432
+- **Redis**:localhost:6379
+
+## 停止服务
+
+```bash
+docker-compose down
+```
+
+## 重新构建
+
+```bash
+docker-compose down
+docker-compose build
+docker-compose up -d
+```
+
+## 测试API
+
+### 1. 注册用户
+```bash
+curl -X POST "http://localhost:8000/api/auth/register" \
+  -H "Content-Type: application/json" \
+  -d '{
+    "username": "test",
+    "email": "test@example.com",
+    "password": "test123"
+  }'
+```
+
+### 2. 登录
+```bash
+curl -X POST "http://localhost:8000/api/auth/login" \
+  -H "Content-Type: application/x-www-form-urlencoded" \
+  -d "username=test&password=test123"
+```
+
+保存返回的`access_token`。
+
+### 3. 配置API Key
+```bash
+curl -X POST "http://localhost:8000/api/auth/api-keys" \
+  -H "Authorization: Bearer {你的access_token}" \
+  -H "Content-Type: application/json" \
+  -d '{
+    "provider": "openai",
+    "api_key": "sk-your-openai-key",
+    "model": "gpt-3.5-turbo"
+  }'
+```
+
+### 4. 创建AI角色
+```bash
+curl -X POST "http://localhost:8000/api/characters" \
+  -H "Authorization: Bearer {你的access_token}" \
+  -H "Content-Type: application/json" \
+  -d '{
+    "name": "小助手",
+    "personality": "友好、乐于助人",
+    "background_story": "一个AI助手",
+    "language": "zh",
+    "llm_provider": "openai",
+    "llm_model": "gpt-3.5-turbo"
+  }'
+```
+
+## 故障排查
+
+### 查看容器状态
+```bash
+docker-compose ps
+```
+
+### 查看日志
+```bash
+docker-compose logs backend
+docker-compose logs celery_worker
+docker-compose logs celery_beat
+```
+
+### 进入容器
+```bash
+docker-compose exec backend bash
+docker-compose exec postgres psql -U postgres -d ai_chat_app
+```
+
+## 下一步
+
+1. 阅读 `README.md` 了解完整功能
+2. 访问 http://localhost:8000/docs 查看API文档
+3. 开始开发移动端应用

+ 111 - 0
mobile/README.md

@@ -0,0 +1,111 @@
+# React Native移动端开发指南
+
+## 初始化项目
+
+```bash
+cd mobile
+
+# 安装依赖(使用pnpm)
+pnpm install
+
+# iOS(需要Mac)
+cd ios
+pod install
+cd ..
+pnpm ios
+
+# Android
+pnpm android
+```
+
+## 开发计划
+
+### Phase 1: 基础UI(1-2周)
+- [ ] 登录/注册界面
+- [ ] 主界面导航
+- [ ] AI角色列表
+- [ ] 基础聊天界面
+
+### Phase 2: 核心功能(2-3周)
+- [ ] AI对话功能
+- [ ] 好感度显示
+- [ ] 角色创建/编辑
+- [ ] API Key配置
+
+### Phase 3: 高级功能(2-3周)
+- [ ] 实时流式回复
+- [ ] 推送通知
+- [ ] 主动消息提醒
+- [ ] 多语言切换
+
+## 项目结构(计划)
+
+```
+src/
+├── screens/          # 页面
+│   ├── Auth/
+│   │   ├── Login.tsx
+│   │   └── Register.tsx
+│   ├── CharacterList.tsx
+│   ├── ChatScreen.tsx
+│   └── Profile.tsx
+├── components/       # 组件
+│   ├── MessageBubble.tsx
+│   ├── CharacterCard.tsx
+│   └── AffectionBar.tsx
+├── services/         # API服务
+│   ├── api.ts
+│   └── auth.ts
+├── stores/           # Zustand状态
+│   ├── authStore.ts
+│   └── chatStore.ts
+├── types/            # TypeScript类型
+└── locales/          # 国际化
+    ├── en.json
+    └── zh.json
+```
+
+## 技术栈
+
+- **框架**:React Native 0.72+
+- **语言**:TypeScript
+- **导航**:React Navigation
+- **状态管理**:Zustand
+- **数据请求**:React Query + Axios
+- **聊天UI**:react-native-gifted-chat
+- **国际化**:i18next
+
+## API集成示例
+
+```typescript
+// services/api.ts
+import axios from 'axios';
+
+const api = axios.create({
+  baseURL: 'http://localhost:8000/api',
+});
+
+export const login = async (username: string, password: string) => {
+  const response = await api.post('/auth/login', { username, password });
+  return response.data;
+};
+
+export const sendMessage = async (conversationId: number, content: string, token: string) => {
+  const response = await api.post(
+    `/conversations/${conversationId}/messages`,
+    { content },
+    { headers: { Authorization: `Bearer ${token}` } }
+  );
+  return response.data;
+};
+```
+
+## 下一步
+
+运行以下命令初始化React Native项目:
+
+```bash
+npx react-native init AIChatApp --template react-native-template-typescript
+```
+
+然后将配置文件复制到新创建的项目中。

+ 34 - 0
mobile/package.json

@@ -0,0 +1,34 @@
+{
+  "name": "AIChatApp",
+  "version": "1.0.0",
+  "private": true,
+  "scripts": {
+    "android": "react-native run-android",
+    "ios": "react-native run-ios",
+    "start": "react-native start",
+    "test": "jest",
+    "lint": "eslint ."
+  },
+  "dependencies": {
+    "react": "^18.2.0",
+    "react-native": "^0.72.0",
+    "@react-navigation/native": "^6.1.0",
+    "@react-navigation/stack": "^6.3.0",
+    "@tanstack/react-query": "^5.0.0",
+    "zustand": "^4.4.0",
+    "react-native-gifted-chat": "^2.4.0",
+    "axios": "^1.6.0",
+    "i18next": "^23.7.0",
+    "react-i18next": "^13.5.0"
+  },
+  "devDependencies": {
+    "@babel/core": "^7.23.0",
+    "@babel/runtime": "^7.23.0",
+    "@react-native/eslint-config": "^0.72.0",
+    "@react-native/metro-config": "^0.72.0",
+    "@tsconfig/react-native": "^3.0.0",
+    "@types/react": "^18.2.0",
+    "@types/react-native": "^0.72.0",
+    "typescript": "^5.0.0"
+  }
+}