宅急送 项目第十天 权限管理

本文详细介绍了自定义权限管理模型的实现,包括数据模型设计、权限控制原理,以及在实际项目中的应用,如角色和用户管理、权限拦截等。通过对Spring Security和Apache Shiro的比较,阐述了自定义权限模型的优势。并分享了动态菜单和页面功能按钮的权限控制策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 权限管理功能 企业实现

第一种 使用开源权限控制框架
第二种 自定义权限管理模块

1.1. 开源权限控制技术

Spring Security是一个能够为基于Spring的企业应用系统提供描述性安全访问控制解决方案的安全框架。
它提供了一组可以在Spring应用上下文中配 置的Bean,充分利用了Spring IoC(依赖注入,也称控制反转)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的 工作。

Apache shiro 开源免费的权限控制框架, Shiro 是一个用 Java 语言实现的框架,通过一个简单易用的 API 提供身份验证和授权。
使用 Shiro,您就能够为您的应用程序提供安全性而又无需从头编写所有代码。(与任何使用技术无关 , 用于任何JavaSE 和 JavaEE 项目中 )

1.2. 自定义权限控制模型

优点: 更加灵活,简化便于使用
目标: 自定义通用权限控制模型,可以用于任何的项目

1.2.1. 权限模型数据模型

问题: 数据表设计 ?
四个概念:
系统功能 (权限管理目的,就是为了对系统的功能访问进行控制 )
功能: 菜单项的功能 和 页面内部按钮功能

这里写图片描述

菜单功能

这里写图片描述

页面按钮功能
权限 ,具有访问某个功能的权限 ,权限和功能关系可以是 一对一 或者 多对多
角色 ,角色是权限的集合 ,为了方便用户授权 ,有了角色后,只需要将角色授予用户
用户 ,登陆系统的用户,具有角色,拥有系统功能权限

功能、权限、角色、用户 四者之间 都可以是多对多关系 —- 完整复杂权限模型有七张表 !

1.2.2. 今天系统的权限模型

这里写图片描述

用户表 user
功能权限表 auth_function
角色表 auth_role
角色-权限关系表 role_function

1.2.3. 自定义权限模型的权限控制原理

使用自定义注解+反射技术,基于代理 对业务方法访问 实现细粒度的权限控制
提供权限管理系统数据:

这里写图片描述

权限控制原理分析: 基于 自定义注解+ 代理+ 反射 实现方法级别权限控制

这里写图片描述

2. 设计权限管理数据表

auth_function 表 (id , name, description )
auth_role 表 (id , name , description )
user 表 添加 role_id 外键

问题: 实现动态菜单功能 ?
根据当前用户权限查询菜单项,生成菜单
需要将菜单一些数据 保存 auth_function 表

设计 Function 类
/**
 * 系统功能权限
 * 
 * @author seawind
 * 
 */
public class Function {
    private String id; // uuid
    private String name; // 功能名称
    private String description; // 功能描述

    // 菜单项功能
    private String generateMenu; // 是否生成菜单
    private int zindex; // 菜单项优先级
    private String page; // 点击菜单 跳转页面

    private Function parentFunction;// 父功能点
    private Set<Function> childrenFunctions = new HashSet<Function>(); // 子功能点

    // 一个Function 属于很多 Role
    private Set<Role> roles = new HashSet<Role>();
}

配置Function.hbm.xml

<class name="cn.itcast.bos.domain.auth.Function" table="auth_function">
    <id name="id">
        <generator class="uuid"></generator>
    </id>
    <!-- 下面列要按照顺序  -->
    <property name="name" unique="true"></property>
    <property name="description"></property>
    <property name="page"></property>
    <property name="generateMenu"></property>
    <property name="zindex"></property>

    <!-- 表关联 -->
    <many-to-one name="parentFunction" class="cn.itcast.bos.domain.auth.Function" column="pid"></many-to-one>
    <set name="childrenFunctions">
        <key column="pid"></key>
        <one-to-many class="cn.itcast.bos.domain.auth.Function"/>
    </set>

    <set name="roles" table="role_function">
        <key column="function_id"></key>
        <many-to-many class="cn.itcast.bos.domain.auth.Role" column="role_id"></many-to-many>
    </set>
