Phase 3.7: Market Expansion - analytics dashboard, featured apps, category system, real API integration (replaces mock data)
This commit is contained in:
1 parent
2c5ab13209
commit
63741b3f0f
3 files changed
+1463
-602
No files matched your search
@@ -1,68 +1,369 @@
|
|||||||
"""
|
"""
|
||||||
应用市场审核 API(管理员)
|
市场集市扩展 API(管理员)
|
||||||
|
Phase 3.7: Market Expansion - Analytics, Featured, Category Management
|
||||||
"""
|
"""
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query, Body
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy import or_, func, desc, case, text
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional, List, Any
|
||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from ...database import get_db, SessionLocal
|
from ...database import get_db, SessionLocal
|
||||||
from ...models import MarketApp, Coupon, UserCoupon, User
|
from ...models import MarketApp, AppPurchase, AppReview, AppInstallation, User
|
||||||
from ...auth import get_current_admin
|
from ...auth import get_current_admin
|
||||||
from ...middleware.log_service import LogService
|
from ...middleware.log_service import LogService
|
||||||
import uuid
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
# App categories
|
||||||
|
APP_CATEGORIES = {
|
||||||
|
"coding": "编程开发",
|
||||||
|
"assistant": "智能助手",
|
||||||
|
"tools": "效率工具",
|
||||||
|
"writing": "内容创作",
|
||||||
|
"education": "教育培训",
|
||||||
|
"marketing": "营销推广",
|
||||||
|
"data": "数据分析",
|
||||||
|
"design": "设计创意",
|
||||||
|
"business": "企业管理",
|
||||||
|
"other": "其他",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class FeaturedUpdateRequest(BaseModel):
|
||||||
|
featured: bool
|
||||||
|
featured_order: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
class CategoryUpdateRequest(BaseModel):
|
||||||
|
category: str
|
||||||
|
tags: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class AppEditRequest(BaseModel):
|
||||||
|
name: Optional[str] = None
|
||||||
|
description: Optional[str] = None
|
||||||
|
icon: Optional[str] = None
|
||||||
|
tutorial: Optional[str] = None
|
||||||
|
category: Optional[str] = None
|
||||||
|
tags: Optional[str] = None
|
||||||
|
price: Optional[float] = None
|
||||||
|
is_member_free: Optional[bool] = None
|
||||||
|
featured: Optional[bool] = None
|
||||||
|
featured_order: Optional[int] = None
|
||||||
|
status: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
def _execute_raw(db, sql, params=None):
|
||||||
|
"""Execute raw SQL and return results."""
|
||||||
|
result = db.execute(text(sql), params or {})
|
||||||
|
try:
|
||||||
|
rows = result.fetchall()
|
||||||
|
return [dict(row._mapping) if hasattr(row, '_mapping') else dict(row) for row in rows]
|
||||||
|
except:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _app_to_dict(app, include_content=False):
|
||||||
|
"""Convert MarketApp to dict."""
|
||||||
|
d = {
|
||||||
|
"id": app.id,
|
||||||
|
"name": app.name,
|
||||||
|
"type": app.type,
|
||||||
|
"category": getattr(app, 'category', 'other'),
|
||||||
|
"tags": getattr(app, 'tags', None),
|
||||||
|
"source": app.source,
|
||||||
|
"publisher_name": app.publisher_name,
|
||||||
|
"description": app.description,
|
||||||
|
"icon": app.icon,
|
||||||
|
"price": float(app.price) if app.price else 0.0,
|
||||||
|
"is_member_free": app.is_member_free,
|
||||||
|
"featured": bool(getattr(app, 'featured', False)),
|
||||||
|
"featured_order": getattr(app, 'featured_order', 0),
|
||||||
|
"status": app.status,
|
||||||
|
"rating_avg": float(app.rating_avg) if app.rating_avg else 0.0,
|
||||||
|
"review_count": app.review_count,
|
||||||
|
"install_count": app.install_count,
|
||||||
|
"version": getattr(app, 'version', '1.0.0'),
|
||||||
|
"created_at": app.created_at.strftime('%Y-%m-%d %H:%M:%S') if app.created_at else None,
|
||||||
|
"updated_at": app.updated_at.strftime('%Y-%m-%d %H:%M:%S') if app.updated_at else None,
|
||||||
|
}
|
||||||
|
if include_content:
|
||||||
|
d["tutorial"] = app.tutorial
|
||||||
|
d["skill_content"] = app.skill_content
|
||||||
|
d["changelog"] = getattr(app, 'changelog', None)
|
||||||
|
d["download_url"] = getattr(app, 'download_url', None)
|
||||||
|
d["agent_endpoint"] = app.agent_endpoint
|
||||||
|
d["mcp_transport"] = app.mcp_transport
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 应用审核管理
|
# Analytics endpoints
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
@router.get("/apps/pending")
|
@router.get("/analytics/overview", summary="市场总览数据")
|
||||||
|
async def get_market_overview(
|
||||||
|
admin_user=Depends(get_current_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""获取市场总览统计数据"""
|
||||||
|
# Total apps by status
|
||||||
|
status_stats = _execute_raw(db, """
|
||||||
|
SELECT status, COUNT(*) as cnt
|
||||||
|
FROM market_apps
|
||||||
|
GROUP BY status
|
||||||
|
""")
|
||||||
|
status_map = {r["status"]: r["cnt"] for r in status_stats}
|
||||||
|
|
||||||
|
# Total installs
|
||||||
|
install_total = _execute_raw(db, "SELECT COUNT(*) as cnt FROM app_installations")
|
||||||
|
total_installs = install_total[0]["cnt"] if install_total else 0
|
||||||
|
|
||||||
|
# Total purchases and revenue
|
||||||
|
purchase_stats = _execute_raw(db, """
|
||||||
|
SELECT COUNT(*) as cnt, COALESCE(SUM(price), 0) as revenue
|
||||||
|
FROM app_purchases
|
||||||
|
WHERE status = 'purchased'
|
||||||
|
""")
|
||||||
|
total_purchases = purchase_stats[0]["cnt"] if purchase_stats else 0
|
||||||
|
total_revenue = float(purchase_stats[0]["revenue"]) if purchase_stats else 0.0
|
||||||
|
|
||||||
|
# Total reviews
|
||||||
|
review_total = _execute_raw(db, "SELECT COUNT(*) as cnt FROM app_reviews")
|
||||||
|
total_reviews = review_total[0]["cnt"] if review_total else 0
|
||||||
|
|
||||||
|
# Avg rating
|
||||||
|
avg_rating = _execute_raw(db, "SELECT COALESCE(AVG(rating), 0) as avg FROM app_reviews")
|
||||||
|
overall_rating = float(avg_rating[0]["avg"]) if avg_rating else 0.0
|
||||||
|
|
||||||
|
# Apps by type
|
||||||
|
type_stats = _execute_raw(db, """
|
||||||
|
SELECT type, COUNT(*) as cnt
|
||||||
|
FROM market_apps
|
||||||
|
WHERE status = 'published'
|
||||||
|
GROUP BY type
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Apps by category
|
||||||
|
category_stats = _execute_raw(db, """
|
||||||
|
SELECT COALESCE(category, 'other') as category, COUNT(*) as cnt
|
||||||
|
FROM market_apps
|
||||||
|
WHERE status = 'published'
|
||||||
|
GROUP BY category
|
||||||
|
ORDER BY cnt DESC
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Recent 7 days trend
|
||||||
|
trend = []
|
||||||
|
for i in range(6, -1, -1):
|
||||||
|
date = (datetime.now() - timedelta(days=i)).strftime('%Y-%m-%d')
|
||||||
|
next_date = (datetime.now() - timedelta(days=i-1)).strftime('%Y-%m-%d') if i > 0 else None
|
||||||
|
|
||||||
|
new_apps = _execute_raw(db, """
|
||||||
|
SELECT COUNT(*) as cnt FROM market_apps
|
||||||
|
WHERE DATE(created_at) = :d
|
||||||
|
""", {"d": date})
|
||||||
|
|
||||||
|
new_installs = _execute_raw(db, """
|
||||||
|
SELECT COUNT(*) as cnt FROM app_installations
|
||||||
|
WHERE DATE(installed_at) = :d
|
||||||
|
""", {"d": date})
|
||||||
|
|
||||||
|
trend.append({
|
||||||
|
"date": date,
|
||||||
|
"new_apps": new_apps[0]["cnt"] if new_apps else 0,
|
||||||
|
"new_installs": new_installs[0]["cnt"] if new_installs else 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"total_apps": sum(status_map.values()),
|
||||||
|
"published": status_map.get("published", 0),
|
||||||
|
"pending": status_map.get("pending", 0),
|
||||||
|
"removed": status_map.get("removed", 0),
|
||||||
|
"total_installs": total_installs,
|
||||||
|
"total_purchases": total_purchases,
|
||||||
|
"total_revenue": total_revenue,
|
||||||
|
"total_reviews": total_reviews,
|
||||||
|
"overall_rating": round(overall_rating, 2),
|
||||||
|
"by_type": {r["type"]: r["cnt"] for r in type_stats},
|
||||||
|
"by_category": {r["category"]: r["cnt"] for r in category_stats},
|
||||||
|
"trend_7d": trend,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/analytics/top-apps", summary="热门应用排行")
|
||||||
|
async def get_top_apps(
|
||||||
|
metric: str = Query("installs", regex="^(installs|rating|revenue|reviews)$"),
|
||||||
|
limit: int = Query(10, le=50),
|
||||||
|
admin_user=Depends(get_current_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""获取热门应用排行"""
|
||||||
|
if metric == "installs":
|
||||||
|
order = desc(MarketApp.install_count)
|
||||||
|
elif metric == "rating":
|
||||||
|
order = desc(MarketApp.rating_avg)
|
||||||
|
elif metric == "reviews":
|
||||||
|
order = desc(MarketApp.review_count)
|
||||||
|
else:
|
||||||
|
# Revenue - need join with purchases
|
||||||
|
revenue_data = _execute_raw(db, """
|
||||||
|
SELECT app_id, SUM(price) as revenue, COUNT(*) as sales
|
||||||
|
FROM app_purchases
|
||||||
|
WHERE status = 'purchased'
|
||||||
|
GROUP BY app_id
|
||||||
|
ORDER BY revenue DESC
|
||||||
|
LIMIT :lim
|
||||||
|
""", {"lim": limit})
|
||||||
|
|
||||||
|
app_ids = [r["app_id"] for r in revenue_data]
|
||||||
|
if not app_ids:
|
||||||
|
return {"code": 0, "data": []}
|
||||||
|
|
||||||
|
apps = db.query(MarketApp).filter(MarketApp.id.in_(app_ids)).all()
|
||||||
|
app_map = {a.id: a for a in apps}
|
||||||
|
result = []
|
||||||
|
for r in revenue_data:
|
||||||
|
app = app_map.get(r["app_id"])
|
||||||
|
if app:
|
||||||
|
d = _app_to_dict(app)
|
||||||
|
d["revenue"] = float(r["revenue"])
|
||||||
|
d["sales"] = r["sales"]
|
||||||
|
result.append(d)
|
||||||
|
return {"code": 0, "data": result}
|
||||||
|
|
||||||
|
apps = db.query(MarketApp).filter(
|
||||||
|
MarketApp.status == "published"
|
||||||
|
).order_by(order).limit(limit).all()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"code": 0,
|
||||||
|
"data": [_app_to_dict(app) for app in apps]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/analytics/recent-reviews", summary="最新评价")
|
||||||
|
async def get_recent_reviews(
|
||||||
|
limit: int = Query(20, le=100),
|
||||||
|
admin_user=Depends(get_current_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""获取最新用户评价"""
|
||||||
|
reviews = _execute_raw(db, """
|
||||||
|
SELECT r.id, r.rating, r.content, r.created_at,
|
||||||
|
u.username, u.avatar,
|
||||||
|
a.name as app_name, a.icon as app_icon
|
||||||
|
FROM app_reviews r
|
||||||
|
LEFT JOIN users u ON r.user_id = u.id
|
||||||
|
LEFT JOIN market_apps a ON r.app_id = a.id
|
||||||
|
ORDER BY r.created_at DESC
|
||||||
|
LIMIT :lim
|
||||||
|
""", {"lim": limit})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"code": 0,
|
||||||
|
"data": [{
|
||||||
|
"id": r["id"],
|
||||||
|
"rating": r["rating"],
|
||||||
|
"content": r["content"],
|
||||||
|
"username": r["username"] or "Anonymous",
|
||||||
|
"avatar": r["avatar"],
|
||||||
|
"app_name": r["app_name"] or "Unknown",
|
||||||
|
"app_icon": r["app_icon"],
|
||||||
|
"created_at": r["created_at"].strftime('%Y-%m-%d %H:%M:%S') if r["created_at"] else None,
|
||||||
|
} for r in reviews]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# App management endpoints
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
@router.get("/apps", summary="获取所有市场应用")
|
||||||
|
async def list_market_apps(
|
||||||
|
status: str = Query(None),
|
||||||
|
app_type: str = Query(None),
|
||||||
|
category: str = Query(None),
|
||||||
|
featured: Optional[bool] = Query(None),
|
||||||
|
keyword: str = Query(None),
|
||||||
|
page: int = Query(1, ge=1),
|
||||||
|
page_size: int = Query(20, ge=1, le=100),
|
||||||
|
admin_user=Depends(get_current_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""获取所有市场应用列表(支持筛选)"""
|
||||||
|
query = db.query(MarketApp)
|
||||||
|
|
||||||
|
if status:
|
||||||
|
query = query.filter(MarketApp.status == status)
|
||||||
|
if app_type:
|
||||||
|
query = query.filter(MarketApp.type == app_type)
|
||||||
|
if category:
|
||||||
|
query = query.filter(text("category = :cat").bindparams(cat=category))
|
||||||
|
if featured is not None:
|
||||||
|
query = query.filter(text("featured = :feat").bindparams(feat=int(featured)))
|
||||||
|
if keyword:
|
||||||
|
query = query.filter(or_(
|
||||||
|
MarketApp.name.contains(keyword),
|
||||||
|
MarketApp.description.contains(keyword),
|
||||||
|
))
|
||||||
|
|
||||||
|
total = query.count()
|
||||||
|
apps = query.order_by(desc(MarketApp.created_at)).offset(
|
||||||
|
(page - 1) * page_size
|
||||||
|
).limit(page_size).all()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"page_size": page_size,
|
||||||
|
"items": [_app_to_dict(app) for app in apps],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/apps/pending", summary="获取待审核应用")
|
||||||
async def get_pending_apps(
|
async def get_pending_apps(
|
||||||
|
page: int = Query(1, ge=1),
|
||||||
|
page_size: int = Query(20, ge=1, le=100),
|
||||||
admin_user=Depends(get_current_admin),
|
admin_user=Depends(get_current_admin),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""获取待审核应用列表"""
|
"""获取待审核应用列表"""
|
||||||
apps = db.query(MarketApp).filter(
|
query = db.query(MarketApp).filter(MarketApp.status == "pending")
|
||||||
MarketApp.status == "pending"
|
total = query.count()
|
||||||
).order_by(MarketApp.created_at.desc()).all()
|
apps = query.order_by(MarketApp.created_at.desc()).offset(
|
||||||
|
(page - 1) * page_size
|
||||||
|
).limit(page_size).all()
|
||||||
|
|
||||||
return [
|
return {
|
||||||
{
|
"code": 0,
|
||||||
"id": a.id,
|
"data": {
|
||||||
"name": a.name,
|
"total": total,
|
||||||
"type": a.type,
|
"page": page,
|
||||||
"source": a.source,
|
"page_size": page_size,
|
||||||
"publisher_id": a.publisher_id,
|
"items": [_app_to_dict(app, include_content=True) for app in apps],
|
||||||
"publisher_name": a.publisher_name,
|
|
||||||
"description": a.description,
|
|
||||||
"icon": a.icon,
|
|
||||||
"tutorial": a.tutorial,
|
|
||||||
"price": float(a.price) if a.price else 0,
|
|
||||||
"is_member_free": a.is_member_free,
|
|
||||||
"join_promotion": a.join_promotion,
|
|
||||||
"status": a.status,
|
|
||||||
"rating_avg": float(a.rating_avg) if a.rating_avg else 0,
|
|
||||||
"review_count": a.review_count,
|
|
||||||
"install_count": a.install_count,
|
|
||||||
"skill_content": a.skill_content,
|
|
||||||
"mcp_transport": a.mcp_transport,
|
|
||||||
"agent_endpoint": a.agent_endpoint,
|
|
||||||
"created_at": a.created_at.isoformat() if a.created_at else None,
|
|
||||||
"updated_at": a.updated_at.isoformat() if a.updated_at else None,
|
|
||||||
}
|
}
|
||||||
for a in apps
|
}
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@router.put("/apps/{id}/approve")
|
@router.put("/apps/{app_id}/approve", summary="审核通过")
|
||||||
async def approve_app(
|
async def approve_app(
|
||||||
id: str,
|
app_id: str,
|
||||||
admin_user=Depends(get_current_admin),
|
admin_user=Depends(get_current_admin),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""审核通过应用"""
|
"""审核通过应用"""
|
||||||
app = db.query(MarketApp).filter(MarketApp.id == id).first()
|
app = db.query(MarketApp).filter(MarketApp.id == app_id).first()
|
||||||
if not app:
|
if not app:
|
||||||
raise HTTPException(status_code=404, detail="应用不存在")
|
raise HTTPException(status_code=404, detail="应用不存在")
|
||||||
|
|
||||||
@@ -70,26 +371,20 @@ async def approve_app(
|
|||||||
app.updated_at = datetime.now()
|
app.updated_at = datetime.now()
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
LogService.log_audit(
|
LogService.log_action(db, "admin", admin_user["id"], "approve_market_app", f"Approved app: {app.name}")
|
||||||
admin_id=admin_user.id,
|
|
||||||
admin_name=admin_user.username,
|
|
||||||
action="approve_app",
|
|
||||||
target_type="market_app",
|
|
||||||
target_id=id,
|
|
||||||
detail=f"审核通过应用 {app.name}",
|
|
||||||
)
|
|
||||||
|
|
||||||
return {"message": "应用审核通过"}
|
return {"code": 0, "message": "审核通过", "data": _app_to_dict(app)}
|
||||||
|
|
||||||
|
|
||||||
@router.put("/apps/{id}/reject")
|
@router.put("/apps/{app_id}/reject", summary="审核拒绝")
|
||||||
async def reject_app(
|
async def reject_app(
|
||||||
id: str,
|
app_id: str,
|
||||||
|
reason: str = Query("", max_length=500),
|
||||||
admin_user=Depends(get_current_admin),
|
admin_user=Depends(get_current_admin),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""审核拒绝应用"""
|
"""审核拒绝应用"""
|
||||||
app = db.query(MarketApp).filter(MarketApp.id == id).first()
|
app = db.query(MarketApp).filter(MarketApp.id == app_id).first()
|
||||||
if not app:
|
if not app:
|
||||||
raise HTTPException(status_code=404, detail="应用不存在")
|
raise HTTPException(status_code=404, detail="应用不存在")
|
||||||
|
|
||||||
@@ -97,316 +392,259 @@ async def reject_app(
|
|||||||
app.updated_at = datetime.now()
|
app.updated_at = datetime.now()
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
LogService.log_audit(
|
LogService.log_action(db, "admin", admin_user["id"], "reject_market_app", f"Rejected app: {app.name}, reason: {reason}")
|
||||||
admin_id=admin_user.id,
|
|
||||||
admin_name=admin_user.username,
|
|
||||||
action="reject_app",
|
|
||||||
target_type="market_app",
|
|
||||||
target_id=id,
|
|
||||||
detail=f"审核拒绝应用 {app.name}",
|
|
||||||
)
|
|
||||||
|
|
||||||
return {"message": "应用审核已拒绝"}
|
return {"code": 0, "message": "已拒绝", "data": _app_to_dict(app)}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/apps")
|
@router.put("/apps/{app_id}", summary="编辑应用")
|
||||||
async def get_all_apps(
|
async def update_app(
|
||||||
status: str = Query(None, description="状态筛选: pending/published/removed"),
|
app_id: str,
|
||||||
app_type: str = Query(None, description="类型筛选: skill/agent/mcp"),
|
req: AppEditRequest,
|
||||||
|
admin_user=Depends(get_current_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""管理员编辑应用信息"""
|
||||||
|
app = db.query(MarketApp).filter(MarketApp.id == app_id).first()
|
||||||
|
if not app:
|
||||||
|
raise HTTPException(status_code=404, detail="应用不存在")
|
||||||
|
|
||||||
|
if req.name is not None:
|
||||||
|
app.name = req.name
|
||||||
|
if req.description is not None:
|
||||||
|
app.description = req.description
|
||||||
|
if req.icon is not None:
|
||||||
|
app.icon = req.icon
|
||||||
|
if req.tutorial is not None:
|
||||||
|
app.tutorial = req.tutorial
|
||||||
|
if req.category is not None:
|
||||||
|
# Use raw SQL for new columns
|
||||||
|
db.execute(text("UPDATE market_apps SET category = :c WHERE id = :id"), {"c": req.category, "id": app_id})
|
||||||
|
if req.tags is not None:
|
||||||
|
db.execute(text("UPDATE market_apps SET tags = :t WHERE id = :id"), {"t": req.tags, "id": app_id})
|
||||||
|
if req.price is not None:
|
||||||
|
app.price = req.price
|
||||||
|
if req.is_member_free is not None:
|
||||||
|
app.is_member_free = req.is_member_free
|
||||||
|
if req.featured is not None:
|
||||||
|
db.execute(text("UPDATE market_apps SET featured = :f WHERE id = :id"), {"f": int(req.featured), "id": app_id})
|
||||||
|
if req.featured_order is not None:
|
||||||
|
db.execute(text("UPDATE market_apps SET featured_order = :fo WHERE id = :id"), {"fo": req.featured_order, "id": app_id})
|
||||||
|
if req.status is not None:
|
||||||
|
app.status = req.status
|
||||||
|
|
||||||
|
app.updated_at = datetime.now()
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
db.refresh(app)
|
||||||
|
return {"code": 0, "message": "更新成功", "data": _app_to_dict(app)}
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/apps/{app_id}/featured", summary="设置推荐应用")
|
||||||
|
async def set_featured(
|
||||||
|
app_id: str,
|
||||||
|
req: FeaturedUpdateRequest,
|
||||||
|
admin_user=Depends(get_current_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""设置/取消推荐应用"""
|
||||||
|
app = db.query(MarketApp).filter(MarketApp.id == app_id).first()
|
||||||
|
if not app:
|
||||||
|
raise HTTPException(status_code=404, detail="应用不存在")
|
||||||
|
|
||||||
|
db.execute(text("UPDATE market_apps SET featured = :f, featured_order = :fo WHERE id = :id"),
|
||||||
|
{"f": int(req.featured), "fo": req.featured_order, "id": app_id})
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
action = "Featured" if req.featured else "Unfeatured"
|
||||||
|
LogService.log_action(db, "admin", admin_user["id"], "feature_app", f"{action} app: {app.name}")
|
||||||
|
|
||||||
|
return {"code": 0, "message": f"{'已推荐' if req.featured else '已取消推荐'}", "data": {"featured": req.featured, "featured_order": req.featured_order}}
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/apps/{app_id}/category", summary="设置应用分类")
|
||||||
|
async def set_category(
|
||||||
|
app_id: str,
|
||||||
|
req: CategoryUpdateRequest,
|
||||||
|
admin_user=Depends(get_current_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""设置应用分类和标签"""
|
||||||
|
if req.category not in APP_CATEGORIES:
|
||||||
|
raise HTTPException(status_code=400, detail=f"无效分类,可选: {list(APP_CATEGORIES.keys())}")
|
||||||
|
|
||||||
|
app = db.query(MarketApp).filter(MarketApp.id == app_id).first()
|
||||||
|
if not app:
|
||||||
|
raise HTTPException(status_code=404, detail="应用不存在")
|
||||||
|
|
||||||
|
db.execute(text("UPDATE market_apps SET category = :c, tags = :t WHERE id = :id"),
|
||||||
|
{"c": req.category, "t": req.tags, "id": app_id})
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return {"code": 0, "message": "分类已更新", "data": {"category": req.category, "tags": req.tags}}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/categories", summary="获取应用分类列表")
|
||||||
|
async def get_categories(
|
||||||
|
admin_user=Depends(get_current_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""获取所有应用分类及每分类的应用数"""
|
||||||
|
cat_stats = _execute_raw(db, """
|
||||||
|
SELECT COALESCE(category, 'other') as category, COUNT(*) as cnt
|
||||||
|
FROM market_apps
|
||||||
|
WHERE status = 'published'
|
||||||
|
GROUP BY category
|
||||||
|
""")
|
||||||
|
cat_map = {r["category"]: r["cnt"] for r in cat_stats}
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for key, label in APP_CATEGORIES.items():
|
||||||
|
result.append({
|
||||||
|
"key": key,
|
||||||
|
"label": label,
|
||||||
|
"count": cat_map.get(key, 0),
|
||||||
|
})
|
||||||
|
|
||||||
|
return {"code": 0, "data": result}
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/apps/{app_id}", summary="删除应用")
|
||||||
|
async def delete_app(
|
||||||
|
app_id: str,
|
||||||
|
admin_user=Depends(get_current_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""删除市场应用"""
|
||||||
|
app = db.query(MarketApp).filter(MarketApp.id == app_id).first()
|
||||||
|
if not app:
|
||||||
|
raise HTTPException(status_code=404, detail="应用不存在")
|
||||||
|
|
||||||
|
app_name = app.name
|
||||||
|
db.delete(app)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
LogService.log_action(db, "admin", admin_user["id"], "delete_market_app", f"Deleted app: {app_name}")
|
||||||
|
|
||||||
|
return {"code": 0, "message": "已删除"}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Coupon management (kept from original)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
@router.get("/coupons", summary="获取优惠券列表")
|
||||||
|
async def list_coupons(
|
||||||
page: int = Query(1, ge=1),
|
page: int = Query(1, ge=1),
|
||||||
page_size: int = Query(20, ge=1, le=100),
|
page_size: int = Query(20, ge=1, le=100),
|
||||||
admin_user=Depends(get_current_admin),
|
admin_user=Depends(get_current_admin),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""获取所有应用列表(管理员)"""
|
"""获取优惠券列表"""
|
||||||
query = db.query(MarketApp)
|
from ...models import Coupon
|
||||||
|
query = db.query(Coupon)
|
||||||
if status:
|
|
||||||
query = query.filter(MarketApp.status == status)
|
|
||||||
if app_type:
|
|
||||||
query = query.filter(MarketApp.type == app_type)
|
|
||||||
|
|
||||||
total = query.count()
|
total = query.count()
|
||||||
apps = query.order_by(MarketApp.created_at.desc()).offset(
|
coupons = query.order_by(desc(Coupon.created_at)).offset(
|
||||||
(page - 1) * page_size
|
(page - 1) * page_size
|
||||||
).limit(page_size).all()
|
).limit(page_size).all()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"total": total,
|
"code": 0,
|
||||||
"page": page,
|
"data": {
|
||||||
"page_size": page_size,
|
"total": total,
|
||||||
"items": [
|
"page": page,
|
||||||
{
|
"page_size": page_size,
|
||||||
"id": a.id,
|
"items": [{
|
||||||
"name": a.name,
|
"id": c.id,
|
||||||
"type": a.type,
|
"code": c.code,
|
||||||
"source": a.source,
|
"type": c.type,
|
||||||
"publisher_id": a.publisher_id,
|
"value": float(c.value) if c.value else 0,
|
||||||
"publisher_name": a.publisher_name,
|
"min_amount": float(c.min_amount) if c.min_amount else 0,
|
||||||
"description": a.description,
|
"valid_from": c.valid_from.strftime('%Y-%m-%d %H:%M:%S') if c.valid_from else None,
|
||||||
"icon": a.icon,
|
"valid_to": c.valid_to.strftime('%Y-%m-%d %H:%M:%S') if c.valid_to else None,
|
||||||
"tutorial": a.tutorial,
|
"total_count": c.total_count,
|
||||||
"price": float(a.price) if a.price else 0,
|
"used_count": c.used_count,
|
||||||
"is_member_free": a.is_member_free,
|
"status": c.status,
|
||||||
"join_promotion": a.join_promotion,
|
} for c in coupons],
|
||||||
"status": a.status,
|
}
|
||||||
"rating_avg": float(a.rating_avg) if a.rating_avg else 0,
|
|
||||||
"review_count": a.review_count,
|
|
||||||
"install_count": a.install_count,
|
|
||||||
"created_at": a.created_at.isoformat() if a.created_at else None,
|
|
||||||
"updated_at": a.updated_at.isoformat() if a.updated_at else None,
|
|
||||||
}
|
|
||||||
for a in apps
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.put("/apps/{id}")
|
class CouponCreateRequest(BaseModel):
|
||||||
async def admin_edit_app(
|
code: str
|
||||||
id: str,
|
type: str # fixed / percent
|
||||||
name: str = None,
|
value: float
|
||||||
description: str = None,
|
min_amount: float = 0
|
||||||
icon: str = None,
|
valid_from: Optional[str] = None
|
||||||
price: float = None,
|
valid_to: Optional[str] = None
|
||||||
is_member_free: bool = None,
|
total_count: int = 100
|
||||||
join_promotion: bool = None,
|
|
||||||
tutorial: str = None,
|
|
||||||
admin_user=Depends(get_current_admin),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
):
|
|
||||||
"""管理员编辑应用"""
|
|
||||||
app = db.query(MarketApp).filter(MarketApp.id == id).first()
|
|
||||||
if not app:
|
|
||||||
raise HTTPException(status_code=404, detail="应用不存在")
|
|
||||||
|
|
||||||
if name is not None:
|
|
||||||
app.name = name
|
|
||||||
if description is not None:
|
|
||||||
app.description = description
|
|
||||||
if icon is not None:
|
|
||||||
app.icon = icon
|
|
||||||
if price is not None:
|
|
||||||
app.price = price
|
|
||||||
if is_member_free is not None:
|
|
||||||
app.is_member_free = is_member_free
|
|
||||||
if join_promotion is not None:
|
|
||||||
app.join_promotion = join_promotion
|
|
||||||
if tutorial is not None:
|
|
||||||
app.tutorial = tutorial
|
|
||||||
app.updated_at = datetime.now()
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
LogService.log_audit(
|
|
||||||
admin_id=admin_user.id,
|
|
||||||
admin_name=admin_user.username,
|
|
||||||
action="edit_market_app",
|
|
||||||
target_type="market_app",
|
|
||||||
target_id=id,
|
|
||||||
detail=f"管理员编辑应用 {app.name}",
|
|
||||||
)
|
|
||||||
|
|
||||||
return {"message": "应用已更新"}
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
@router.post("/coupons", summary="创建优惠券")
|
||||||
# 优惠券管理
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
@router.get("/coupons")
|
|
||||||
async def get_coupons(
|
|
||||||
status: str = Query(None, description="状态筛选: active/expired/disabled"),
|
|
||||||
admin_user=Depends(get_current_admin),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
):
|
|
||||||
"""获取优惠券列表"""
|
|
||||||
query = db.query(Coupon)
|
|
||||||
|
|
||||||
if status:
|
|
||||||
query = query.filter(Coupon.status == status)
|
|
||||||
|
|
||||||
coupons = query.order_by(Coupon.created_at.desc()).all()
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
"id": c.id,
|
|
||||||
"code": c.code,
|
|
||||||
"type": c.type,
|
|
||||||
"value": float(c.value) if c.value else 0,
|
|
||||||
"min_amount": float(c.min_amount) if c.min_amount else 0,
|
|
||||||
"valid_from": c.valid_from.isoformat() if c.valid_from else None,
|
|
||||||
"valid_to": c.valid_to.isoformat() if c.valid_to else None,
|
|
||||||
"total_count": c.total_count,
|
|
||||||
"used_count": c.used_count,
|
|
||||||
"status": c.status,
|
|
||||||
"created_at": c.created_at.isoformat() if c.created_at else None,
|
|
||||||
}
|
|
||||||
for c in coupons
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/coupons")
|
|
||||||
async def create_coupon(
|
async def create_coupon(
|
||||||
code: str,
|
req: CouponCreateRequest,
|
||||||
coupon_type: str,
|
|
||||||
value: float,
|
|
||||||
valid_from: str,
|
|
||||||
valid_to: str,
|
|
||||||
min_amount: float = 0,
|
|
||||||
total_count: int = None,
|
|
||||||
status: str = "active",
|
|
||||||
admin_user=Depends(get_current_admin),
|
admin_user=Depends(get_current_admin),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""创建优惠券"""
|
"""创建优惠券"""
|
||||||
# 检查code是否已存在
|
from ...models import Coupon
|
||||||
existing = db.query(Coupon).filter(Coupon.code == code).first()
|
|
||||||
if existing:
|
|
||||||
raise HTTPException(status_code=400, detail="优惠券码已存在")
|
|
||||||
|
|
||||||
coupon = Coupon(
|
coupon = Coupon(
|
||||||
id=str(uuid.uuid4()),
|
id=str(uuid.uuid4())[:8],
|
||||||
code=code,
|
code=req.code,
|
||||||
type=coupon_type,
|
type=req.type,
|
||||||
value=value,
|
value=req.value,
|
||||||
min_amount=min_amount,
|
min_amount=req.min_amount,
|
||||||
valid_from=datetime.fromisoformat(valid_from),
|
valid_from=datetime.strptime(req.valid_from, '%Y-%m-%d') if req.valid_from else datetime.now(),
|
||||||
valid_to=datetime.fromisoformat(valid_to),
|
valid_to=datetime.strptime(req.valid_to, '%Y-%m-%d') if req.valid_to else None,
|
||||||
total_count=total_count,
|
total_count=req.total_count,
|
||||||
status=status,
|
used_count=0,
|
||||||
|
status="active",
|
||||||
)
|
)
|
||||||
db.add(coupon)
|
db.add(coupon)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
LogService.log_audit(
|
return {"code": 0, "message": "创建成功", "data": {"id": coupon.id, "code": coupon.code}}
|
||||||
admin_id=admin_user.id,
|
|
||||||
admin_name=admin_user.username,
|
|
||||||
action="create_coupon",
|
|
||||||
target_type="coupon",
|
|
||||||
target_id=coupon.id,
|
|
||||||
detail=f"创建优惠券 {code}",
|
|
||||||
)
|
|
||||||
|
|
||||||
return {"message": "优惠券已创建", "id": coupon.id}
|
|
||||||
|
|
||||||
|
|
||||||
@router.put("/coupons/{id}")
|
@router.put("/coupons/{coupon_id}", summary="更新优惠券")
|
||||||
async def update_coupon(
|
async def update_coupon(
|
||||||
id: str,
|
coupon_id: str,
|
||||||
code: str = None,
|
req: CouponCreateRequest,
|
||||||
value: float = None,
|
|
||||||
min_amount: float = None,
|
|
||||||
valid_from: str = None,
|
|
||||||
valid_to: str = None,
|
|
||||||
total_count: int = None,
|
|
||||||
status: str = None,
|
|
||||||
admin_user=Depends(get_current_admin),
|
admin_user=Depends(get_current_admin),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""更新优惠券"""
|
"""更新优惠券"""
|
||||||
coupon = db.query(Coupon).filter(Coupon.id == id).first()
|
from ...models import Coupon
|
||||||
|
coupon = db.query(Coupon).filter(Coupon.id == coupon_id).first()
|
||||||
if not coupon:
|
if not coupon:
|
||||||
raise HTTPException(status_code=404, detail="优惠券不存在")
|
raise HTTPException(status_code=404, detail="优惠券不存在")
|
||||||
|
|
||||||
if code is not None:
|
coupon.code = req.code
|
||||||
# 检查新code是否重复
|
coupon.type = req.type
|
||||||
if code != coupon.code:
|
coupon.value = req.value
|
||||||
dup = db.query(Coupon).filter(Coupon.code == code).first()
|
coupon.min_amount = req.min_amount
|
||||||
if dup:
|
coupon.total_count = req.total_count
|
||||||
raise HTTPException(status_code=400, detail="优惠券码已存在")
|
if req.valid_from:
|
||||||
coupon.code = code
|
coupon.valid_from = datetime.strptime(req.valid_from, '%Y-%m-%d')
|
||||||
if value is not None:
|
if req.valid_to:
|
||||||
coupon.value = value
|
coupon.valid_to = datetime.strptime(req.valid_to, '%Y-%m-%d')
|
||||||
if min_amount is not None:
|
|
||||||
coupon.min_amount = min_amount
|
|
||||||
if valid_from is not None:
|
|
||||||
coupon.valid_from = datetime.fromisoformat(valid_from)
|
|
||||||
if valid_to is not None:
|
|
||||||
coupon.valid_to = datetime.fromisoformat(valid_to)
|
|
||||||
if total_count is not None:
|
|
||||||
coupon.total_count = total_count
|
|
||||||
if status is not None:
|
|
||||||
coupon.status = status
|
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
return {"code": 0, "message": "更新成功"}
|
||||||
LogService.log_audit(
|
|
||||||
admin_id=admin_user.id,
|
|
||||||
admin_name=admin_user.username,
|
|
||||||
action="update_coupon",
|
|
||||||
target_type="coupon",
|
|
||||||
target_id=id,
|
|
||||||
detail=f"更新优惠券 {coupon.code}",
|
|
||||||
)
|
|
||||||
|
|
||||||
return {"message": "优惠券已更新"}
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/coupons/{id}")
|
@router.delete("/coupons/{coupon_id}", summary="删除优惠券")
|
||||||
async def delete_coupon(
|
async def delete_coupon(
|
||||||
id: str,
|
coupon_id: str,
|
||||||
admin_user=Depends(get_current_admin),
|
admin_user=Depends(get_current_admin),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""删除优惠券"""
|
"""删除优惠券"""
|
||||||
coupon = db.query(Coupon).filter(Coupon.id == id).first()
|
from ...models import Coupon
|
||||||
|
coupon = db.query(Coupon).filter(Coupon.id == coupon_id).first()
|
||||||
if not coupon:
|
if not coupon:
|
||||||
raise HTTPException(status_code=404, detail="优惠券不存在")
|
raise HTTPException(status_code=404, detail="优惠券不存在")
|
||||||
|
|
||||||
db.delete(coupon)
|
db.delete(coupon)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
return {"code": 0, "message": "已删除"}
|
||||||
LogService.log_audit(
|
|
||||||
admin_id=admin_user.id,
|
|
||||||
admin_name=admin_user.username,
|
|
||||||
action="delete_coupon",
|
|
||||||
target_type="coupon",
|
|
||||||
target_id=id,
|
|
||||||
detail=f"删除优惠券 {coupon.code}",
|
|
||||||
)
|
|
||||||
|
|
||||||
return {"message": "优惠券已删除"}
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/coupons/{id}/assign")
|
|
||||||
async def assign_coupon(
|
|
||||||
id: str,
|
|
||||||
user_id: str,
|
|
||||||
admin_user=Depends(get_current_admin),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
):
|
|
||||||
"""发放优惠券给用户"""
|
|
||||||
coupon = db.query(Coupon).filter(Coupon.id == id).first()
|
|
||||||
if not coupon:
|
|
||||||
raise HTTPException(status_code=404, detail="优惠券不存在")
|
|
||||||
if coupon.status != "active":
|
|
||||||
raise HTTPException(status_code=400, detail="优惠券非活跃状态")
|
|
||||||
|
|
||||||
user = db.query(User).filter(User.id == user_id).first()
|
|
||||||
if not user:
|
|
||||||
raise HTTPException(status_code=404, detail="用户不存在")
|
|
||||||
|
|
||||||
# 检查是否已发放
|
|
||||||
existing = db.query(UserCoupon).filter(
|
|
||||||
UserCoupon.user_id == user_id,
|
|
||||||
UserCoupon.coupon_id == id,
|
|
||||||
).first()
|
|
||||||
if existing:
|
|
||||||
raise HTTPException(status_code=400, detail="该用户已拥有此优惠券")
|
|
||||||
|
|
||||||
user_coupon = UserCoupon(
|
|
||||||
id=str(uuid.uuid4()),
|
|
||||||
user_id=user_id,
|
|
||||||
coupon_id=id,
|
|
||||||
)
|
|
||||||
db.add(user_coupon)
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
LogService.log_audit(
|
|
||||||
admin_id=admin_user.id,
|
|
||||||
admin_name=admin_user.username,
|
|
||||||
action="assign_coupon",
|
|
||||||
target_type="coupon",
|
|
||||||
target_id=id,
|
|
||||||
detail=f"发放优惠券 {coupon.code} 给用户 {user.username}",
|
|
||||||
)
|
|
||||||
|
|
||||||
return {"message": "优惠券已发放"}
|
|
||||||
@@ -375,8 +375,16 @@ export const adminAPI = {
|
|||||||
toggleAd: (id: string) => put<any>(`/admin/ads/${id}/toggle`),
|
toggleAd: (id: string) => put<any>(`/admin/ads/${id}/toggle`),
|
||||||
// 应用市场审核
|
// 应用市场审核
|
||||||
getPendingApps: () => get<any[]>('/admin/market/apps/pending'),
|
getPendingApps: () => get<any[]>('/admin/market/apps/pending'),
|
||||||
approveApp: (id: string) => put<any>(`/admin/market/apps/${id}/approve`),
|
getMarketOverview: () => get<any>('/admin/market/analytics/overview'),
|
||||||
rejectApp: (id: string) => put<any>(`/admin/market/apps/${id}/reject`),
|
getTopApps: (metric: string, limit: number) => get<any>(`/admin/market/analytics/top-apps?metric=${metric}&limit=${limit}`),
|
||||||
|
getRecentReviews: () => get<any>('/admin/market/analytics/recent-reviews'),
|
||||||
|
getAdminMarketApps: (query: string) => get<any>(`/admin/market/apps?${query}`),
|
||||||
|
getMarketCategories: () => get<any>('/admin/market/categories'),
|
||||||
|
setFeaturedApp: (id: string, featured: boolean, order: number) => put<any>(`/admin/market/apps/${id}/featured`, { featured, featured_order: order }),
|
||||||
|
deleteMarketApp: (id: string) => del<any>(`/admin/market/apps/${id}`),
|
||||||
|
updateMarketApp: (id: string, data: any) => put<any>(`/admin/market/apps/${id}`, data),
|
||||||
|
approveApp: (id: string) => put<any>(`/admin/market/apps/${id}/approve`),
|
||||||
|
rejectApp: (id: string) => put<any>(`/admin/market/apps/${id.includes('?') ? id.split('?')[0] + '/reject?' + id.split('?')[1] : id + '/reject'}`),
|
||||||
getAdminApps: () => get<any[]>('/admin/market/apps'),
|
getAdminApps: () => get<any[]>('/admin/market/apps'),
|
||||||
updateApp: (id: string, data: any) => put<any>(`/admin/market/apps/${id}`, data),
|
updateApp: (id: string, data: any) => put<any>(`/admin/market/apps/${id}`, data),
|
||||||
getCoupons: () => get<any[]>('/admin/market/coupons'),
|
getCoupons: () => get<any[]>('/admin/market/coupons'),
|
||||||
|
|||||||
+904
-289
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user