Files
qihuaiflow/backend/python/app/api/market.py
T

599 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from sqlalchemy import and_, desc, asc, func
from pydantic import BaseModel, Field
from typing import Optional
import uuid
from datetime import datetime
from ..database import get_db
from ..models import (
User, MarketApp, AppPurchase, AppReview, AppInstallation, Order,
gen_uuid
)
from ..auth import get_current_user, get_current_user_id, get_current_user_id_optional
from ..middleware.log_service import LogService
router = APIRouter()
class UploadAppRequest(BaseModel):
name: str = Field(min_length=1, max_length=200)
description: Optional[str] = None
icon: Optional[str] = None
type: Optional[str] = None # skill, agent, mcp
source: Optional[str] = None
price: float = Field(default=0, ge=0)
download_url: Optional[str] = None
category: Optional[str] = None
tags: Optional[str] = None
is_member_free: Optional[bool] = False
join_promotion: Optional[bool] = False
agent_endpoint: Optional[str] = None
class PurchaseAppRequest(BaseModel):
amount: Optional[float] = None
class ReviewAppRequest(BaseModel):
rating: int = Field(ge=1, le=5)
content: Optional[str] = None
@router.get("/apps")
def list_apps(
type: Optional[str] = Query(None),
source: Optional[str] = Query(None),
category: Optional[str] = Query(None),
search: Optional[str] = Query(None),
sort: str = Query(default="latest"),
page: int = Query(default=1, ge=1),
page_size: int = Query(default=20, ge=1, le=100),
db: Session = Depends(get_db),
user_id: Optional[str] = Depends(get_current_user_id_optional),
):
query = db.query(MarketApp).filter(MarketApp.status == "published")
if type:
query = query.filter(MarketApp.type == type)
if source:
query = query.filter(MarketApp.source == source)
if category:
query = query.filter(MarketApp.type == category)
if search:
query = query.filter(
and_(
MarketApp.name.ilike(f"%{search}%")
| MarketApp.description.ilike(f"%{search}%")
)
)
if sort == "popular":
query = query.order_by(desc(MarketApp.install_count))
elif sort == "rating":
query = query.order_by(desc(MarketApp.rating_avg))
else:
query = query.order_by(desc(MarketApp.created_at))
total = query.count()
apps = query.offset((page - 1) * page_size).limit(page_size).all()
# 如果用户已登录,获取该用户已安装的应用ID集合(确保按用户隔离)
installed_app_ids = set()
if user_id:
installations = db.query(AppInstallation.app_id).filter(
AppInstallation.user_id == user_id
).all()
installed_app_ids = {inst[0] for inst in installations}
# 查询已安装状态时始终带 user_id 过滤
result = []
for app in apps:
item = {
"id": app.id,
"name": app.name,
"description": app.description,
"icon": app.icon,
"type": app.type,
"source": app.source,
"publisher_id": app.publisher_id,
"publisher_name": app.publisher_name,
"is_member_free": app.is_member_free if app.is_member_free else False,
"join_promotion": app.join_promotion if app.join_promotion else False,
"status": app.status,
"price": float(app.price) if app.price else 0,
"install_count": app.install_count or 0,
"rating_avg": float(app.rating_avg) if app.rating_avg else 0,
"review_count": app.review_count or 0,
"created_at": str(app.created_at) if app.created_at else None,
"installed": app.id in installed_app_ids if user_id else False,
}
result.append(item)
return {
"code": 0,
"data": {
"total": total,
"page": page,
"page_size": page_size,
"items": result,
},
"message": "ok",
}
@router.get("/apps/{app_id}")
def get_app(
app_id: str,
db: Session = Depends(get_db),
current_user: Optional[User] = Depends(get_current_user),
):
app = db.query(MarketApp).filter(MarketApp.id == app_id).first()
if not app:
raise HTTPException(status_code=404, detail="应用不存在")
# 检查当前用户是否已购买(按用户隔离)
purchased = False
installed = False
if current_user:
purchase = db.query(AppPurchase).filter(
and_(AppPurchase.app_id == app_id, AppPurchase.user_id == current_user.id)
).first()
if purchase:
purchased = True
installed = purchase.status == "installed"
# 通过 AppInstallation 表检查是否已安装(按用户隔离)
installation = db.query(AppInstallation).filter(
and_(AppInstallation.app_id == app_id, AppInstallation.user_id == current_user.id)
).first()
if installation:
installed = True
return {
"code": 0,
"data": {
"id": app.id, "name": app.name, "description": app.description,
"icon": app.icon, "type": app.type, "source": app.source,
"type": app.type,
"price": float(app.price) if app.price else 0,
"install_count": app.install_count or 0,
"rating_avg": float(app.rating_avg) if app.rating_avg else 0,
"review_count": app.review_count or 0,
"publisher_id": app.publisher_id,
"download_url": app.download_url,
"detail": app.detail,
"created_at": str(app.created_at),
"purchased": purchased,
"installed": installed,
},
"message": "ok",
}
@router.post("/apps")
def upload_app(
req: UploadAppRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
app = MarketApp(
id=gen_uuid(),
name=req.name,
description=req.description,
icon=req.icon,
type=req.type if req.type in ("skill","agent","mcp") else "skill",
source=req.source if req.source in ("official","user") else "user",
price=req.price,
install_count=0,
rating_avg=0,
review_count=0,
publisher_id=current_user.id,
publisher_name=current_user.username,
is_member_free=req.is_member_free if req.is_member_free is not None else False,
join_promotion=req.join_promotion if req.join_promotion is not None else False,
agent_endpoint=req.agent_endpoint,
status="published", # 用户创建的应用直接发布
created_at=datetime.now(),
updated_at=datetime.now(),
)
db.add(app)
db.commit()
db.refresh(app)
# 用户创建/导入的应用自动标记为已安装
existing_install = db.query(AppInstallation).filter(
AppInstallation.user_id == current_user.id,
AppInstallation.app_id == app.id,
).first()
if not existing_install:
installation = AppInstallation(
user_id=current_user.id,
app_id=app.id,
installed_at=datetime.now(),
)
db.add(installation)
db.commit()
LogService.log_operation(
user_id=current_user.id, username=current_user.username, action="upload_app",
target="market_app", target_id=app.id,
detail=f"上传应用 {app.name}",
)
return {
"code": 0,
"data": {
"id": app.id, "name": app.name, "status": app.status,
"created_at": str(app.created_at),
},
"message": "提交成功,等待审核",
}
@router.post("/apps/{app_id}/purchase")
def purchase_app(
app_id: str,
req: PurchaseAppRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
app = db.query(MarketApp).filter(
and_(MarketApp.id == app_id, MarketApp.status == "published")
).first()
if not app:
raise HTTPException(status_code=404, detail="应用不存在或未上架")
# 检查是否已购买
existing = db.query(AppPurchase).filter(
and_(AppPurchase.app_id == app_id, AppPurchase.user_id == current_user.id)
).first()
if existing and existing.status in ("purchased", "installed"):
raise HTTPException(status_code=400, detail="您已购买该应用")
amount = req.amount or app.price
# 创建订单
order = Order(
id=gen_uuid(),
user_id=current_user.id,
order_type="app",
item_id=app_id,
item_name=app.name,
amount=amount,
status="pending",
created_at=datetime.now(),
)
db.add(order)
db.flush()
# 创建购买记录
purchase = AppPurchase(
id=gen_uuid(),
user_id=current_user.id,
app_id=app_id,
order_id=order.id,
status="purchased",
purchased_at=datetime.now(),
)
db.add(purchase)
# 更新应用下载量
app.install_count = (app.install_count or 0) + 1
app.updated_at = datetime.now()
db.commit()
return {
"code": 0,
"data": {
"order_id": order.id,
"purchase_id": purchase.id,
"amount": amount,
"status": "purchased",
},
"message": "购买成功",
}
@router.post("/apps/{app_id}/install")
def install_app(
app_id: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
app = db.query(MarketApp).filter(MarketApp.id == app_id).first()
if not app:
raise HTTPException(status_code=404, detail="应用不存在")
# 检查是否已安装(通过 AppInstallation 表)
existing_install = db.query(AppInstallation).filter(
and_(AppInstallation.app_id == app_id, AppInstallation.user_id == current_user.id)
).first()
if existing_install:
raise HTTPException(status_code=400, detail="应用已安装")
# 检查是否已购买(付费应用需要先购买)
purchase = db.query(AppPurchase).filter(
and_(AppPurchase.app_id == app_id, AppPurchase.user_id == current_user.id)
).first()
# 对于付费应用,必须已购买才能安装
if float(app.price or 0) > 0:
if not purchase:
raise HTTPException(status_code=403, detail="请先购买应用")
if purchase.status == "installed":
raise HTTPException(status_code=400, detail="应用已安装")
# 创建安装记录
installation = AppInstallation(
id=gen_uuid(),
user_id=current_user.id,
app_id=app_id,
installed_at=datetime.now(),
)
db.add(installation)
# 如果有购买记录,更新状态为已安装
if purchase:
purchase.status = "installed"
purchase.installed_at = datetime.now()
# 更新应用安装量
app.install_count = (app.install_count or 0) + 1
app.updated_at = datetime.now()
db.commit()
return {
"code": 0,
"data": {"installation_id": installation.id, "status": "installed"},
"message": "安装成功",
}
@router.delete("/apps/{app_id}/uninstall")
def uninstall_app(
app_id: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""卸载应用 - 删除当前用户的安装记录(按用户隔离)"""
app = db.query(MarketApp).filter(MarketApp.id == app_id).first()
if not app:
raise HTTPException(status_code=404, detail="应用不存在")
# 查找当前用户的安装记录(按用户隔离)
installation = db.query(AppInstallation).filter(
and_(AppInstallation.app_id == app_id, AppInstallation.user_id == current_user.id)
).first()
if not installation:
raise HTTPException(status_code=400, detail="应用未安装")
# 删除安装记录
db.delete(installation)
# 问题4修复: 将AppPurchase状态从installed重置为purchased
purchase = db.query(AppPurchase).filter(
and_(AppPurchase.app_id == app_id, AppPurchase.user_id == current_user.id)
).first()
if purchase and purchase.status == "installed":
purchase.status = "purchased"
# 更新应用安装量(减1,最小为0)
if app.install_count and app.install_count > 0:
app.install_count = app.install_count - 1
app.updated_at = datetime.now()
db.commit()
LogService.log_operation(
user_id=current_user.id, username=current_user.username, action="uninstall_app",
target="market_app", target_id=app_id,
detail=f"卸载应用 {app.name}",
)
return {
"code": 0,
"message": "卸载成功",
}
@router.post("/apps/{app_id}/review")
def review_app(
app_id: str,
req: ReviewAppRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
app = db.query(MarketApp).filter(MarketApp.id == app_id).first()
if not app:
raise HTTPException(status_code=404, detail="应用不存在")
# 检查是否已评价
existing = db.query(AppReview).filter(
and_(AppReview.app_id == app_id, AppReview.user_id == current_user.id)
).first()
if existing:
# 更新已有评价
old_rating = existing.rating
existing.rating = req.rating
existing.content = req.content
existing.updated_at = datetime.now()
# 重新计算平均分 (总数不变,减去旧评分加新评分)
if app.review_count and app.review_count > 0:
total_rating = float(app.rating_avg or 0) * app.review_count
total_rating = total_rating - old_rating + req.rating
app.rating_avg = total_rating / app.review_count
else:
app.rating_avg = req.rating
app.review_count = 1
else:
# 创建新评价
review = AppReview(
id=gen_uuid(),
app_id=app_id,
user_id=current_user.id,
rating=req.rating,
content=req.content,
created_at=datetime.now(),
updated_at=datetime.now(),
)
db.add(review)
# 重新计算平均分
app.review_count = (app.review_count or 0) + 1
if app.review_count == 1:
app.rating_avg = req.rating
else:
total_rating = float(app.rating_avg or 0) * (app.review_count - 1) + req.rating
app.rating_avg = round(total_rating / app.review_count, 1)
app.updated_at = datetime.now()
db.commit()
return {
"code": 0,
"data": {
"rating_avg": float(app.rating_avg or 0),
"review_count": app.review_count or 0,
},
"message": "评价成功",
}
@router.get("/my-apps")
def get_my_apps(
status: Optional[str] = Query(None),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
query = db.query(AppPurchase).filter(AppPurchase.user_id == current_user.id)
purchases = query.order_by(desc(AppPurchase.created_at)).all()
result = []
for p in purchases:
app = db.query(MarketApp).filter(MarketApp.id == p.app_id).first()
if app:
result.append({
"id": p.id,
"app_id": app.id,
"name": app.name,
"icon": app.icon,
"type": app.type,
"price": float(app.price or 0),
"purchased_at": str(p.created_at) if p.created_at else None,
})
return {"code": 0, "data": result, "message": "ok"}
@router.get("/reviews/{app_id}")
def get_reviews(
app_id: str,
page: int = Query(default=1, ge=1),
page_size: int = Query(default=20, ge=1, le=100),
db: Session = Depends(get_db),
):
query = db.query(AppReview).filter(AppReview.app_id == app_id)
total = query.count()
reviews = (
query.order_by(desc(AppReview.created_at))
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
result = []
for r in reviews:
user = db.query(User).filter(User.id == r.user_id).first()
result.append({
"id": r.id,
"user_id": r.user_id,
"username": user.username if user else "unknown",
"avatar": user.avatar if user else None,
"rating": r.rating,
"content": r.content,
"created_at": str(r.created_at) if r.created_at else None,
})
return {
"code": 0,
"data": {"total": total, "page": page, "page_size": page_size, "items": result},
"message": "ok",
}
# ============================================================
# Agent部署资源查询接口
# ============================================================
@router.get("/agent-resources")
def get_agent_resources(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Agent部署时需要的资源列表:可用的应用、知识库、文档"""
# 用户已发布的应用
my_apps = db.query(MarketApp).filter(
MarketApp.publisher_id == current_user.id,
MarketApp.status == "published",
).order_by(desc(MarketApp.updated_at)).all()
apps_result = []
for app in my_apps:
apps_result.append({
"id": app.id,
"name": app.name,
"type": app.type,
"description": app.description,
"install_count": app.install_count or 0,
})
# 用户知识库
from ..models import KnowledgeBase, KnowledgeDocument
kb_list = db.query(KnowledgeBase).filter(
KnowledgeBase.user_id == current_user.id
).order_by(desc(KnowledgeBase.updated_at)).all()
kb_result = []
for kb in kb_list:
doc_count = db.query(KnowledgeDocument).filter(
KnowledgeDocument.knowledge_base_id == kb.id
).count()
kb_result.append({
"id": kb.id,
"name": kb.name,
"description": kb.description,
"document_count": doc_count,
})
# 用户已安装的应用(从AppPurchase)
installed = db.query(AppPurchase).filter(
AppPurchase.user_id == current_user.id
).all()
installed_ids = [p.app_id for p in installed]
installed_apps = []
if installed_ids:
iapps = db.query(MarketApp).filter(MarketApp.id.in_(installed_ids)).all()
for ia in iapps:
installed_apps.append({
"id": ia.id,
"name": ia.name,
"type": ia.type,
"description": ia.description,
})
return {
"code": 0,
"data": {
"my_apps": apps_result,
"knowledge_bases": kb_result,
"installed_apps": installed_apps,
},
"message": "ok",
}