FastAPI 进阶指南:数据验证、项目架构与部署实战

Python3.8

Python3.8

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

一. 数据验证与 Pydantic 模型

1.1 Pydantic 基础​

Pydantic 是 FastAPI 的数据验证核心,它基于 Python 类型提示进行数据验证和设置管理

(1)基本数据模型

from pydantic import BaseModel, Field, EmailStr, HttpUrl
from typing import Union, List, Optional, Dict
from datetime import datetime, date

# 基础模型
class UserBase(BaseModel):
    """用户基础信息模型"""
    username: str = Field(
        ...,
        title="用户名",
        description="用户的登录用户名",
        min_length=3,
        max_length=20,
        regex=r"^[a-zA-Z0-9_]+$",
        example="johndoe"
    )
    email: EmailStr = Field(
        ...,
        title="邮箱地址",
        description="用户的电子邮箱地址",
        example="john.doe@example.com"
    )
    full_name: Optional[str] = Field(
        None,
        title="全名",
        description="用户的完整姓名",
        max_length=100,
        example="John Doe"
    )

# 创建用户模型(包含密码)
class UserCreate(UserBase):
    """创建用户请求模型"""
    password: str = Field(
        ...,
        title="密码",
        description="用户登录密码",
        min_length=8,
        example="secure_password123"
    )
    confirm_password: str = Field(
        ...,
        title="确认密码",
        description="再次输入密码进行确认",
        example="secure_password123"
    )

    # 自定义验证方法
    def validate_passwords(self):
        if self.password != self.confirm_password:
            raise ValueError("密码和确认密码不匹配")
        return True

# 用户响应模型(不包含敏感信息)
class UserResponse(UserBase):
    """用户响应模型"""
    id: int = Field(..., title="用户ID", description="用户的唯一标识符")
    is_active: bool = Field(True, title="是否激活", description="用户账号状态")
    created_at: datetime = Field(
        default_factory=datetime.utcnow,
        title="创建时间",
        description="用户账号创建时间"
    )

    class Config:
        """配置类"""
        orm_mode = True  # 支持ORM对象转换
        json_encoders = {
            datetime: lambda v: v.isoformat()
        }

(2)复杂数据类型

from pydantic import BaseModel, Field, HttpUrl
from typing import List, Optional, Dict, Union
from enum import Enum

# 枚举类型
class ItemCategory(str, Enum):
    """商品分类枚举"""
    ELECTRONICS = "electronics"
    CLOTHING = "clothing"
    BOOKS = "books"
    HOME = "home"

# 嵌套模型
class Address(BaseModel):
    """地址模型"""
    street: str = Field(..., title="街道", example="123 Main St")
    city: str = Field(..., title="城市", example="New York")
    state: str = Field(..., title="州/省份", example="NY")
    zip_code: str = Field(..., title="邮政编码", example="10001")
    country: str = Field("USA", title="国家", example="USA")

class PaymentMethod(BaseModel):
    """支付方式模型"""
    type: str = Field(..., title="支付类型", example="credit_card")
    last_four: str = Field(..., title="卡号后四位", example="4242")
    expiry_date: str = Field(..., title="过期日期", example="12/25")

# 复杂商品模型
class Item(BaseModel):
    """商品模型"""
    id: int = Field(..., title="商品ID")
    name: str = Field(..., title="商品名称", example="iPhone 13")
    description: Optional[str] = Field(None, title="商品描述", example="Latest iPhone model")
    price: float = Field(..., title="价格", gt=0, example=999.99)
    category: ItemCategory = Field(..., title="分类", example=ItemCategory.ELECTRONICS)
    tags: List[str] = Field(default=[], title="标签", example=["smartphone", "apple"])
    images: List[HttpUrl] = Field(
        default=[],
        title="商品图片",
        example=["https://example.com/iphone13.jpg"]
    )
    metadata: Dict[str, Union[str, int, float]] = Field(
        default={},
        title="商品元数据",
        example={"weight": "0.17kg", "color": "black"}
    )

