diff --git a/backend/app/api/orders.py b/backend/app/api/orders.py index 41979bf..b6201af 100644 --- a/backend/app/api/orders.py +++ b/backend/app/api/orders.py @@ -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() diff --git a/backend/app/api/payment.py b/backend/app/api/payment.py index f491447..9888922 100644 --- a/backend/app/api/payment.py +++ b/backend/app/api/payment.py @@ -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() diff --git a/backend/app/api/upload.py b/backend/app/api/upload.py index 144a215..f3f2571 100644 --- a/backend/app/api/upload.py +++ b/backend/app/api/upload.py @@ -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}" diff --git a/backend/app/api/works.py b/backend/app/api/works.py index fb4bfb4..ad32bb4 100644 --- a/backend/app/api/works.py +++ b/backend/app/api/works.py @@ -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) diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 1fe0c65..2c0be1f 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -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" diff --git a/backend/app/main.py b/backend/app/main.py index ded5002..462d3e6 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -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, diff --git a/backend/app/models/download.py b/backend/app/models/download.py index 52c57a8..ca415fe 100644 --- a/backend/app/models/download.py +++ b/backend/app/models/download.py @@ -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()) diff --git a/backend/app/services/download_tracker.py b/backend/app/services/download_tracker.py new file mode 100644 index 0000000..6b05d79 --- /dev/null +++ b/backend/app/services/download_tracker.py @@ -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)) diff --git a/backend/app/services/wecom_bot.py b/backend/app/services/wecom_bot.py new file mode 100644 index 0000000..a739cb8 --- /dev/null +++ b/backend/app/services/wecom_bot.py @@ -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