Flyway 数据库版本控制和迁移工具详细使用指南

flyway的快速入门教程 Flyway是一款开源的数据库版本管理工具。它可以很方便的在命令行中使用,或者在Java应用程序中引入,用于管理我们的数据库版本。在项目或产品中,很难一开始就把业务理清楚,把数据库表设计好,因此数据表也会在迭代周期不断迭代。在Java应用程序中使用Flyway,能快速有效地用于迭代数据库表结构,并保证部署到测试环境或生产环境时,数据表都是保持一致的。flyway官方文档[这里是图片001]https在控制台可以看到相关日志打印,并在数据库中查看到已经创建好的表和相关记录变更。... 阅读详情

1. Flyway 是什么?

Flyway 是一个开源的数据库版本控制和迁移工具,用于管理数据库 schema 的变更。它帮助开发者以版本化、可重复、可追踪的方式管理数据库结构和数据的演进。

2. Flyway 的作用

核心功能

  • 版本控制:为数据库变更提供版本号管理
  • 自动迁移:应用未执行的数据库变更脚本
  • 一致性保证:确保所有环境(开发、测试、生产)的数据库结构一致
  • 回滚支持:支持撤销数据库变更(需手动编写回滚脚本)
  • 团队协作:多人开发时避免数据库结构冲突

解决的问题

  • 手动执行 SQL 脚本容易出错和遗漏
  • 不同环境数据库结构不一致
  • 数据库变更无法追踪和版本化
  • 部署时数据库升级复杂

3. Flyway 的特点

主要特性

  • 简单易用:基于约定优于配置的原则
  • 支持多种数据库:MySQL、PostgreSQL、Oracle、SQL Server 等 20+ 种数据库
  • 多种脚本格式:支持 SQL 和 Java 代码两种迁移方式
  • 自动版本跟踪:自动创建 flyway_schema_history 表记录执行历史
  • 事务支持:每个迁移脚本在一个事务中执行
  • 校验机制:防止已执行的脚本被意外修改

脚本命名规范

  • 版本化迁移:V1__Initial.sql、V2__Add_user_table.sql
  • 可重复迁移:R__View_user_summary.sql
  • 撤消迁移:U1__Undo_initial.sql(仅限 Flyway Teams 版本)

4. 在 Spring Boot 3 项目中使用 Flyway

4.1 项目依赖配置

Maven 配置 (pom.xml)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
        <relativePath/>
    </parent>
    
    <groupId>com.example</groupId>
    <artifactId>flyway-demo</artifactId>
    <version>1.0.0</version>
    <name>flyway-demo</name>
    
    <properties>
        <java.version>17</java.version>
    </properties>
    
    <dependencies>
        <!-- Spring Boot Web Starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <!-- Spring Boot Data JPA Starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        
        <!-- MySQL 驱动 -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        
        <!-- Flyway 核心依赖 -->
        <dependency>
            <groupId>org.flywaydb</groupId>
            <artifactId>flyway-core</artifactId>
        </dependency>
        
        <!-- Flyway MySQL 支持 -->
        <dependency>
            <groupId>org.flywaydb</groupId>
            <artifactId>flyway-mysql</artifactId>
        </dependency>
        
        <!-- Spring Boot Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Gradle 配置 (build.gradle)

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.2.0'
    id 'io.spring.dependency-management' version '1.1.4'
}

group = 'com.example'
version = '1.0.0'
sourceCompatibility = '17'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    runtimeOnly 'com.mysql:mysql-connector-j'
    implementation 'org.flywaydb:flyway-core'
    implementation 'org.flywaydb:flyway-mysql'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

4.2 应用配置

application.yml 配置

# 应用基本信息配置
spring:
  application:
    name: flyway-demo
  
  # 数据源配置
  datasource:
    url: jdbc:mysql://localhost:3306/flyway_demo?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
    username: root
    password: root123
    driver-class-name: com.mysql.cj.jdbc.Driver
    
  # JPA 配置
  jpa:
    hibernate:
      # 禁用 Hibernate 自动建表,由 Flyway 管理
      ddl-auto: none
    show-sql: true
    properties:
      hibernate:
        format_sql: true
        
  # Flyway 配置
  flyway:
    # 启用 Flyway(默认已启用)
    enabled: true
    # 数据库编码
    encoding: UTF-8
    # SQL 脚本位置(默认: classpath:db/migration)
    locations: classpath:db/migration
    # 表名(默认: flyway_schema_history)
    table: flyway_schema_history
    # 是否在迁移时进行校验
    validate-on-migrate: true
    # 基线版本(用于已有数据库的首次集成)
    baseline-version: 1
    # 是否在非空数据库上创建基线
    baseline-on-migrate: false
    # 是否清理 schema(生产环境慎用)
    clean-disabled: true
    # 支持的数据库类型
    database: mysql

# 服务器配置
server:
  port: 8080

# 日志配置
logging:
  level:
    org.flywaydb: DEBUG
    org.springframework.jdbc: DEBUG

application.properties 配置(备选)

# 数据源配置
spring.datasource.url=jdbc:mysql://localhost:3306/flyway_demo?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
spring.datasource.username=root
spring.datasource.password=root123
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

# JPA 配置
spring.jpa.hibernate.ddl-auto=none
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

# Flyway 配置
spring.flyway.enabled=true
spring.flyway.encoding=UTF-8
spring.flyway.locations=classpath:db/migration
spring.flyway.table=flyway_schema_history
spring.flyway.validate-on-migrate=true
spring.flyway.baseline-version=1
spring.flyway.baseline-on-migrate=false
spring.flyway.clean-disabled=true

