feat: track designer downloads and notifications

This commit is contained in:
2026-03-08 23:42:18 +08:00
parent 5ff85debdc
commit 147fc58409
9 changed files with 119 additions and 37 deletions

View File

@@ -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()

View File

@@ -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()

View File

@@ -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}"

View File

@@ -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)

View File

@@ -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"

View File

@@ -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,

View File

@@ -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())

View 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))

View 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