feat: 新增后端API路由模块
- 新增faqs.py常见问题路由 - 新增feedback.py反馈路由 - 新增orders.py订单路由 - 新增plans.py套餐路由 - 新增quota.py配额路由 - 新增user.py用户路由 - 更新main.py注册新路由 - 更新requirements.txt添加依赖
This commit is contained in:
11
api/main.py
11
api/main.py
@@ -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("/")
|
||||
|
||||
@@ -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
135
api/routers/faqs.py
Normal 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
76
api/routers/feedback.py
Normal 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
40
api/routers/orders.py
Normal 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
149
api/routers/plans.py
Normal 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
126
api/routers/quota.py
Normal 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
42
api/routers/user.py
Normal 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()
|
||||
)
|
||||
Reference in New Issue
Block a user