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.order import Order, OrderStatus
|
||||||
from app.models.download import DownloadRecord
|
from app.models.download import DownloadRecord
|
||||||
from app.schemas.order import OrderCreate, OrderResponse, PaymentResponse
|
from app.schemas.order import OrderCreate, OrderResponse, PaymentResponse
|
||||||
|
from app.services.download_tracker import record_download
|
||||||
|
|
||||||
router = APIRouter(prefix="/orders", tags=["订单"])
|
router = APIRouter(prefix="/orders", tags=["订单"])
|
||||||
|
|
||||||
@@ -148,17 +149,9 @@ def pay_order(
|
|||||||
order.status = OrderStatus.PAID
|
order.status = OrderStatus.PAID
|
||||||
order.paid_at = datetime.now()
|
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 = db.query(Work).filter(Work.id == order.work_id).first()
|
||||||
work.downloads += 1
|
if work:
|
||||||
|
record_download(db, order, work, current_user)
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from app.models.download import DownloadRecord
|
|||||||
from app.ysm_sdk import create_payment, query_order, PaymentNotify
|
from app.ysm_sdk import create_payment, query_order, PaymentNotify
|
||||||
from app.api.orders import get_current_user
|
from app.api.orders import get_current_user
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
|
from app.services.download_tracker import record_download
|
||||||
|
|
||||||
router = APIRouter(prefix="/payment", tags=["支付"])
|
router = APIRouter(prefix="/payment", tags=["支付"])
|
||||||
|
|
||||||
@@ -139,18 +140,10 @@ async def payment_notify(
|
|||||||
order.payment_method = "ysm_wechat" # 易收米微信支付
|
order.payment_method = "ysm_wechat" # 易收米微信支付
|
||||||
order.paid_at = datetime.now()
|
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()
|
work = db.query(Work).filter(Work.id == order.work_id).first()
|
||||||
if work:
|
buyer = db.query(User).filter(User.id == order.user_id).first()
|
||||||
work.downloads += 1
|
if work and buyer:
|
||||||
|
record_download(db, order, work, buyer)
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
@@ -204,18 +197,9 @@ async def query_order_status(
|
|||||||
order.payment_method = "ysm_wechat"
|
order.payment_method = "ysm_wechat"
|
||||||
order.paid_at = datetime.now()
|
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()
|
work = db.query(Work).filter(Work.id == order.work_id).first()
|
||||||
if work:
|
if work:
|
||||||
work.downloads += 1
|
record_download(db, order, work, current_user)
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ async def upload_work(
|
|||||||
description: Optional[str] = Form(None, description="作品描述"),
|
description: Optional[str] = Form(None, description="作品描述"),
|
||||||
category: str = Form(..., description="作品分类"),
|
category: str = Form(..., description="作品分类"),
|
||||||
tags: Optional[str] = Form(None, description="标签,逗号分隔"),
|
tags: Optional[str] = Form(None, description="标签,逗号分隔"),
|
||||||
|
designer_name: Optional[str] = Form(None, description="归属设计师"),
|
||||||
price: float = Form(..., ge=0, description="作品价格"),
|
price: float = Form(..., ge=0, description="作品价格"),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
@@ -189,6 +190,7 @@ async def upload_work(
|
|||||||
tags_str = ",".join(tags_list) # 转成逗号分隔的字符串
|
tags_str = ",".join(tags_list) # 转成逗号分隔的字符串
|
||||||
else:
|
else:
|
||||||
tags_str = None
|
tags_str = None
|
||||||
|
resolved_designer = str(designer_name or "").strip() or current_user.nickname or current_user.phone
|
||||||
|
|
||||||
# 创建数据库记录 - 修复所有字段问题
|
# 创建数据库记录 - 修复所有字段问题
|
||||||
work = Work(
|
work = Work(
|
||||||
@@ -197,7 +199,7 @@ async def upload_work(
|
|||||||
category=category,
|
category=category,
|
||||||
tags=tags_str, # 修复:使用字符串而不是列表
|
tags=tags_str, # 修复:使用字符串而不是列表
|
||||||
price=price,
|
price=price,
|
||||||
designer=current_user.nickname or current_user.phone,
|
designer=resolved_designer,
|
||||||
original_image=f"/uploads/original/{timestamp}/{original_filename}",
|
original_image=f"/uploads/original/{timestamp}/{original_filename}",
|
||||||
thumbnail_image=f"/uploads/thumbnail/{timestamp}/{thumb_filename}",
|
thumbnail_image=f"/uploads/thumbnail/{timestamp}/{thumb_filename}",
|
||||||
watermarked_image=f"/uploads/watermarked/{timestamp}/{watermarked_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 fastapi.responses import FileResponse
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from sqlalchemy import desc
|
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.security import decode_access_token
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.schemas.work import WorkResponse, WorkListResponse
|
from app.schemas.work import WorkResponse, WorkListResponse
|
||||||
|
from app.services.download_tracker import notify_download
|
||||||
|
|
||||||
router = APIRouter(prefix="/works", tags=["作品"])
|
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="下载作品原图")
|
@router.get("/{work_id}/download", summary="下载作品原图")
|
||||||
def download_work(
|
def download_work(
|
||||||
work_id: int,
|
work_id: int,
|
||||||
|
background_tasks: BackgroundTasks,
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
@@ -166,6 +168,8 @@ def download_work(
|
|||||||
detail="文件不存在"
|
detail="文件不存在"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
background_tasks.add_task(notify_download, work, current_user, paid_order)
|
||||||
|
|
||||||
# 返回文件
|
# 返回文件
|
||||||
filename = os.path.basename(full_path)
|
filename = os.path.basename(full_path)
|
||||||
media_type = mimetypes.guess_type(filename)[0] or "application/octet-stream"
|
media_type = mimetypes.guess_type(filename)[0] or "application/octet-stream"
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ class Settings(BaseSettings):
|
|||||||
YSM_NOTIFY_URL: str = "https://tuhui.cloud/api/payment/notify" # 支付回调地址
|
YSM_NOTIFY_URL: str = "https://tuhui.cloud/api/payment/notify" # 支付回调地址
|
||||||
YSM_CALLBACK_URL: str = "https://tuhui.cloud/payment/success" # 支付成功跳转地址
|
YSM_CALLBACK_URL: str = "https://tuhui.cloud/payment/success" # 支付成功跳转地址
|
||||||
YSM_NOPAY_URL: str = "https://tuhui.cloud/payment/cancel" # 未支付跳转地址
|
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:
|
class Config:
|
||||||
env_file = ".env"
|
env_file = ".env"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from fastapi import FastAPI, Request, Response
|
from fastapi import FastAPI, Request, Response
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from sqlalchemy import inspect, text
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.core.database import Base, engine
|
from app.core.database import Base, engine
|
||||||
from app.api import auth, works, orders, payment, upload
|
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)
|
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 应用
|
# 创建 FastAPI 应用
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title=settings.PROJECT_NAME,
|
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 sqlalchemy.sql import func
|
||||||
from app.core.database import Base
|
from app.core.database import Base
|
||||||
|
|
||||||
@@ -9,5 +9,8 @@ class DownloadRecord(Base):
|
|||||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
|
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
|
||||||
work_id = Column(Integer, ForeignKey("works.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)
|
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())
|
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