使用Springboot+shiro+layui打造漂亮简洁的权限控制系统

Springboot集成shiro

pom.xml

<!-- shiro权限 -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.6.0</version>
</dependency>
<!-- shiro权限支持thymeleaf -->
<dependency>
    <groupId>com.github.theborakompanioni</groupId>
    <artifactId>thymeleaf-extras-shiro</artifactId>
    <version>2.0.0</version>
</dependency>

新建shiro配置

/**
 * 权限配置文件
 * @ClassName: ShiroConfiguration
 * @author YoungJ
 * @date 2020年8月25日
 *
 */
@Configuration
public class ShiroConfig {

	/**
	 * 这是shiro的大管家,相当于mybatis里的SqlSessionFactoryBean
	 * @param securityManager
	 * @return
	 */
	@Bean
	public ShiroFilterFactoryBean shiroFilterFactoryBean(org.apache.shiro.mgt.SecurityManager securityManager) {
		ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
		//登录
		shiroFilterFactoryBean.setLoginUrl("/user/login");
		//首页
		shiroFilterFactoryBean.setSuccessUrl("/");
		//错误页面,认证不通过跳转
		shiroFilterFactoryBean.setUnauthorizedUrl("/error/403");
		//页面权限控制
		shiroFilterFactoryBean.setFilterChainDefinitionMap(ShiroFilterMapFactory.shiroFilterMap());

		//自定义认证过滤器
		/*Map<String, Filter> filterMap = Maps.newHashMap();
		//权限认证过滤器
		filterMap.put("authc", new PermissionFilter());

		shiroFilterFactoryBean.setFilters(filterMap);*/

		shiroFilterFactoryBean.setSecurityManager(securityManager);
		return shiroFilterFactoryBean;
	}
	
	/**
	 * web应用管理配置
	 * @param shiroRealm
	 * @param cacheManager
	 * @param manager
	 * @return
	 */
	@Bean
	public DefaultWebSecurityManager securityManager(Realm shiroRealm,CacheManager cacheManager,RememberMeManager manager) {
		DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
		securityManager.setCacheManager(cacheManager);
		securityManager.setRememberMeManager(manager);//记住Cookie
		securityManager.setRealm(shiroRealm);
		securityManager.setSessionManager(sessionManager());
		return securityManager;
	}
	/**
	 * session过期控制
	 * @return
	 * @author fuce
	 * @Date 2019年11月2日 下午12:49:49
	 */
	@Bean
	public  DefaultWebSessionManager sessionManager() {
		DefaultWebSessionManager defaultWebSessionManager=new DefaultWebSessionManager();
		// 设置session过期时间3600s
		Long timeout=60L*1000*60;//毫秒级别
		defaultWebSessionManager.setGlobalSessionTimeout(timeout);
		return defaultWebSessionManager;
	}
	/**
	 * 加密算法
	 * @return
	 */
	@Bean
	public HashedCredentialsMatcher hashedCredentialsMatcher() {
		HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
		hashedCredentialsMatcher.setHashAlgorithmName("MD5");//采用MD5 进行加密
		hashedCredentialsMatcher.setHashIterations(1);//加密次数
		return hashedCredentialsMatcher;
	}
	
	/**
	 * 记住我的配置
	 * @return
	 */
	@Bean
	public RememberMeManager rememberMeManager() {
		Cookie cookie = new SimpleCookie("rememberMe");
        cookie.setHttpOnly(true);//通过js脚本将无法读取到cookie信息
        cookie.setMaxAge(60 * 60 * 24);//cookie保存一天
		CookieRememberMeManager manager=new CookieRememberMeManager();
		manager.setCookie(cookie);
		return manager;
	}
	/**
	 * 缓存配置
	 * @return
	 */
	@Bean
	public CacheManager cacheManager() {
		MemoryConstrainedCacheManager cacheManager=new MemoryConstrainedCacheManager();//使用内存缓存
		return cacheManager;
	}
	
	/**
	 * 配置realm,用于认证和授权
	 * @param hashedCredentialsMatcher
	 * @return
	 */
	@Bean
	public AuthorizingRealm shiroRealm(HashedCredentialsMatcher hashedCredentialsMatcher) {
		MyShiroRealm shiroRealm = new MyShiroRealm();
		//校验密码用到的算法
		shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher);
		return shiroRealm;
	}
	
	/**
	 * 启用shiro方言,这样能在页面上使用shiro标签
	 * @return
	 */
	@Bean
    public ShiroDialect shiroDialect() {
        return new ShiroDialect();
    }
	
	/**
     * 启用shiro注解
     *加入注解的使用,不加入这个注解不生效
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor(org.apache.shiro.mgt.SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
    

}

ShiroFilterMapFactory.java

package com.szcl.verify.shiro.config;

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * @ClassName: ShiroFilterMapFactory
 * @author YoungJ
 * @date 2020年8月26日
 *
 */