# 服务器端口
server.port=8080

4.3 创建数据库

在 MySQL 中创建数据库:

-- 创建数据库(如果不存在)
CREATE DATABASE IF NOT EXISTS flyway_demo 
CHARACTER SET utf8mb4 
COLLATE utf8mb4_unicode_ci;

-- 使用数据库
USE flyway_demo;

4.4 编写迁移脚本

在 src/main/resources/db/migration/ 目录下创建 SQL 迁移脚本:

V1__Create_users_table.sql

-- 版本 1: 创建用户表
-- 文件命名规范: V<版本号>__<描述>.sql
-- 注意: 版本号和描述之间是两个下划线 (__)

-- 创建用户表
CREATE TABLE users (
    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '用户ID,主键自增',
    username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名,唯一约束',
    email VARCHAR(100) NOT NULL UNIQUE COMMENT '邮箱,唯一约束',
    password VARCHAR(255) NOT NULL COMMENT '密码(加密存储)',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    is_active BOOLEAN DEFAULT TRUE COMMENT '是否激活'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

-- 添加索引以提高查询性能
CREATE INDEX idx_users_username ON users(username);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_created_at ON users(created_at);

V2__Create_products_table.sql

-- 版本 2: 创建产品表

-- 创建产品表
CREATE TABLE products (
    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '产品ID,主键自增',
    name VARCHAR(100) NOT NULL COMMENT '产品名称',
    description TEXT COMMENT '产品描述',
    price DECIMAL(10, 2) NOT NULL COMMENT '产品价格',
    stock_quantity INT DEFAULT 0 COMMENT '库存数量',
    category VARCHAR(50) COMMENT '产品分类',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    is_available BOOLEAN DEFAULT TRUE COMMENT '是否可用'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='产品表';

-- 添加索引
CREATE INDEX idx_products_category ON products(category);
CREATE INDEX idx_products_price ON products(price);
CREATE INDEX idx_products_created_at ON products(created_at);

V3__Add_user_profile_table.sql

-- 版本 3: 创建用户资料表并建立外键关系

-- 创建用户资料表
CREATE TABLE user_profiles (
    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '资料ID,主键自增',
    user_id BIGINT NOT NULL COMMENT '关联的用户ID',
    first_name VARCHAR(50) COMMENT '名字',
    last_name VARCHAR(50) COMMENT '姓氏',
    phone VARCHAR(20) COMMENT '电话号码',
    address TEXT COMMENT '地址',
    birth_date DATE COMMENT '出生日期',
    avatar_url VARCHAR(255) COMMENT '头像URL',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    CONSTRAINT fk_user_profile_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户资料表';

-- 添加唯一约束,确保一个用户只有一个资料
ALTER TABLE user_profiles ADD CONSTRAINT uk_user_profile_user_id UNIQUE (user_id);

-- 添加索引
CREATE INDEX idx_user_profiles_user_id ON user_profiles(user_id);

V4__Insert_initial_data.sql

-- 版本 4: 插入初始数据

-- 插入初始用户数据
INSERT INTO users (username, email, password, is_active) VALUES
('admin', 'admin@example.com', '$2a$10$8a7d5b1e3f4c6a9b2d1e3f4c6a9b2d1e3f4c6a9b2d1e3f4c6a9b2d1e', TRUE),
('john_doe', 'john@example.com', '$2a$10$8a7d5b1e3f4c6a9b2d1e3f4c6a9b2d1e3f4c6a9b2d1e3f4c6a9b2d1e', TRUE),
('jane_smith', 'jane@example.com', '$2a$10$8a7d5b1e3f4c6a9b2d1e3f4c6a9b2d1e3f4c6a9b2d1e3f4c6a9b2d1e', TRUE);

-- 插入初始产品数据
INSERT INTO products (name, description, price, stock_quantity, category, is_available) VALUES
('iPhone 15', '最新款苹果手机', 999.99, 100, 'Electronics', TRUE),
('MacBook Pro', '专业级笔记本电脑', 2499.99, 50, 'Electronics', TRUE),
('Coffee Mug', '陶瓷咖啡杯', 15.99, 200, 'Home', TRUE),
('Running Shoes', '专业跑步鞋', 129.99, 75, 'Sports', TRUE);

4.5 创建实体类

User.java

package com.example.flywaydemo.entity;

import jakarta.persistence.*;
import java.time.LocalDateTime;

/**
 * 用户实体类
 * 对应数据库中的 users 表
 */
@Entity
@Table(name = "users")
public class User {
    
    /**
     * 用户ID,主键
     */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    /**
     * 用户名,唯一约束
     */
    @Column(nullable = false, unique = true, length = 50)
    private String username;
    
    /**
     * 邮箱,唯一约束
     */
    @Column(nullable = false, unique = true, length = 100)
    private String email;
    
    /**
     * 密码(已加密)
     */
    @Column(nullable = false)
    private String password;
    
    /**
     * 创建时间
     */
    @Column(name = "created_at", nullable = false, updatable = false)
    private LocalDateTime createdAt;
    
    /**
     * 更新时间
     */
    @Column(name = "updated_at", nullable = false)
    private LocalDateTime updatedAt;
    
    /**
     * 是否激活
     */
    @Column(name = "is_active")
    private Boolean active = true;
    
    // 构造函数
    public User() {}
    
    public User(String username, String email, String password) {
        this.username = username;
        this.email = email;
        this.password = password;
    }
    
    // Getter 和 Setter 方法
    public Long getId() {
        return id;
    }
    
    public void setId(Long id) {
        this.id = id;
    }
    
    public String getUsername() {
        return username;
    }
    
    public void setUsername(String username) {
        this.username = username;
    }
    
    public String getEmail() {
        return email;
    }
    
    public void setEmail(String email) {
        this.email = email;
    }
    
    public String getPassword() {
        return password;
    }
    
    public void setPassword(String password) {
        this.password = password;
    }
    
    public LocalDateTime getCreatedAt() {
        return createdAt;
    }
    
    public void setCreatedAt(LocalDateTime createdAt) {
        this.createdAt = createdAt;
    }
    
    public LocalDateTime getUpdatedAt() {
        return updatedAt;
    }
    
    public void setUpdatedAt(LocalDateTime updatedAt) {
        this.updatedAt = updatedAt;
    }
    
    public Boolean getActive() {
        return active;
    }
    
    public void setActive(Boolean active) {
        this.active = active;
    }
}

Product.java

package com.example.flywaydemo.entity;

import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
 * 产品实体类
 * 对应数据库中的 products 表
 */
@Entity
@Table(name = "products")
public class Product {
    
    /**
     * 产品ID,主键
     */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    /**
     * 产品名称
     */
    @Column(nullable = false, length = 100)
    private String name;
    
    /**
     * 产品描述
     */
    @Column(columnDefinition = "TEXT")
    private String description;
    
    /**
     * 产品价格
     */
    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal price;
    
    /**
     * 库存数量
     */
    @Column(name = "stock_quantity")
    private Integer stockQuantity = 0;
    
    /**
     * 产品分类
     */
    @Column(length = 50)
    private String category;
    
    /**
     * 创建时间
     */
    @Column(name = "created_at", nullable = false, updatable = false)
    private LocalDateTime createdAt;
    
    /**
     * 更新时间
     */
    @Column(name = "updated_at", nullable = false)
    private LocalDateTime updatedAt;
    
    /**
     * 是否可用
     */
    @Column(name = "is_available")
    private Boolean available = true;
    
    // 构造函数
    public Product() {}
    
    public Product(String name, String description, BigDecimal price, Integer stockQuantity, String category) {
        this.name = name;
        this.description = description;
        this.price = price;
        this.stockQuantity = stockQuantity;
        this.category = category;
    }
    
    // Getter 和 Setter 方法
    public Long getId() {
        return id;
    }
    
    public void setId(Long id) {
        this.id = id;
    }
    
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
    
    public String getDescription() {
        return description;
    }
    
    public void setDescription(String description) {
        this.description = description;
    }
    
    public BigDecimal getPrice() {
        return price;
    }
    
    public void setPrice(BigDecimal price) {
        this.price = price;
    }
    
    public Integer getStockQuantity() {
        return stockQuantity;
    }
    
    public void setStockQuantity(Integer stockQuantity) {
        this.stockQuantity = stockQuantity;
    }
    
    public String getCategory() {
        return category;
    }
    
    public void setCategory(String category) {
        this.category = category;
    }
    
    public LocalDateTime getCreatedAt() {
        return createdAt;
    }
    
    public void setCreatedAt(LocalDateTime createdAt) {
        this.createdAt = createdAt;
    }
    
    public LocalDateTime getUpdatedAt() {
        return updatedAt;
    }
    
    public void setUpdatedAt(LocalDateTime updatedAt) {
        this.updatedAt = updatedAt;
    }
    
    public Boolean getAvailable() {
        return available;
    }
    
    public void setAvailable(Boolean available) {
        this.available = available;
    }
}

4.6 创建 Repository 接口

UserRepository.java

package com.example.flywaydemo.repository;

import com.example.flywaydemo.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

/**
 * 用户数据访问接口
 * 继承 JpaRepository 获得基本的 CRUD 操作
 */
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    
    /**
     * 根据用户名查找用户
     * @param username 用户名
     * @return 用户实体
     */
    User findByUsername(String username);
    
    /**
     * 根据邮箱查找用户
     * @param email 邮箱地址
     * @return 用户实体
     */
    User findByEmail(String email);
}

ProductRepository.java

package com.example.flywaydemo.repository;

import com.example.flywaydemo.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.math.BigDecimal;
import java.util.List;

/**
 * 产品数据访问接口
 */
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
    
    /**
     * 根据分类查找产品
     * @param category 产品分类
     * @return 产品列表
     */
    List<Product> findByCategory(String category);
    
    /**
     * 查找价格低于指定值的产品
     * @param maxPrice 最高价格
     * @return 产品列表
     */
    List<Product> findByPriceLessThan(BigDecimal maxPrice);
    
    /**
     * 自定义查询:查找可用的产品
     * @return 可用产品列表
     */
    @Query("SELECT p FROM Product p WHERE p.available = true")
    List<Product> findAvailableProducts();
    
    /**
     * 自定义查询:根据价格范围查找产品
     * @param minPrice 最低价格
     * @param maxPrice 最高价格
     * @return 产品列表
     */
    @Query("SELECT p FROM Product p WHERE p.price BETWEEN :minPrice AND :maxPrice")
    List<Product> findByPriceRange(@Param("minPrice") BigDecimal minPrice, 
                                  @Param("maxPrice") BigDecimal maxPrice);
}

4.7 创建 Service 层

UserService.java

package com.example.flywaydemo.service;

import com.example.flywaydemo.entity.User;
import com.example.flywaydemo.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

/**
 * 用户业务逻辑服务类
 */
@Service
public class UserService {
    
    /**
     * 注入用户数据访问接口
     */
    @Autowired
    private UserRepository userRepository;
    
    /**
     * 获取所有用户
     * @return 用户列表
     */
    public List<User> getAllUsers() {
        return userRepository.findAll();
    }
    
    /**
     * 根据ID获取用户
     * @param id 用户ID
     * @return 用户实体(Optional包装)
     */
    public Optional<User> getUserById(Long id) {
        return userRepository.findById(id);
    }
    
    /**
     * 根据用户名获取用户
     * @param username 用户名
     * @return 用户实体
     */
    public User getUserByUsername(String username) {
        return userRepository.findByUsername(username);
    }
    
    /**
     * 保存用户
     * @param user 用户实体
     * @return 保存后的用户实体
     */
    public User saveUser(User user) {
        return userRepository.save(user);
    }
    
    /**
     * 删除用户
     * @param id 用户ID
     */
    public void deleteUser(Long id) {
        userRepository.deleteById(id);
    }
}

ProductService.java

package com.example.flywaydemo.service;

import com.example.flywaydemo.entity.Product;
import com.example.flywaydemo.repository.ProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;

/**
 * 产品业务逻辑服务类
 */
@Service
public class ProductService {
    
    /**
     * 注入产品数据访问接口
     */
    @Autowired
    private ProductRepository productRepository;
    
    /**
     * 获取所有产品
     * @return 产品列表
     */
    public List<Product> getAllProducts() {
        return productRepository.findAll();
    }
    
    /**
     * 获取所有可用产品
     * @return 可用产品列表
     */
    public List<Product> getAvailableProducts() {
        return productRepository.findAvailableProducts();
    }
    
    /**
     * 根据ID获取产品
     * @param id 产品ID
     * @return 产品实体(Optional包装)
     */
    public Optional<Product> getProductById(Long id) {
        return productRepository.findById(id);
    }
    
    /**
     * 根据分类获取产品
     * @param category 产品分类
     * @return 产品列表
     */
    public List<Product> getProductsByCategory(String category) {
        return productRepository.findByCategory(category);
    }
    
    /**
     * 根据价格范围获取产品
     * @param minPrice 最低价格
     * @param maxPrice 最高价格
     * @return 产品列表
     */
    public List<Product> getProductsByPriceRange(BigDecimal minPrice, BigDecimal maxPrice) {
        return productRepository.findByPriceRange(minPrice, maxPrice);
    }
    
    /**
     * 保存产品
     * @param product 产品实体
     * @return 保存后的产品实体
     */
    public Product saveProduct(Product product) {
        return productRepository.save(product);
    }
    
    /**
     * 删除产品
     * @param id 产品ID
     */
    public void deleteProduct(Long id) {
        productRepository.deleteById(id);
    }
}

4.8 创建 Controller 层

UserController.java

package com.example.flywaydemo.controller;

import com.example.flywaydemo.entity.User;
import com.example.flywaydemo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Optional;

/**
 * 用户控制器
 * 提供 RESTful API 接口
 */
@RestController
@RequestMapping("/api/users")
public class UserController {
    
    /**
     * 注入用户服务
     */
    @Autowired
    private UserService userService;
    
    /**
     * 获取所有用户
     * GET /api/users
     * @return 用户列表
     */
    @GetMapping
    public ResponseEntity<List<User>> getAllUsers() {
        List<User> users = userService.getAllUsers();
        return ResponseEntity.ok(users);
    }
    
    /**
     * 根据ID获取用户
     * GET /api/users/{id}
     * @param id 用户ID
     * @return 用户实体
     */
    @GetMapping("/{id}")
    public ResponseEntity<User> getUserById(@PathVariable Long id) {
        Optional<User> user = userService.getUserById(id);
        return user.map(ResponseEntity::ok)
                  .orElse(ResponseEntity.notFound().build());
    }
    
    /**
     * 根据用户名获取用户
     * GET /api/users/username/{username}
     * @param username 用户名
     * @return 用户实体
     */
    @GetMapping("/username/{username}")
    public ResponseEntity<User> getUserByUsername(@PathVariable String username) {
        User user = userService.getUserByUsername(username);
        if (user != null) {
            return ResponseEntity.ok(user);
        }
        return ResponseEntity.notFound().build();
    }
    
    /**
     * 创建或更新用户
     * POST /api/users
     * @param user 用户实体
     * @return 保存后的用户实体
     */
    @PostMapping
    public ResponseEntity<User> createUser(@RequestBody User user) {
        User savedUser = userService.saveUser(user);
        return ResponseEntity.ok(savedUser);
    }
    
    /**
     * 删除用户
     * DELETE /api/users/{id}
     * @param id 用户ID
     * @return 删除成功响应
     */
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        userService.deleteUser(id);
        return ResponseEntity.noContent().build();
    }
}

