diff --git a/video-gen-api/app/api/admin/__init__.py b/video-gen-api/app/api/admin/__init__.py index 16818bc5..f7d1db0e 100644 --- a/video-gen-api/app/api/admin/__init__.py +++ b/video-gen-api/app/api/admin/__init__.py @@ -5,6 +5,8 @@ from app.api.admin.resource_capacity import router as resource_capacity_router from app.api.admin.team import router as team_router from app.api.admin.home_material import router as home_material_router from app.api.admin.private_portrait import router as private_portrait_router +from app.api.admin.recharge_package import router as recharge_package_router +from app.api.admin.menu_config import router as menu_config_router router = APIRouter() router.include_router(video_prompt_schema_config_router) @@ -12,3 +14,5 @@ router.include_router(resource_capacity_router) router.include_router(team_router) router.include_router(home_material_router) router.include_router(private_portrait_router) +router.include_router(recharge_package_router) +router.include_router(menu_config_router) diff --git a/video-gen-api/app/api/admin/menu_config.py b/video-gen-api/app/api/admin/menu_config.py new file mode 100644 index 00000000..d1f5ffc1 --- /dev/null +++ b/video-gen-api/app/api/admin/menu_config.py @@ -0,0 +1,124 @@ +import json + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.dependencies import get_db, get_admin_user, get_backend_user +from app.models.user import User +from app.models.menu_config import MenuConfig +from app.schemas.menu import MenuConfigCreate, MenuConfigOut +from app.services.operation_log import log_operation +from app.utils.id_gen import generate_id + +router = APIRouter(prefix="/admin/menu-configs", tags=["admin-menu-configs"]) + + +@router.get("", response_model=list[MenuConfigOut]) +async def admin_list_menu_configs( + admin: User = Depends(get_backend_user), + db: AsyncSession = Depends(get_db), +): + result = await db.execute(select(MenuConfig).order_by(MenuConfig.sort_order)) + return result.scalars().all() + + +@router.post("", response_model=MenuConfigOut) +async def create_menu_config( + req: MenuConfigCreate, + admin: User = Depends(get_admin_user), + db: AsyncSession = Depends(get_db), +): + menu = MenuConfig(id=generate_id(), **req.model_dump()) + db.add(menu) + await db.flush() + await log_operation( + db, + admin.id, + admin.username, + f"创建菜单 {menu.label}", + "POST", + "/admin/menu-configs", + detail=json.dumps( + { + "id": menu.id, + "label": menu.label, + "path": menu.path, + "menu_target": menu.menu_target, + "menu_type": menu.menu_type, + }, + ensure_ascii=False, + ), + ) + return menu + + +@router.put("/{menu_id}", response_model=MenuConfigOut) +async def update_menu_config( + menu_id: str, + req: MenuConfigCreate, + admin: User = Depends(get_admin_user), + db: AsyncSession = Depends(get_db), +): + result = await db.execute(select(MenuConfig).where(MenuConfig.id == menu_id).limit(1)) + menu = result.scalar_one_or_none() + if not menu: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="菜单不存在") + before_label = menu.label + for k, v in req.model_dump().items(): + setattr(menu, k, v) + await db.flush() + await log_operation( + db, + admin.id, + admin.username, + f"更新菜单 {before_label}", + "PUT", + f"/admin/menu-configs/{menu_id}", + detail=json.dumps( + { + "id": menu_id, + "before": {"label": before_label}, + "after": { + "label": menu.label, + "path": menu.path, + "is_active": menu.is_active, + "sort_order": menu.sort_order, + }, + }, + ensure_ascii=False, + ), + ) + return menu + + +@router.delete("/{menu_id}") +async def delete_menu_config( + menu_id: str, + admin: User = Depends(get_admin_user), + db: AsyncSession = Depends(get_db), +): + result = await db.execute(select(MenuConfig).where(MenuConfig.id == menu_id).limit(1)) + menu = result.scalar_one_or_none() + if not menu: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="菜单不存在") + menu_label = menu.label + await db.delete(menu) + await db.flush() + await log_operation( + db, + admin.id, + admin.username, + f"删除菜单 {menu_label}", + "DELETE", + f"/admin/menu-configs/{menu_id}", + detail=json.dumps( + { + "id": menu_id, + "label": menu_label, + "path": menu.path, + }, + ensure_ascii=False, + ), + ) + return {"message": "ok"} diff --git a/video-gen-api/app/api/admin/recharge_package.py b/video-gen-api/app/api/admin/recharge_package.py new file mode 100644 index 00000000..4875c96c --- /dev/null +++ b/video-gen-api/app/api/admin/recharge_package.py @@ -0,0 +1,145 @@ +import json + +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.dependencies import get_db, get_admin_user +from app.models.user import User +from app.models.recharge_package import RechargePackage +from app.schemas.recharge_package import ( + RechargePackageCreate, + RechargePackageUpdate, + RechargePackageOut, +) +from app.services.operation_log import log_operation +from app.utils.id_gen import generate_id + +router = APIRouter(prefix="/admin/recharge-packages", tags=["admin-recharge-packages"]) + + +def _to_out(pkg: RechargePackage) -> dict: + return { + "id": pkg.id, + "name": pkg.name, + "credits": round(pkg.credits, 2), + "price": round(pkg.price, 2), + "bonus_credits": round(pkg.bonus_credits, 2), + "total_credits": round(pkg.credits + pkg.bonus_credits, 2), + "description": pkg.description, + "package_type": pkg.package_type, + "is_gift": pkg.is_gift, + "is_active": pkg.is_active, + "sort_order": pkg.sort_order, + } + + +@router.get("", response_model=list[RechargePackageOut]) +async def admin_list_packages( + admin: User = Depends(get_admin_user), + db: AsyncSession = Depends(get_db), +): + result = await db.execute( + select(RechargePackage).order_by(RechargePackage.sort_order) + ) + return [_to_out(p) for p in result.scalars().all()] + + +@router.post("", response_model=RechargePackageOut) +async def create_package( + data: RechargePackageCreate, + admin: User = Depends(get_admin_user), + db: AsyncSession = Depends(get_db), +): + pkg = RechargePackage(id=generate_id(), **data.model_dump()) + db.add(pkg) + await db.flush() + await log_operation( + db, + admin.id, + admin.username, + f"创建充值套餐 {pkg.name}", + "POST", + "/admin/recharge-packages", + detail=json.dumps( + { + "id": pkg.id, + "name": pkg.name, + "price": round(pkg.price, 2), + "credits": round(pkg.credits, 2), + "bonus_credits": round(pkg.bonus_credits, 2), + }, + ensure_ascii=False, + ), + ) + return _to_out(pkg) + + +@router.put("/{pkg_id}", response_model=RechargePackageOut) +async def update_package( + pkg_id: str, + data: RechargePackageUpdate, + admin: User = Depends(get_admin_user), + db: AsyncSession = Depends(get_db), +): + result = await db.execute( + select(RechargePackage).where(RechargePackage.id == pkg_id).limit(1) + ) + pkg = result.scalar_one_or_none() + if not pkg: + raise HTTPException(status_code=404, detail="套餐不存在") + before = _to_out(pkg) + for k, v in data.model_dump(exclude_unset=True).items(): + setattr(pkg, k, v) + await db.flush() + after = _to_out(pkg) + await log_operation( + db, + admin.id, + admin.username, + f"更新充值套餐 {pkg.name}", + "PUT", + f"/admin/recharge-packages/{pkg_id}", + detail=json.dumps( + { + "id": pkg_id, + "before": before, + "after": after, + }, + ensure_ascii=False, + ), + ) + return _to_out(pkg) + + +@router.delete("/{pkg_id}") +async def delete_package( + pkg_id: str, + admin: User = Depends(get_admin_user), + db: AsyncSession = Depends(get_db), +): + result = await db.execute( + select(RechargePackage).where(RechargePackage.id == pkg_id).limit(1) + ) + pkg = result.scalar_one_or_none() + if not pkg: + raise HTTPException(status_code=404, detail="套餐不存在") + pkg_name = pkg.name + await db.delete(pkg) + await db.flush() + await log_operation( + db, + admin.id, + admin.username, + f"删除充值套餐 {pkg_name}", + "DELETE", + f"/admin/recharge-packages/{pkg_id}", + detail=json.dumps( + { + "id": pkg_id, + "name": pkg_name, + }, + ensure_ascii=False, + ), + ) + return {"ok": True} diff --git a/video-gen-api/app/api/admin/video_prompt_schema_config.py b/video-gen-api/app/api/admin/video_prompt_schema_config.py index 57006823..69ee31cb 100644 --- a/video-gen-api/app/api/admin/video_prompt_schema_config.py +++ b/video-gen-api/app/api/admin/video_prompt_schema_config.py @@ -74,6 +74,12 @@ async def reset_default( "恢复默认视频提词 Schema 配置", "POST", "/admin/video-prompt-schema-config/reset-default", + detail=json.dumps( + { + "reset_to_default": True, + }, + ensure_ascii=False, + ), ) return result diff --git a/video-gen-api/app/api/v1/admin.py b/video-gen-api/app/api/v1/admin.py index fb11b4b1..c80d854a 100644 --- a/video-gen-api/app/api/v1/admin.py +++ b/video-gen-api/app/api/v1/admin.py @@ -218,7 +218,21 @@ async def update_user_menus( raise HTTPException(status_code=404, detail="用户不存在") user.allowed_menus = req.allowed_menus await db.flush() - await log_operation(db, admin.id, admin.username, f"更新菜单权限", "PUT", f"/admin/users/{user_id}/menus") + await log_operation( + db, + admin.id, + admin.username, + f"更新菜单权限", + "PUT", + f"/admin/users/{user_id}/menus", + detail=json.dumps( + { + "user_id": user_id, + "allowed_menus": req.allowed_menus, + }, + ensure_ascii=False, + ), + ) return {"message": "ok"} @@ -259,7 +273,22 @@ async def adjust_credits( f"您的积分已{'增加' if req.amount > 0 else '扣除'}{abs(req.amount)}积分。原因:{req.description}", "credit", ) - await log_operation(db, admin.id, admin.username, f"调整积分 {'+' if req.amount > 0 else ''}{req.amount}", "POST", f"/admin/users/{user_id}/credits") + await log_operation( + db, + admin.id, + admin.username, + f"调整积分 {'+' if req.amount > 0 else ''}{req.amount}", + "POST", + f"/admin/users/{user_id}/credits", + detail=json.dumps( + { + "user_id": user_id, + "amount": req.amount, + "description": req.description, + }, + ensure_ascii=False, + ), + ) return {"message": "ok"} @@ -275,7 +304,21 @@ async def update_user_status( update(User).where(User.id == user_id).values(is_active=is_active) ) await db.flush() - await log_operation(db, admin.id, admin.username, f"{'启用' if is_active else '禁用'}用户", "PUT", f"/admin/users/{user_id}/status") + await log_operation( + db, + admin.id, + admin.username, + f"{'启用' if is_active else '禁用'}用户", + "PUT", + f"/admin/users/{user_id}/status", + detail=json.dumps( + { + "user_id": user_id, + "is_active": is_active, + }, + ensure_ascii=False, + ), + ) return {"message": "ok"} @@ -295,7 +338,21 @@ async def update_user_admin_status( raise HTTPException(status_code=400, detail="仅后台用户支持设置超级管理员状态") user.is_admin = is_admin await db.flush() - await log_operation(db, admin.id, admin.username, f"{is_admin and '设为' or '取消'}超级管理员", "PUT", f"/admin/users/{user_id}/admin-status") + await log_operation( + db, + admin.id, + admin.username, + f"{is_admin and '设为' or '取消'}超级管理员", + "PUT", + f"/admin/users/{user_id}/admin-status", + detail=json.dumps( + { + "user_id": user_id, + "is_admin": is_admin, + }, + ensure_ascii=False, + ), + ) return {"message": "ok"} @@ -314,7 +371,21 @@ async def update_user_frontend_kind( raise HTTPException(status_code=400, detail="仅前台用户支持设置内部/外部归类") user.frontend_user_kind = req.frontend_user_kind or FrontendUserKind.EXTERNAL.value await db.flush() - await log_operation(db, admin.id, admin.username, f"设置前台用户归类为 {user.frontend_user_kind}", "PUT", f"/admin/users/{user_id}/frontend-kind") + await log_operation( + db, + admin.id, + admin.username, + f"设置前台用户归类为 {user.frontend_user_kind}", + "PUT", + f"/admin/users/{user_id}/frontend-kind", + detail=json.dumps( + { + "user_id": user_id, + "frontend_user_kind": user.frontend_user_kind, + }, + ensure_ascii=False, + ), + ) return user @@ -354,7 +425,21 @@ async def reset_user_password( raise HTTPException(status_code=404, detail="用户不存在") user.hashed_password = hash_password(req.new_password) await db.flush() - await log_operation(db, admin.id, admin.username, f"重置密码", "PUT", f"/admin/users/{user_id}/reset-password") + await log_operation( + db, + admin.id, + admin.username, + f"重置密码", + "PUT", + f"/admin/users/{user_id}/reset-password", + detail=json.dumps( + { + "user_id": user_id, + "new_password": "***", + }, + ensure_ascii=False, + ), + ) return {"message": "ok"} @@ -372,7 +457,21 @@ async def admin_change_password( raise HTTPException(status_code=400, detail="密码至少6位") admin.hashed_password = hash_password(new_password) await db.flush() - await log_operation(db, admin.id, admin.username, "修改密码", "POST", "/admin/change-password") + await log_operation( + db, + admin.id, + admin.username, + "修改密码", + "POST", + "/admin/change-password", + detail=json.dumps( + { + "old_password": "***", + "new_password": "***", + }, + ensure_ascii=False, + ), + ) return {"message": "密码修改成功"} diff --git a/video-gen-api/app/api/v1/menu_configs.py b/video-gen-api/app/api/v1/menu_configs.py index 0880989a..d2e13ff6 100644 --- a/video-gen-api/app/api/v1/menu_configs.py +++ b/video-gen-api/app/api/v1/menu_configs.py @@ -2,11 +2,9 @@ from fastapi import APIRouter, Depends from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from app.dependencies import get_db, get_admin_user, get_backend_user, get_current_user +from app.dependencies import get_db, get_current_user from app.models.user import User from app.models.menu_config import MenuConfig -from app.schemas.menu import MenuConfigCreate, MenuConfigOut -from app.utils.id_gen import generate_id router = APIRouter(tags=["menu-configs"]) @@ -57,58 +55,3 @@ async def public_list_menu_configs( } for m in menus ] - - -@router.get("/admin/menu-configs", response_model=list[MenuConfigOut]) -async def admin_list_menu_configs( - admin: User = Depends(get_backend_user), - db: AsyncSession = Depends(get_db), -): - result = await db.execute(select(MenuConfig).order_by(MenuConfig.sort_order)) - return result.scalars().all() - - -@router.post("/admin/menu-configs", response_model=MenuConfigOut) -async def create_menu_config( - req: MenuConfigCreate, - admin: User = Depends(get_admin_user), - db: AsyncSession = Depends(get_db), -): - menu = MenuConfig(id=generate_id(), **req.model_dump()) - db.add(menu) - await db.flush() - return menu - - -@router.put("/admin/menu-configs/{menu_id}", response_model=MenuConfigOut) -async def update_menu_config( - menu_id: str, - req: MenuConfigCreate, - admin: User = Depends(get_admin_user), - db: AsyncSession = Depends(get_db), -): - result = await db.execute(select(MenuConfig).where(MenuConfig.id == menu_id).limit(1)) - menu = result.scalar_one_or_none() - if not menu: - from fastapi import HTTPException, status - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="菜单不存在") - for k, v in req.model_dump().items(): - setattr(menu, k, v) - await db.flush() - return menu - - -@router.delete("/admin/menu-configs/{menu_id}") -async def delete_menu_config( - menu_id: str, - admin: User = Depends(get_admin_user), - db: AsyncSession = Depends(get_db), -): - result = await db.execute(select(MenuConfig).where(MenuConfig.id == menu_id).limit(1)) - menu = result.scalar_one_or_none() - if not menu: - from fastapi import HTTPException, status - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="菜单不存在") - await db.delete(menu) - await db.flush() - return {"message": "ok"} diff --git a/video-gen-api/app/api/v1/recharge_packages.py b/video-gen-api/app/api/v1/recharge_packages.py index acc82560..a6d95b14 100644 --- a/video-gen-api/app/api/v1/recharge_packages.py +++ b/video-gen-api/app/api/v1/recharge_packages.py @@ -1,16 +1,10 @@ -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from app.dependencies import get_db, get_admin_user, get_current_user +from app.dependencies import get_db, get_current_user from app.models.user import User from app.models.recharge_package import RechargePackage -from app.schemas.recharge_package import ( - RechargePackageCreate, - RechargePackageUpdate, - RechargePackageOut, -) -from app.utils.id_gen import generate_id router = APIRouter(tags=["recharge-packages"]) @@ -43,63 +37,3 @@ async def list_active_packages( .order_by(RechargePackage.sort_order) ) return [_to_out(p) for p in result.scalars().all()] - - -@router.get("/admin/recharge-packages") -async def admin_list_packages( - _admin=Depends(get_admin_user), - db: AsyncSession = Depends(get_db), -): - """Admin: list all packages.""" - result = await db.execute( - select(RechargePackage).order_by(RechargePackage.sort_order) - ) - return [_to_out(p) for p in result.scalars().all()] - - -@router.post("/admin/recharge-packages") -async def create_package( - data: RechargePackageCreate, - _admin=Depends(get_admin_user), - db: AsyncSession = Depends(get_db), -): - pkg = RechargePackage(id=generate_id(), **data.model_dump()) - db.add(pkg) - await db.flush() - return _to_out(pkg) - - -@router.put("/admin/recharge-packages/{pkg_id}") -async def update_package( - pkg_id: str, - data: RechargePackageUpdate, - _admin=Depends(get_admin_user), - db: AsyncSession = Depends(get_db), -): - result = await db.execute( - select(RechargePackage).where(RechargePackage.id == pkg_id).limit(1) - ) - pkg = result.scalar_one_or_none() - if not pkg: - raise HTTPException(status_code=404, detail="套餐不存在") - for k, v in data.model_dump(exclude_unset=True).items(): - setattr(pkg, k, v) - await db.flush() - return _to_out(pkg) - - -@router.delete("/admin/recharge-packages/{pkg_id}") -async def delete_package( - pkg_id: str, - _admin=Depends(get_admin_user), - db: AsyncSession = Depends(get_db), -): - result = await db.execute( - select(RechargePackage).where(RechargePackage.id == pkg_id).limit(1) - ) - pkg = result.scalar_one_or_none() - if not pkg: - raise HTTPException(status_code=404, detail="套餐不存在") - await db.delete(pkg) - await db.flush() - return {"ok": True}