</class>

设计 Role 类

/**
 * 系统角色
 * 
 * @author seawind
 * 
 */
public class Role {
    private String id; // uuid
    private String name; // 角色名称
    private String description; // 角色描述

    // 关联Function
    private Set<Function> functions = new HashSet<Function>();

    // 关联user
    private Set<User> users = new HashSet<User>();
}

编写Role.hbm.xml

<class name="cn.itcast.bos.domain.auth.Role" table="auth_role">
    <id name="id">
        <generator class="uuid"></generator>
    </id>
    <property name="name" unique="true"></property>
    <property name="description"></property>

    <!-- 关联 -->
    <set name="functions" table="role_function">
        <key column="role_id"></key>
        <many-to-many class="cn.itcast.bos.domain.auth.Function" column="function_id"></many-to-many>
    </set>
    <set name="users">
        <key column="role_id"></key>
        <one-to-many class="cn.itcast.bos.domain.user.User"/>
    </set>
</class>

在User类 添加Role 关联

private Role role;
public Role getRole() {
    return role;
}
public void setRole(Role role) {
    this.role = role;
}

编写 User.hbm.xml 为 User关联Role

<!-- 关联角色 -->
<many-to-one name="role" class="cn.itcast.bos.domain.auth.Role" column="role_id"></many-to-one>

3. 功能权限管理

3.1. 导入初始化数据

将auth_function.sql 初始化数据导入 auth_function表
初始化数据参照 menu.json 菜单文件制作 !

3.2. 功能权限添加

点击功能权限管理— 添加权限 — /WEB-INF/pages/admin/function_add.jsp
在添加权限页面中,获取父功能点列表,从列表中选择父功能点

<input name="parentFunction.id" class="easyui-combobox"
data-options="valueField:'id',textField:'info',
url:''${pageContext.request.contextPath}/function_ajaxlist.action"/>

编写 服务器代码 FunctionAction 查询所有列表显示

public class FunctionAction extends BaseAction implements ModelDriven<Function> {
}
public interface FunctionService {
}
public class FunctionServiceImpl extends BaseService implements FunctionService {
}

将 DAO 注入 BaseService
将Service 注入BaseAction

<result name="ajaxlistSUCCESS" type="json">
    <param name="root">functions</param>
    <param name="includeProperties">
        \[\d+\]\.id,
        \[\d+\]\.info 
    </param>
</result>

在Function 添加 getInfo public String getInfo() {
return name + “(” + id + “)”;
}单提交

// 点击保存
$('#save').click(function(){
    // 先判断form 是否通过校验,如果通过提交表单
    if($("#functionForm").form('validate')){
        $('#functionForm').submit();
    }else{
        $.messager.alert('警告','表单存在非法数据项!','warning');
    }
});

提交添加表单,在FunctionAction 添加save方法 ,保存权限信息

@Override
public void saveFunction(Function function) {
    // 防止 "" 的id外键关联
    if (function.getParentFunction() != null && 
        function.getParentFunction().getId() != null && 
        function.getParentFunction().getId().trim().length() == 0) {
        function.setParentFunction(null);
    }
    funtionDAO.save(function);
}

3.3. 权限列表查询

为function.jsp的datagrid 添加url ,使用不分页的列表查询,服务器返回list

url : '${pageContext.request.contextPath}/function_list.action'

在服务器FunctionAction 添加 list 方法

<!-- 权限所有数据查询 -->
<result name="listSUCCESS" type="json">
    <param name="root">functions</param>
    <param name="includeProperties">
        \[\d+\]\.id,
        \[\d+\]\.name,
        \[\d+\]\.description,
        \[\d+\]\.page,
        \[\d+\]\.generateMenu,
        \[\d+\]\.zindex
    </param>
</result>

4. 角色管理(关联权限)

4.1. 添加角色(为角色授权)

4.1.1. 授权树的编写

/WEB-INF/pages/admin/role_add.jsp 添加角色页面
制作ztree 勾选树,主要配置

var setting = {
    check : {
        enable : true 
    }
}

