From 3690417fdcc02a3e309201c70b5faccf81003150 Mon Sep 17 00:00:00 2001 From: iammm0 Date: Fri, 23 Jan 2026 14:02:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=90=8E=E7=AB=AFAPI?= =?UTF-8?q?=E8=B7=AF=E7=94=B1=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增faqs.py常见问题路由 - 新增feedback.py反馈路由 - 新增orders.py订单路由 - 新增plans.py套餐路由 - 新增quota.py配额路由 - 新增user.py用户路由 - 更新main.py注册新路由 - 更新requirements.txt添加依赖 --- api/main.py | 11 ++- api/requirements.txt | 3 + api/routers/faqs.py | 135 ++++++++++++++++++++++++++++++++++++ api/routers/feedback.py | 76 ++++++++++++++++++++ api/routers/orders.py | 40 +++++++++++ api/routers/plans.py | 149 ++++++++++++++++++++++++++++++++++++++++ api/routers/quota.py | 126 +++++++++++++++++++++++++++++++++ api/routers/user.py | 42 +++++++++++ 8 files changed, 581 insertions(+), 1 deletion(-) create mode 100644 api/routers/faqs.py create mode 100644 api/routers/feedback.py create mode 100644 api/routers/orders.py create mode 100644 api/routers/plans.py create mode 100644 api/routers/quota.py create mode 100644 api/routers/user.py diff --git a/api/main.py b/api/main.py index d9203b3..c1e4952 100644 --- a/api/main.py +++ b/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("/") diff --git a/api/requirements.txt b/api/requirements.txt index 563ec9d..2ad871a 100644 --- a/api/requirements.txt +++ b/api/requirements.txt @@ -42,4 +42,7 @@ bcrypt>=4.0.0 # pydub==0.25.1 # speech-recognition==3.10.4 +# Image Processing +Pillow>=10.0.0 + openai \ No newline at end of file diff --git a/api/routers/faqs.py b/api/routers/faqs.py new file mode 100644 index 0000000..dacdc1e --- /dev/null +++ b/api/routers/faqs.py @@ -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 diff --git a/api/routers/feedback.py b/api/routers/feedback.py new file mode 100644 index 0000000..3c496ff --- /dev/null +++ b/api/routers/feedback.py @@ -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="提交反馈失败,请稍后重试" + ) diff --git a/api/routers/orders.py b/api/routers/orders.py new file mode 100644 index 0000000..4cb69e0 --- /dev/null +++ b/api/routers/orders.py @@ -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 [] diff --git a/api/routers/plans.py b/api/routers/plans.py new file mode 100644 index 0000000..f754c6b --- /dev/null +++ b/api/routers/plans.py @@ -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 + ) diff --git a/api/routers/quota.py b/api/routers/quota.py new file mode 100644 index 0000000..409cda2 --- /dev/null +++ b/api/routers/quota.py @@ -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 + ) diff --git a/api/routers/user.py b/api/routers/user.py new file mode 100644 index 0000000..2ec0419 --- /dev/null +++ b/api/routers/user.py @@ -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() + )