Phase 3.7: Market Expansion - analytics dashboard, featured apps, category system, real API integration (replaces mock data)

This commit is contained in:
qyyfw committed 2026-07-06 09:46:53 +08:00
1 parent 2c5ab13209
commit 63741b3f0f
3 files changed
+1442 -581

No files matched your search

+540 -302
View File
@@ -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 {
"code": 0,
"data": {
"total": total, "total": total,
"page": page, "page": page,
"page_size": page_size, "page_size": page_size,
"items": [ "items": [{
{
"id": a.id,
"name": a.name,
"type": a.type,
"source": a.source,
"publisher_id": a.publisher_id,
"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,
"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}")
async def admin_edit_app(
id: str,
name: str = None,
description: str = None,
icon: str = None,
price: float = None,
is_member_free: bool = None,
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.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, "id": c.id,
"code": c.code, "code": c.code,
"type": c.type, "type": c.type,
"value": float(c.value) if c.value else 0, "value": float(c.value) if c.value else 0,
"min_amount": float(c.min_amount) if c.min_amount 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_from": c.valid_from.strftime('%Y-%m-%d %H:%M:%S') if c.valid_from else None,
"valid_to": c.valid_to.isoformat() if c.valid_to else None, "valid_to": c.valid_to.strftime('%Y-%m-%d %H:%M:%S') if c.valid_to else None,
"total_count": c.total_count, "total_count": c.total_count,
"used_count": c.used_count, "used_count": c.used_count,
"status": c.status, "status": c.status,
"created_at": c.created_at.isoformat() if c.created_at else None, } for c in coupons],
}
} }
for c in coupons
]
@router.post("/coupons") class CouponCreateRequest(BaseModel):
code: str
type: str # fixed / percent
value: float
min_amount: float = 0
valid_from: Optional[str] = None
valid_to: Optional[str] = None
total_count: int = 100
@router.post("/coupons", summary="创建优惠券")
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": "优惠券已发放"}
+9 -1
View File
@@ -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'),
getMarketOverview: () => get<any>('/admin/market/analytics/overview'),
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`), approveApp: (id: string) => put<any>(`/admin/market/apps/${id}/approve`),
rejectApp: (id: string) => put<any>(`/admin/market/apps/${id}/reject`), 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'),
File diff suppressed because it is too large. Load diff