public class ShiroFilterMapFactory {
	public static Map<String, String> shiroFilterMap() {
		// 设置路径映射,注意这里要用LinkedHashMap 保证有序
		LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
		//对所有用户认证
		filterChainDefinitionMap.put("/static/**", "anon");
		filterChainDefinitionMap.put("/admin/login", "anon");
		filterChainDefinitionMap.put("/admin/logout", "logout");
		//放验证码
		filterChainDefinitionMap.put("/captcha/**", "anon");
		// 释放 druid 监控画面
		filterChainDefinitionMap.put("/druid/**", "anon");
		//释放websocket请求
		filterChainDefinitionMap.put("/websocket", "anon");
		//前端
		filterChainDefinitionMap.put("/", "anon");
		filterChainDefinitionMap.put("/index", "anon");
		
		filterChainDefinitionMap.put("/quartz/**", "anon");
		
		//开放APicontroller
		filterChainDefinitionMap.put("/ApiController/**", "anon");
		
		//对所有页面进行认证
		filterChainDefinitionMap.put("/**","authc");
		return filterChainDefinitionMap;
	}
}

MyShiroRealm.java

/**
 * 身份校验核心类
 *
 * @author YoungJ
 * @date 2020年8月25日
 */
@Service
public class MyShiroRealm extends AuthorizingRealm {

    @Autowired
    private UserMapper tsysUserDao;

    @Autowired
    private PermissionMapper permissionDao;

    @Autowired
    private RoleMapper roleDao;


    /**
     * 认证登陆
     */
    @SuppressWarnings("unused")
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(
            AuthenticationToken token) throws AuthenticationException {

        if (token.getPrincipal() == null) {
            return null;
        }
        String username = (String) token.getPrincipal();
        String password = new String((char[]) token.getCredentials());
        // 通过username从数据库中查找 User对象
        // 实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
        User userInfo = tsysUserDao.queryUserName(username);
        if (userInfo == null) {
            return null;
        } else {
            //错误次数控制
            checkLoginErrorTimes(username);
            //单一登录控制
            checkSingleSingOn(userInfo);
            return new SimpleAuthenticationInfo(
                    userInfo,
                    userInfo.getPassword(),
                    getName()
            );
        }

    }

    /**
     * 错误次数控制
     * @param userName
     */
    private void checkLoginErrorTimes(String userName) {

    }

    /**
     * 单一登录控制
     * @param user
     */
    private void checkSingleSingOn(User user) {
        DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager) SecurityUtils.getSecurityManager();
        DefaultWebSessionManager sessionManager = (DefaultWebSessionManager) securityManager.getSessionManager();
        //获取当前已登录的用户session列表
        SessionDAO sessionDAO = sessionManager.getSessionDAO();
        Collection<Session> sessions = sessionDAO.getActiveSessions();
        for (Session session : sessions) {
            //清除该用户以前登录时保存的session

            Object obj = session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
            SimplePrincipalCollection coll = (SimplePrincipalCollection) obj;
            if(coll !=null){
                User userLogin =  (User)coll.getPrimaryPrincipal();
                if(user.getUsername().equals(userLogin.getUsername())){
                    sessionDAO.delete(session);
                }
            }
        }
    }

    /**
     * 授权查询回调函数, 进行鉴权但缓存中无用户的授权信息时调用.
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        if (principals == null) {
            throw new AuthorizationException("principals should not be null");
        }
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        User userinfo = (User) principals.getPrimaryPrincipal();
        Long id = userinfo.getId();
        List<Role> tsysRoles = roleDao.queryUserRole(id);
        for (Role userrole : tsysRoles) {
            Long rolid = userrole.getId();
            authorizationInfo.addRole(userrole.getName());
            List<Permission> permissions = permissionDao.queryRoleId(rolid);
            for (Permission p : permissions) {
                if (StringUtils.isNotEmpty(p.getPerms())) {
                    authorizationInfo.addStringPermission(p.getPerms());
                }

            }
        }
        return authorizationInfo;
    }

    /**
     * 清理缓存权限
     */
    public void clearCachedAuthorizationInfo() {
        this.clearCachedAuthorizationInfo(SecurityUtils.getSubject().getPrincipals());
    }
}

