若依-权限管理、日志、代码生成

目录

1、若依-权限控制

1.1、介绍

1.2、数据库表结构设计

1.3、菜单管理

1.4、菜单权限

1.5、api接口权限

1.5.1、后台权限注解

@RequiresLogin

@RequiresPermissions

@RequiresRoles

1.5.2、实现原理

1.6、数据权限

1.7、完善系统权限功能

1.7.1、添加按钮权限数据

1.7.2、Controller类权限控制

1.7.3、vue页面按钮控制

2、若依-系统日志

2.1、注解参数说明

2.2、自定义操作功能

2.3、完善系统日志

3、若依-代码生成

3.1、介绍

3.2、代码生成器的使用

3.2.1、修改配置文件

1、数据库连接配置

2、代码生成器配置

3.2.2、引入数据库表

3.2.3、启动spzx-gen模块

3.2.4、代码生成


1、若依-权限控制

1.1、介绍

权限控制主要目的是保护系统的安全性和完整性,防止未经授权的用户获取敏感信息、执行非法操作或对系统进行恶意操作 。

常见的权限控制框架有SpringSecurity和Shiro。

若依的权限管理是通过RBAC(Role-based Access Control 基于角色的访问控制)模型自己设计的。

RBAC模型将权限控制分为角色管理和权限管理两个部分。在若依中,角色是指对系统的一类用户或操作者的定义,而权限是指对系统中某个资源或操作的访问控制。通过为每个角色分配相应的权限,可以实现对系统的全面管理和控制。

具体来说,若依的权限管理包括以下几个方面:

  1. 菜单管理:通过对系统菜单进行管理,可以控制用户在系统中能够访问的页面和功能。

  2. 按钮权限:在系统中,某些操作需要特定的权限才能进行,例如删除、修改等操作。通过对按钮权限的控制,可以限制用户对系统的访问和操作。

  3. 数据权限:在某些情况下,需要根据用户的角色或部门来限制其对数据的访问。通过数据权限的设置,可以实现对数据的细粒度控制。

  4. API接口权限:在若依中,API也可以通过权限的方式进行控制。通过对API的权限进行管理,可以限制用户对API的访问和使用。

1.2、数据库表结构设计

在数据库表结构方面,若依采用了RBAC模型的设计。其中,主要包括以下表:

  • sys_menu:存储系统菜单信息,包括菜单ID、菜单名称、访问路径、菜单类型等字段。

  • sys_role:存储系统角色信息,包括角色ID、角色名称、角色标识、角色描述等字段。

  • sys_user:存储系统用户信息,包括用户ID、用户名、密码、昵称、邮箱、电话等字段。

  • sys_role_menu:存储角色和菜单之间的关联关系,包括角色ID和菜单ID两个字段。

  • sys_user_role:存储用户和角色之间的关联关系,包括用户ID和角色ID两个字段。

通过这些表的设计,可以实现对系统中菜单、角色和用户的管理。同时,通过角色和菜单之间的关联关系,可以实现对菜单访问权限的控制。通过用户和角色之间的关联关系,可以实现对用户访问权限的控制。

1.3、菜单管理

目录、菜单和按钮的区别

在若依(RuoYi)中,菜单和目录是两个不同的概念,它们之间的区别如下:

1.目录(Directory):

目录是用来组织和分类菜单的容器。目录本身没有功能,它只是一个容器,可以包含若干个菜单。目录通常是一个抽象的概念,用于将一组相关的菜单组织在一起。

在若依中,目录是以“系统管理”、“系统监控”等大模块的方式组织菜单的,用于区分不同的功能模块。目录通常以左侧的菜单树的形式展现,用户可以通过点击不同的目录来展开或收缩对应的菜单列表。

2.菜单(Menu):

菜单是具有一定功能的操作项,通常是一组具有相同功能的页面或功能点的集合。每个菜单通常对应一个页面或者一个功能模块。

在若依中,菜单通常是以左侧的树形菜单的形式展现,用户可以通过点击不同的菜单来跳转到对应的页面或功能模块。每个菜单都有一个唯一的标识符,通常以URL的形式表示。