# 订单模型
class Order(BaseModel):
    """订单模型"""
    id: str = Field(..., title="订单ID", example="ORD-12345")
    items: List[Item] = Field(..., title="订单商品列表")
    total_amount: float = Field(..., title="订单总金额", gt=0, example=1299.99)
    shipping_address: Address = Field(..., title="收货地址")
    payment_method: PaymentMethod = Field(..., title="支付方式")
    status: str = Field("pending", title="订单状态", example="pending")
    created_at: str = Field(..., title="创建时间", example="2024-01-15T10:30:00Z")

1.2 数据验证与错误处理

(1)请求体验证

from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, ValidationError, Field
from typing import Optional

app = FastAPI()

class UserCreate(BaseModel):
    """创建用户请求模型"""
    username: str = Field(..., min_length=3, max_length=20)
    email: str = Field(..., regex=r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$")
    password: str = Field(..., min_length=8)

@app.post("/users/", status_code=status.HTTP_201_CREATED)
def create_user(user: UserCreate):
    """创建新用户"""
    try:
        # 模拟数据库操作
        user_id = 1  # 实际应该从数据库获取
        
        # 返回用户信息(不包含密码)
        return {
            "id": user_id,
            "username": user.username,
            "email": user.email,
            "message": "用户创建成功"
        }
        
    except ValidationError as e:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=str(e)
        )
    except Exception as e:
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail="服务器内部错误"
        )

(2)自定义验证器

from pydantic import BaseModel, validator, Field
from typing import Optional

class PasswordReset(BaseModel):
    """密码重置模型"""
    current_password: str = Field(..., min_length=8)
    new_password: str = Field(..., min_length=8)
    confirm_password: str = Field(..., min_length=8)

    @validator('new_password')
    def password_strength(cls, v):
        """密码强度验证"""
        if not any(c.isupper() for c in v):
            raise ValueError('密码必须包含至少一个大写字母')
        if not any(c.islower() for c in v):
            raise ValueError('密码必须包含至少一个小写字母')
        if not any(c.isdigit() for c in v):
            raise ValueError('密码必须包含至少一个数字')
        if not any(c in '!@#$%^&*()_+-=[]{}|;:,.<>?' for c in v):
            raise ValueError('密码必须包含至少一个特殊字符')
        return v

    @validator('confirm_password')
    def passwords_match(cls, v, values, **kwargs):
        """确认密码匹配验证"""
        if 'new_password' in values and v != values['new_password']:
            raise ValueError('新密码和确认密码不匹配')
        return v

1.3 响应模型与数据转换

(1)响应模型配置

from fastapi import FastAPI
from pydantic import BaseModel
from typing import List, Optional
from datetime import datetime

app = FastAPI()

# 数据库模型(ORM)
class DBUser:
    def __init__(self, id, username, email, hashed_password, is_active=True):
        self.id = id
        self.username = username
        self.email = email
        self.hashed_password = hashed_password
        self.is_active = is_active
        self.created_at = datetime.utcnow()

# 响应模型
class UserResponse(BaseModel):
    """用户响应模型"""
    id: int
    username: str
    email: str
    is_active: bool
    created_at: datetime

    class Config:
        """配置ORM模式"""
        orm_mode = True
        json_encoders = {
            datetime: lambda v: v.strftime("%Y-%m-%d %H:%M:%S")
        }

@app.get("/users/{user_id}", response_model=UserResponse)
def get_user(user_id: int):
    """获取用户信息"""
    # 模拟数据库查询
    db_user = DBUser(
        id=user_id,
        username="johndoe",
        email="john@example.com",
        hashed_password="hashed_password_123"
    )
    
    # 自动转换为响应模型,密码字段会被过滤
    return db_user

(2)列表响应和分页

from fastapi import FastAPI, Query
from pydantic import BaseModel
from typing import List, Optional