第一步: 在显示树位置 <ul class=”ztree” id=”functionTree” >
第二步: setting
第三步: zNodes 节点数据
第四步: 初始化树 $.fn.zTree.init($("#functionTree"), setting, zNodes);

显示授权树,根据auth_function 表查询,返回json数据

url : '${pageContext.request.contextPath}/function_treedata.action' 

修改setting

var setting = {
    data : {
        key : {
            title : "t"
        },
        simpleData : {
            enable : true,
            pIdKey: "parentId",
        }
    },
    check : {
        enable : true,
    }
};

服务器返回 ztree节点数据,应该包含 id、name、parentId !
在FunctionAction 添加treedata的方法

// 查询树,进行排序
DetachedCriteria detachedCriteria = DetachedCriteria.forClass(Function.class);
// 按照zindex 升序
detachedCriteria.addOrder(Order.asc("zindex"));

配置返回结果:

<!-- 授权树 -->
<result name="treedataSUCCESS" type="json">
    <param name="root">functions</param>
    <param name="includeProperties">
        \[\d+\]\.id,
        \[\d+\]\.name,
        \[\d+\]\.parentId
    </param>
</result>

在Function 实体类 添加getParentId 方法

public String getParentId() {
    if (parentFunction == null) {
        // 这是根节点
        return "0";
    } else {
        return parentFunction.getId();
    }
}
4.1.2. 提交添加角色表单
// 点击保存
$('#save').click(function(){
    if($("#roleForm").form('validate')){
        // 获得勾选ztree节点 
        var treeObj = $.fn.zTree.getZTreeObj("functionTree");
        var nodes = treeObj.getCheckedNodes(true);
        // 将多个勾选id 转换为字符串,用, 分隔
        var ids = [];
        for(var i=0;i<nodes.length; i++){
            ids.push(nodes[i].id);// 将id 加入数组
        }
        // 放入form 隐藏域
        $('#functionIds').val(ids.join(","));

        $("#roleForm").submit();
    }else{
        $.messager.alert('警告','表单存在非法数据项','warning');
    }
});

编写服务器 RoleAction

public class RoleAction extends BaseAction implements ModelDriven<Role> {}
public interface RoleService {
}
public class RoleServiceImpl extends BaseService implements RoleService {
}

将DAO 注入 BaseService
将Service 注入BaseAction

添加角色业务代码

@Override
public void saveRole(Role role, String functionIds) {
    // 将role信息保存角色表
    roleDAO.save(role); // 持久态
    // 建立 role 和 function联系,向role_function 中间表插入数据
    if (functionIds != null) {
        String[] ids = functionIds.split(",");
        for (String id : ids) {
            Function function = funtionDAO.findById(id); // 功能权限
            role.getFunctions().add(function); // 多对多关联,向中间表插入数据
        }
    }
}

配置跳转

<!-- 角色管理 -->
<action name="role_*" class="roleAction" method="{1}">
    <result name="saveSUCCESS">/WEB-INF/pages/admin/role.jsp</result>
</action>

4.2. 角色列表查询

/WEB-INF/pages/admin/role.jsp 修改 datagrid 的url

url : '${pageContext.request.contextPath}/role_list.action', 

在服务器 RoleAction 添加 list 方法

<!-- 角色列表-->
<result name="listSUCCESS" type="json">
    <param name="root">roles</param>
    <param name="includeProperties">
        \[\d+\]\.id,
        \[\d+\]\.name,
        \[\d+\]\.description
    </param>
</result>

5. 用户管理(关联角色)

已经在第二天, 创建 UserAction 完成修改密码

5.1. 添加用户

/**
 * 用户 User entity. @author MyEclipse Persistence Tools
 */
public class User implements java.io.Serializable {
    // Fields
    private String id; // 编号 修改生成策略 uuid
    private String username; // 用户名
    private String password; // 密码
    private Double salary; // 工资
    private Date birthday; // 生日
    private String gender; // 性别
    private String station; // 单位
    private String telephone; // 电话
    private String remark; // 电话
}

修改 /WEB-INF/pages/admin/userinfo.jsp

<tr>
    <td>为用户授予角色</td>
    <td colspan="3">
        <input name="role.id" class="easyui-combobox"       data-options="valueField:'id',textField:'name',url:'${pageContext.request.contextPath }/role_list.action'" />
    </td>