3.按钮(Button):

按钮是指菜单中的操作按钮,用于触发一些具体的操作。在若依中,按钮通常是与表格或表单等组件配合使用的,用于进行数据的增删改查等操作。按钮通常会与权限控制结合起来,只有拥有相应权限的用户才能看到并使用该按钮。

总的来说,目录、菜单、按钮是若依系统中的三种不同的概念。目录是为了方便管理菜单和模块,菜单是系统的核心功能模块,按钮是菜单中的具体操作按钮。在实际应用中,它们通常会结合起来,形成一个完整的用户界面和操作流程。

1.4、菜单权限

在若依中,实现不同用户看到不同的菜单可以通过以下步骤实现:

  1. 在数据库中维护菜单的权限信息,可以为每个菜单设置一个权限标识。

  2. 在用户登录系统时,将该用户所拥有的菜单权限信息从数据库中获取出来。

  3. 根据用户的菜单权限信息动态生成菜单,使用户只能看到其拥有权限的菜单。

用户登录之后会请求后端的SysMenuController#getRouters接口获取登录用户可访问的菜单数据:

select distinct m.menu_id, m.parent_id, m.menu_name, m.path, m.component, m.`query`, m.visible, m.status, ifnull(m.perms,'') as perms, m.is_frame, m.is_cache, m.menu_type, m.icon, m.order_num, m.create_time
from sys_menu m
    left join sys_role_menu rm on m.menu_id = rm.menu_id
    left join sys_user_role ur on rm.role_id = ur.role_id
    left join sys_role ro on ur.role_id = ro.role_id
    left join sys_user u on ur.user_id = u.user_id
where u.user_id = #{userId} and m.menu_type in ('M', 'C') and m.status = 0  AND ro.status = 0
order by m.parent_id, m.order_num

菜单类型(M目录 C菜单 F按钮);菜单状态(0显示 1隐藏)

前端会根据该接口返回的数据渲染出不同的菜单。

1.5、api接口权限

配置方法

每一个按钮基本上都会对应着一个后端的接口,前端会根据权限标志显示或者隐藏按钮,但是如果用户不点击按钮,直接通过http请求工具请求后端咋办?所以接口权限也是要有的,该权限和按钮上权限完全一致。

若依系统实现了这部分功能,实现模块:spzx-common-security,比如,用户管理页面中的修改用户按钮对应的后端接口长这个样子。

@RequiresPermissions("system:user:edit")
@PutMapping
public AjaxResult edit(@Validated @RequestBody SysUser user) {
    ...
    return toAjax();
}

和其对应的前端按钮权限标志一样

 

前端控制

v-hasPermi="['system:user:edit']"

<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:user:edit']"></el-button>

 

1.5.1、后台权限注解

 

 

@RequiresLogin

@RequiresLogin注解用于配置接口要求用户必须登录才可访问

示例1: 以下代码表示必须登录才可访问

@RequiresLogin
public AjaxResult allCheckCart(){
    return success();
}
@RequiresPermissions

@RequiresPermissions注解用于配置接口要求用户拥有某(些)权限才可访问,它拥有两个参数

参数类型描述
valueString[]权限列表
logicalLogical权限之间的判断关系,默认为Logical.AND

示例1: 以下代码表示必须拥有system:user:add权限才可访问

@RequiresPermissions("system:user:add")
public AjaxResult save(...) 
{
    return AjaxResult.success(...);
}

示例2: 以下代码表示必须拥有system:user:addsystem:user:edit权限才可访问

@RequiresPermissions({"system:user:add", "system:user:edit"})
public AjaxResult save(...)
{
    return AjaxResult.success(...);
}

 示例3: 以下代码表示需要拥有system:user:addsystem:user:edit权限才可访问

@RequiresPermissions(value = {"system:user:add", "system:user:edit"}, logical = Logical.OR)
public AjaxResult save(...)
{
    return AjaxResult.success(...);
}
@RequiresRoles

@RequiresRoles注解用于配置接口要求用户拥有某(些)角色才可访问,它拥有两个参数

参数类型描述
valueString[]角色列表
logicalLogical角色之间的判断关系,默认为Logical.AND