app = FastAPI()

class Item(BaseModel):
    """商品模型"""
    id: int
    name: str
    price: float

class PaginatedResponse(BaseModel):
    """分页响应模型"""
    items: List[Item]
    total: int
    page: int
    page_size: int
    total_pages: int

@app.get("/items/", response_model=PaginatedResponse)
def get_items(
    page: int = Query(1, ge=1, description="页码"),
    page_size: int = Query(10, ge=1, le=100, description="每页数量")
):
    """分页获取商品列表"""
    # 模拟数据库查询
    total_items = 100  # 总商品数量
    total_pages = (total_items + page_size - 1) // page_size
    
    # 生成模拟数据
    items = []
    for i in range(page_size):
        item_id = (page - 1) * page_size + i + 1
        if item_id > total_items:
            break
        items.append({
            "id": item_id,
            "name": f"商品{item_id}",
            "price": 99.99 + item_id * 10
        })
    
    return {
        "items": items,
        "total": total_items,
        "page": page,
        "page_size": page_size,
        "total_pages": total_pages
    }

二. 路由分发与模块化架构

2.1 模块化项目结构​

良好的项目结构是大型应用的基础:

bookstore_api/
├── main.py                  # 应用入口
├── core/                    # 核心配置
│   ├── __init__.py
│   ├── config.py            # 配置管理
│   ├── database.py          # 数据库配置
│   └── security.py          # 安全相关
├── api/                     # API路由
│   ├── __init__.py
│   ├── deps.py              # 依赖项
│   ├── v1/                  # API v1版本
│   │   ├── __init__.py
│   │   ├── endpoints/       # 具体端点
│   │   │   ├── __init__.py
│   │   │   ├── users.py     # 用户相关接口
│   │   │   ├── items.py     # 商品相关接口
│   │   │   └── orders.py    # 订单相关接口
│   │   └── api.py           # API路由组装
├── models/                  # 数据模型
│   ├── __init__.py
│   ├── user.py              # 用户模型
│   ├── item.py              # 商品模型
│   └── order.py             # 订单模型
├── schemas/                 # Pydantic模型
│   ├── __init__.py
│   ├── user.py              # 用户模式
│   ├── item.py              # 商品模式
│   └── order.py             # 订单模式
├── crud/                    # 数据库操作
│   ├── __init__.py
│   ├── base.py              # 基础CRUD
│   ├── user.py              # 用户CRUD
│   ├── item.py              # 商品CRUD
│   └── order.py             # 订单CRUD
├── services/                # 业务逻辑
│   ├── __init__.py
│   ├── user_service.py      # 用户服务
│   └── order_service.py     # 订单服务
├── utils/                   # 工具函数
│   ├── __init__.py
│   ├── exceptions.py        # 自定义异常
│   └── helpers.py           # 辅助函数
├── tests/                   # 测试
│   ├── __init__.py
│   ├── test_users.py
│   └── test_items.py
├── requirements.txt         # 依赖列表
└── README.md                # 项目说明

2.2 路由分发实现

(1)创建路由模块

# bookstore_api/api/v1/endpoints/users.py
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List

from core.database import get_db
from schemas.user import UserCreate, UserResponse, UserUpdate
from crud.user import get_user, get_users, create_user, update_user, delete_user

router = APIRouter(
    prefix="/users",
    tags=["用户管理"],
    responses={
        404: {"description": "用户不存在"},
        400: {"description": "请求参数错误"}
    }
)

@router.get("/", response_model=List[UserResponse], summary="获取用户列表")
def read_users(
    skip: int = 0,
    limit: int = 100,
    db: Session = Depends(get_db)
):
    """
    获取用户列表:
    
    - **skip**: 跳过的用户数量(默认0)
    - **limit**: 获取的用户数量(默认100,最大100)
    - 返回用户列表
    """
    users = get_users(db, skip=skip, limit=limit)
    return users