</tr>

直接使用在RoleAction 的 list 方法
点击 保存按钮提交form表单

$('#save').click(function(){
    // 先校验form
    if($('#userForm').form('validate')){
        $('#userForm').submit();
    }else{
        $.messager.alert('警告','表单存在非法数据项','warning');
    }
});

在 UserAction 添加 save 方法

public void saveUser(User user) {
    // 防止 Role的id 为空串
    if (user.getRole() != null && user.getRole().getId() != null && user.getRole().getId().trim().length() == 0) {
        user.setRole(null);
    }
    // 对密码 进行 md5 加密
    user.setPassword(MD5Utils.md5(user.getPassword()));
    userDAO.save(user);
}

配置结果跳转

<!-- 添加用户 -->
<result name="saveSUCCESS">
/WEB-INF/pages/admin/userlist.jsp
</result>

如果添加的中文账户,在登陆页面无法登陆 !

这里写图片描述

无法输入中文

5.2. 用户列表查询

/WEB-INF/pages/admin/userlist.jsp
修改 datagrid 的 url

url : "${pageContext.request.contextPath}/user_list.action", 

在服务器 UserAction 提供 list 查询方法

<!-- 查询所有用户列表 -->
<result name="listSUCCESS" type="json">
    <param name="root">users</param>
    <param name="includeProperties">
        \[\d+\]\.id,
        \[\d+\]\.username,
        \[\d+\]\.telephone,
        \[\d+\]\.gender,
        \[\d+\]\.station,
        \[\d+\]\.salary,
        \[\d+\]\.birthday,
        \[\d+\]\.role\.name
    </param>
</result>

修改 userlist.jsp 定义 datagrid 的 columns
添加 一列 显示角色

{
    field : 'role',
    title :'角色',
    width : 400 ,
    align : 'left',
    rowspan : 2,
    formatter : function(value,rowData,rowIndex){
        if(value == null){
            return "";
        }else{
            return value.name;
        }
    }
}

5.3. 为用户授予(修改)角色

1、 为用户列表 每行数据,添加右键效果
原始js 事件 : oncontextmenu 事件 (鼠标右键事件)
使用 datagrid 数据行 右键事件

这里写图片描述

阻止默认右键菜单显示 :

e.preventDefault(); // 阻止默认事件

2、 自定义右键菜单
使用 easyui 提供 menu 控件 制作菜单

<!-- 自定义菜单 -->
<div id="mm" class="easyui-menu" style="width:120px;"> 
    <div onclick="alert('点击我了!');">菜单一</div>
    <div>菜单二</div>
</div>

点击右键时,在鼠标位置弹出菜单

// 弹出自定义菜单
$('#mm').menu('show', { 
    left: e.pageX, 
    top: e.pageY
});

复制 “授予角色.txt”window代码 进行授权
业务代码

@Override
public void grantRole(User user) {
    User exist = userDAO.findById(user.getId());
    exist.setRole(user.getRole()); // 关联角色 自动更新
}

6. 使用代理进行权限拦截

基于 自定义注解 + 反射 + 代理 实现方法级别细粒度权限控制
URL粗粒度权限控制, 比如admin登陆,访问 /admin/* 开始页面

6.1. 自定义注解

/**
 * 自定义注解
 * 
 * @author seawind
 * 
 */
@Retention(RetentionPolicy.RUNTIME)
// 运行时使用
@Target(value = { ElementType.METHOD })
// 修改方法
@Inherited
// 使用注解应用具有继承性
public @interface Privilege {
    String value(); // 这个属性代表访问业务方法 需要权限
}

在需要进行权限控制业务方法上,使用该注解

// 业务方法 --- 添加用户
@Privilege("添加用户")
public String save() {
    // 调用业务层 保存用户
    userService.saveUser(user);
    return "saveSUCCESS";
}

6.2. 为目标业务方法建立代理

为目标创建对象创建代理 : JDK动态代理、Cglib动态代理、 静态代理
对于Struts2 Action 而言,Interceptor 就是代理