示例1: 以下代码表示必须拥有admin角色才可访问

@RequiresRoles("admin")
public AjaxResult save(...)
{
    return AjaxResult.success(...);
}

示例2: 以下代码表示必须拥有admincommon角色才可访问

@RequiresRoles({"admin", "common"})
public AjaxResult save(...)
{
    return AjaxResult.success(...);
}

 示例3: 以下代码表示需要拥有admincommon角色才可访问

@RequiresRoles(value = {"admin", "common"}, logical = Logical.OR)
public AjaxResult save(...)
{
    return AjaxResult.success(...);
}
1.5.2、实现原理

实现原理:一个拦截器【HeaderInterceptor】 + 一个AOP【PreAuthorizeAspect

springboot 拦截器与AOP执行顺序:

 

拦截器HeaderInterceptor负责解析用户token信息,并将解析出的用户信息存入ThreadLocal中,然后PreAuthorizeAspect负责解析权限注解,判断当前用户是否拥有注解权限。

1.6、数据权限

实现模块:spzx-common-datascope

数据权限实现的关键在于DataScopeAspect这个AOP类。该类是一个切面类,凡是加上DataScope注解的方法,在执行的时候都会被它拦截。

该切面定义了五种权限范围

该切面的核心逻辑是“拼SQL”,方法执行之前,会给参数的一个params属性添加一个dataScope键值对,key为"dataScope",值为AND (" + sqlString.substring(4) + ")"样式的一段SQL,这段SQL会根据当前用户所在的部门以及当前用户角色的权限范围发生变化。

以用户列表查询为例,执行sql为

