| from __future__ import annotations |
|
|
| from fastapi import APIRouter, Header, HTTPException |
| from pydantic import BaseModel, Field |
|
|
| from api.support import require_admin, require_identity |
| from services.config import config |
| from services.auth_service import auth_service |
| from services.shop_service import shop_service |
|
|
|
|
| class RedeemRequest(BaseModel): |
| code: str = "" |
|
|
|
|
| class CodeBatchCreateRequest(BaseModel): |
| count: int = Field(default=1, ge=1, le=1000) |
| credits: int = Field(default=1, ge=1) |
| prefix: str = "CS" |
| batch: str = "" |
| note: str = "" |
| validity_days: int | None = Field(default=None, ge=1, le=3650) |
| credit_expires_days: int | None = Field(default=None, ge=1, le=3650) |
| batch_single_use: bool = False |
| code_type: str = "normal" |
| daily_gift_date: str = "" |
| package_name: str = "" |
| package_duration_days: int = Field(default=1, ge=1, le=3650) |
| package_daily_limit: int | None = Field(default=None, ge=0, le=1000000) |
| package_weekly_limit: int | None = Field(default=None, ge=0, le=1000000) |
| package_monthly_limit: int | None = Field(default=None, ge=0, le=1000000) |
| package_total_limit: int | None = Field(default=None, ge=0, le=1000000) |
|
|
|
|
| class CodeDisableRequest(BaseModel): |
| code_id: str = "" |
|
|
|
|
| class CodeCleanupRequest(BaseModel): |
| delete_expired: bool = True |
| delete_disabled: bool = False |
| archive_redeemed: bool = False |
| redeemed_older_than_days: int = Field(default=90, ge=1, le=3650) |
| limit: int = Field(default=1000, ge=1, le=20000) |
| dry_run: bool = True |
|
|
|
|
| class CreditAdjustRequest(BaseModel): |
| delta: int |
| note: str = "" |
|
|
|
|
| class ProfileUpdateRequest(BaseModel): |
| name: str = "" |
|
|
|
|
| def create_router() -> APIRouter: |
| router = APIRouter() |
|
|
| @router.get("/api/me/profile") |
| async def get_my_profile(authorization: str | None = Header(default=None)): |
| identity = require_identity(authorization) |
| profile = auth_service.get_profile(str(identity.get("id") or "")) |
| if profile is None and str(identity.get("role") or "").strip().lower() == "admin": |
| profile = { |
| "id": str(identity.get("id") or "admin"), |
| "name": str(identity.get("name") or "管理员"), |
| "role": "admin", |
| "created_at": None, |
| "account_pool_enabled": True, |
| "wechat_openid": "", |
| "wechat_bound_at": None, |
| } |
| if profile is None: |
| raise HTTPException(status_code=404, detail={"error": "当前用户不存在,请重新登录"}) |
| return {"profile": profile} |
|
|
| @router.post("/api/me/profile") |
| async def update_my_profile(body: ProfileUpdateRequest, authorization: str | None = Header(default=None)): |
| identity = require_identity(authorization) |
| if str(identity.get("role") or "").strip().lower() != "normal": |
| raise HTTPException(status_code=403, detail={"error": "只有普通用户可以设置昵称"}) |
| try: |
| item = auth_service.update_self_name(str(identity.get("id") or ""), body.name) |
| except ValueError as exc: |
| raise HTTPException(status_code=400, detail={"error": str(exc)}) from exc |
| return {"profile": auth_service.get_profile(str(item.get("id") or ""))} |
|
|
| @router.get("/api/me/credits") |
| async def get_my_credits(authorization: str | None = Header(default=None)): |
| identity = require_identity(authorization) |
| return {"credits": auth_service.credit_summary(identity)} |
|
|
| @router.get("/api/me/redeem-codes") |
| async def get_my_redeem_codes(authorization: str | None = Header(default=None)): |
| identity = require_identity(authorization) |
| if str(identity.get("role") or "").strip().lower() != "normal": |
| raise HTTPException(status_code=403, detail={"error": "只有普通用户可以查看自己的卡密"}) |
| return {"items": shop_service.list_user_codes(str(identity.get("id") or ""))} |
|
|
| @router.get("/api/shop/public") |
| async def get_shop_public_config(authorization: str | None = Header(default=None)): |
| require_identity(authorization) |
| return {"purchase_url": config.shop_purchase_url} |
|
|
| @router.get("/api/me/api-key") |
| async def get_my_api_key(authorization: str | None = Header(default=None)): |
| identity = require_identity(authorization) |
| if str(identity.get("role") or "").strip().lower() not in {"normal", "user"}: |
| raise HTTPException(status_code=403, detail={"error": "只有微信用户或内部用户可以查看密钥"}) |
| api_key = auth_service.get_api_key_display(str(identity.get("id") or "")) |
| if not api_key: |
| raise HTTPException(status_code=404, detail={"error": "当前用户密钥无法回显,请联系管理员重置密钥"}) |
| return {"api_key": api_key} |
|
|
| @router.post("/api/redeem") |
| async def redeem_code(body: RedeemRequest, authorization: str | None = Header(default=None)): |
| identity = require_identity(authorization) |
| try: |
| return shop_service.redeem(body.code, identity) |
| except ValueError as exc: |
| raise HTTPException(status_code=400, detail={"error": str(exc)}) from exc |
|
|
| @router.post("/api/redeem/preview") |
| async def preview_redeem_code(body: RedeemRequest, authorization: str | None = Header(default=None)): |
| identity = require_identity(authorization) |
| try: |
| return shop_service.preview_code(body.code, identity) |
| except ValueError as exc: |
| raise HTTPException(status_code=400, detail={"error": str(exc)}) from exc |
|
|
| @router.get("/api/shop/codes") |
| async def list_codes(authorization: str | None = Header(default=None)): |
| require_admin(authorization) |
| return {"items": shop_service.list_codes()} |
|
|
| @router.post("/api/shop/codes") |
| async def create_codes(body: CodeBatchCreateRequest, authorization: str | None = Header(default=None)): |
| require_admin(authorization) |
| try: |
| return shop_service.generate_codes( |
| count=body.count, |
| credits=body.credits, |
| prefix=body.prefix, |
| batch=body.batch, |
| note=body.note, |
| validity_days=body.validity_days, |
| credit_expires_days=body.credit_expires_days, |
| batch_single_use=body.batch_single_use, |
| code_type=body.code_type, |
| daily_gift_date=body.daily_gift_date, |
| package_name=body.package_name, |
| package_duration_days=body.package_duration_days, |
| package_daily_limit=body.package_daily_limit, |
| package_weekly_limit=body.package_weekly_limit, |
| package_monthly_limit=body.package_monthly_limit, |
| package_total_limit=body.package_total_limit, |
| ) |
| except ValueError as exc: |
| raise HTTPException(status_code=400, detail={"error": str(exc)}) from exc |
|
|
| @router.post("/api/shop/codes/disable") |
| async def disable_code(body: CodeDisableRequest, authorization: str | None = Header(default=None)): |
| require_admin(authorization) |
| try: |
| item = shop_service.disable_code(body.code_id) |
| except ValueError as exc: |
| raise HTTPException(status_code=400, detail={"error": str(exc)}) from exc |
| return {"item": item, "items": shop_service.list_codes()} |
|
|
| @router.post("/api/shop/codes/enable") |
| async def enable_code(body: CodeDisableRequest, authorization: str | None = Header(default=None)): |
| require_admin(authorization) |
| try: |
| item = shop_service.enable_code(body.code_id) |
| except ValueError as exc: |
| raise HTTPException(status_code=400, detail={"error": str(exc)}) from exc |
| return {"item": item, "items": shop_service.list_codes()} |
|
|
| @router.post("/api/shop/codes/delete") |
| async def delete_code(body: CodeDisableRequest, authorization: str | None = Header(default=None)): |
| require_admin(authorization) |
| try: |
| item = shop_service.delete_code(body.code_id) |
| except ValueError as exc: |
| raise HTTPException(status_code=400, detail={"error": str(exc)}) from exc |
| return {"item": item, "items": shop_service.list_codes()} |
|
|
| @router.post("/api/shop/codes/cleanup") |
| async def cleanup_codes(body: CodeCleanupRequest, authorization: str | None = Header(default=None)): |
| require_admin(authorization) |
| try: |
| return shop_service.cleanup_codes( |
| delete_expired=body.delete_expired, |
| delete_disabled=body.delete_disabled, |
| delete_redeemed=body.archive_redeemed, |
| redeemed_older_than_days=body.redeemed_older_than_days, |
| limit=body.limit, |
| dry_run=body.dry_run, |
| ) |
| except ValueError as exc: |
| raise HTTPException(status_code=400, detail={"error": str(exc)}) from exc |
|
|
| @router.get("/api/shop/ledger") |
| async def list_ledger(authorization: str | None = Header(default=None)): |
| require_admin(authorization) |
| return {"items": shop_service.list_ledger()} |
|
|
| @router.post("/api/shop/users/{key_id}/credits") |
| async def adjust_user_credits( |
| key_id: str, |
| body: CreditAdjustRequest, |
| authorization: str | None = Header(default=None), |
| ): |
| require_admin(authorization) |
| try: |
| item = shop_service.adjust_user_credits(key_id, body.delta, note=body.note) |
| except ValueError as exc: |
| raise HTTPException(status_code=400, detail={"error": str(exc)}) from exc |
| return {"item": item, "items": auth_service.list_keys(role="normal")} |
|
|
| return router |
|
|