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