<select id="selectUserList" parameterType="SysUser" resultMap="SysUserResult">
    select u.user_id, u.dept_id, u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark, d.dept_name, d.leader from sys_user u
    left join sys_dept d on u.dept_id = d.dept_id
    where u.del_flag = '0'
    <if test="userId != null and userId != 0">
        AND u.user_id = #{userId}
    </if>
    <if test="userName != null and userName != ''">
        AND u.user_name like concat('%', #{userName}, '%')
    </if>
    <if test="status != null and status != ''">
        AND u.status = #{status}
    </if>
    <if test="phonenumber != null and phonenumber != ''">
        AND u.phonenumber like concat('%', #{phonenumber}, '%')
    </if>
    <if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 -->
        AND date_format(u.create_time,'%y%m%d') &gt;= date_format(#{params.beginTime},'%y%m%d')
    </if>
    <if test="params.endTime != null and params.endTime != ''"><!-- 结束时间检索 -->
        AND date_format(u.create_time,'%y%m%d') &lt;= date_format(#{params.endTime},'%y%m%d')
    </if>
    <if test="deptId != null and deptId != 0">
        AND (u.dept_id = #{deptId} OR u.dept_id IN ( SELECT t.dept_id FROM sys_dept t WHERE find_in_set(#{deptId}, ancestors) ))
    </if>
    <!-- 数据范围过滤 -->
    ${params.dataScope}
</select>

1.7、完善系统权限功能

我们以品牌管理为例,其他模块类似,自行完善

1.7.1、添加按钮权限数据

在菜单管理添加品牌管理对应的增删改查权限数据。

 

1.7.2、Controller类权限控制
@Tag(name = "品牌接口管理")
@RestController
@RequestMapping("/brand")
public class BrandController extends BaseController {
    @Autowired
    private IBrandService brandService;

    /**
     * 查询品牌列表
     */
    @RequiresPermissions("product:brand:list")
    @Operation(summary = "查询品牌列表")
    @GetMapping("/list")
    public TableDataInfo list(Brand brand) {
        startPage();
        List<Brand> list = brandService.selectBrandList(brand);
        return getDataTable(list);
    }

    /**
     * 获取品牌详细信息
     */
    @RequiresPermissions("product:brand:query")
    @Operation(summary = "获取品牌详细信息")
    @GetMapping(value = "/{id}")
    public AjaxResult getInfo(@PathVariable("id") Long id) {
        return success(brandService.selectBrandById(id));
    }

    /**
     * 新增品牌
     */
    @RequiresPermissions("product:brand:add")
    @Operation(summary = "新增品牌")
    @PostMapping
    public AjaxResult add(@RequestBody @Validated Brand brand) {
        brand.setCreateBy(SecurityUtils.getUsername());
        return toAjax(brandService.insertBrand(brand));
    }

    /**
     * 修改品牌
     */
    @RequiresPermissions("product:brand:edit")
    @Operation(summary = "修改品牌")
    @PutMapping
    public AjaxResult edit(@RequestBody @Validated Brand brand) {
        brand.setUpdateBy(SecurityUtils.getUsername());
        return toAjax(brandService.updateBrand(brand));
    }

    /**
     * 删除品牌
     */
    @RequiresPermissions("product:brand:remove")
    @Operation(summary = "删除品牌")
    @DeleteMapping("/{ids}")
    public AjaxResult remove(@PathVariable Long[] ids) {
        return toAjax(brandService.deleteBrandByIds(ids));
    }

    @RequiresPermissions("product:brand:query")
    @Operation(summary = "获取全部品牌")
    @GetMapping("getBrandAll")
    public AjaxResult getBrandAll() {
        return success(brandService.selectBrandAll());
    }
}
1.7.3、vue页面按钮控制
<template>
  <div class="app-container">

    <!-- 搜索表单 -->
    ...

    <!-- 功能按钮栏 -->
    <el-row :gutter="10" class="mb8">
      <el-col :span="1.5">
        <el-button
          type="primary"
          plain
          icon="Plus"
          @click="handleAdd"
          v-hasPermi="['product:brand:add']"
        >新增</el-button>
      </el-col>
      <el-col :span="1.5">
        <el-button
          type="success"
          plain
          icon="Edit"
          :disabled="single"
          @click="handleUpdate"
          v-hasPermi="['product:brand:edit']"
        >修改</el-button>
      </el-col>
      <el-col :span="1.5">
        <el-button
          type="danger"
          plain
          icon="Delete"
          :disabled="multiple"
          @click="handleDelete"
          v-hasPermi="['product:brand:remove']"
        >删除</el-button>
      </el-col>
      <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
    </el-row>

    <!-- 数据展示表格 -->
    <el-table  v-loading="loading" :data="brandList" @selection-change="handleSelectionChange">
      ...
      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
        <template #default="scope">
          <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['product:brand:edit']">修改</el-button>
          <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['product:brand:remove']">删除</el-button>
        </template>
      </el-table-column>
    </el-table>

    ...
    
  </div>
</template>

2、若依-系统日志

实现模块:spzx-common-log

在实际开发中,对于某些关键业务,我们通常需要记录该操作的内容,一个操作调一次记录方法,每次还得去收集参数等等,会造成大量代码重复。 我们希望代码中只有业务相关的操作,在项目中使用注解来完成此项功能。

在需要被记录日志的controller方法上添加@Log注解,使用方法如下:

@Log(title = "用户管理", businessType = BusinessType.INSERT)
public AjaxResult addSave(...)
{
    return success(...);
}

2.1、注解参数说明

参数类型默认值描述
titleString操作模块
businessTypeBusinessTypeOTHER操作功能(OTHER其他、INSERT新增、UPDATE修改、DELETE删除、GRANT授权、EXPORT导出、IMPORT导入、FORCE强退、GENCODE生成代码、CLEAN清空数据)
operatorTypeOperatorTypeMANAGE操作人类别(OTHER其他、MANAGE后台用户、MOBILE手机端用户)
isSaveRequestDatabooleantrue是否保存请求的参数
isSaveResponseDatabooleantrue是否保存响应的参数
excludeParamNamesString[]{}排除指定的请求参数

2.2、自定义操作功能

1、在BusinessType中新增业务操作类型如:

/**
 * 测试
 */
TEST,

2、在sys_dict_data字典数据表中初始化操作业务类型

insert into sys_dict_data values(25, 10, '测试',     '10', 'sys_oper_type',       '',   'primary', 'N', '0', 'admin', '2018-03-16 11-33-00', 'ry', '2018-03-16 11-33-00', '测试操作');

3、在Controller中使用注解

@Log(title = "测试标题", businessType = BusinessType.TEST)
public AjaxResult test(...)
{
    return success(...);
}

操作日志记录逻辑实现代码 LogAspect.java 登录系统(系统管理-操作日志)可以查询操作日志列表和详细信息。

2.3、完善系统日志

我们以品牌管理为例,其他模块类似,自行完善

/**
 * 新增品牌
 */
@Log(title = "品牌管理", businessType = BusinessType.INSERT)
@RequiresPermissions("product:brand:add")
@Operation(summary = "新增品牌")
@PostMapping
public AjaxResult add(@RequestBody @Validated Brand brand) {
    brand.setCreateBy(SecurityUtils.getUsername());
    return toAjax(brandService.insertBrand(brand));
}

/**
 * 修改品牌
 */
@Log(title = "品牌管理", businessType = BusinessType.UPDATE)
@RequiresPermissions("product:brand:edit")
@Operation(summary = "修改品牌")
@PutMapping
public AjaxResult edit(@RequestBody @Validated Brand brand) {
    brand.setUpdateBy(SecurityUtils.getUsername());
    return toAjax(brandService.updateBrand(brand));
}

/**
 * 删除品牌
 */
@Log(title = "品牌管理", businessType = BusinessType.DELETE)
@RequiresPermissions("product:brand:remove")
@Operation(summary = "删除品牌")
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable Long[] ids) {
    return toAjax(brandService.deleteBrandByIds(ids));
}

3、若依-代码生成

大部分项目里其实有很多代码都是重复的,几乎每个基础模块的代码都有增删改查的功能,而这些功能都是大同小异, 如果这些功能都要自己去写,将会大大浪费我们的精力降低效率。所以这种重复性的代码可以使用代码生成。

实现模块:spzx-gen

3.1、介绍

若依(Ruoyi)项目的代码生成模块(spzx-gen)中,使用了Apache Velocity作为模板引擎,根据模板(spzx-gen模块模板文件在resources/vm下面,我们可以根据需要自行调整模板。)来生成具体的代码。

Velocity根据模板文件中的占位符和变量替换规则,将元数据信息嵌入到生成的代码中,生成具体的代码文件。通过导入表结构和生成代码两个后端接口,实现了快速导入数据库表结构和生成代码的功能。导入表结构会从information_schema数据库的tables和columns表中查询表和列的信息,并插入到ruoyi数据库的gen_tablegen_table_column表中。生成代码时,会根据查询到的表和列信息,初始化Velocity模板引擎,并准备上下文信息,包括变量值信息。然后,读取模板文件,渲染模板,并将渲染后的内容添加到压缩流中生成zip压缩文件,供前后端下载使用。ruoyi-vue代码生成器大大提高了开发效率,使得开发人员能够快速生成符合规范的代码文件。

3.2、代码生成器的使用

通过以下步骤,若依可以根据数据库表的设计信息自动生成相应的代码文件,极大地提高了开发效率。开发人员可以根据生成的代码文件进行进一步的开发和定制。

以下是若依实现代码自动生成的一般流程:

3.2.1、修改配置文件

登录nacos:http://localhost:8848/nacos

在spzx-gen-dev.yml 修改以下两个配置:

1、数据库连接配置

我们需要对哪个数据库表生成代码,那么就连接哪个数据库,当前我们以"商品库"为例

datasource:
  type: com.zaxxer.hikari.HikariDataSource
  driver-class-name: com.mysql.jdbc.Driver
  url: jdbc:mysql://localhost:3306/spzx-product?characterEncoding=utf-8&useSSL=false
  username: root
  password: root
2、代码生成器配置
# 代码生成
gen:
  # 作者
  author: atguigu
  # 默认生成包路径 system 需改成自己的模块名称 如 system monitor tool
  packageName: com.spzx.product
  # 自动去除表前缀,默认是false
  autoRemovePre: false
  # 表前缀(生成类名不会包含表前缀,多个用逗号分隔)
  tablePrefix: 
3.2.2、引入数据库表

代码生成依赖gen_table和gen_table_column这两张表,spzx-product库中创建这两张表

-- ----------------------------
-- 代码生成业务表
-- ----------------------------
drop table if exists gen_table;
create table gen_table (
  table_id          bigint(20)      not null auto_increment    comment '编号',
  table_name        varchar(200)    default ''                 comment '表名称',
  table_comment     varchar(500)    default ''                 comment '表描述',
  sub_table_name    varchar(64)     default null               comment '关联子表的表名',
  sub_table_fk_name varchar(64)     default null               comment '子表关联的外键名',
  class_name        varchar(100)    default ''                 comment '实体类名称',
  tpl_category      varchar(200)    default 'crud'             comment '使用的模板(crud单表操作 tree树表操作)',
  tpl_web_type      varchar(30)     default ''                 comment '前端模板类型(element-ui模版 element-plus模版)',
  package_name      varchar(100)                               comment '生成包路径',
  module_name       varchar(30)                                comment '生成模块名',
  business_name     varchar(30)                                comment '生成业务名',
  function_name     varchar(50)                                comment '生成功能名',
  function_author   varchar(50)                                comment '生成功能作者',
  gen_type          char(1)         default '0'                comment '生成代码方式(0zip压缩包 1自定义路径)',
  gen_path          varchar(200)    default '/'                comment '生成路径(不填默认项目路径)',
  options           varchar(1000)                              comment '其它生成选项',
  create_by         varchar(64)     default ''                 comment '创建者',
  create_time      datetime                                   comment '创建时间',
  update_by         varchar(64)     default ''                 comment '更新者',
  update_time       datetime                                   comment '更新时间',
  remark            varchar(500)    default null               comment '备注',
  primary key (table_id)
) engine=innodb auto_increment=1 comment = '代码生成业务表';


-- ----------------------------
-- 代码生成业务表字段
-- ----------------------------
drop table if exists gen_table_column;
create table gen_table_column (
  column_id         bigint(20)      not null auto_increment    comment '编号',
  table_id          bigint(20)                                 comment '归属表编号',
  column_name       varchar(200)                               comment '列名称',
  column_comment    varchar(500)                               comment '列描述',
  column_type       varchar(100)                               comment '列类型',
  java_type         varchar(500)                               comment 'JAVA类型',
  java_field        varchar(200)                               comment 'JAVA字段名',
  is_pk             char(1)                                    comment '是否主键(1是)',
  is_increment      char(1)                                    comment '是否自增(1是)',
  is_required       char(1)                                    comment '是否必填(1是)',
  is_insert         char(1)                                    comment '是否为插入字段(1是)',
  is_edit           char(1)                                    comment '是否编辑字段(1是)',
  is_list           char(1)                                    comment '是否列表字段(1是)',
  is_query          char(1)                                    comment '是否查询字段(1是)',
  query_type        varchar(200)    default 'EQ'               comment '查询方式(等于、不等于、大于、小于、范围)',
  html_type         varchar(200)                               comment '显示类型(文本框、文本域、下拉框、复选框、单选框、日期控件)',
  dict_type         varchar(200)    default ''                 comment '字典类型',
  sort              int                                        comment '排序',
  create_by         varchar(64)     default ''                 comment '创建者',
  create_time      datetime                                   comment '创建时间',
  update_by         varchar(64)     default ''                 comment '更新者',
  update_time       datetime                                   comment '更新时间',
  primary key (column_id)
) engine=innodb auto_increment=1 comment = '代码生成业务表字段';
3.2.3、启动spzx-gen模块

启动spzx-gen模块

3.2.4、代码生成

进入系统工具-代码生成页面,点击导入按钮,找到product表并导入,如下图所示

 

若依根据配置和模板,通过解析数据库表的元数据信息,自动生成对应的Java类、Mapper接口、Service类、Controller类等代码文件。

点击编辑按钮之后,跳转修改生成配置页面

点击预览按钮,可以查看生成的代码,点击“生成代码”,可以下载生成文件  

 此处生成的代码我们只作为测试,不使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

贰陆.256

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

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

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

打赏作者

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

抵扣说明:

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

余额充值