add
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,299 @@
|
||||
"""管理后台路由(/admin/*,对应《03-接口文档》第 11 章)
|
||||
|
||||
角色矩阵(super 拥有全部权限):
|
||||
- reviewer:仅审核 → 待审核列表 / 审核视频
|
||||
- operator:套餐、活动、看板 → 仪表盘 / 套餐 CRUD / 赠送
|
||||
- 其余接口仅 super
|
||||
"""
|
||||
import redis.asyncio as aioredis
|
||||
from fastapi import APIRouter, Depends, Query, Request
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.response import ok
|
||||
from app.database import get_db
|
||||
from app.deps import AdminIdentity, require_roles
|
||||
from app.redis_client import get_redis
|
||||
from app.schemas import admin as schema
|
||||
from app.services import admin_service
|
||||
|
||||
router = APIRouter(prefix="/admin", tags=["管理后台"])
|
||||
|
||||
# 角色依赖:super 自动放行
|
||||
require_reviewer = require_roles("reviewer")
|
||||
require_operator = require_roles("operator")
|
||||
require_super = require_roles()
|
||||
|
||||
|
||||
def _ip(request: Request) -> str:
|
||||
return request.client.host if request.client else ""
|
||||
|
||||
|
||||
# ---------- 认证 / 看板 ----------
|
||||
|
||||
@router.post("/auth/login")
|
||||
async def login(req: schema.AdminLoginReq, request: Request, db: AsyncSession = Depends(get_db)):
|
||||
return ok(await admin_service.login(db, req, _ip(request)))
|
||||
|
||||
|
||||
@router.get("/dashboard")
|
||||
async def dashboard(db: AsyncSession = Depends(get_db),
|
||||
admin: AdminIdentity = Depends(require_operator)):
|
||||
return ok(await admin_service.dashboard(db))
|
||||
|
||||
|
||||
# ---------- 视频 ----------
|
||||
|
||||
@router.get("/videos/pending")
|
||||
async def list_pending_videos(page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
admin: AdminIdentity = Depends(require_reviewer)):
|
||||
return ok(await admin_service.list_pending_videos(db, page, page_size))
|
||||
|
||||
|
||||
@router.get("/videos")
|
||||
async def list_videos(keyword: str | None = Query(None), status: int | None = Query(None),
|
||||
author_id: int | None = Query(None),
|
||||
page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
admin: AdminIdentity = Depends(require_super)):
|
||||
return ok(await admin_service.list_videos(db, keyword, status, author_id, page, page_size))
|
||||
|
||||
|
||||
@router.post("/videos/{video_id}/review")
|
||||
async def review_video(video_id: int, req: schema.VideoReviewReq, request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
redis: aioredis.Redis = Depends(get_redis),
|
||||
admin: AdminIdentity = Depends(require_reviewer)):
|
||||
return ok(await admin_service.review_video(db, redis, admin, video_id, req, _ip(request)))
|
||||
|
||||
|
||||
@router.post("/videos/{video_id}/offline")
|
||||
async def offline_video(video_id: int, req: schema.ReasonReq, request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
redis: aioredis.Redis = Depends(get_redis),
|
||||
admin: AdminIdentity = Depends(require_super)):
|
||||
await admin_service.offline_video(db, redis, admin, video_id, req.reason, _ip(request))
|
||||
return ok()
|
||||
|
||||
|
||||
# ---------- 用户 / 积分 ----------
|
||||
|
||||
@router.get("/users")
|
||||
async def list_users(keyword: str | None = Query(None), status: int | None = Query(None),
|
||||
page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
admin: AdminIdentity = Depends(require_super)):
|
||||
return ok(await admin_service.list_users(db, keyword, status, page, page_size))
|
||||
|
||||
|
||||
@router.get("/users/{user_id}")
|
||||
async def get_user_detail(user_id: int, db: AsyncSession = Depends(get_db),
|
||||
admin: AdminIdentity = Depends(require_super)):
|
||||
return ok(await admin_service.get_user_detail(db, user_id))
|
||||
|
||||
|
||||
@router.put("/users/{user_id}/status")
|
||||
async def set_user_status(user_id: int, req: schema.UserStatusReq, request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
redis: aioredis.Redis = Depends(get_redis),
|
||||
admin: AdminIdentity = Depends(require_super)):
|
||||
return ok(await admin_service.set_user_status(db, redis, admin, user_id, req, _ip(request)))
|
||||
|
||||
|
||||
@router.post("/points/adjust")
|
||||
async def adjust_points(req: schema.PointsAdjustReq, request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
admin: AdminIdentity = Depends(require_super)):
|
||||
return ok(await admin_service.adjust_points(db, admin, req, _ip(request)))
|
||||
|
||||
|
||||
@router.get("/points/records")
|
||||
async def list_point_records(user_id: int | None = Query(None), biz_type: int | None = Query(None),
|
||||
page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
admin: AdminIdentity = Depends(require_super)):
|
||||
return ok(await admin_service.list_point_records(db, user_id, biz_type, page, page_size))
|
||||
|
||||
|
||||
# ---------- 订单 / 套餐 ----------
|
||||
|
||||
@router.get("/orders")
|
||||
async def list_orders(order_no: str | None = Query(None), user_id: int | None = Query(None),
|
||||
pay_method: int | None = Query(None), status: int | None = Query(None),
|
||||
start_date: str | None = Query(None), end_date: str | None = Query(None),
|
||||
page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
admin: AdminIdentity = Depends(require_super)):
|
||||
return ok(await admin_service.list_orders(
|
||||
db, order_no, user_id, pay_method, status, start_date, end_date, page, page_size
|
||||
))
|
||||
|
||||
|
||||
@router.post("/orders/{order_no}/resettle")
|
||||
async def resettle_order(order_no: str, req: schema.ResettleReq, request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
admin: AdminIdentity = Depends(require_super)):
|
||||
return ok(await admin_service.resettle_order(db, admin, order_no, req, _ip(request)))
|
||||
|
||||
|
||||
@router.post("/orders/{order_no}/refund")
|
||||
async def refund_order(order_no: str, req: schema.RefundReq, request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
admin: AdminIdentity = Depends(require_super)):
|
||||
return ok(await admin_service.refund_order(db, admin, order_no, req, _ip(request)))
|
||||
|
||||
|
||||
@router.get("/packages")
|
||||
async def list_packages(db: AsyncSession = Depends(get_db),
|
||||
admin: AdminIdentity = Depends(require_operator)):
|
||||
return ok(await admin_service.list_packages(db))
|
||||
|
||||
|
||||
@router.post("/packages")
|
||||
async def create_package(req: schema.PackageCreateReq, request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
admin: AdminIdentity = Depends(require_operator)):
|
||||
return ok(await admin_service.create_package(db, admin, req, _ip(request)))
|
||||
|
||||
|
||||
@router.put("/packages/{package_id}")
|
||||
async def update_package(package_id: int, req: schema.PackageUpdateReq, request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
admin: AdminIdentity = Depends(require_operator)):
|
||||
return ok(await admin_service.update_package(db, admin, package_id, req, _ip(request)))
|
||||
|
||||
|
||||
@router.delete("/packages/{package_id}")
|
||||
async def delete_package(package_id: int, request: Request, db: AsyncSession = Depends(get_db),
|
||||
admin: AdminIdentity = Depends(require_operator)):
|
||||
return ok(await admin_service.delete_package(db, admin, package_id, _ip(request)))
|
||||
|
||||
|
||||
# ---------- 赠送 / 日志 ----------
|
||||
|
||||
@router.post("/gift")
|
||||
async def gift(req: schema.GiftReq, request: Request, db: AsyncSession = Depends(get_db),
|
||||
admin: AdminIdentity = Depends(require_operator)):
|
||||
return ok(await admin_service.gift(db, admin, req, _ip(request)))
|
||||
|
||||
|
||||
@router.get("/logs")
|
||||
async def list_logs(admin_id: int | None = Query(None), action: str | None = Query(None),
|
||||
page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
admin: AdminIdentity = Depends(require_super)):
|
||||
return ok(await admin_service.list_logs(db, admin_id, action, page, page_size))
|
||||
|
||||
|
||||
# ---------- 提现 ----------
|
||||
|
||||
@router.get("/withdrawals")
|
||||
async def list_withdrawals(status: int | None = Query(None), user_id: int | None = Query(None),
|
||||
page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
admin: AdminIdentity = Depends(require_super)):
|
||||
return ok(await admin_service.list_withdrawals(db, status, user_id, page, page_size))
|
||||
|
||||
|
||||
@router.post("/withdrawals/{withdraw_no}/review")
|
||||
async def review_withdrawal(withdraw_no: str, req: schema.WithdrawReviewReq, request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
redis: aioredis.Redis = Depends(get_redis),
|
||||
admin: AdminIdentity = Depends(require_super)):
|
||||
return ok(await admin_service.review_withdrawal(db, redis, admin, withdraw_no, req, _ip(request)))
|
||||
|
||||
|
||||
# ---------- 推荐 ----------
|
||||
|
||||
@router.get("/referrals")
|
||||
async def list_referrals(inviter_id: int | None = Query(None), status: int | None = Query(None),
|
||||
risk_flag: str | None = Query(None),
|
||||
page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
admin: AdminIdentity = Depends(require_super)):
|
||||
return ok(await admin_service.list_referrals(db, inviter_id, status, risk_flag, page, page_size))
|
||||
|
||||
|
||||
@router.post("/referrals/{referral_id}/review")
|
||||
async def review_referral(referral_id: int, req: schema.ReferralReviewReq, request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
admin: AdminIdentity = Depends(require_super)):
|
||||
return ok(await admin_service.review_referral(db, admin, referral_id, req, _ip(request)))
|
||||
|
||||
|
||||
# ---------- 配置 ----------
|
||||
|
||||
@router.get("/configs")
|
||||
async def list_configs(db: AsyncSession = Depends(get_db),
|
||||
admin: AdminIdentity = Depends(require_super)):
|
||||
return ok(await admin_service.list_configs(db))
|
||||
|
||||
|
||||
@router.put("/configs/{config_key}")
|
||||
async def update_config(config_key: str, req: schema.ConfigUpdateReq, request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
redis: aioredis.Redis = Depends(get_redis),
|
||||
admin: AdminIdentity = Depends(require_super)):
|
||||
return ok(await admin_service.update_config(db, redis, admin, config_key, req.value, _ip(request)))
|
||||
|
||||
|
||||
# ---------- 评论 / 举报 ----------
|
||||
|
||||
@router.get("/comments")
|
||||
async def list_comments(video_id: int | None = Query(None), user_id: int | None = Query(None),
|
||||
keyword: str | None = Query(None),
|
||||
page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
admin: AdminIdentity = Depends(require_super)):
|
||||
return ok(await admin_service.list_comments(db, video_id, user_id, keyword, page, page_size))
|
||||
|
||||
|
||||
@router.delete("/comments/{comment_id}")
|
||||
async def delete_comment(comment_id: int, req: schema.ReasonReq, request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
redis: aioredis.Redis = Depends(get_redis),
|
||||
admin: AdminIdentity = Depends(require_super)):
|
||||
await admin_service.delete_comment(db, redis, admin, comment_id, req.reason, _ip(request))
|
||||
return ok()
|
||||
|
||||
|
||||
@router.get("/reports")
|
||||
async def list_reports(status: int | None = Query(None), target_type: int | None = Query(None),
|
||||
page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
admin: AdminIdentity = Depends(require_super)):
|
||||
return ok(await admin_service.list_reports(db, status, target_type, page, page_size))
|
||||
|
||||
|
||||
@router.post("/reports/{report_id}/review")
|
||||
async def review_report(report_id: int, req: schema.ReportReviewReq, request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
redis: aioredis.Redis = Depends(get_redis),
|
||||
admin: AdminIdentity = Depends(require_super)):
|
||||
return ok(await admin_service.review_report(db, redis, admin, report_id, req, _ip(request)))
|
||||
|
||||
|
||||
# ---------- 敏感词 ----------
|
||||
|
||||
@router.get("/sensitive-words")
|
||||
async def list_sensitive_words(keyword: str | None = Query(None),
|
||||
page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
admin: AdminIdentity = Depends(require_super)):
|
||||
return ok(await admin_service.list_sensitive_words(db, keyword, page, page_size))
|
||||
|
||||
|
||||
@router.post("/sensitive-words")
|
||||
async def add_sensitive_word(req: schema.SensitiveWordAddReq, request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
redis: aioredis.Redis = Depends(get_redis),
|
||||
admin: AdminIdentity = Depends(require_super)):
|
||||
return ok(await admin_service.add_sensitive_word(db, redis, admin, req.word, _ip(request)))
|
||||
|
||||
|
||||
@router.delete("/sensitive-words/{word_id}")
|
||||
async def delete_sensitive_word(word_id: int, request: Request, db: AsyncSession = Depends(get_db),
|
||||
redis: aioredis.Redis = Depends(get_redis),
|
||||
admin: AdminIdentity = Depends(require_super)):
|
||||
await admin_service.delete_sensitive_word(db, redis, admin, word_id, _ip(request))
|
||||
return ok()
|
||||
@@ -0,0 +1,77 @@
|
||||
"""认证模块路由(/auth/*,对应《03-接口文档》第 3 章)"""
|
||||
import redis.asyncio as aioredis
|
||||
from fastapi import APIRouter, Depends, Header, Request
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.response import ok
|
||||
from app.core.security import decode_token
|
||||
from app.database import get_db
|
||||
from app.deps import Identity, get_identity, require_user
|
||||
from app.redis_client import get_redis
|
||||
from app.schemas.auth import (
|
||||
EmailSendReq,
|
||||
LoginReq,
|
||||
PasswordResetReq,
|
||||
RefreshReq,
|
||||
RegisterReq,
|
||||
SmsSendReq,
|
||||
)
|
||||
from app.services import auth_service
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["认证"])
|
||||
|
||||
|
||||
def _client_ip(request: Request) -> str:
|
||||
# Nginx 反代后取真实 IP
|
||||
return request.headers.get("X-Real-IP") or (request.client.host if request.client else "")
|
||||
|
||||
|
||||
@router.post("/sms/send")
|
||||
async def sms_send(req: SmsSendReq, request: Request, db: AsyncSession = Depends(get_db),
|
||||
redis: aioredis.Redis = Depends(get_redis)):
|
||||
data = await auth_service.send_code(db, redis, "sms", req.phone, req.scene, _client_ip(request))
|
||||
return ok(data)
|
||||
|
||||
|
||||
@router.post("/email/send")
|
||||
async def email_send(req: EmailSendReq, request: Request, db: AsyncSession = Depends(get_db),
|
||||
redis: aioredis.Redis = Depends(get_redis)):
|
||||
data = await auth_service.send_code(db, redis, "email", req.email, req.scene, _client_ip(request))
|
||||
return ok(data)
|
||||
|
||||
|
||||
@router.post("/register")
|
||||
async def register(req: RegisterReq, request: Request, db: AsyncSession = Depends(get_db),
|
||||
redis: aioredis.Redis = Depends(get_redis),
|
||||
identity: Identity = Depends(get_identity)):
|
||||
data = await auth_service.register(db, redis, req, identity.device_id, _client_ip(request))
|
||||
return ok(data)
|
||||
|
||||
|
||||
@router.post("/login")
|
||||
async def login(req: LoginReq, request: Request, db: AsyncSession = Depends(get_db),
|
||||
redis: aioredis.Redis = Depends(get_redis)):
|
||||
data = await auth_service.login(db, redis, req, _client_ip(request))
|
||||
return ok(data)
|
||||
|
||||
|
||||
@router.post("/token/refresh")
|
||||
async def token_refresh(req: RefreshReq, db: AsyncSession = Depends(get_db),
|
||||
redis: aioredis.Redis = Depends(get_redis)):
|
||||
return ok(await auth_service.refresh(db, redis, req.refresh_token))
|
||||
|
||||
|
||||
@router.post("/logout")
|
||||
async def logout(authorization: str = Header(...),
|
||||
redis: aioredis.Redis = Depends(get_redis),
|
||||
identity: Identity = Depends(require_user)):
|
||||
payload = decode_token(authorization.removeprefix("Bearer ").strip())
|
||||
await auth_service.logout(redis, identity.user_id, payload.get("jti", ""), int(payload.get("exp", 0)))
|
||||
return ok()
|
||||
|
||||
|
||||
@router.post("/password/reset")
|
||||
async def password_reset(req: PasswordResetReq, db: AsyncSession = Depends(get_db),
|
||||
redis: aioredis.Redis = Depends(get_redis)):
|
||||
await auth_service.reset_password(db, redis, req)
|
||||
return ok()
|
||||
@@ -0,0 +1,54 @@
|
||||
"""通用与消息模块路由(/app/version + /notifications + /reports,对应《03-接口文档》第 10 章)"""
|
||||
import redis.asyncio as aioredis
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core import ratelimit
|
||||
from app.core.response import ok
|
||||
from app.database import get_db
|
||||
from app.deps import Identity, get_identity, require_user
|
||||
from app.redis_client import get_redis
|
||||
from app.schemas.common import NotificationReadReq, ReportReq
|
||||
from app.services import common_service
|
||||
|
||||
router = APIRouter(tags=["通用"])
|
||||
|
||||
|
||||
@router.get("/app/version")
|
||||
async def get_app_version(platform: str = Query(pattern="^(ios|android)$"),
|
||||
version_code: int = Query(ge=1),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
redis: aioredis.Redis = Depends(get_redis),
|
||||
identity: Identity = Depends(get_identity)):
|
||||
return ok(await common_service.get_app_version(db, redis, platform, version_code))
|
||||
|
||||
|
||||
@router.get("/notifications")
|
||||
async def list_notifications(page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=50),
|
||||
type: int | None = Query(None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
identity: Identity = Depends(require_user)):
|
||||
return ok(await common_service.list_notifications(db, identity.user_id, page, page_size, type))
|
||||
|
||||
|
||||
@router.get("/notifications/unread_count")
|
||||
async def get_unread_count(db: AsyncSession = Depends(get_db), redis: aioredis.Redis = Depends(get_redis),
|
||||
identity: Identity = Depends(require_user)):
|
||||
return ok({"unread": await common_service.get_unread_count(db, redis, identity.user_id)})
|
||||
|
||||
|
||||
@router.post("/notifications/read")
|
||||
async def mark_read(req: NotificationReadReq, db: AsyncSession = Depends(get_db),
|
||||
redis: aioredis.Redis = Depends(get_redis),
|
||||
identity: Identity = Depends(require_user)):
|
||||
await common_service.mark_read(db, redis, identity.user_id, req.ids, bool(req.all))
|
||||
return ok()
|
||||
|
||||
|
||||
@router.post("/reports")
|
||||
async def create_report(req: ReportReq, db: AsyncSession = Depends(get_db),
|
||||
redis: aioredis.Redis = Depends(get_redis),
|
||||
identity: Identity = Depends(require_user)):
|
||||
await ratelimit.hit_subject(redis, identity.subject, "report", 10, 86400, "今日举报次数已达上限")
|
||||
await common_service.create_report(db, redis, identity.user_id, req)
|
||||
return ok(message="已收到举报,我们会尽快处理")
|
||||
@@ -0,0 +1,62 @@
|
||||
"""激励模块路由(/referral/* + /assets/*,对应《03-接口文档》第 9 章)"""
|
||||
import redis.asyncio as aioredis
|
||||
from fastapi import APIRouter, Depends, Query, Request
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core import ratelimit
|
||||
from app.core.response import ok
|
||||
from app.database import get_db
|
||||
from app.deps import Identity, require_user
|
||||
from app.redis_client import get_redis
|
||||
from app.schemas.incentive import BindInviteReq, WithdrawReq
|
||||
from app.services import incentive_service
|
||||
|
||||
router_referral = APIRouter(prefix="/referral", tags=["推荐"])
|
||||
router_assets = APIRouter(prefix="/assets", tags=["资产"])
|
||||
|
||||
|
||||
@router_referral.get("/me")
|
||||
async def get_my_referral(db: AsyncSession = Depends(get_db), redis: aioredis.Redis = Depends(get_redis),
|
||||
identity: Identity = Depends(require_user)):
|
||||
return ok(await incentive_service.get_my_referral(db, redis, identity.user_id))
|
||||
|
||||
|
||||
@router_referral.post("/bind")
|
||||
async def bind_invite_code(req: BindInviteReq, request: Request, db: AsyncSession = Depends(get_db),
|
||||
redis: aioredis.Redis = Depends(get_redis),
|
||||
identity: Identity = Depends(require_user)):
|
||||
ip = request.client.host if request.client else ""
|
||||
return ok(await incentive_service.bind_invite_code(
|
||||
db, redis, identity.user_id, req.invite_code, identity.device_id, ip
|
||||
))
|
||||
|
||||
|
||||
@router_assets.get("/me")
|
||||
async def get_my_assets(db: AsyncSession = Depends(get_db), redis: aioredis.Redis = Depends(get_redis),
|
||||
identity: Identity = Depends(require_user)):
|
||||
return ok(await incentive_service.get_my_assets(db, redis, identity.user_id))
|
||||
|
||||
|
||||
@router_assets.get("/records")
|
||||
async def list_asset_records(page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=50),
|
||||
biz_type: int | None = Query(None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
identity: Identity = Depends(require_user)):
|
||||
return ok(await incentive_service.list_asset_records(db, identity.user_id, page, page_size, biz_type))
|
||||
|
||||
|
||||
@router_assets.post("/withdraw")
|
||||
async def apply_withdraw(req: WithdrawReq, db: AsyncSession = Depends(get_db),
|
||||
redis: aioredis.Redis = Depends(get_redis),
|
||||
identity: Identity = Depends(require_user)):
|
||||
await ratelimit.hit_subject(redis, identity.subject, "withdraw", 1, 60, "提现申请过于频繁,请稍后再试")
|
||||
return ok(await incentive_service.apply_withdraw(
|
||||
db, redis, identity.user_id, req.assets, req.channel, req.account
|
||||
))
|
||||
|
||||
|
||||
@router_assets.get("/withdraw/records")
|
||||
async def list_withdraw_records(page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=50),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
identity: Identity = Depends(require_user)):
|
||||
return ok(await incentive_service.list_withdraw_records(db, identity.user_id, page, page_size))
|
||||
@@ -0,0 +1,55 @@
|
||||
"""互动模块路由(对应《03-接口文档》第 6 章)"""
|
||||
import redis.asyncio as aioredis
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core import ratelimit
|
||||
from app.core.response import ok
|
||||
from app.database import get_db
|
||||
from app.deps import Identity, get_identity, require_user
|
||||
from app.redis_client import get_redis
|
||||
from app.schemas.interaction import CommentCreateReq
|
||||
from app.services import interaction_service
|
||||
|
||||
# /videos/{video_id} 下的互动接口
|
||||
router_videos = APIRouter(prefix="/videos", tags=["互动"])
|
||||
# /comments 下的接口
|
||||
router_comments = APIRouter(prefix="/comments", tags=["互动"])
|
||||
|
||||
|
||||
@router_videos.post("/{video_id}/like")
|
||||
async def toggle_like(video_id: int, db: AsyncSession = Depends(get_db),
|
||||
redis: aioredis.Redis = Depends(get_redis),
|
||||
identity: Identity = Depends(require_user)):
|
||||
return ok(await interaction_service.toggle_like(db, redis, identity.user_id, video_id))
|
||||
|
||||
|
||||
@router_videos.post("/{video_id}/favorite")
|
||||
async def toggle_favorite(video_id: int, db: AsyncSession = Depends(get_db),
|
||||
redis: aioredis.Redis = Depends(get_redis),
|
||||
identity: Identity = Depends(require_user)):
|
||||
return ok(await interaction_service.toggle_favorite(db, redis, identity.user_id, video_id))
|
||||
|
||||
|
||||
@router_videos.get("/{video_id}/comments")
|
||||
async def list_comments(video_id: int, page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=50),
|
||||
parent_id: int = Query(0, ge=0),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
identity: Identity = Depends(get_identity)):
|
||||
return ok(await interaction_service.list_comments(db, video_id, page, page_size, parent_id))
|
||||
|
||||
|
||||
@router_videos.post("/{video_id}/comments")
|
||||
async def create_comment(video_id: int, req: CommentCreateReq, db: AsyncSession = Depends(get_db),
|
||||
redis: aioredis.Redis = Depends(get_redis),
|
||||
identity: Identity = Depends(require_user)):
|
||||
await ratelimit.hit_subject(redis, identity.subject, "comment", 10, 60, "评论过于频繁,请稍后再试")
|
||||
return ok(await interaction_service.create_comment(db, redis, identity.user_id, video_id, req))
|
||||
|
||||
|
||||
@router_comments.delete("/{comment_id}")
|
||||
async def delete_comment(comment_id: int, db: AsyncSession = Depends(get_db),
|
||||
redis: aioredis.Redis = Depends(get_redis),
|
||||
identity: Identity = Depends(require_user)):
|
||||
return ok(await interaction_service.delete_comment(db, redis, identity.user_id, comment_id))
|
||||
@@ -0,0 +1,45 @@
|
||||
"""推广归因路由(/invite/* + H5 中间页 /r/{code})
|
||||
|
||||
三段式归因:分享口令(主)→ H5 点击埋点模糊匹配(辅)→ 注册手填邀请码(兜底)
|
||||
"""
|
||||
import redis.asyncio as aioredis
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.response import ok
|
||||
from app.database import get_db
|
||||
from app.redis_client import get_redis
|
||||
from app.schemas.invite import InviteClickReq, InviteMatchReq
|
||||
from app.services import invite_service
|
||||
|
||||
router = APIRouter(prefix="/invite", tags=["推广归因"])
|
||||
router_page = APIRouter(tags=["推广落地页"], include_in_schema=False)
|
||||
|
||||
|
||||
def _client_ip(request: Request) -> str:
|
||||
# Nginx 反代后取真实 IP
|
||||
return request.headers.get("X-Real-IP") or (request.client.host if request.client else "")
|
||||
|
||||
|
||||
@router.post("/click")
|
||||
async def invite_click(req: InviteClickReq, request: Request,
|
||||
redis: aioredis.Redis = Depends(get_redis)):
|
||||
"""H5 中间页点击埋点(打开页面即上报,记录 code+IP+UA,24h 有效)"""
|
||||
ua = request.headers.get("User-Agent", "")
|
||||
return ok(await invite_service.record_click(redis, req.code.upper(), _client_ip(request), ua))
|
||||
|
||||
|
||||
@router.post("/match")
|
||||
async def invite_match(req: InviteMatchReq, request: Request,
|
||||
redis: aioredis.Redis = Depends(get_redis)):
|
||||
"""App 首启归因匹配:按 IP+时间就近匹配点击记录,结果按 device_id 服务端缓存"""
|
||||
ua = request.headers.get("User-Agent", "")
|
||||
return ok(await invite_service.match_and_cache(redis, req.device_id, _client_ip(request), ua))
|
||||
|
||||
|
||||
@router_page.get("/r/{code}", response_class=HTMLResponse)
|
||||
async def invite_landing(code: str, db: AsyncSession = Depends(get_db),
|
||||
redis: aioredis.Redis = Depends(get_redis)):
|
||||
"""推广落地页(复制口令 + 下载入口,打开即自动埋点)"""
|
||||
return await invite_service.render_landing_page(db, redis, code.upper())
|
||||
@@ -0,0 +1,28 @@
|
||||
"""会员模块路由(/membership/*,对应《03-接口文档》第 7 章)"""
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.response import ok
|
||||
from app.database import get_db
|
||||
from app.deps import Identity, require_user
|
||||
from app.schemas.common import RedeemReq
|
||||
from app.services import membership_service
|
||||
|
||||
router = APIRouter(prefix="/membership", tags=["会员"])
|
||||
|
||||
|
||||
@router.get("/plans")
|
||||
async def list_plans():
|
||||
return ok(membership_service.list_plans())
|
||||
|
||||
|
||||
@router.get("/me")
|
||||
async def get_my_membership(db: AsyncSession = Depends(get_db),
|
||||
identity: Identity = Depends(require_user)):
|
||||
return ok(await membership_service.get_my_membership(db, identity.user_id))
|
||||
|
||||
|
||||
@router.post("/redeem")
|
||||
async def redeem(req: RedeemReq, db: AsyncSession = Depends(get_db),
|
||||
identity: Identity = Depends(require_user)):
|
||||
return ok(await membership_service.redeem(db, identity.user_id, req.plan_code))
|
||||
@@ -0,0 +1,58 @@
|
||||
"""支付模块路由(/payments/* + /callbacks/*,对应《03-接口文档》第 8 章)"""
|
||||
import redis.asyncio as aioredis
|
||||
from fastapi import APIRouter, Depends, Header, Request
|
||||
from fastapi.responses import JSONResponse, PlainTextResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.response import ok
|
||||
from app.database import get_db
|
||||
from app.deps import Identity, require_user
|
||||
from app.redis_client import get_redis
|
||||
from app.schemas.payment import RechargeReq
|
||||
from app.services import payment_service
|
||||
|
||||
router = APIRouter(prefix="/payments", tags=["支付"])
|
||||
router_callbacks = APIRouter(prefix="/callbacks", tags=["支付回调"])
|
||||
|
||||
|
||||
@router.get("/packages")
|
||||
async def list_packages(db: AsyncSession = Depends(get_db), redis: aioredis.Redis = Depends(get_redis),
|
||||
identity: Identity = Depends(require_user)):
|
||||
return ok(await payment_service.list_packages(db, redis))
|
||||
|
||||
|
||||
@router.post("/recharge")
|
||||
async def create_recharge(req: RechargeReq, db: AsyncSession = Depends(get_db),
|
||||
redis: aioredis.Redis = Depends(get_redis),
|
||||
x_platform: str | None = Header(default=None),
|
||||
identity: Identity = Depends(require_user)):
|
||||
return ok(await payment_service.create_recharge(db, redis, identity.user_id, req, x_platform))
|
||||
|
||||
|
||||
@router.get("/orders/{order_no}")
|
||||
async def get_order(order_no: str, db: AsyncSession = Depends(get_db),
|
||||
identity: Identity = Depends(require_user)):
|
||||
return ok(await payment_service.get_order(db, identity.user_id, order_no))
|
||||
|
||||
|
||||
@router.post("/orders/{order_no}/cancel")
|
||||
async def cancel_order(order_no: str, db: AsyncSession = Depends(get_db),
|
||||
identity: Identity = Depends(require_user)):
|
||||
await payment_service.cancel_order(db, identity.user_id, order_no)
|
||||
return ok()
|
||||
|
||||
|
||||
@router_callbacks.post("/alipay", include_in_schema=False)
|
||||
async def alipay_callback(request: Request, db: AsyncSession = Depends(get_db)):
|
||||
"""支付宝异步通知:平台要求纯文本 success/failure"""
|
||||
form = dict(await request.form())
|
||||
result = await payment_service.handle_alipay_callback(db, form)
|
||||
return PlainTextResponse(result)
|
||||
|
||||
|
||||
@router_callbacks.post("/wechat", include_in_schema=False)
|
||||
async def wechat_callback(request: Request, db: AsyncSession = Depends(get_db)):
|
||||
"""微信支付 V3 异步通知:{"code": "SUCCESS"} 或 {"code": "FAIL"}"""
|
||||
body = await request.body()
|
||||
result = await payment_service.handle_wechat_callback(db, dict(request.headers), body)
|
||||
return JSONResponse(result)
|
||||
@@ -0,0 +1,89 @@
|
||||
"""用户模块路由(/users/*,对应《03-接口文档》第 4 章)"""
|
||||
import redis.asyncio as aioredis
|
||||
from fastapi import APIRouter, Depends, Query, UploadFile
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.response import ok
|
||||
from app.database import get_db
|
||||
from app.deps import Identity, get_identity, require_user
|
||||
from app.redis_client import get_redis
|
||||
from app.schemas.user import DeactivateReq, UpdateMeReq
|
||||
from app.services import interaction_service, storage_service, user_service, video_service
|
||||
|
||||
router = APIRouter(prefix="/users", tags=["用户"])
|
||||
|
||||
|
||||
# 注意:/users/me 系列须先于 /users/{user_id} 注册,避免 "me" 被路径参数匹配
|
||||
@router.get("/me")
|
||||
async def get_me(db: AsyncSession = Depends(get_db), identity: Identity = Depends(require_user)):
|
||||
return ok(await user_service.get_me(db, identity.user_id))
|
||||
|
||||
|
||||
@router.put("/me")
|
||||
async def update_me(req: UpdateMeReq, db: AsyncSession = Depends(get_db),
|
||||
redis: aioredis.Redis = Depends(get_redis),
|
||||
identity: Identity = Depends(require_user)):
|
||||
return ok(await user_service.update_me(db, redis, identity.user_id, req))
|
||||
|
||||
|
||||
@router.post("/me/avatar")
|
||||
async def upload_avatar(file: UploadFile, db: AsyncSession = Depends(get_db),
|
||||
identity: Identity = Depends(require_user)):
|
||||
url = await storage_service.save_avatar(file, identity.user_id)
|
||||
return ok(await user_service.set_avatar(db, identity.user_id, url))
|
||||
|
||||
|
||||
@router.get("/me/wallet")
|
||||
async def get_wallet(db: AsyncSession = Depends(get_db), redis: aioredis.Redis = Depends(get_redis),
|
||||
identity: Identity = Depends(require_user)):
|
||||
return ok(await user_service.get_wallet(db, redis, identity.user_id))
|
||||
|
||||
|
||||
@router.get("/me/points/records")
|
||||
async def get_points_records(page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=50),
|
||||
biz_type: int | None = Query(None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
identity: Identity = Depends(require_user)):
|
||||
return ok(await user_service.get_points_records(db, identity.user_id, page, page_size, biz_type))
|
||||
|
||||
|
||||
@router.post("/me/deactivate")
|
||||
async def deactivate(req: DeactivateReq, db: AsyncSession = Depends(get_db),
|
||||
redis: aioredis.Redis = Depends(get_redis),
|
||||
identity: Identity = Depends(require_user)):
|
||||
return ok(await user_service.deactivate(db, redis, identity.user_id, req.password))
|
||||
|
||||
|
||||
@router.get("/me/favorites")
|
||||
async def get_my_favorites(page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=50),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
identity: Identity = Depends(require_user)):
|
||||
return ok(await interaction_service.list_my_favorites(db, identity.user_id, page, page_size))
|
||||
|
||||
|
||||
@router.get("/me/history")
|
||||
async def get_my_history(page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=50),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
identity: Identity = Depends(require_user)):
|
||||
return ok(await interaction_service.list_my_history(db, identity.user_id, page, page_size))
|
||||
|
||||
|
||||
@router.get("/me/videos")
|
||||
async def get_my_videos(page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=50),
|
||||
status: int | None = Query(None, ge=0, le=5),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
identity: Identity = Depends(require_user)):
|
||||
return ok(await video_service.list_my_videos(db, identity.user_id, page, page_size, status))
|
||||
|
||||
|
||||
@router.get("/{user_id}")
|
||||
async def get_public_profile(user_id: int, db: AsyncSession = Depends(get_db)):
|
||||
return ok(await user_service.get_public_profile(db, user_id))
|
||||
|
||||
|
||||
@router.get("/{user_id}/videos")
|
||||
async def get_user_videos(user_id: int, page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=50),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
identity: Identity = Depends(get_identity)):
|
||||
return ok(await user_service.get_user_videos(db, user_id, page, page_size, identity.user_id))
|
||||
@@ -0,0 +1,117 @@
|
||||
"""视频模块路由(/videos/* + /tags/* + /search/*,对应《03-接口文档》第 5 章)"""
|
||||
import redis.asyncio as aioredis
|
||||
from fastapi import APIRouter, Depends, Form, Query, Request, UploadFile
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core import ratelimit
|
||||
from app.core.response import ok
|
||||
from app.database import get_db
|
||||
from app.deps import Identity, get_identity, require_user
|
||||
from app.redis_client import get_redis
|
||||
from app.schemas.video import EditVideoReq, PublishVideoReq, UploadTokenReq, WatchCompleteReq, WatchReq
|
||||
from app.services import video_service
|
||||
|
||||
router = APIRouter(prefix="/videos", tags=["视频"])
|
||||
router_tags = APIRouter(prefix="/tags", tags=["视频"])
|
||||
router_search = APIRouter(prefix="/search", tags=["搜索"])
|
||||
|
||||
# 搜索限流:30 次/分钟(登录按用户,游客按 IP)
|
||||
SEARCH_RATE_LIMIT = 30
|
||||
# 上传凭证 + 发布限流:5 次/小时(同一动作桶)
|
||||
UPLOAD_RATE_LIMIT = 5
|
||||
|
||||
|
||||
# 注意:/videos/feed、/videos/categories、/videos/search、/videos/upload/* 须先于 /videos/{video_id} 注册
|
||||
@router.get("/feed")
|
||||
async def get_feed(cursor: str | None = Query(None), limit: int = Query(10, ge=1, le=20),
|
||||
category_id: int | None = Query(None),
|
||||
type: str = Query("recommend", pattern="^(recommend|latest|hot)$"),
|
||||
db: AsyncSession = Depends(get_db), redis: aioredis.Redis = Depends(get_redis),
|
||||
identity: Identity = Depends(get_identity)):
|
||||
return ok(await video_service.get_feed(db, redis, identity, type, cursor, limit, category_id))
|
||||
|
||||
|
||||
@router.get("/categories")
|
||||
async def get_categories(db: AsyncSession = Depends(get_db)):
|
||||
return ok(await video_service.list_categories(db))
|
||||
|
||||
|
||||
@router.get("/search")
|
||||
async def search_videos(request: Request, keyword: str = Query(min_length=1, max_length=30),
|
||||
sort: str = Query("hot", pattern="^(hot|latest)$"),
|
||||
page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=50),
|
||||
db: AsyncSession = Depends(get_db), redis: aioredis.Redis = Depends(get_redis),
|
||||
identity: Identity = Depends(get_identity)):
|
||||
subject = identity.subject or (request.client.host if request.client else "anon")
|
||||
await ratelimit.hit_subject(redis, subject, "search", SEARCH_RATE_LIMIT, 60)
|
||||
return ok(await video_service.search_videos(db, redis, identity, keyword, sort, page, page_size))
|
||||
|
||||
|
||||
@router_search.get("/hot")
|
||||
async def get_hot_search(redis: aioredis.Redis = Depends(get_redis)):
|
||||
return ok(await video_service.get_hot_search(redis))
|
||||
|
||||
|
||||
@router_tags.get("/{tag_name}/videos")
|
||||
async def get_tag_videos(tag_name: str, sort: str = Query("hot", pattern="^(hot|latest)$"),
|
||||
page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=50),
|
||||
db: AsyncSession = Depends(get_db), identity: Identity = Depends(get_identity)):
|
||||
return ok(await video_service.get_tag_videos(db, identity, tag_name, sort, page, page_size))
|
||||
|
||||
|
||||
@router.post("/upload/token")
|
||||
async def get_upload_token(req: UploadTokenReq, redis: aioredis.Redis = Depends(get_redis),
|
||||
identity: Identity = Depends(require_user)):
|
||||
await ratelimit.hit_subject(redis, identity.subject, "upload", UPLOAD_RATE_LIMIT, 3600, "上传过于频繁,请稍后再试")
|
||||
return ok(await video_service.get_upload_token(redis, identity, req))
|
||||
|
||||
|
||||
@router.post("/upload/file")
|
||||
async def upload_file(file: UploadFile, object_key: str = Form(...),
|
||||
redis: aioredis.Redis = Depends(get_redis),
|
||||
identity: Identity = Depends(require_user)):
|
||||
return ok(await video_service.register_upload(redis, identity, object_key, file))
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def publish_video(req: PublishVideoReq, db: AsyncSession = Depends(get_db),
|
||||
redis: aioredis.Redis = Depends(get_redis),
|
||||
identity: Identity = Depends(require_user)):
|
||||
await ratelimit.hit_subject(redis, identity.subject, "upload", UPLOAD_RATE_LIMIT, 3600, "发布过于频繁,请稍后再试")
|
||||
return ok(await video_service.publish_video(db, redis, identity, req))
|
||||
|
||||
|
||||
@router.get("/{video_id}")
|
||||
async def get_detail(video_id: int, db: AsyncSession = Depends(get_db),
|
||||
redis: aioredis.Redis = Depends(get_redis),
|
||||
identity: Identity = Depends(get_identity)):
|
||||
return ok(await video_service.get_detail(db, redis, identity, video_id))
|
||||
|
||||
|
||||
@router.put("/{video_id}")
|
||||
async def update_video(video_id: int, req: EditVideoReq, db: AsyncSession = Depends(get_db),
|
||||
redis: aioredis.Redis = Depends(get_redis),
|
||||
identity: Identity = Depends(require_user)):
|
||||
return ok(await video_service.update_video(db, redis, identity, video_id, req))
|
||||
|
||||
|
||||
@router.delete("/{video_id}")
|
||||
async def delete_video(video_id: int, db: AsyncSession = Depends(get_db),
|
||||
redis: aioredis.Redis = Depends(get_redis),
|
||||
identity: Identity = Depends(require_user)):
|
||||
await video_service.delete_video(db, redis, identity, video_id)
|
||||
return ok()
|
||||
|
||||
|
||||
@router.post("/{video_id}/watch")
|
||||
async def report_watch(video_id: int, req: WatchReq, db: AsyncSession = Depends(get_db),
|
||||
redis: aioredis.Redis = Depends(get_redis),
|
||||
identity: Identity = Depends(get_identity)):
|
||||
return ok(await video_service.report_watch(db, redis, identity, video_id, req.position))
|
||||
|
||||
|
||||
@router.post("/{video_id}/watch/complete")
|
||||
async def report_complete(video_id: int, req: WatchCompleteReq, db: AsyncSession = Depends(get_db),
|
||||
redis: aioredis.Redis = Depends(get_redis),
|
||||
identity: Identity = Depends(get_identity)):
|
||||
return ok(await video_service.report_complete(db, redis, identity, video_id, req.watch_duration))
|
||||
Reference in New Issue
Block a user