ShiroUtils.java

/**
 * shiro 工具类
 *
 * @author YoungJ
 */
public class ShiroUtils {

    private ShiroUtils() {
    }

    /**
     * 获取shiro subject
     *
     * @return
     * @author YoungJ
     * @Date 2019年11月21日 上午10:00:55
     */
    public static Subject getSubjct() {
        return SecurityUtils.getSubject();
    }

    /**
     * 获取登录session
     *
     * @return
     * @author YoungJ
     * @Date 2019年11月21日 上午10:00:41
     */
    public static Session getSession() {
        return SecurityUtils.getSubject().getSession();
    }

    /**
     * 退出登录
     *
     * @author YoungJ
     * @Date 2019年11月21日 上午10:00:24
     */
    public static void logout() {
        getSubjct().logout();
    }

    /**
     * 获取登录用户model
     *
     * @return
     * @author YoungJ
     * @Date 2019年11月21日 上午10:00:10
     */
    public static User getUser() {
        User user = null;
        Object obj = getSubjct().getPrincipal();
        if (!StringUtils.isEmpty(obj)) {
            user = new User();
            BeanUtils.copyBeanProp(user, obj);
        }
        return user;
    }

    /**
     * set用户
     *
     * @param user
     * @author YoungJ
     * @Date 2019年11月21日 上午9:59:52
     */
    public static void setUser(User user) {
        Subject subject = getSubjct();
        PrincipalCollection principalCollection = subject.getPrincipals();
        String realmName = principalCollection.getRealmNames().iterator().next();
        PrincipalCollection newPrincipalCollection = new SimplePrincipalCollection(user, realmName);
        // 重新加载Principal
        subject.runAs(newPrincipalCollection);
    }

    /**
     * 清除授权信息
     *
     * @author YoungJ
     * @Date 2019年11月21日 上午9:59:37
     */
    public static void clearCachedAuthorizationInfo() {
        RealmSecurityManager rsm = (RealmSecurityManager) SecurityUtils.getSecurityManager();
        MyShiroRealm realm = (MyShiroRealm) rsm.getRealms().iterator().next();
        realm.clearCachedAuthorizationInfo();
    }

    /**
     * 获取登录用户id
     *
     * @return
     * @author YoungJ
     * @Date 2019年11月21日 上午9:58:55
     */
    public static Long getUserId() {
        User tsysUser = getUser();
        if (tsysUser == null || tsysUser.getId() == null) {
            throw new RuntimeException("用户不存在!");
        }
        return tsysUser.getId();
    }

    /**
     * 获取登录用户name
     *
     * @return
     * @author YoungJ
     * @Date 2019年11月21日 上午9:58:48
     */
    public static String getLoginName() {
        User tsysUser = getUser();
        if (tsysUser == null) {
            throw new RuntimeException("用户不存在!");
        }
        return tsysUser.getUsername();
    }

    /**
     * 获取登录用户ip
     *
     * @return
     * @author YoungJ
     * @Date 2019年11月21日 上午9:58:26
     */
    public static String getIp() {
        return getSubjct().getSession().getHost();
    }

    /**
     * 获取登录用户sessionid
     *
     * @return
     * @author YoungJ
     * @Date 2019年11月21日 上午9:58:37
     */
    public static String getSessionId() {
        return String.valueOf(getSubjct().getSession().getId());
    }

    /**
     * 缓存获取当前用户权限
     */
    public static Set<String> getPermissions() {
        Set<String> userPermissions =
                (Set<String>) SecurityUtils.getSubject().getSession().getAttribute(Constants.SHIRO_PERMISSIONKEY);
        return userPermissions;
    }

    /**
     * 设置权限缓存
     */
    public static void setPermissions(Set<String> userPermissions) {
        Session session = SecurityUtils.getSubject().getSession();
        session.setAttribute(Constants.SHIRO_PERMISSIONKEY, userPermissions);
    }

    /**
     * 设置缓存
     */
    public static void setCacheParam(String key, Object value) {
        Session session = SecurityUtils.getSubject().getSession();
        session.setAttribute(key, value);
    }

    /**
     * 获取缓存
     */
    public static Object getCacheParam(String key) {
        Session session = SecurityUtils.getSubject().getSession();
        return session.getAttribute(key);
    }
}