ProductController.java

package com.example.flywaydemo.controller;

import com.example.flywaydemo.entity.Product;
import com.example.flywaydemo.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;

/**
 * 产品控制器
 * 提供 RESTful API 接口
 */
@RestController
@RequestMapping("/api/products")
public class ProductController {
    
    /**
     * 注入产品服务
     */
    @Autowired
    private ProductService productService;
    
    /**
     * 获取所有产品
     * GET /api/products
     * @return 产品列表
     */
    @GetMapping
    public ResponseEntity<List<Product>> getAllProducts() {
        List<Product> products = productService.getAllProducts();
        return ResponseEntity.ok(products);
    }
    
    /**
     * 获取所有可用产品
     * GET /api/products/available
     * @return 可用产品列表
     */
    @GetMapping("/available")
    public ResponseEntity<List<Product>> getAvailableProducts() {
        List<Product> products = productService.getAvailableProducts();
        return ResponseEntity.ok(products);
    }
    
    /**
     * 根据ID获取产品
     * GET /api/products/{id}
     * @param id 产品ID
     * @return 产品实体
     */
    @GetMapping("/{id}")
    public ResponseEntity<Product> getProductById(@PathVariable Long id) {
        Optional<Product> product = productService.getProductById(id);
        return product.map(ResponseEntity::ok)
                     .orElse(ResponseEntity.notFound().build());
    }
    
