feat: 新增后端API路由模块

- 新增faqs.py常见问题路由
- 新增feedback.py反馈路由
- 新增orders.py订单路由
- 新增plans.py套餐路由
- 新增quota.py配额路由
- 新增user.py用户路由
- 更新main.py注册新路由
- 更新requirements.txt添加依赖
This commit is contained in:
iammm0
2026-01-23 14:02:36 +08:00
parent d104377d26
commit 3690417fdc
8 changed files with 581 additions and 1 deletions

View File

@@ -52,7 +52,10 @@ else:
load_dotenv()
from database import init_db
from routers import websocket, chapters, books, conversations, auth, memoir_state, tasks
from routers import (
websocket, chapters, books, conversations, auth, memoir_state, tasks,
user, plans, orders, faqs, quota, feedback
)
# 初始化数据库
logger.info("正在初始化数据库...")
@@ -135,6 +138,12 @@ app.include_router(chapters.router)
app.include_router(books.router)
app.include_router(memoir_state.router)
app.include_router(tasks.router) # 任务状态路由
app.include_router(user.router) # 用户相关路由
app.include_router(plans.router) # 订阅计划路由
app.include_router(orders.router) # 订单路由
app.include_router(faqs.router) # 常见问题路由
app.include_router(quota.router) # 配额检查路由
app.include_router(feedback.router) # 反馈路由
@app.get("/")

View File

@@ -42,4 +42,7 @@ bcrypt>=4.0.0
# pydub==0.25.1
# speech-recognition==3.10.4
# Image Processing
Pillow>=10.0.0
openai

135
api/routers/faqs.py Normal file
View File

@@ -0,0 +1,135 @@
"""
常见问题 FAQ API 路由
"""
from fastapi import APIRouter
from pydantic import BaseModel
from typing import List
router = APIRouter(prefix="/api/faqs", tags=["faqs"])
class FAQResponse(BaseModel):
"""常见问题响应"""
id: str
question: str
answer: str
category: str
order: int
# 预定义的常见问题
FAQS = [
FAQResponse(
id="1",
question="如何使用回忆录功能?",
answer="创建对话后与AI助手交流分享您的人生故事。AI会自动整理并生成回忆录章节。您可以在'我的回忆录'页面查看所有章节,并选择对话进行整理。",
category="使用指南",
order=1
),
FAQResponse(
id="2",
question="免费版和高级版有什么区别?",
answer="免费版限制3次对话和10个章节高级版提供无限对话和章节以及优先处理服务。高级版用户还可以享受更快的处理速度和专属客服支持。",
category="订阅计划",
order=2
),
FAQResponse(
id="3",
question="如何导出回忆录?",
answer="'我的回忆录'页面,您可以查看所有章节。导出功能正在开发中,敬请期待!",
category="使用指南",
order=3
),
FAQResponse(
id="4",
question="数据安全吗?",
answer="我们采用加密存储,严格保护用户隐私,您的数据仅用于生成回忆录,不会用于其他用途。所有数据都经过加密处理,确保您的隐私安全。",
category="隐私安全",
order=4
),
FAQResponse(
id="5",
question="如何升级到高级版?",
answer="'我的'页面点击'订阅计划',选择高级版并完成支付即可升级。升级后立即生效,享受所有高级功能。",
category="订阅计划",
order=5
),
FAQResponse(
id="6",
question="可以修改已生成的章节吗?",
answer="可以,在章节详情页面可以编辑内容,修改后会自动保存。您也可以重新整理对话来更新章节内容。",
category="使用指南",
order=6
),
FAQResponse(
id="7",
question="如何整理对话内容成章节?",
answer="'我的回忆录'页面,点击'整理对话'按钮选择要整理的对话AI会自动将对话内容整理成一个个小章节。每个章节展开后可以看到详细内容。",
category="使用指南",
order=7
),
FAQResponse(
id="8",
question="章节是如何生成的?",
answer="AI会根据对话内容自动识别主题将相关内容整理成章节。每个章节都有标题和详细内容您可以随时查看和编辑。",
category="使用指南",
order=8
),
FAQResponse(
id="9",
question="可以删除对话或章节吗?",
answer="可以,在对话列表或章节列表中,您可以长按或点击删除按钮来删除不需要的内容。删除后无法恢复,请谨慎操作。",
category="使用指南",
order=9
),
FAQResponse(
id="10",
question="如何联系客服?",
answer="您可以在'我的'页面点击'反馈与客服',填写反馈表单或联系客服。我们会尽快回复您的问题。",
category="帮助支持",
order=10
),
FAQResponse(
id="11",
question="回忆录支持哪些格式?",
answer="目前支持文本格式的回忆录。PDF导出功能正在开发中敬请期待",
category="使用指南",
order=11
),
FAQResponse(
id="12",
question="如何备份我的数据?",
answer="您的数据会自动保存在云端,无需手动备份。导出功能正在开发中,完成后您可以导出数据到本地。",
category="数据管理",
order=12
),
FAQResponse(
id="13",
question="语音功能什么时候上线?",
answer="语音模块正在开发中,包括语音输入和语音播放功能。敬请期待!",
category="功能预告",
order=13
),
FAQResponse(
id="14",
question="可以多人协作编辑回忆录吗?",
answer="目前不支持多人协作,每个账号只能编辑自己的回忆录。多人协作功能正在规划中。",
category="功能预告",
order=14
),
FAQResponse(
id="15",
question="如何提高回忆录的质量?",
answer="建议您详细描述每个话题提供更多细节和感受。AI会根据您提供的信息生成更丰富、更生动的回忆录内容。",
category="使用技巧",
order=15
)
]
@router.get("", response_model=List[FAQResponse])
async def get_faqs():
"""
获取常见问题列表
"""
return FAQS