新建表

用户表

CREATE TABLE `t_sys_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `username` varchar(50) CHARACTER SET utf8 DEFAULT NULL COMMENT '用户账号',
  `password` varchar(50) CHARACTER SET utf8 DEFAULT NULL COMMENT '用户密码',
  `nickname` varchar(20) COLLATE utf8_bin DEFAULT NULL COMMENT '昵称',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='用户表';

角色表

CREATE TABLE `t_sys_role` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `name` varchar(20) DEFAULT NULL COMMENT '角色名称',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色表';

用户角色中间表

CREATE TABLE `t_sys_role_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `sys_user_id` bigint(20) DEFAULT NULL COMMENT '用户id',
  `sys_role_id` bigint(20) DEFAULT NULL COMMENT '角色id',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户角色中间表';

权限表

CREATE TABLE `t_sys_permission` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `name` varchar(20) DEFAULT NULL COMMENT '权限名称',
  `descripion` varchar(50) DEFAULT NULL COMMENT '权限描述',
  `url` varchar(255) DEFAULT NULL COMMENT '授权链接',
  `is_blank` int(1) DEFAULT '0' COMMENT '是否跳转 0 不跳转 1跳转',
  `pid` bigint(20) DEFAULT NULL COMMENT '父节点id',
  `perms` varchar(255) DEFAULT NULL COMMENT '权限标识',
  `type` int(1) DEFAULT NULL COMMENT '类型   0:目录   1:菜单   2:按钮',
  `icon` varchar(255) DEFAULT NULL COMMENT '菜单图标',
  `order_num` int(11) DEFAULT NULL COMMENT '排序',
  `visible` int(1) DEFAULT NULL COMMENT '是否可见',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='权限表';

角色权限中间表

CREATE TABLE `t_sys_permission_role` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `role_id` bigint(20) DEFAULT NULL COMMENT '角色id',
  `permission_id` bigint(20) DEFAULT NULL COMMENT '权限id',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色权限中间表';

权限控制

LoginController.java

@Controller
public class LoginController {

    private static Logger log = LoggerFactory.getLogger(LoginController.class);

    @Autowired
    private IPermissionService permissionService;

    @GetMapping("/index")
    public String index(HttpServletRequest request) {
        Long userId = null;
        try {
            userId = ShiroUtils.getUserId();
        } catch (Exception e) {
            return "login";
        }
        if (userId == null) {
            return "login";
        }
        //获取菜单栏
        MenuTree tree= permissionService.getTreePerm(userId);
//        log.info(GsonConvertUtil.toJson(tree));
        request.getSession().setAttribute("tree", tree);
        request.getSession().setAttribute("sessionUserName", ShiroUtils.getUser().getNickname());
        return "home";
    }