    /**
     * 根据分类获取产品
     * GET /api/products/category/{category}
     * @param category 产品分类
     * @return 产品列表
     */
    @GetMapping("/category/{category}")
    public ResponseEntity<List<Product>> getProductsByCategory(@PathVariable String category) {
        List<Product> products = productService.getProductsByCategory(category);
        return ResponseEntity.ok(products);
    }
    
    /**
     * 根据价格范围获取产品
     * GET /api/products/price-range?min={min}&max={max}
     * @param minPrice 最低价格
     * @param maxPrice 最高价格
     * @return 产品列表
     */
    @GetMapping("/price-range")
    public ResponseEntity<List<Product>> getProductsByPriceRange(
            @RequestParam BigDecimal minPrice,
            @RequestParam BigDecimal maxPrice) {
        List<Product> products = productService.getProductsByPriceRange(minPrice, maxPrice);
        return ResponseEntity.ok(products);
    }
    
    /**
     * 创建或更新产品
     * POST /api/products
     * @param product 产品实体
     * @return 保存后的产品实体
     */
    @PostMapping
    public ResponseEntity<Product> createProduct(@RequestBody Product product) {
        Product savedProduct = productService.saveProduct(product);
        return ResponseEntity.ok(savedProduct);
    }
    
    /**
     * 删除产品
     * DELETE /api/products/{id}
     * @param id 产品ID
     * @return 删除成功响应
     */
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
        productService.deleteProduct(id);
        return ResponseEntity.noContent().build();
    }
}