76
api/routers/feedback.py Normal file
View File

@@ -0,0 +1,76 @@
"""
反馈相关 API 路由
"""
import logging
import uuid
from datetime import datetime, timezone
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, Field
from sqlalchemy.ext.asyncio import AsyncSession
from database import get_async_db
from middleware.auth import get_current_user
from database.models import User
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/feedback", tags=["feedback"])
class SubmitFeedbackRequest(BaseModel):
"""提交反馈请求"""
content: str = Field(..., min_length=1, max_length=2000, description="反馈内容")
contact: Optional[str] = Field(None, max_length=100, description="联系方式(可选)")
class FeedbackResponse(BaseModel):
"""反馈响应"""
id: str
message: str
@router.post("", response_model=FeedbackResponse, status_code=status.HTTP_201_CREATED)
async def submit_feedback(
request: SubmitFeedbackRequest,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_async_db)
):
"""
提交用户反馈
用户可以通过此接口提交反馈意见或联系客服
"""
try:
feedback_id = str(uuid.uuid4())
# 记录反馈(这里可以保存到数据库,目前先记录日志)
logger.info(
f"用户反馈 - ID: {feedback_id}, "
f"用户ID: {current_user.id}, "
f"内容: {request.content[:100]}..., "
f"联系方式: {request.contact or '未提供'}"
)
# TODO: 保存反馈到数据库
# feedback = Feedback(
# id=feedback_id,
# user_id=current_user.id,
# content=request.content,
# contact=request.contact,
# created_at=datetime.now(timezone.utc)
# )
# db.add(feedback)
# await db.commit()
return FeedbackResponse(
id=feedback_id,
message="反馈已提交,我们会尽快处理"
)
except Exception as e:
logger.error(f"提交反馈失败: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="提交反馈失败,请稍后重试"
)

40
api/routers/orders.py Normal file
View File

@@ -0,0 +1,40 @@
"""
订单相关 API 路由
"""
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from typing import List, Optional
from datetime import datetime
from middleware.auth import get_current_user
from database.models import User
router = APIRouter(prefix="/api/orders", tags=["orders"])
class OrderResponse(BaseModel):
"""订单响应"""
id: str
plan_id: str
plan_name: str
amount: float
currency: str
status: str # pending, paid, cancelled, refunded
created_at: str
paid_at: Optional[str] = None
payment_method: Optional[str] = None
@router.get("", response_model=List[OrderResponse])
async def get_orders(
current_user: User = Depends(get_current_user)
):
"""
获取当前用户的订单列表
目前返回空列表,因为还没有实现订单系统
未来可以添加订单表来存储订单信息
"""
# TODO: 从数据库查询订单
# 目前返回空列表,避免前端报错
return []