@router.get("/{user_id}", response_model=UserResponse, summary="获取用户详情")
def read_user(
    user_id: int,
    db: Session = Depends(get_db)
):
    """
    根据ID获取用户详情:
    
    - **user_id**: 用户ID
    - 返回用户详细信息
    """
    db_user = get_user(db, user_id=user_id)
    if db_user is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="用户不存在"
        )
    return db_user

@router.post(
    "/", 
    response_model=UserResponse, 
    status_code=status.HTTP_201_CREATED,
    summary="创建新用户"
)
def create_new_user(
    user: UserCreate,
    db: Session = Depends(get_db)
):
    """
    创建新用户:
    
    - **user**: 用户创建信息
    - 返回创建的用户信息
    """
    return create_user(db=db, user=user)

@router.put("/{user_id}", response_model=UserResponse, summary="更新用户信息")
def update_existing_user(
    user_id: int,
    user: UserUpdate,
    db: Session = Depends(get_db)
):
    """
    更新用户信息:
    
    - **user_id**: 用户ID
    - **user**: 更新的用户信息
    - 返回更新后的用户信息
    """
    db_user = get_user(db, user_id=user_id)
    if db_user is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="用户不存在"
        )
    return update_user(db=db, user_id=user_id, user_update=user)

@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT, summary="删除用户")
def delete_existing_user(
    user_id: int,
    db: Session = Depends(get_db)
):
    """
    删除用户:
    
    - **user_id**: 用户ID
    - 成功返回204 No Content
    """
    db_user = get_user(db, user_id=user_id)
    if db_user is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="用户不存在"
        )
    delete_user(db=db, user_id=user_id)
    return None

(2)路由组装

# bookstore_api/api/v1/api.py
from fastapi import APIRouter

from .endpoints import users, items, orders

api_router = APIRouter()
api_router.include_router(users.router)
api_router.include_router(items.router)
api_router.include_router(orders.router)
# bookstore_api/main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from api.v1.api import api_router
from core.config import settings

app = FastAPI(
    title=settings.PROJECT_NAME,
    description=settings.PROJECT_DESCRIPTION,
    version=settings.PROJECT_VERSION,
    openapi_url=f"{settings.API_V1_STR}/openapi.json",
)

# 设置CORS
if settings.BACKEND_CORS_ORIGINS:
    app.add_middleware(
        CORSMiddleware,
        allow_origins=[str(origin) for origin in settings.BACKEND_CORS_ORIGINS],
        allow_credentials=True,
        allow_methods=["*"],
        allow_headers=["*"],
    )

# 注册API路由
app.include_router(api_router, prefix=settings.API_V1_STR)

@app.get("/")
def root():
    """根路径"""
    return {
        "message": "欢迎使用在线书店API",
        "version": settings.PROJECT_VERSION,
        "docs_url": "/docs",
        "redoc_url": "/redoc"
    }

2.3 依赖注入系统

(1)数据库依赖

# bookstore_api/core/database.py
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from core.config import settings

# 创建数据库引擎
engine = create_engine(
    settings.DATABASE_URL,
    connect_args={"check_same_thread": False} if "sqlite" in settings.DATABASE_URL else {}
)

# 创建会话工厂
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

# 基础模型类
Base = declarative_base()

# 依赖项 - 获取数据库会话
def get_db():
    """获取数据库会话"""
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

(2)认证依赖

# bookstore_api/api/deps.py
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from sqlalchemy.orm import Session
from typing import Optional

from core.config import settings
from core.database import get_db
from core.security import verify_password
from models.user import User
from schemas.token import TokenData

# OAuth2配置
oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"{settings.API_V1_STR}/token")

def get_current_user(
    db: Session = Depends(get_db),
    token: str = Depends(oauth2_scheme)
):
    """获取当前登录用户"""
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="无法验证凭据",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(
            token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]
        )
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
        token_data = TokenData(username=username)
    except JWTError:
        raise credentials_exception
    
    user = db.query(User).filter(User.username == token_data.username).first()
    if user is None:
        raise credentials_exception
    return user