4.9 创建主启动类

FlywayDemoApplication.java

package com.example.flywaydemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * Spring Boot 应用主启动类
 * 
 * 应用启动时,Flyway 会自动执行以下操作:
 * 1. 检查数据库中是否存在 flyway_schema_history 表
 * 2. 如果不存在,则创建该表用于记录迁移历史
 * 3. 扫描 classpath:db/migration 目录下的所有迁移脚本
 * 4. 按照版本号顺序执行未执行的迁移脚本
 * 5. 在 flyway_schema_history 表中记录已执行的脚本信息
 */
@SpringBootApplication
public class FlywayDemoApplication {
    
    /**
     * 应用程序入口点
     * @param args 命令行参数
     */
    public static void main(String[] args) {
        SpringApplication.run(FlywayDemoApplication.class, args);
    }
}

5. 运行和测试

5.1 启动应用

# 使用 Maven 启动
./mvnw spring-boot:run

# 或者构建后运行
./mvnw clean package
java -jar target/flyway-demo-1.0.0.jar

5.2 观察 Flyway 执行日志

启动时会看到类似以下日志:

INFO  o.f.c.i.license.VersionPrinter - Flyway Community Edition 10.0.0 by Redgate
INFO  o.f.c.i.database.base.DatabaseType - Database: jdbc:mysql://localhost:3306/flyway_demo (MySQL 8.0)
INFO  o.f.c.i.s.JdbcTableSchemaHistory - Creating Schema History table `flyway_demo`.`flyway_schema_history` ...
INFO  o.f.c.i.c.DbMigrate - Current version of schema `flyway_demo`: << Empty Schema >>
INFO  o.f.c.i.c.DbMigrate - Migrating schema `flyway_demo` to version "1 - Create users table"
INFO  o.f.c.i.c.DbMigrate - Migrating schema `flyway_demo` to version "2 - Create products table"
INFO  o.f.c.i.c.DbMigrate - Migrating schema `flyway_demo` to version "3 - Add user profile table"
INFO  o.f.c.i.c.DbMigrate - Migrating schema `flyway_demo` to version "4 - Insert initial data"
INFO  o.f.c.i.c.DbMigrate - Successfully applied 4 migrations to schema `flyway_demo`

5.3 验证数据库结构

检查数据库中的表:

-- 查看创建的表
SHOW TABLES;

-- 查看 Flyway 历史记录表
SELECT * FROM flyway_schema_history;

-- 查看用户表数据
SELECT * FROM users;

-- 查看产品表数据
SELECT * FROM products;

5.4 测试 API 接口

# 获取所有用户
curl http://localhost:8080/api/users

# 获取所有产品
curl http://localhost:8080/api/products

# 根据分类获取产品
curl http://localhost:8080/api/products/category/Electronics

# 根据价格范围获取产品
curl "http://localhost:8080/api/products/price-range?min=100&max=1000"

6. 高级配置和最佳实践

6.1 多环境配置

application-dev.yml(开发环境)

