feat: track designer downloads and notifications
This commit is contained in:
@@ -9,6 +9,7 @@ from app.models.work import Work
|
||||
from app.models.order import Order, OrderStatus
|
||||
from app.models.download import DownloadRecord
|
||||
from app.schemas.order import OrderCreate, OrderResponse, PaymentResponse
|
||||
from app.services.download_tracker import record_download
|
||||
|
||||
router = APIRouter(prefix="/orders", tags=["订单"])
|
||||
|
||||
@@ -148,17 +149,9 @@ def pay_order(
|
||||
order.status = OrderStatus.PAID
|
||||
order.paid_at = datetime.now()
|
||||
|
||||
# 创建下载记录
|
||||
download_record = DownloadRecord(
|
||||
user_id=current_user.id,
|
||||
work_id=order.work_id,
|
||||
order_id=order.id
|
||||
)
|
||||
db.add(download_record)
|
||||
|
||||
# 增加作品下载量
|
||||
work = db.query(Work).filter(Work.id == order.work_id).first()
|
||||
work.downloads += 1
|
||||
if work:
|
||||
record_download(db, order, work, current_user)
|
||||
|
||||
db.commit()
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ from app.models.download import DownloadRecord
|
||||
from app.ysm_sdk import create_payment, query_order, PaymentNotify
|
||||
from app.api.orders import get_current_user
|
||||
from app.models.user import User
|
||||
from app.services.download_tracker import record_download
|
||||
|
||||
router = APIRouter(prefix="/payment", tags=["支付"])
|
||||
|
||||
@@ -138,19 +139,11 @@ async def payment_notify(
|
||||
order.status = OrderStatus.PAID
|
||||
order.payment_method = "ysm_wechat" # 易收米微信支付
|
||||
order.paid_at = datetime.now()
|
||||
|
||||
# 创建下载记录
|
||||
download_record = DownloadRecord(
|
||||
user_id=order.user_id,
|
||||
work_id=order.work_id,
|
||||
order_id=order.id
|
||||
)
|
||||
db.add(download_record)
|
||||
|
||||
# 更新作品下载次数
|
||||
|
||||
work = db.query(Work).filter(Work.id == order.work_id).first()
|
||||
if work:
|
||||
work.downloads += 1
|
||||
buyer = db.query(User).filter(User.id == order.user_id).first()
|
||||
if work and buyer:
|
||||
record_download(db, order, work, buyer)
|
||||
|
||||
db.commit()
|
||||
|
||||
@@ -203,19 +196,10 @@ async def query_order_status(
|
||||
order.status = OrderStatus.PAID
|
||||
order.payment_method = "ysm_wechat"
|
||||
order.paid_at = datetime.now()
|
||||
|
||||
# 创建下载记录
|
||||
download_record = DownloadRecord(
|
||||
user_id=order.user_id,
|
||||
work_id=order.work_id,
|
||||
order_id=order.id
|
||||
)
|
||||
db.add(download_record)
|
||||
|
||||
# 更新作品下载次数
|
||||
|
||||
work = db.query(Work).filter(Work.id == order.work_id).first()
|
||||
if work:
|
||||
work.downloads += 1
|
||||
record_download(db, order, work, current_user)
|
||||
|
||||
db.commit()
|
||||
|
||||
|
||||
@@ -119,6 +119,7 @@ async def upload_work(
|
||||
description: Optional[str] = Form(None, description="作品描述"),
|
||||
category: str = Form(..., description="作品分类"),
|
||||
tags: Optional[str] = Form(None, description="标签,逗号分隔"),
|
||||
designer_name: Optional[str] = Form(None, description="归属设计师"),
|
||||
price: float = Form(..., ge=0, description="作品价格"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
@@ -189,6 +190,7 @@ async def upload_work(
|
||||
tags_str = ",".join(tags_list) # 转成逗号分隔的字符串
|
||||
else:
|
||||
tags_str = None
|
||||
resolved_designer = str(designer_name or "").strip() or current_user.nickname or current_user.phone
|
||||
|
||||
# 创建数据库记录 - 修复所有字段问题
|
||||
work = Work(
|
||||
@@ -197,7 +199,7 @@ async def upload_work(
|
||||
category=category,
|
||||
tags=tags_str, # 修复:使用字符串而不是列表
|
||||
price=price,
|
||||
designer=current_user.nickname or current_user.phone,
|
||||
designer=resolved_designer,
|
||||
original_image=f"/uploads/original/{timestamp}/{original_filename}",
|
||||
thumbnail_image=f"/uploads/thumbnail/{timestamp}/{thumb_filename}",
|
||||
watermarked_image=f"/uploads/watermarked/{timestamp}/{watermarked_filename}"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query, Header
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query, Header, BackgroundTasks
|
||||
from fastapi.responses import FileResponse
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import desc
|
||||
@@ -13,6 +13,7 @@ from app.models.user import User
|
||||
from app.core.security import decode_access_token
|
||||
from app.core.config import settings
|
||||
from app.schemas.work import WorkResponse, WorkListResponse
|
||||
from app.services.download_tracker import notify_download
|
||||
|
||||
router = APIRouter(prefix="/works", tags=["作品"])
|
||||
|
||||
@@ -126,6 +127,7 @@ def get_current_user(authorization: str = Header(None), db: Session = Depends(ge
|
||||
@router.get("/{work_id}/download", summary="下载作品原图")
|
||||
def download_work(
|
||||
work_id: int,
|
||||
background_tasks: BackgroundTasks,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
@@ -165,6 +167,8 @@ def download_work(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="文件不存在"
|
||||
)
|
||||
|
||||
background_tasks.add_task(notify_download, work, current_user, paid_order)
|
||||
|
||||
# 返回文件
|
||||
filename = os.path.basename(full_path)
|
||||
|
||||
@@ -42,6 +42,7 @@ class Settings(BaseSettings):
|
||||
YSM_NOTIFY_URL: str = "https://tuhui.cloud/api/payment/notify" # 支付回调地址
|
||||
YSM_CALLBACK_URL: str = "https://tuhui.cloud/payment/success" # 支付成功跳转地址
|
||||
YSM_NOPAY_URL: str = "https://tuhui.cloud/payment/cancel" # 未支付跳转地址
|
||||
WECOM_BOT_WEBHOOK: str = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=cc88bdef-a13f-4d7e-bdb6-ee51b68b8205"
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from fastapi import FastAPI, Request, Response
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from sqlalchemy import inspect, text
|
||||
from app.core.config import settings
|
||||
from app.core.database import Base, engine
|
||||
from app.api import auth, works, orders, payment, upload
|
||||
@@ -8,6 +9,25 @@ from app.api import auth, works, orders, payment, upload
|
||||
# 创建数据库表
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
|
||||
def ensure_runtime_schema():
|
||||
inspector = inspect(engine)
|
||||
columns = {col["name"] for col in inspector.get_columns("download_records")}
|
||||
statements = []
|
||||
if "designer_name" not in columns:
|
||||
statements.append("ALTER TABLE download_records ADD COLUMN designer_name VARCHAR(100)")
|
||||
if "work_title" not in columns:
|
||||
statements.append("ALTER TABLE download_records ADD COLUMN work_title VARCHAR(255)")
|
||||
if "amount" not in columns:
|
||||
statements.append("ALTER TABLE download_records ADD COLUMN amount FLOAT DEFAULT 0")
|
||||
if statements:
|
||||
with engine.begin() as conn:
|
||||
for sql in statements:
|
||||
conn.execute(text(sql))
|
||||
|
||||
|
||||
ensure_runtime_schema()
|
||||
|
||||
# 创建 FastAPI 应用
|
||||
app = FastAPI(
|
||||
title=settings.PROJECT_NAME,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from sqlalchemy import Column, Integer, DateTime, ForeignKey
|
||||
from sqlalchemy import Column, Integer, DateTime, ForeignKey, String, Float
|
||||
from sqlalchemy.sql import func
|
||||
from app.core.database import Base
|
||||
|
||||
@@ -9,5 +9,8 @@ class DownloadRecord(Base):
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
|
||||
work_id = Column(Integer, ForeignKey("works.id"), nullable=False, index=True)
|
||||
order_id = Column(Integer, ForeignKey("orders.id"), nullable=False, index=True)
|
||||
|
||||
designer_name = Column(String(100), nullable=True, index=True)
|
||||
work_title = Column(String(255), nullable=True)
|
||||
amount = Column(Float, default=0.0)
|
||||
|
||||
downloaded_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
46
backend/app/services/download_tracker.py
Normal file
46
backend/app/services/download_tracker.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.download import DownloadRecord
|
||||
from app.models.order import Order
|
||||
from app.models.user import User
|
||||
from app.models.work import Work
|
||||
from app.services.wecom_bot import send_wecom_text
|
||||
|
||||
|
||||
def _build_download_notice(work: Work, buyer: User, order: Order) -> str:
|
||||
return "\n".join(
|
||||
[
|
||||
"【图绘下载通知】",
|
||||
f"作品ID:{work.id}",
|
||||
f"作品标题:{work.title}",
|
||||
f"设计师:{work.designer or '-'}",
|
||||
f"购买用户:{buyer.nickname or buyer.phone or buyer.id}",
|
||||
f"金额:{order.amount}元",
|
||||
f"下载时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
|
||||
f"作品链接:https://tuhui.cloud/detail/{work.id}",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def record_download(db: Session, order: Order, work: Work, buyer: User) -> DownloadRecord:
|
||||
existing = db.query(DownloadRecord).filter(DownloadRecord.order_id == order.id).first()
|
||||
if existing:
|
||||
return existing
|
||||
|
||||
record = DownloadRecord(
|
||||
user_id=buyer.id,
|
||||
work_id=work.id,
|
||||
order_id=order.id,
|
||||
designer_name=work.designer or "",
|
||||
work_title=work.title or "",
|
||||
amount=float(order.amount or 0.0),
|
||||
)
|
||||
db.add(record)
|
||||
work.downloads = int(work.downloads or 0) + 1
|
||||
return record
|
||||
|
||||
|
||||
def notify_download(work: Work, buyer: User, order: Order):
|
||||
send_wecom_text(_build_download_notice(work, buyer, order))
|
||||
29
backend/app/services/wecom_bot.py
Normal file
29
backend/app/services/wecom_bot.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import logging
|
||||
|
||||
import httpx
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def send_wecom_text(content: str) -> bool:
|
||||
text = str(content or "").strip()
|
||||
webhook = str(getattr(settings, "WECOM_BOT_WEBHOOK", "") or "").strip()
|
||||
if not text or not webhook:
|
||||
return False
|
||||
try:
|
||||
response = httpx.post(
|
||||
webhook,
|
||||
json={"msgtype": "text", "text": {"content": text[:3500]}},
|
||||
timeout=10.0,
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
ok = int(data.get("errcode", -1)) == 0
|
||||
if not ok:
|
||||
logger.warning(f"WeCom bot send failed: {data}")
|
||||
return ok
|
||||
except Exception as e:
|
||||
logger.warning(f"WeCom bot send exception: {e}")
|
||||
return False
|
||||
Reference in New Issue
Block a user