149
api/routers/plans.py Normal file
View File

@@ -0,0 +1,149 @@
"""
订阅计划相关 API 路由
"""
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from typing import List, Optional
from datetime import datetime
from sqlalchemy.ext.asyncio import AsyncSession
from middleware.auth import get_current_user
from database.models import User
from database import get_async_db
router = APIRouter(prefix="/api/plans", tags=["plans"])
class PlanResponse(BaseModel):
"""订阅计划响应"""
id: str
name: str
display_name: str
price: float
currency: str
features: List[str]
max_conversations: Optional[int] = None # None表示无限制
max_chapters: Optional[int] = None
max_words: Optional[int] = None
is_popular: bool = False
class CurrentPlanResponse(BaseModel):
"""当前订阅计划响应"""
plan_id: str
plan_name: str
subscription_type: str
expires_at: Optional[str] = None # 过期时间None表示永久
features: List[str]
usage: dict # 使用情况统计
# 预定义的订阅计划
AVAILABLE_PLANS = [
PlanResponse(
id="free",
name="free",
display_name="免费版",
price=0.0,
currency="CNY",
features=[
"基础对话功能",
"生成回忆录章节",
"最多3次对话",
"最多10个章节"
],
max_conversations=3,
max_chapters=10,
max_words=50000,
is_popular=False
),
PlanResponse(
id="premium",
name="premium",
display_name="高级版",
price=99.0,
currency="CNY",
features=[
"无限对话",
"无限章节",
"无限字数",
"优先处理",
"专属客服支持"
],
max_conversations=None,
max_chapters=None,
max_words=None,
is_popular=True
)
]
def get_plan_by_type(subscription_type: str) -> Optional[PlanResponse]:
"""根据订阅类型获取计划信息"""
for plan in AVAILABLE_PLANS:
if plan.id == subscription_type:
return plan
return AVAILABLE_PLANS[0] # 默认返回免费版
@router.get("", response_model=List[PlanResponse])
async def get_plans():
"""
获取所有可用的订阅计划
"""
return AVAILABLE_PLANS
@router.get("/current", response_model=CurrentPlanResponse)
async def get_current_plan(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_async_db)
):
"""
获取当前用户的订阅计划信息
"""
plan = get_plan_by_type(current_user.subscription_type)
# 计算使用情况
from sqlalchemy import select, func
# 统计对话数量
from database.models import Conversation
stmt = select(func.count(Conversation.id)).where(
Conversation.user_id == current_user.id
)
result = await db.execute(stmt)
conversation_count = result.scalar() or 0
# 统计章节数量
from database.models import Chapter
stmt = select(func.count(Chapter.id)).where(
Chapter.user_id == current_user.id
)
result = await db.execute(stmt)
chapter_count = result.scalar() or 0
# 统计总字数
stmt = select(func.sum(func.length(Chapter.content))).where(
Chapter.user_id == current_user.id
)
result = await db.execute(stmt)
total_words = result.scalar() or 0
usage = {
"conversations": conversation_count,
"chapters": chapter_count,
"words": total_words,
"max_conversations": plan.max_conversations,
"max_chapters": plan.max_chapters,
"max_words": plan.max_words
}
return CurrentPlanResponse(
plan_id=plan.id,
plan_name=plan.display_name,
subscription_type=current_user.subscription_type,
expires_at=None, # 目前没有过期时间概念
features=plan.features,
usage=usage
)

126
api/routers/quota.py Normal file
View File