spring:
  flyway:
    enabled: true
    clean-disabled: false  # 开发环境允许 clean 操作

application-prod.yml(生产环境)

spring:
  flyway:
    enabled: true
    clean-disabled: true   # 生产环境禁止 clean 操作
    validate-on-migrate: true

6.2 Java 迁移脚本示例

V5__JavaMigration.java

package db.migration;

import org.flywaydb.core.api.migration.BaseJavaMigration;
import org.flywaydb.core.api.migration.Context;
import java.sql.Statement;

/**
 * Java 迁移脚本示例
 * 适用于复杂的数据库操作,如数据转换、条件逻辑等
 */
public class V5__JavaMigration extends BaseJavaMigration {
    
    /**
     * 执行迁移逻辑
     * @param context Flyway 上下文,包含数据库连接等信息
     * @throws Exception 迁移过程中可能出现的异常
     */
    @Override
    public void migrate(Context context) throws Exception {
        // 获取数据库连接
        try (Statement statement = context.getConnection().createStatement()) {
            // 执行复杂的 SQL 操作
            statement.execute("ALTER TABLE users ADD COLUMN last_login TIMESTAMP NULL");
            statement.execute("UPDATE users SET last_login = created_at WHERE last_login IS NULL");
        }
    }
}

6.3 回滚脚本(Flyway Teams 版本)

U1__Undo_create_users_table.sql

-- 撤消版本 1 的变更
DROP TABLE IF EXISTS users;

6.4 常用 Flyway 命令

// 在代码中手动控制 Flyway
@Autowired
private Flyway flyway;

// 执行迁移
flyway.migrate();

// 验证迁移
flyway.validate();

// 清理数据库(慎用!)
flyway.clean();

// 获取当前版本信息
flyway.info();

7. 常见问题和解决方案

7.1 已有数据库集成 Flyway

spring:
  flyway:
    # 为已有数据库创建基线
    baseline-on-migrate: true
    # 基线版本号
    baseline-version: 1

7.2 脚本校验失败

  • 原因:已执行的脚本被修改
  • 解决方案:
    • 恢复脚本到原始状态
    • 或者禁用校验:spring.flyway.validate-on-migrate=false(不推荐)

7.3 生产环境安全考虑

  • 禁用 clean 操作
  • 启用脚本校验
  • 严格控制数据库用户权限
  • 迁移脚本经过充分测试

总结

Flyway 是一个强大而简单的数据库版本控制工具,通过本文的详细示例,你应该能够:

  1. 理解 Flyway 的核心概念和工作原理
  2. 在 Spring Boot 3 项目中正确配置和使用 Flyway
  3. 编写规范的 SQL 迁移脚本
  4. 构建完整的 CRUD 应用并与 Flyway 集成
  5. 处理常见的生产环境问题

Flyway 的关键优势在于简单性和可靠性,它让数据库变更变得像代码版本控制一样简单和可追踪。

H3C-HCL模拟器-STP生成树协议实验 边缘接口的存在是为了提高网络的收敛速度和性能。由于边缘接口连接的是终端设备,这些设备通常不会产生环路,因此不需要参与生成树协议的计算和决策。边缘接口通常是指那些不会连接到其他交换机或生成树的冗余路径的接口。H3C交换机千兆口的cost默认是20,把SW2的g1/0/2接口的cost改为100;SW4是根网桥,根据STP计算,闭塞端口应该在SW1上;方法:使得SW2到SW1的cost比SW4到SW1更大;7)修改配置:把SW1连接PC的所有接口配置为边缘接口。协议的计算开销,提高网络的收敛速度。 阅读详情

相关推荐

TL3562/3568内核驱动移植实战:解决无锡沐创N500L-AM4网口异常问题

本文详细介绍了在TL3562/3568平台上移植无锡沐创N500L-AM4网卡驱动的实战经验,重点解决了第四个网口通讯异常问题。通过升级驱动版本至rnpgbe-0.2.3,生成关键头文件并正确配置内核,最终实现四个网口稳定工作。文章还提供了性能优化建议和避坑指南,为类似硬件驱动移植项目提供参考。

weixin_28273593的博客 180

Flyway详解(使用说明及避坑指南、一文搞懂flyway)

Flyway详解,包含工作原理、使用说明、避坑指南、以及团队使用时的注意事项

Jiao1225的博客 8万+

面试复习题--RN面试

考察点:热更新方案的深度理解,选型能力,实战落地经验。答题思路主流热更新方案对比。

hqiangtai的博客 723

Flyway简介及使用

Flyway简介及使用 1、简介 1.1 Flyway是什么? Flyway是一款开源的数据库版本管理工具,它更倾向于规约优于配置的方式。 Flyway可以独立于应用实现管理并跟踪数据库变更,支持数据库版本自动升级,并且有一套默认的规约,不需要复杂的配置。 Migrations可以写成SQL脚本,也可以写在Java代码中,不仅支持Command Line和Java API,还支持Build构建工具和Spring Boot等。 同时在分布式环境下能够安全可靠地升级数据库,同时也支持失败恢复等。 关于F

solo的博客 1万+

修改注册表导致开机时电脑显示“无法登陆到你的账户”的问题解决方案

1.问题描述:开机电脑显示“无法登陆到你的账户:通常可以通过从你的账户注销,然后重新登陆来解决问题,如果不立即注销,你创建的任何文件或进行的任何更改都将会丢失。” 2.问题原因:想要修改cmd中的用户名(红色部分),修改了注册表,重启时出现问题。 3.问题解决: win+R输入cmd,确定进入注册表界面,将下面的地址复制进地址栏(你也可以自己按目录查找),双击ProfileImage...