public class PrivilegeInterceptor extends AbstractInterceptor {
    @Override
    public String intercept(ActionInvocation invocation) throws Exception {
        return null;
    }
}

6.3. 在代理中通过反射解析注解信息,控制权限

public class PrivilegeInterceptor extends AbstractInterceptor {
    @Override
    public String intercept(ActionInvocation invocation) throws Exception {
        // 假设 用户已经登陆
        // 1、 判断目标Action业务方法上,是否具有Privilege 注解
        Class c = invocation.getAction().getClass(); // 目标Action的Class对象
        String methodName = invocation.getProxy().getMethod(); // 目标业务方法名称
        Method method = c.getDeclaredMethod(methodName);

        // 判断是否具有注解
        if (method.isAnnotationPresent(Privilege.class)) {
            // 有注解 ,需要权限
            // 2、 获得注解中需要权限
            Privilege privilege = method.getAnnotation(Privilege.class);
            String needPrivilege = privilege.value();

            // 3、判断当前用户是否具有该权限
            User user = (User) ServletActionContext.getRequest().getSession().getAttribute("user");
            if (PrivilegeUtils.checkHasPrivilege(user, needPrivilege)) {
                // 有权限
                return invocation.invoke();
            } else {
                // 没有权限
                return "noprivilege";
            }
        } else {
            // 无注解,不需要权限
            return invocation.invoke();
        }
    }
}

抽取 PrivilegeUtils 工具类

/**
     * 判断 用户是否具有 访问权限
     * 
     * @param user
     * @param needPrivilege
     * @return
     */
    public static boolean checkHasPrivilege(User user, String needPrivilege) {
        // admin 直接放行
        if (user.getUsername().equals("admin")) {
            return true;
        }

        // 正常判断流程
        Role role = user.getRole();
        if (role == null) {
            // 当前登录用户没角色,没权限
            return false;
        } else {
            // 有角色
            Set<Function> functions = role.getFunctions();
            for (Function function : functions) {
                if (function.getName().equals(needPrivilege)) {
                    // 满足权限
                    return true;
                }
            }
            return false;
        }
    }

在 struts.xml 配置 权限拦截器

<!-- 注册拦截器  -->
<interceptors>
    <interceptor name="login" class="cn.itcast.bos.web.interceptor.LoginInterceptor"></interceptor>
    <interceptor name="privilege" class="cn.itcast.bos.web.interceptor.PrivilegeInterceptor"></interceptor>
    <!-- 定义新的拦截器栈  -->
    <interceptor-stack name="loginStack">
        <interceptor-ref name="defaultStack"></interceptor-ref>
        <interceptor-ref name="login"></interceptor-ref>
    </interceptor-stack>
    <interceptor-stack name="privilegeStack">
        <interceptor-ref name="defaultStack"></interceptor-ref>
        <interceptor-ref name="login"></interceptor-ref>
        <interceptor-ref name="privilege"></interceptor-ref>
    </interceptor-stack>
</interceptors>


<!-- 设置默认拦截器栈 -->
<default-interceptor-ref name="privilegeStack"></default-interceptor-ref>

定义全局结果集 noprivilege 页面

<!-- 配置全局结果集 -->
<global-results>
    <result name="login">/login.jsp</result>
    <result name="noprivilege">/noprivilege.jsp</result>
</global-results>

问题: 控制权限 出现 No Session 异常?
原因,参考图四
解决: 在登陆业务方法中 UserServiceImpl 的 login 方法

public User login(User user) {
    List<User> list = userDAO.findByNamedQuery("User.login", user.getUsername(), MD5Utils.md5(user.getPassword()));
    User loginUser = list.isEmpty() ? null : list.get(0);
    if (loginUser.getRole() != null) {
        // 手动对用户管理 权限信息初始化
        Hibernate.initialize(loginUser.getRole().getFunctions());
    }
    return loginUser;
}

7. 在应用权限管理后,控制页面显示

7.1. 动态菜单

根据当前用户具有权限,来显示菜单
修改 /WEB-INF/pages/common/index.jsp

在动态返回 json 菜单数据时, ztree使用 SimpleData ,存在id 和 pId , 如果使用pId 返回会有问题, 配置ztree setting ,将返回pId 定制为 parentId