def get_current_active_user(
    current_user: User = Depends(get_current_user)
):
    """获取当前活跃用户"""
    if not current_user.is_active:
        raise HTTPException(status_code=400, detail="用户已禁用")
    return current_user

def authenticate_user(
    db: Session, 
    username: str, 
    password: str
):
    """验证用户凭据"""
    user = db.query(User).filter(User.username == username).first()
    if not user:
        return False
    if not verify_password(password, user.hashed_password):
        return False
    return user

3. Request 对象与高级参数处理

3.1 Request 对象详解​

(1) 获取请求信息

from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse
from typing import Dict, Any

app = FastAPI()

@app.get("/request-info/")
async def get_request_info(request: Request):
    """获取详细的请求信息"""
    # 基本信息
    basic_info = {
        "method": request.method,
        "url": str(request.url),
        "headers": dict(request.headers),
        "query_params": dict(request.query_params),
        "path_params": dict(request.path_params),
        "client": {
            "host": request.client.host,
            "port": request.client.port
        }
    }
    
    # 获取请求体(如果有)
    try:
        body = await request.body()
        basic_info["body"] = body.decode("utf-8") if body else None
    except Exception as e:
        basic_info["body_error"] = str(e)
    
    return basic_info

@app.post("/submit-form/")
async def submit_form(request: Request):
    """处理表单数据"""
    try:
        form_data = await request.form()
        return {
            "form_data": dict(form_data),
            "files": {
                key: {
                    "filename": value.filename,
                    "content_type": value.content_type,
                    "size": len(await value.read())
                }
                for key, value in form_data.items()
                if hasattr(value, "filename")
            }
        }
    except Exception as e:
        raise HTTPException(status_code=400, detail=f"表单处理错误: {str(e)}")

(2) 自定义请求处理

from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import StreamingResponse
import asyncio

app = FastAPI()

@app.post("/process-large-data/")
async def process_large_data(request: Request):
    """处理大型请求数据"""
    if request.headers.get("content-type") != "application/json":
        raise HTTPException(status_code=400, detail="仅支持JSON格式")
    
    # 流式读取请求体
    body_chunks = []
    async for chunk in request.stream():
        body_chunks.append(chunk)
    
    body = b"".join(body_chunks).decode("utf-8")
    
    # 模拟处理大型数据
    await asyncio.sleep(2)  # 模拟处理时间
    
    return {
        "status": "success",
        "data_size": len(body),
        "message": "大型数据处理完成"
    }

@app.get("/stream-response/")
async def stream_response():
    """流式响应"""
    async def generate():
        for i in range(10):
            yield f"数据块 {i}\n"
            await asyncio.sleep(0.5)
    
    return StreamingResponse(generate(), media_type="text/plain")

3.2 文件上传处理

(1) 单文件上传

from fastapi import FastAPI, UploadFile, File, HTTPException
from fastapi.responses import FileResponse
import os
from typing import Optional

app = FastAPI()

# 确保上传目录存在
UPLOAD_DIR = "uploads"
os.makedirs(UPLOAD_DIR, exist_ok=True)

@app.post("/upload-file/")
async def upload_file(
    file: UploadFile = File(...),
    description: Optional[str] = None
):
    """单文件上传"""
    # 验证文件类型
    allowed_types = ["image/jpeg", "image/png", "application/pdf", "text/plain"]
    if file.content_type not in allowed_types:
        raise HTTPException(
            status_code=400,
            detail=f"不支持的文件类型: {file.content_type}。支持的类型: {allowed_types}"
        )
    
    # 验证文件大小(最大5MB)
    MAX_FILE_SIZE = 5 * 1024 * 1024  # 5MB
    file_content = await file.read()
    if len(file_content) > MAX_FILE_SIZE:
        raise HTTPException(
            status_code=400,
            detail=f"文件太大,最大支持 {MAX_FILE_SIZE/1024/1024:.1f}MB"
        )
    
    # 保存文件
    file_path = os.path.join(UPLOAD_DIR, file.filename)
    with open(file_path, "wb") as f:
        f.write(file_content)
    
    return {
        "filename": file.filename,
        "content_type": file.content_type,
        "size": len(file_content),
        "description": description,
        "path": file_path
    }