qq_33835957的博客 7万+

ANSYS APDL齿轮建模实战:从渐开线方程到完整3D模型的保姆级教程

本文详细介绍了使用ANSYS APDL进行齿轮建模的全过程,从渐开线方程的数学基础到完整3D模型的创建。通过参数化命令流实现高精度齿轮建模,特别适合机械设计和仿真分析需求。教程包含关键参数计算、APDL环境配置、单齿齿廓建模以及全齿轮阵列复制等实用技巧,帮助工程师快速掌握ANSYS APDL在齿轮建模中的应用。

lg888的博客 445

数据库版本管理框架-Flyway(从入门到精通)

数据库版本管理框架-Flyway(从入门到精通)

可乐要加冰!!! 4753

AI 自动生成商业计划书:AIGC 如何助力创业者?

在创业的早期阶段,商业计划书(Business Plan)是至关重要的。它不仅用于吸引投资者、明确发展路径,还能帮助团队对业务模式、市场定位和财务预测有清晰认知。然而,撰写一份高质量的商业计划书往往需要投入大量时间和精力。如今,AIGC(AI Generated Content,人工智能生成内容)技术正在改变这一现状。借助 AIGC,创业者可以更快速、高效地创建商业计划书,并通过 AI 进行优化和数据分析。

2401_82808034的博客 3779

flyway使用

一、Flyway是什么 官网解释地非常全面,可先大致阅读一下。 简单地说,flyway是一个能对数据库变更做版本控制的工具。 二、为什么要用Flyway 在多人开发的项目中,我们都习惯了使用SVN或者Git来对代码做版本控制,主要的目的就是为了解决多人开发代码冲突和版本回退的问题。 其实,数据库的变更也需要版本控制,在日常开发中,我们经常会遇到下面的问题: 自己写的SQL忘了在所有环境执行; 别人写的SQL我们不能确定是否都在所有环境执行过了; 有人修改了已经执行过的SQL,期望再次执行;

幽静净土的技术分享 6299

史上最小白之RNN详解

1.前言 网上目前已经有诸多优秀的RNN相关博客,但是我写博客的出发点主要是为了加深和巩固自己的理解,所以还是决定自己再进行一下总结和描述,如有不正确的地方欢迎指正~ 2.区分RNN 循环神经网络(Recurrent Neural Network),递归神经网络(Recursive Neural Network),你有没有发现他们的缩写都是RNN,那他们两个是同一回事儿吗?网上有一些博客把这两就当...

Tink1995的博客 4万+

SQL注入详解

SQL注入(SQL Injection)是一种常见的Web安全漏洞,主要形成的原因是在数据交互中,前端的数据传入到后台处理时,没有做严格的判断,导致其传入的“数据”拼接到SQL语句中后,被当作SQL语句的一部分执行。 从而导致数据库受损(被脱库、被删除、甚至整个服务器权限陷)。 即:注入产生的原因是后台服务器接收相关参数未经过滤直接带入数据库查询...

渗透之路,攻防之间皆学问 26万+

springboot 集成flyway数据库迁移版本控制详细教程

Flyway 是一个开源的数据库迁移工具,它帮助开发者在项目中管理数据库模式的版本控制。Spring Boot 对 Flyway 提供了很好的支持,使得集成过程变得非常简单。下面是一个详细的教程,指导如何在 Spring Boot 应用程序中集成 Flyway。

洛阳泰山的博客 1449

2025年9月GESP(C++四级):排兵布阵

作为将军,你自然需要合理地排兵布阵。地图可以视为n行m列的网格,适合排兵的网格以 1 标注,不适合排兵的网格以 0 标注。现在你需要在地图上选择一个矩形区域排兵,这个矩形区域内不能包含不适合排兵的网格。请问可选择的矩形区域最多能包含多少网格?

王老师青少年编程 1377

数据库版本控制和迁移的工具之Flyway

Flyway 是一个用于数据库版本控制和迁移的工具,它使用纯 SQL 来管理数据库的变化。Flyway 帮助开发人员自动化数据库的部署过程,确保数据库在不同环境中的一致性。通过 Flyway,开发人员可以轻松地跟踪和管理数据库的变更历史,从而避免手动操作带来的错误和不一致性。

西京刀客 1225

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

【数据库迁移与版本控制:使用Flyway和Liquibase】

数据库迁移工具如何选?本文深度对比 Flyway 和 Liquibase,解析两者特点与适用场景,结合代码示例展示实际用法。无论你是追求简单高效的 SQL 爱好者,还是需要跨数据库支持的架构师,都能找到适合自己的解决方案!

weixin_74887700的博客 1465

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

数据库迁移、版本控制管理工具-Flyway

随着项目不断的增大,数据库也需要不断的扩充,加表加字段,(每一次的增加称作一次DB的迁移migration)你是否还在用着最原始的方式, 用文件管理每次的SQL升级脚本,加了哪些字段,加了那些表,现在可以用数据库版本控制工具搞定了。

yangyang2183的博客 1521

蓝牙模块HM-10手册

蓝牙模块HM-10手册,该手册对蓝牙模块HM-10进行了详细的介绍和使用方法以及一些AT指令。

Flyway数据库版本控制与数据迁移工具

