若依框架(RuoYi-Vue):权限功能模块设计分析
权限功能模块设计分析
- 一、若依Vue系统中的权限分类
- 1.菜单权限
- 2.按钮权限
- 3.接口权限
- 4.数据权限
- 二、各种类型权限实现原理
- 1.菜单权限
- 2.按钮权限
- 3.接口权限
- 4.数据权限
- 三、若依Vue系统SpringSecurity+JWT
- 1.用户登录
- 2.接口权限校验
- 四、实战
- 1.前端页面修改
- 2.创建按钮权限
- 3.使用代码生成代码
- 4.准备后端接口
- 5.修改前端页面请求地址
- 五、测试
- 1.超级管理员测试
- 2.普通用户测试
若依Vue系统中的权限管理部分的功能都集中在了系统管理菜单模块中,如下图所示。其中权限部分主要涉及到了用户管理、角色管理、菜单管理、部门管理这四个部分。
一、若依Vue系统中的权限分类
根据观察,若依Vue系统中的权限分为以下几类
- 菜单权限:用户登录系统之后能看到哪些菜单
- 按钮权限:用户在一个页面上能看到哪些按钮,比如新增、删除等按钮
- 接口权限:用户带着认证信息请求后端接口,是否有权限访问,该接口和前端页面上的按钮一一对应
- 数据权限:用户有权限访问后端某个接口,但是不同的用户相同的接口相同的入参,根据权限大小不同,返回的结果应当不一样——权限大的能够看到的数据更多。
1.菜单权限
这个比较好理解,拥有不同权限的用户登录系统之后看到的菜单是不一样的,从新建菜单到给一个用户分配菜单权限,上一篇文章已经讲过,不赘述。
用户登录之后会请求后端的com.ruoyi.web.controller.system.SysLoginController#getRouters
接口获取登录用户的菜单数据:
select distinct m.menu_id, m.parent_id, m.menu_name, m.path, m.component, 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_timefrom sys_menu mleft join sys_role_menu rm on m.menu_id = rm.menu_idleft join sys_user_role ur on rm.role_id = ur.role_idleft join sys_role ro on ur.role_id = ro.role_idleft join sys_user u on ur.user_id = u.user_idwhere u.user_id = #{userId} and m.menu_type in ('M', 'C') and m.status = 0 AND ro.status = 0order by m.parent_id, m.order_num
菜单类型(M目录 C菜单 F按钮);菜单状态(0显示 1隐藏)
这是典型的用户-角色-菜单模型。
前端会根据该接口返回的数据渲染出不同的菜单。
2.按钮权限
新增按钮权限和新增菜单差不多,下图是我在新闻列表页面上新增了一个按钮叫做新闻新增
,该按钮的权限分配和菜单的权限分配方法是一样的。
3.接口权限
每一个按钮基本上都会对应着一个后端的接口,前端会根据权限标志显示或者隐藏按钮,但是如果用户不点击按钮,直接通过http请求工具请求后端咋办?所以接口权限也是要有的,该权限和按钮上权限完全一致。
若依系统使用了SpringSecurity框架,接口权限都是基于注解@PreAuthorize
实现的,比如,用户管理页面中的修改用户按钮对应的后端接口长这个样子
@PreAuthorize("@ss.hasPermi('system:user:edit')")@Log(title = "用户管理", businessType = BusinessType.UPDATE)@PutMappingpublic AjaxResult edit(@Validated @RequestBody SysUser user){...}
和其对应的前端按钮权限标志一样
如果没有权限访问接口,则会返回类似如下信息:
{"msg": "请求访问:/system/user/list,认证失败,无法访问系统资源",
"code": 401
}
4.数据权限
用户有权限访问后端某个接口,但是不同的用户相同的接口相同的入参,根据权限大小不同,返回的结果应当不一样——权限大的能够看到的数据更多。
体现在若依Vue系统中,举个例子,现在若以系统中有两个用户,一个是超级管理员admin,一个是普通用户kdyzm
它们两者均有用户管理、菜单管理、角色管理权限,所以它们能够看到相应的菜单并作出相应的操作,比如删除、新增、修改等。这里有个问题,kdyzm应当只能看到一部分数据,而超级管理员应当能够看到所有数据,在若依系统中,是通过对角色数据权限
修改进行控制的。
所以,相同的权限,超级管理员能够看到的用户数量和普通用户kdyzm能够看到的用户数量是不一样的。
超级管理员看到的用户管理页面:
普通用户kdyzm看到的用户管理页面:
二、各种类型权限实现原理
1.菜单权限
菜单权限很简单,实际上就是简单的用户-角色-菜单模型,那么菜单是什么时候加载的呢?ruoyi-ui\src\permission.js
,加载的逻辑在这个文件中。
permission.js
文件中设置了导航守卫,每次路由发生变化的时候就会触发router.beforeEach的回调函数。
router.beforeEach((to, from, next) => {NProgress.start()if (getToken()) {/* has token*/if (to.path === '/login') {next({ path: '/' })NProgress.done()} else {if (store.getters.roles.length === 0) {// 判断当前用户是否已拉取完user_info信息store.dispatch('GetInfo').then(res => {// 拉取user_infoconst roles = res.rolesstore.dispatch('GenerateRoutes', { roles }).then(accessRoutes => {// 根据roles权限生成可访问的路由表router.addRoutes(accessRoutes) // 动态添加可访问路由表next({ ...to, replace: true }) // hack方法 确保addRoutes已完成})}).catch(err => {store.dispatch('LogOut').then(() => {Message.error(err)next({ path: '/' })})})} else {next()}}} else {// 没有tokenif (whiteList.indexOf(to.path) !== -1) {// 在免登录白名单,直接进入next()} else {next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页NProgress.done()}}
})
注意if (store.getters.roles.length === 0) {
这段逻辑,可以看出,如果不刷新当前页面,就算给用户添加了新的菜单权限,用户也看不到新的菜单。
2.按钮权限
按钮权限设置上和菜单权限基本上是一样的,是附着于页面中的细粒度权限。按钮权限体现在如果用户没有相应的权限,则看不到相关的按钮。这个是咋实现的呢?
先看下系统管理下的菜单管理中的修改、新增和删除按钮前端vue代码
<el-table-column label="操作" align="center" class-name="small-padding fixed-width"><template slot-scope="scope"><el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"v-hasPermi="['system:menu:edit']">修改</el-button><el-button size="mini" type="text" icon="el-icon-plus" @click="handleAdd(scope.row)"v-hasPermi="['system:menu:add']">新增</el-button><el-buttonsize="mini"type="text"icon="el-icon-delete"@click="handleDelete(scope.row)"v-hasPermi="['system:menu:remove']">删除</el-button></template></el-table-column>
el-button上有个属性v-hasPermi
,这实际上是vue的自定义指令,属性值就是创建按钮的时候定义的那个权限标志
。其定义在src/directive/permission/index.js
文件
import hasRole from './hasRole'
import hasPermi from './hasPermi'const install = function(Vue) {Vue.directive('hasRole', hasRole)Vue.directive('hasPermi', hasPermi)
}if (window.Vue) {window['hasRole'] = hasRolewindow['hasPermi'] = hasPermiVue.use(install); // eslint-disable-line
}export default install
其具体实现逻辑就在同目录的hasPermi.js
文件中
import store from '@/store'export default {inserted(el, binding, vnode) {const { value } = bindingconst all_permission = "*:*:*";const permissions = store.getters && store.getters.permissionsif (value && value instanceof Array && value.length > 0) {const permissionFlag = valueconst hasPermissions = permissions.some(permission => {return all_permission === permission || permissionFlag.includes(permission)})if (!hasPermissions) {el.parentNode && el.parentNode.removeChild(el)}} else {throw new Error(`请设置操作权限标签值`)}}
}
注意代码el.parentNode && el.parentNode.removeChild(el)
,可以看到,如果没有按钮权限,则会将按钮本身从dom中移除。
3.接口权限
接口权限和前端的按钮权限一一对应。为的是防止用户绕过按钮直接请求后端接口获取数据。在若依Vue系统中,是使用SpringSecurity的注解@PreAuthorize
实现的。
虽然只是一个注解,但是它是SpringSecurity+JWT集成的结晶~这个之后再细谈。
4.数据权限
数据权限实现的关键在于com.ruoyi.framework.aspectj.DataScopeAspect
类。该类是一个切面类,凡是加上com.ruoyi.common.annotation.DataScope
注解的方法,在执行的时候都会被它拦截。
该切面定义了五种权限范围
name | code | desc |
---|---|---|
DATA_SCOPE_ALL | 1 | 全部数据权限 |
DATA_SCOPE_CUSTOM | 2 | 自定数据权限 |
DATA_SCOPE_DEPT | 3 | 部门数据权限 |
DATA_SCOPE_DEPT_AND_CHILD | 4 | 部门及以下数据权限 |
DATA_SCOPE_SELF | 5 | 仅本人数据权限 |
该切面的核心逻辑是“拼SQL”,方法执行之前,会给参数的一个params属性添加一个dataScope键值对,key为"dataScope",值为AND (" + sqlString.substring(4) + ")"
样式的一段SQL,这段SQL会根据当前用户所在的部门以及当前用户角色的权限范围发生变化。
StringBuilder sqlString = new StringBuilder();for (SysRole role : user.getRoles()){String dataScope = role.getDataScope();if (DATA_SCOPE_ALL.equals(dataScope)){sqlString = new StringBuilder();break;}else if (DATA_SCOPE_CUSTOM.equals(dataScope)){sqlString.append(StringUtils.format(" OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", deptAlias,role.getRoleId()));}else if (DATA_SCOPE_DEPT.equals(dataScope)){sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId()));}else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope)){sqlString.append(StringUtils.format(" OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )",deptAlias, user.getDeptId(), user.getDeptId()));}else if (DATA_SCOPE_SELF.equals(dataScope)){if (StringUtils.isNotBlank(userAlias)){sqlString.append(StringUtils.format(" OR {}.user_id = {} ", userAlias, user.getUserId()));}else{// 数据权限为仅本人且没有userAlias别名不查询任何数据sqlString.append(" OR 1=0 ");}}}
简单来说,这段代码的逻辑就是用户所在的部门权限越高,数据权限范围越大,查出来的结果集将会越大。
DataScope注解分别加到了部门列表查询、角色列表查询、用户列表查询的接口上,很明显,这几个接口需要根据不同的人查出不同的结果。
以用户列表查询为例,执行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.password, 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 uleft join sys_dept d on u.dept_id = d.dept_idwhere u.del_flag = '0'<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') >= 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') <= 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>
其中,有这么一段代码
<!-- 数据范围过滤 -->
${params.dataScope}
实际上DataScopeAspect
切面就只干了填充params的dataScope属性这么一件事情。
三、若依Vue系统SpringSecurity+JWT
若依Vue系统中从用户登录到后端接口权限校验,都是基于SpringSecurity+JWT实现的,其中,SpringSecurity是核心,jwt只是为了保证token合法性的一种手段(签名防止篡改)。spring security集成的相关代码在ruoyi-framework
模块的com.ruoyi.framework.security
包以及com.ruoyi.framework.config.SecurityConfig
类中。
SecurityConfig
是核心配置类,所有的配置均在该类中。
1.用户登录
用户登录的逻辑在方法com.ruoyi.web.controller.system.SysLoginController#login
中,一个典型的登录请求体如下所示
{"username": "admin","password": "admin123","code": "0","uuid": "a9fdbcbcb28748b796b5b77ad71bbb97"
}
username和password分别是用户名和密码,code为验证码,uuid为验证码的唯一标识。登录成功之后会返回前端一个jwt令牌
{"msg": "操作成功","code": 200,"token": "eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjIzZjRhNjJjLTY5NzMtNDcxZS04ZmU4LWJmYWQ4YzllNWFkMiJ9.9d3iIaNq62CkjTXlxFOQgdDMOAZiu5tAsEn0cEuV23opT6PAqu_CiaN7kQY8_XhlQrHX5RgZ2bH7LpsiKLLcSw"
}
在登录方法中,做了以下几件事情
- 根据uuid获取redis中的验证码并对请求的验证码做验证
- 如果验证码没问题,则对用户名和密码进行校验
- 如果用户名和密码校验成功,则使用token作为key将用户信息保存到redis
- 使用jwt对token签名并返回前端
在整个过程中,会抛出一些自定义异常,比如
throw new CaptchaExpireException();
throw new CaptchaException();
throw new UserPasswordNotMatchException();
throw new CustomException(e.getMessage());
这些异常最终会被全局异常处理器处理掉:com.ruoyi.framework.web.exception.GlobalExceptionHandler
2.接口权限校验
前端请求完成登录接口之后会将token存储到cookie,key为Admin-Token,value是jwt令牌。登录逻辑:user.actions.Login
。
之后,每次请求后端接口的时候都会带上Authentication Header
这实际上是通过axios的请求拦截器实现的:详情可见src/utils/request.js
文件
带着Authentication Header的请求打到后端的时候会经过过滤器com.ruoyi.framework.security.filter.JwtAuthenticationTokenFilter
,该过滤器做了以下几件事情
- 从请求头中取出jwt令牌,并对其进行jwt验签,验签若是成功,则取出原始token
- 根据token从redis中取出用户数据
- 将用户信息封装成
UsernamePasswordAuthenticationToken
对象,并将该对象填充到Spring Security上下文中
填充到SpringSecurity上下文才能让Controller接口上的@PreAuthorize
注解发挥作用(存疑,这里若依作者并非使用原生的SpringSecurity提供的spel表达式,也没有用authorities,而是使用了PermissionService类)。
接着,Controller接口正式执行之前会进入com.ruoyi.framework.web.service.PermissionService#hasPermi
方法判定权限,这里重新从redis中取出用户数据并进行权限校验,权限校验失败则不再执行接口中逻辑(存疑,这里并没有使用SpringSecurity上下文中的用户数据,那么JwtAuthenticationTokenFilter
中的用户信息填充上下文中的代码是干啥用的)。
四、实战
上一篇文章讲解了如何创建一个菜单并创建页面,但是是个空页面
这篇文章将会讲解如何实现增删查改功能。
一切开始之前,新建表news,建表SQL如下
CREATE TABLE `news` (`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',`title` varchar(128) NOT NULL COMMENT '新闻标题',`brief` varchar(256) DEFAULT NULL COMMENT '新闻概述',`content` text COMMENT '新闻正文',`create_time` datetime DEFAULT NULL,`create_by` varchar(64) DEFAULT NULL,`update_time` datetime DEFAULT NULL,`update_by` varchar(64) DEFAULT NULL,`delete_flag` tinyint(1) DEFAULT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
1.前端页面修改
可以仿照角色管理的页面写,直接将角色管理页面的代码直接拷贝到news文件中,效果如下
没错,新闻列表的标题,角色管理的页面。。。
之后对页面中元素进行修改,使其和上面创建的表结构一致,修改后的页面样子
这是预期中的样子,但是内容还是角色管理页面的内容。
2.创建按钮权限
上一步已经完成了页面外观的改造,接下来需要修改页面内容了,首先需要把按钮权限给加上
按照这个样子添加按钮权限,之后把权限标志分配到前端页面中
3.使用代码生成代码
在系统工具-代码生成页面中生成news表对应的相关实体类、mapper、xml对象等,可以极大的简化开发过程。
4.准备后端接口
将上一步代码生成器生成的NewsController
拿过来改一改就行,修改后的代码如下所示:
package com.ruoyi.web.controller.business;import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.system.domain.News;
import com.ruoyi.system.mapper.NewsMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;import java.util.Date;
import java.util.List;/*** @author kdyzm*/
@RestController
@RequestMapping("/business/news")
public class NewsController extends BaseController {@Autowiredprivate NewsMapper newsMapper;/*** 获取新闻列表*/@PreAuthorize("@ss.hasPermi('business:news:list')")@GetMapping("/list")public TableDataInfo list(News post) {startPage();List<News> list = newsMapper.selectNewsList(post);return getDataTable(list);}@Log(title = "新闻管理", businessType = BusinessType.EXPORT)@PreAuthorize("@ss.hasPermi('business:news:export')")@GetMapping("/export")public AjaxResult export(News post) {List<News> list = newsMapper.selectNewsList(post);ExcelUtil<News> util = new ExcelUtil<>(News.class);return util.exportExcel(list, "新闻数据");}/*** 根据新闻编号获取详细信息*/@PreAuthorize("@ss.hasPermi('business:news:query')")@GetMapping(value = "/{postId}")public AjaxResult getInfo(@PathVariable Long postId) {return AjaxResult.success(newsMapper.selectNewsById(postId));}/*** 新增新闻*/@PreAuthorize("@ss.hasPermi('business:news:add')")@Log(title = "新闻管理", businessType = BusinessType.INSERT)@PostMappingpublic AjaxResult add(@Validated @RequestBody News post) {post.setCreateBy(SecurityUtils.getUsername());post.setCreateTime(new Date());return toAjax(newsMapper.insertNews(post));}/*** 修改新闻*/@PreAuthorize("@ss.hasPermi('business:news:update')")@Log(title = "新闻管理", businessType = BusinessType.UPDATE)@PutMappingpublic AjaxResult edit(@Validated @RequestBody News post) {post.setUpdateBy(SecurityUtils.getUsername());return toAjax(newsMapper.updateNews(post));}/*** 删除新闻*/@PreAuthorize("@ss.hasPermi('business:news:delete')")@Log(title = "新闻管理", businessType = BusinessType.DELETE)@DeleteMapping("/{postIds}")public AjaxResult remove(@PathVariable Long[] postIds) {return toAjax(newsMapper.deleteNewsByIds(postIds));}
}
5.修改前端页面请求地址
将生成的代码中的news.js文件放到api目录,并修改其中的接口路径与后端接口地址一一对应。
import request from '@/utils/request'// 查询角色列表
export function listNews(query) {return request({url: '/business/news/list',method: 'get',params: query})
}// 查询角色详细
export function getNews(roleId) {return request({url: '/business/news/' + roleId,method: 'get'})
}// 新增角色
export function addNews(data) {return request({url: '/business/news',method: 'post',data: data})
}// 修改角色
export function updateNews(data) {return request({url: '/business/news',method: 'put',data: data})
}// 删除角色
export function delNews(roleId) {return request({url: '/business/news/' + roleId,method: 'delete'})
}// 导出角色
export function exportNews(query) {return request({url: '/business/news/export',method: 'get',params: query})
}
然后修改页面中的请求地址使用这里的地址。
五、测试
1.超级管理员测试
超级管理员拥有最大权限,所有权限校验都会跳过对超级管理员的权限校验。这里先使用超级管理员进行测试可以规避权限问题,大体上先看看能否跑的通。
下面是演示超级管理员的CRUD操作。
2.普通用户测试
这里用用户kdyzm进行测试,在测试之前,先看下kdyzm的角色
可以看到该用户是运营角色,那么修改角色权限,只给查询、修改、新增权限,不给导出和删除权限,如下所示
这时候切换登录用户为kdyzm,看看新闻列表页面
可以看到,kdyzm在新闻列表页面中,看不到导出导出按钮和删除按钮,符合预期设想。
请关注原博主:一枝梅的技术收录
若依框架(RuoYi-Vue):权限功能模块设计分析相关推荐
- ruoyi vue 其功能介绍
1.介绍 RuoYi-Vue 是一个 Java EE 企业级快速开发平台,基于经典技术组合(Spring Boot.Spring Security.MyBatis.Jwt.Vue),内置模块如:部门管 ...
- vue聊天功能模块(九)艾特功能实现
预览 艾特开始 思路:输入ctrl +@ ,弹出群成员列表弹框,点击成员时,把成员名称添加到输入框,同时隐藏弹框 1.点击弹框外任意位置,把弹框关闭即可 2.点击删除键时,把@和名称删掉 3.要实现艾 ...
- Ruoyi数据权限功能实现
若依实现数据实现实例 我们都知道,在一个系统中存在着不同的用户,而不同的用户在一个页面中所看到的数据是完全不一样的 而若依的数据权限是按照不同的部门进行实现的 我们先看普通用户的数据权限 可以看见他只 ...
- vue聊天功能模块(四)聊天消息换行
需求:ctrl+回车换行 回车发送消息 <!--输入区域--> <div class="messagesBox-BigRight-down20-input"> ...
- 数据权限框架:一个实现数据权限与业务模块完全分离,让数据权限变成独立功能模块的数据权限框架
meng框架 介绍 meng框架是一个实现了行数据权限和列数据权限的数据权限框架: meng框架能让数据权限变成一个独立的功能模块,与业务模块完全分离,在已经实现的业务逻辑中添加数据权限不需要对原来的 ...
- RuoYi Vue - 若依框架的 Vue 版本,免费开源、生态强大、专业的 admin 后台管理系统,基于 Vue + Element
有一段时间没有推荐 admin 框架了,若依这款后台框架很强大,提供了 vue 版本,很实用,推荐给大家. 关于若依 RuoYi 中文名称叫若依,名字十分二次元,是一个后台管理系统,后端基于经典技术组 ...
- [系统安全] 四十五.APT系列(10)Metasploit后渗透技术信息收集、权限提权和功能模块详解
您可能之前看到过我写的类似文章,为什么还要重复撰写呢?只是想更好地帮助初学者了解病毒逆向分析和系统安全,更加成体系且不破坏之前的系列.因此,我重新开设了这个专栏,准备系统整理和深入学习系统安全.逆向分 ...
- vue 功能模块后台可配置_Github14k的Springboot后台管理系统
关注爱编码.挖掘优秀项目. 本期给各位带来的一款优质的后台管理系统:EL-ADMIN 后台管理系统. 简介 该项目由大神elunez一手大打造.它是一个基于 Spring Boot 2.1.0 . S ...
- 【SSH框架/国际物流商综平台】-01-分三期(仓储管理,货运全流程管理,决策分析)- 项目背景 界面原型 用例图 企业组织结构 功能模块图 系统框架 项目表单收集
项目背景 商务综合管理平台是国际物流行业一家专门从事进出口玻璃器皿贸易的公司.业务遍及欧美.随着公司不断发展壮大,旧的信息系统已无法满足公司的快速发展需求,妨碍公司成长,在此背景下,公司领导决定研发& ...
最新文章
- cs架构嵌入bs_CS与BS架构区别、比较、及现状与趋势分析
- 手把手教你如何新建scrapy爬虫框架的第一个项目(下)
- html跳转过度,jq实现锚点跳转过度
- 解读谷歌事件中的失语症现象
- Hybris Commerce的 Master Tenant和Netweaver的System Client
- python列表遍历 空列表_Python list列表执行reversed()后执行pop()返回迭代对象遍历为空问题...
- leetcode 高薪_利用两种不同的方法解LeetCode第1312题:让字符串成为回文串的最少插入次数
- 引入其他字体库 和 字体样式设置
- 【路径规划】全局路径规划算法——Dijkstra算法(含python实现)
- WPS中插入论文公式
- IDEA打包jar 跳过测试
- ps图像放大后变清晰和文字模糊变清晰
- 突破同一账号不能同时在不同电脑登录限制程序软件(多电脑端登录器多开软件)
- 尼尔森数据显示谷歌雅虎今夏流量均增长
- 数学与物理桥梁下的鸟瞰
- 小程序中将lees转成wxss
- Maven手工安装jar包到本地仓库
- 婚宴座位图html5,婚礼酒席座位该怎么安排好?婚宴座位安排图解
- 嵌入式设备的知识产权保护
- 通讯录(初级)程序编写(C语言版)