var setting = {
    data : {
        key : {
            title : "t" // 鼠标在停留在菜单上提示
        },
        simpleData : { // 简单数据 
            enable : true,
            pIdKey: "parentId",
        }
    },
    callback : {
        onClick : onClick
    }
};

将页面 加载menu.json 的url 改为动态地址

url : '${pageContext.request.contextPath}/function_menu.action', 

在FunctionAction 提供 menu 方法,返回当前用户具有权限对应菜单

public String menu() {
    // 复杂查询 使用 QBC
    DetachedCriteria detachedCriteria = DetachedCriteria.forClass(Function.class);

    // 查询当前用户具有权限菜单
    User user = (User) ServletActionContext.getRequest().getSession().getAttribute("user");
    if (!user.getUsername().equals("admin")) {// 如果用户是admin 需要显示所有功能
        // 多表关联,每关联一个表,创建一个别名
        detachedCriteria.createAlias("roles", "r");
        detachedCriteria.createAlias("r.users", "u");
        detachedCriteria.add(Restrictions.eq("u.id", user.getId()));
    }
    // 查询generateMenu 为 1 的功能
    detachedCriteria.add(Restrictions.eq("generateMenu", "1"));
    // 按照zindex 排序
    detachedCriteria.addOrder(Order.asc("zindex"));

    // 调用业务层
    List<Function> functions = functionService.findTreeData(detachedCriteria);
    // 压入值栈
    ActionContext.getContext().put("functions", functions);

    return "menuSUCCESS";
}

配置 struts-auth.xml 返回结果

<!-- 动态菜单 -->
<result name="menuSUCCESS" type="json">
    <param name="root">functions</param>
    <param name="includeProperties">
        \[\d+\]\.id,
        \[\d+\]\.name,
        \[\d+\]\.parentId,
        \[\d+\]\.click,
        \[\d+\]\.page
    </param>
</result>

没有click属性 ,在Function添加 getClick 方法

public boolean getClick() { 
    if (page == null || page.trim().length() == 0) {
        // 没有page属性
        return false;
    } else {
        return true;
    }
}

7.2. 页面内部动态功能按钮

实现细粒度显示控制 ,控制到页面内部每一个按钮的显示
使用 自定义标签来实现

这里写图片描述

步骤一: 自定义标签类
使用简单标签库

/**
 * 自定义标签类
 * 
 * @author seawind
 * 
 */
public class PrivilegeTag extends SimpleTagSupport {

    // 接收属性
    private String value; // 显示内容需要的权限

    public void setValue(String value) {
        this.value = value;
    }

    @Override
    public void doTag() throws JspException, IOException {
        User user = (User) ServletActionContext.getRequest().getSession().getAttribute("user");
        if (PrivilegeUtils.checkHasPrivilege(user, value)) {
            // 具有权限, 显示标签体内容
            this.getJspBody().invoke(null); // 等价
            // this.getJspBody().invoke(this.getJspContext().getOut());
        }
    }
}

步骤二: 配置标签描述文件 tld

这里写图片描述

修改头信息 , 没有提示

<taglib version="2.0" xmlns="http://java.sun.com/xml/ns/j2ee"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd">


<tag>
    <name>privilege</name>
    <tag-class>cn.itcast.bos.web.tag.PrivilegeTag</tag-class>
    <body-content>scriptless</body-content>
    <!-- 定义属性 -->
    <attribute>
        <name>value</name>
        <required>true</required>
        <rtexprvalue>true</rtexprvalue> <!-- 接收EL -->
    </attribute>
</tag>

步骤三: 在jsp导入标签库

<%@taglib  uri="http://www.itcast.cn/tag" prefix="itcast"%>   
<itcast:privilege value="收派标准">
{
    id : 'button-add',
    text : '增加',
    iconCls : 'icon-add',
    handler : doAdd
}, 
</itcast:privilege>

其它

课前资料

这里写图片描述

课后资料

这里写图片描述

权限数据模型分析

这里写图片描述

自定义权限模型原理分析

这里写图片描述

控制权限发生noSession

这里写图片描述

需要功能动态生成菜单分析

这里写图片描述

课程视频内容

这里写图片描述

这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值