官网地址: https://flywaydb.org/ Flyway是一个开源数据库迁移工具。与配置相比,它极力主张简单性和约定性。 它仅基于7个基本命令:迁移,清理,信息,验证,撤消,基线和修复。 可以使用SQL(支持特定于数据库的语法(例如PL / SQL,T-SQL等))或Java(用于高级数据转换或处理LOB)编写迁移。 它具有一个命令行客户端。如果您使用的是JVM,建议您在应用程序启动时使用Java API(也适用于Android)迁移数据库。或者,您也可以使用Maven...

纸上得来终觉浅,绝知此事要躬行 2233

Quarkus数据库迁移:Flyway和Liquibase的版本控制

在现代应用开发中,数据库schema(模式)的变更管理是一个关键挑战。传统的数据库变更方式往往存在以下痛点: - **手动执行SQL脚本**容易出错且难以跟踪 - **多环境一致性**难以保证(开发、测试、生产) - **版本回退**复杂且风险高 - **团队协作**时schema变更冲突频繁 Quarkus作为云原生Java框架,提供了与Flyway和Liquibase的无缝集成,让数据库版...

gitblog_00445的博客 1022

Flyway数据库迁移:Spring Boot数据库版本控制终极指南

在当今的软件开发中,数据库版本控制已经成为项目成功的关键因素。Flyway作为一款轻量级的数据库迁移工具,能够帮助开发团队轻松管理数据库变更,确保开发、测试和生产环境的一致性。🚀 ## 什么是Flyway数据库迁移? Flyway是一个开源的数据库迁移工具,它通过简单的SQL脚本实现了数据库的版本控制。与传统的数据库变更管理方式相比,Flyway具有以下显著优势: - **版本控制**:每

gitblog_00198的博客 1079

pig系统数据库迁移:Flyway版本控制实践

在多团队协作开发中,数据库结构变更常常导致以下问题:开发环境与生产环境表结构不一致、手动执行SQL脚本出错、回滚操作复杂、变更历史不可追溯。根据DB-Engines 2025年排行,78%的数据库事故源于非结构化的版本管理。Flyway作为一款开源的数据库版本控制工具,通过**版本化SQL脚本**和**自动化执行流程**,可有效解决这些问题。 ### Flyway核心优势 | 特性 | 传统手动...

gitblog_00746的博客 310

JeecgBoot数据库版本控制终极指南:Flyway自动化迁移实践

JeecgBoot作为企业级低代码平台,其数据库版本管理是确保系统稳定迭代的关键环节。本文将详细介绍如何通过Flyway实现JeecgBoot项目的数据库自动化迁移,帮助开发团队摆脱手动SQL执行的繁琐流程,实现版本控制的标准化与自动化。 ## 为什么选择Flyway进行数据库版本控制? 在传统开发模式中,数据库变更往往依赖手动执行SQL脚本,这种方式容易导致版本混乱、环境不一致等问题。Jee

gitblog_00418的博客 1058

【Flyway】数据库版本控制,批量数据迁移

一、参考资料 Homepage - Flyway Flyway快速上手教程 - 简书

后端研发工程师Marion的博客 619

Clawdbot数据迁移:Flyway数据库版本控制

本文介绍了如何在星图GPU平台上自动化部署Clawdbot整合qwen3:32b代理网关与管理平台镜像,实现数据库版本控制与迁移功能。通过Flyway工具,用户可轻松管理数据库结构变更,应用于AI应用的数据层维护,确保多环境部署的一致性,提升开发与运维效率。

weixin_42599558的博客 328

终极指南:如何使用 db-migration 实现国产数据库无缝迁移与版本控制

db-migration 是一款专为国产数据库打造的开源迁移框架,由 mengweijin 开发,支持 Flyway 和 Liquibase 扩展,完美适配达梦(DM)、南大通用(GBase 8s)、OpenGauss 等主流国产数据库,帮助开发者在持续集成/部署中安全管理数据库架构变更。 ## ???? 核心功能与支持矩阵 ### ???? 数据库兼容性速览 | 数据库类型 ...

gitblog_01122的博客 1087

国产数据库迁移革命:5大理由选择db-migration实现无缝数据迁移

在当今国产化浪潮中,数据库迁移已成为每个技术团队必须面对的挑战。db-migration作为一款专为国产数据库设计的开源迁移框架,在短短几年内帮助数百家企业解决了达梦、南大通用、OpenGauss等主流国产数据库的版本控制难题。 ## 🎯 为什么你的项目急需数据库迁移工具? 数据库架构变更管理是软件开发中最容易被忽视却至关重要的环节。传统的手工执行SQL脚本方式存在诸多痛点:版本混乱、回滚困

gitblog_00062的博客 1124

db-migration:国产数据库迁移的革命性解决方案

在数字化转型浪潮中,国产数据库迁移与版本控制已成为企业级应用开发的关键环节。db-migration 项目作为一款专为国产数据库深度优化的开源框架,彻底解决了传统迁移工具在国产数据库环境中的兼容性痛点。该项目由 mengweijin 开发维护,通过双引擎架构同时支持 Flyway 和 Liquibase 两大主流迁移工具,为开发者提供了前所未有的灵活性。 ## 🎯 为什么选择 db-migra

gitblog_01146的博客 943
上一篇: Spring Boot 3 + Spring Cloud 2024 + Nacos 配置中心完整指南
下一篇: Mybatis、MyBatis-Plus 深度分页性能问题与游标分页解决方案
龙茶清欢
博客等级 码龄7年 3720粉丝 375原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龙茶清欢

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值