(2) 多文件上传

from fastapi import FastAPI, UploadFile, File, Form
from typing import List, Optional
import os
import uuid

app = FastAPI()

UPLOAD_DIR = "uploads"
os.makedirs(UPLOAD_DIR, exist_ok=True)

@app.post("/upload-multiple-files/")
async def upload_multiple_files(
    files: List[UploadFile] = File(...),
    album: str = Form(...),
    description: Optional[str] = Form(None)
):
    """多文件上传"""
    upload_results = []
    
    for file in files:
        # 生成唯一文件名
        unique_filename = f"{uuid.uuid4()}_{file.filename}"
        file_path = os.path.join(UPLOAD_DIR, unique_filename)
        
        # 保存文件
        with open(file_path, "wb") as f:
            f.write(await file.read())
        
        upload_results.append({
            "original_filename": file.filename,
            "unique_filename": unique_filename,
            "content_type": file.content_type,
            "size": os.path.getsize(file_path),
            "path": file_path
        })
    
    return {
        "album": album,
        "description": description,
        "total_files": len(upload_results),
        "files": upload_results
    }

3.3 高级参数验证

(1)自定义验证函数

from fastapi import FastAPI, Query, Path, Body, HTTPException
from pydantic import BaseModel, validator, Field
from typing import Optional, List, Dict
import re

app = FastAPI()

class Product(BaseModel):
    """产品模型"""
    name: str = Field(..., min_length=3, max_length=100)
    price: float = Field(..., gt=0)
    category: str = Field(..., min_length=2, max_length=50)
    tags: List[str] = Field(default=[], max_items=10)
    
    @validator('category')
    def category_must_be_valid(cls, v):
        """验证分类必须是字母数字和空格"""
        if not re.match(r'^[a-zA-Z0-9 ]+$', v):
            raise ValueError('分类只能包含字母、数字和空格')
        return v.title()  # 首字母大写
    
    @validator('tags')
    def tags_must_be_valid(cls, v):
        """验证标签"""
        for tag in v:
            if len(tag) < 2 or len(tag) > 20:
                raise ValueError(f'标签 "{tag}" 长度必须在2-20之间')
            if not re.match(r'^[a-zA-Z0-9_-]+$', tag):
                raise ValueError(f'标签 "{tag}" 只能包含字母、数字、下划线和连字符')
        return v

@app.post("/products/")
def create_product(product: Product = Body(..., embed=True)):
    """创建产品"""
    return {"status": "success", "product": product}

# 查询参数验证
@app.get("/search/")
def search_products(
    q: str = Query(
        ...,
        min_length=3,
        max_length=100,
        description="搜索关键词",
        example="手机"
    ),
    price_min: Optional[float] = Query(
        None,
        gt=0,
        description="最低价格",
        example=1000
    ),
    price_max: Optional[float] = Query(
        None,
        gt=0,
        description="最高价格",
        example=5000
    ),
    categories: Optional[List[str]] = Query(
        None,
        description="商品分类",
        example=["电子产品", "手机"]
    )
):
    """搜索产品"""
    if price_min and price_max and price_min > price_max:
        raise HTTPException(
            status_code=400,
            detail="最低价格不能大于最高价格"
        )
    
    return {
        "query": q,
        "price_min": price_min,
        "price_max": price_max,
        "categories": categories,
        "results": []  # 模拟搜索结果
    }

您可能感兴趣的与本文相关的镜像

Python3.8

Python3.8

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值