    @GetMapping("/user/login")
    public String login(ModelMap model) {
        try {
            if ((null != SecurityUtils.getSubject() && SecurityUtils.getSubject().isAuthenticated()) || SecurityUtils.getSubject().isRemembered()) {
                return "redirect:index";
            } else {
                return "login";
            }
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
        return "login";
    }



    @PostMapping(value = "/user/login")
    @ResponseBody
    public BaseDTO<String> login(@RequestParam("username") String username,
                                 @RequestParam("password") String password,
                                 @RequestParam(value = "isRemeberMe", required = false) Boolean isRemeberMe,
                                 Model model) {
        try {
            if ((null != SecurityUtils.getSubject() && SecurityUtils.getSubject().isAuthenticated()) || SecurityUtils.getSubject().isRemembered()) {
                return DtoConvertUtil.toDTO(null, "登录成功", Constants.CODE_SUCCESS, true);
            } else {
                System.out.println("--进行登录验证..验证开始");
                Subject currentUser = SecurityUtils.getSubject();
                UsernamePasswordToken token =new UsernamePasswordToken(username, password);
                if (isRemeberMe != null && isRemeberMe) {
                    token.setRememberMe(true);
                }
                currentUser.login(token);
                // 设置登录过期时间30分钟
                SecurityUtils.getSubject().getSession().setTimeout(30 * 60 * 1000);
//                model.addAttribute("RollVerification", V2Config.getRollVerification());
//                System.out.println("V2Config.getRollVerification()>>>"+V2Config.getRollVerification());
                return DtoConvertUtil.toDTO(null, "登录成功", Constants.CODE_SUCCESS, true);
            }
        }catch (UnknownAccountException e) {
            log.error("[登录异常] >> 账户不存在 account : {}", username);
            model.addAttribute("msg", "账号不存在");
            return DtoConvertUtil.toDTO(null, "账号不存在", Constants.CODE_PARAMA_ERR, false);
        } catch (IncorrectCredentialsException e) {
            log.error("[登录异常] >> 密码错误 account : {}", username);
            model.addAttribute("msg", "密码错误");
            return DtoConvertUtil.toDTO(null, "密码错误", Constants.CODE_PARAMA_ERR, false);
        } catch (LockedAccountException e) {
            log.error("[登录异常] >> 账户已锁定 account : {}", username);
            model.addAttribute("msg", "账户已锁定");
            return DtoConvertUtil.toDTO(null, "账户已锁定", Constants.CODE_PARAMA_ERR, false);
        } catch (DisabledAccountException e) {
            log.error("[登录异常] >> 账户已停用 account : {}", username);
            model.addAttribute("msg", "账户已停用");
            return DtoConvertUtil.toDTO(null, "账户已停用", Constants.CODE_PARAMA_ERR, false);
        } catch (Exception e) {
            log.error("[登录异常] >> 未知异常 account : {}", username);
            model.addAttribute("msg", "未知异常");
            return DtoConvertUtil.toDTO(null, "未知异常", Constants.CODE_PARAMA_ERR, false);
        }
    }

    @GetMapping("/loginout")
    public String loginOut(HttpServletRequest request, HttpServletResponse response){
        //在这里执行退出系统前需要清空的数据
        ShiroUtils.logout();
        return "redirect:/user/login";
    }
}

controller请求权限判断

/**
 * 修改保存用户
 */
//@Log(title = "修改保存用户", action = "1")
@ApiOperation(value = "修改保存用户", notes = "修改保存用户")
@RequiresPermissions("system:user:edit")
@PostMapping("/user/edit")
@ResponseBody
public BaseDTO<String> editSave(User user, @RequestParam(value = "roles") List<Long> roles) {
    if (roles == null || roles.isEmpty()) {
        return DtoConvertUtil.toDTO(null, "请选择角色", Constants.CODE_NO_PERMISSION, false);
    }
    userService.updateUserRoles(user, roles);
    return DtoConvertUtil.toDTO(null, "保存成功", Constants.CODE_SUCCESS, true);
}

页面权限判断

<html class="x-admin-sm" xmlns:th="http://www.thymeleaf.org"
      xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<shiro:hasPermission name="system:user:edit">
    
</shiro:hasPermission>

展示效果

image-20200901113541717

  • 5
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 12
    评论
很抱歉,由于篇幅和涉及版权等问题,无法提供完整的代码实现。以下是基于springboot+shiro+layui实现qq聊天室功能的代码结构和主要实现思路供参考: 1. 后端代码结构 - controller:控制层,实现用户登录、注册等接口。 - service:服务层,实现用户管理、聊天消息管理等业务逻辑。 - dao:数据访问层,实现对数据库的增删改查操作。 - entity:实体类,包括用户信息、聊天消息等。 - config:配置类,实现Shiro安全框架的配置。 - websocket:WebSocket实现,接收消息并广播给在线用户。 2. 前端代码结构 - index.html:聊天室主页面。 - login.html:用户登录页面。 - register.html:用户注册页面。 - layuiLayui框架相关文件。 - js:前端JS代码,实现聊天室界面和逻辑。 3. 主要实现思路 - 用户登录:前端发送用户名和密码到后台,后台进行密码验证并返回登录结果。 - 用户注册:前端发送用户名和密码到后台,后台将用户信息保存到数据库中。 - 聊天室界面:前端使用Layui实现聊天室界面,包括聊天消息显示和发送消息等功能。 - 实时通讯:使用WebSocket实现前后端实时通讯功能,后端接收消息并将消息广播给所有在线用户,前端接收消息并显示在聊天室界面中。 - 私聊功能:在聊天室界面中添加私聊功能,用户可以选择一个在线用户进行私聊,后端接收私聊消息并发送给对应的用户。 - 数据库存储:使用MySQL作为持久化存储,将用户信息、聊天记录等数据保存到数据库中。 以上是springboot+shiro+layui实现qq聊天室功能的主要实现思路和代码结构,具体实现过程需要根据实际需求进行调整和改进。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

YoungJ5788

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

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

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

打赏作者

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

抵扣说明:

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

余额充值