@@ -0,0 +1,126 @@
"""
配额检查 API 路由
"""
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from typing import Optional
from middleware.auth import get_current_user
from database.models import User
from sqlalchemy.ext.asyncio import AsyncSession
from database import get_async_db
from sqlalchemy import select, func
router = APIRouter(prefix="/api/quota", tags=["quota"])
class QuotaCheckResponse(BaseModel):
"""配额检查响应"""
has_quota: bool # 是否有配额
remaining_conversations: Optional[int] = None # 剩余对话次数
remaining_chapters: Optional[int] = None # 剩余章节数
remaining_words: Optional[int] = None # 剩余字数
message: str # 提示信息
# 计划配额限制
PLAN_QUOTAS = {
"free": {
"max_conversations": 3,
"max_chapters": 10,
"max_words": 50000
},
"premium": {
"max_conversations": None, # 无限制
"max_chapters": None,
"max_words": None
}
}
@router.get("/check", response_model=QuotaCheckResponse)
async def check_quota(
current_user: User = Depends(get_current_user)
):
"""
检查用户配额使用情况
根据用户的订阅计划检查是否还有配额可以使用
"""
plan_type = current_user.subscription_type
quotas = PLAN_QUOTAS.get(plan_type, PLAN_QUOTAS["free"])
# 如果是高级版,无限制
if plan_type == "premium":
return QuotaCheckResponse(
has_quota=True,
remaining_conversations=None,
remaining_chapters=None,
remaining_words=None,
message="高级版用户,无使用限制"
)
# 统计使用情况
async for db in get_async_db():
# 统计对话数量
from database.models import Conversation
stmt = select(func.count(Conversation.id)).where(
Conversation.user_id == current_user.id
)
result = await db.execute(stmt)
conversation_count = result.scalar() or 0
# 统计章节数量
from database.models import Chapter
stmt = select(func.count(Chapter.id)).where(
Chapter.user_id == current_user.id
)
result = await db.execute(stmt)
chapter_count = result.scalar() or 0
# 统计总字数
stmt = select(func.sum(func.length(Chapter.content))).where(
Chapter.user_id == current_user.id
)
result = await db.execute(stmt)
total_words = result.scalar() or 0
# 计算剩余配额
max_conversations = quotas.get("max_conversations")
max_chapters = quotas.get("max_chapters")
max_words = quotas.get("max_words")
remaining_conversations = None
remaining_chapters = None
remaining_words = None
if max_conversations is not None:
remaining_conversations = max(0, max_conversations - conversation_count)
if max_chapters is not None:
remaining_chapters = max(0, max_chapters - chapter_count)
if max_words is not None:
remaining_words = max(0, max_words - total_words)
# 检查是否有配额
has_quota = True
message = "配额充足"
if max_conversations is not None and conversation_count >= max_conversations:
has_quota = False
message = "对话次数已用完,请升级到高级版"
elif max_chapters is not None and chapter_count >= max_chapters:
has_quota = False
message = "章节数量已达上限,请升级到高级版"
elif max_words is not None and total_words >= max_words:
has_quota = False
message = "字数已达上限,请升级到高级版"
return QuotaCheckResponse(
has_quota=has_quota,
remaining_conversations=remaining_conversations,
remaining_chapters=remaining_chapters,
remaining_words=remaining_words,
message=message
)

42
api/routers/user.py Normal file
View File

@@ -0,0 +1,42 @@
"""
用户相关 API 路由
"""
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from typing import Optional
from middleware.auth import get_current_user
from database.models import User
router = APIRouter(prefix="/api/user", tags=["user"])
class UserProfileResponse(BaseModel):
"""用户资料响应"""
id: str
phone: str
email: Optional[str]
nickname: str
avatar_url: Optional[str]
subscription_type: str
created_at: str
@router.get("/profile", response_model=UserProfileResponse)
async def get_user_profile(
current_user: User = Depends(get_current_user)
):
"""
获取当前用户资料
与 /api/auth/me 功能相同,但路径不同以满足前端需求
"""
return UserProfileResponse(
id=current_user.id,
phone=current_user.phone,
email=current_user.email,
nickname=current_user.nickname,
avatar_url=current_user.avatar_url,
subscription_type=current_user.subscription_type,
created_at=current_user.created_at.isoformat()
)