up
This commit is contained in:
@@ -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