【Java实战篇】Day11.在线教育网课平台--RBAC

news2024/10/6 8:32:29

文章目录

  • 一、用户授权
    • 1、RBAC
    • 2、资源服务授权流程
    • 3、授权相关的数据模型
    • 4、查询用户权限
    • 5、细粒度授权
  • 二、找回密码与注册
    • 1、找回密码
    • 2、注册
  • 三、需求:学生选课
    • 1、添加选课需求分析
    • 2、数据模型设计
    • 2、查询课程信息接口
    • 3、添加选课接口
    • 4、完善controller

一、用户授权

1、RBAC

RBAC有两种:

  • 基于角色的访问控制(Role-Based Access Control)
  • 基于资源的访问控制(Resource-Based Access Control)

基于角色访问控制

判断当前访问者的身份,符合要求则放行,否则拒绝访问。
在这里插入图片描述
伪代码:

if(主体.hasRole("总经理角色id")){

	查询工资
	
}

此时,如果需要修改角色的权限,就得修改代码:

if(主体.hasRole("总经理角色id") || 主体.hasRole("部门经理角色id")){

	查询工资
	
}

很明显,基于角色的访问控制,扩展性差

基于资源的访问控制

即按资源(权限)进行授权。判断主体是否有某个权限,而不判断主体是谁

在这里插入图片描述
伪代码:

if(主体.hasPermission("查询工资权限标识")){
    查询工资
}

2、资源服务授权流程

  • 在资源服务集成Spring Security:在需要授权的接口处使用@PreAuthorize("hasAuthority('权限标识符')")进行控制
    在这里插入图片描述

  • 此时,用户请求该接口且无此权限,则抛出异常org.springframework.security.access.AccessDeniedException: 不允许访问

  • 在统一异常处理器中捕捉处理一下

//要是直接捕捉AccessDeniedException,则需要在异常处理器所在的包引入Spring Security
//引入以后当前包就会被管控
//因此直接在Exception的捕捉处加一个IF分支来完成

@ResponseBody
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public RestErrorResponse exception(Exception e) {

   log.error("【系统异常】{}",e.getMessage(),e);
   e.printStackTrace();
   
   //!!!!!!!
   if(e.getMessage().equals("不允许访问")){
      return new RestErrorResponse("没有操作此功能的权限");
   }
   //!!!!!!
   return new RestErrorResponse(CommonError.UNKOWN_ERROR.getErrMessage());


}

以上的实现是通过解析令牌拿到当前操作用户的权限,拿这个权限和接口上注解的权限比对。

3、授权相关的数据模型

表结构和字段如下:

在这里插入图片描述

  • xc_user:用户表,存储所有用户的基本信息,姓名、邮箱…
  • xc_role:角色表,根据业务需求。角色的创建是为了方便给用户分配权限。(一个用户有多个角色,一个角色下也可以有多个用户,多对多,需要中间表,中间表存两个表的主键即可)
  • xc_user_role:用户角色表(中间表,用户和角色的关系表)
  • xc_menu:模块表,记录了菜单及菜单下的权限。权限,是对资源的访问控制。一个角色可拥有多个权限,一个权限可被多个角色所拥有。(角色与权限多对多,需要中间表)
  • xc_permission:角色权限表(中间表,角色和权限的关系表)

基于以上五张经典的权限控制表(三个单表+两个关系表),此时,查询用户拥有的权限可以:

# 根据用户id在用户角色关系表查角色id
# 根据角色id在角色权限表中查到权限id
# 根据权限id,查权限表
SELECT * FROM xc_menu WHERE id IN(
    SELECT menu_id FROM xc_permission WHERE role_id IN(
        SELECT role_id FROM xc_user_role WHERE user_id = '49'
    )
)

此时给用户分配(添加或者删除)权限:(创建角色就是为了方便分配权限,加权限就是加角色)

思路一:
- 不变用户角色,给角色本身加权限(update 角色权限关系表)

思路二:
- 给用户加角色,不变角色本身(update 用户角色关系表)

4、查询用户权限

框架判断用户权限通过解析jwt,jwt中的信息来自返回给框架的userDetail对象,对象中的权限在昨天的代码中是写死的:

在这里插入图片描述

定义Mapper接口,根据id查询权限:

public interface XcMenuMapper extends BaseMapper<XcMenu> {
    @Select("SELECT * FROM xc_menu WHERE id IN (SELECT menu_id FROM xc_permission WHERE role_id IN ( SELECT role_id FROM xc_user_role WHERE user_id = #{userId} ))")
    List<XcMenu> selectPermissionByUserId(@Param("userId") String userId);
}

修改UserServiceImpl类的getUserPrincipal方法,查询权限信息:

//查权限,XcUserExt包装成UserDetails对象

public UserDetails getUserPrincipal(XcUserExt user){

	//先拿个密码存下来,后面要置为null,密码要放在userDetail对象中,但user对象做为username去写jwt不能有密码
    String password = user.getPassword();
    //查询用户权限
    List<XcMenu> xcMenus = menuMapper.selectPermissionByUserId(user.getId());
    List<String> permissions = new ArrayList<>();
    if(xcMenus.size() > 0){
		 xcMenus.forEach(menu->{
            permissions.add(menu.getCode());
          });
        
    }else{
       //用户权限,如果不加则报Cannot pass a null GrantedAuthority collection
        permissions.add("p1");
        
    }
    //将用户权限放在XcUserExt中
    user.setPermissions(permissions);

    //为了安全在令牌中不放密码
    user.setPassword(null);
    //将user对象转json
    String userString = JSON.toJSONString(user);
    String[] authorities = permissions.toArray(new String[0]);
    UserDetails userDetails = User.withUsername(userString).password(password).authorities(authorities).build();
    return userDetails;

}

这一段的forEach很妙,创建一个需要的字段的List,通过一个forEach把po对象的List转成了我们只需要的字段的List,有种PO转VO而没建新类的味道:

...
List<String> permissions = new ArrayList<>();
    if(xcMenus.size() > 0){
		 xcMenus.forEach(menu->{
            permissions.add(menu.getCode());
          });
        
    }else{
.....

5、细粒度授权

细粒度授权也叫数据范围授权,即不同的用户所拥有的操作权限相同,但是能够操作的数据范围是不一样的。

一个例子:用户A和用户B都是教学机构,他们都拥有“我的课程”权限,但是两个用户所查询到的数据是不一样的。

细粒度授权涉及到不同的业务逻辑,通常在service层实现,根据不同的用户进行校验,根据不同的参数查询不同的数据或操作不同的数据。

@ApiOperation("课程查询接口")
@PreAuthorize("hasAuthority('xc_teachmanager_course_list')")//拥有课程列表查询的权限方可访问
@PostMapping("/course/list")
public PageResult<CourseBase> list(PageParams pageParams, @RequestBody QueryCourseParamsDto queryCourseParams){
    //取出用户身份Day9定义的工具类,从框架上下文拿当前用户
    XcUser user = SecurityUtil.getUser();
    //机构id
    String companyId = user.getCompanyId();
    return courseBaseInfoService.queryCourseBaseList(Long.parseLong(companyId),pageParams,queryCourseParams);
}

写Service层:

@Override
public PageResult<CourseBase> queryCourseBaseList(Long companyId,PageParams pageParams, QueryCourseParamsDto queryCourseParamsDto) {

 //构建查询条件对象
 LambdaQueryWrapper<CourseBase> queryWrapper = new LambdaQueryWrapper<>();
 //机构id
 queryWrapper.eq(CourseBase::getCompanyId,companyId);
 ....

二、找回密码与注册

1、找回密码

在这里插入图片描述
请求参数:

{
     cellphone: '',
     email: '',
     checkcodekey: '',
     checkcode: '',
     confirmpwd: '',
     password: ''
 }

执行逻辑:

  • 校验验证码,验证码不一致则抛异常
  • 校验两次密码是否一致,不一致则抛异常
  • 根据手机号或者邮箱查用户
  • 查到则update密码

2、注册

在这里插入图片描述
请求参数:

{
   cellphone: '',
   username: '',
   email: '',
   nickname: '',
   password: '',
   confirmpwd: '',
   checkcodekey: '',
   checkcode: ''
}

代码逻辑:

  • 校验验证码,如果不一致则抛出异常
  • 校验两次密码是否一致,如果不一致则抛出异常
  • 校验用户是否存在,如果存在则抛出异常
  • 向用户表、用户角色关系表添加数据。角色为学生角色

三、需求:学生选课

选课的整体流程是:学生选课、下单支付、开始学习,三个模块流程图如下:

在这里插入图片描述

1、添加选课需求分析

UI设计:

  • 在课程详情页点击马上学习

在这里插入图片描述

  • 课程为免费课程时,用户可将其加入自己的课程表进行学习
    在这里插入图片描述

  • 课程为收费课程时,可选择支付或者试学
    在这里插入图片描述
    逻辑设计:

  • 选课是将课程加入我的课程表的过程

  • 对免费课程选课后可直接加入我的课程表

  • 对收费课程选课后需要下单支付成功系统自动加入我的课程表

在这里插入图片描述

2、数据模型设计

我的课程表里的课,是能学习的。而对于收费课程,要确定支付成功后才能加入我的课程表。因此中间用选课记录表来过渡,该表中用status字段来标明是待支付、已支付

  • 课程记录表
    在这里插入图片描述
# 字段说明

选课类型: 免费课程、收费课程。
选课状态: 选课成功、待支付、选课删除。
对于免费课程: 课程价格为0,有效期默认365,开始服务时间为选课时间,结束服务时间为选课时间加1年后的时间,选课状态为选课成功。
对于收费课程: 按课程的现价、有效期确定开始服务时间、结束服务时间,选课状态为待支付。
收费课程的选课记录需要支付成功后选课状态为成功。

  • 我的课程表
    在这里插入图片描述
    选课订单id字段,是两张表的关联字段。时序图:

在这里插入图片描述

2、查询课程信息接口

学习中心服务需要远程调用内容管理服务,来查询课程信息。在课程发布controller中没找到这个功能的接口,在里面重新定义课程查询接口。该接口主要是给其他微服务调用,因此不用授权,这里用/r打头做个标记,以后加白名单/r/*就可以被放行

@ApiOperation("查询课程发布信息")
@ResponseBody
@GetMapping("/r/coursepublish/{courseId}")
public CoursePublish getCoursepublish(@PathVariable("courseId") Long courseId) {
    CoursePublish coursePublish = coursePublishService.getCoursePublish(courseId);
     return coursePublish;
}

新加Service接口及其实现:

CoursePublish getCoursePublish(Long courseId);
@Override
public CoursePublish getCoursePublish(Long courseId){
    CoursePublish coursePublish = coursePublishMapper.selectById(courseId);
    return coursePublish ;
}

在调用方(学习中心服务)定义Feign

package com.xuecheng.learning.feignclient;

import com.xuecheng.content.model.po.CoursePublish;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * @description 内容管理远程接口
 * value即服务名
 * fallbackFactory即降级处理的类
 * 注意只拿接口的定义就行,方法体不要
 */
@FeignClient(value = "content-api",fallbackFactory = ContentServiceClientFallbackFactory.class)
public interface ContentServiceClient {

    @ResponseBody
    @GetMapping("/content/r/coursepublish/{courseId}")
    public CoursePublish getCoursepublish(@PathVariable("courseId") Long courseId);

}

定义发生异常后的熔断处理类:

package com.xuecheng.learning.feignclient;

import com.xuecheng.content.model.po.CoursePublish;
import feign.hystrix.FallbackFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class ContentServiceClientFallbackFactory implements FallbackFactory<ContentServiceClient> {
    @Override
    public ContentServiceClient create(Throwable throwable) {
        return new ContentServiceClient() {

            @Override
            public CoursePublish getCoursepublish(Long courseId) {
                log.error("调用内容管理服务发生熔断:{}", throwable.toString(),throwable);
                return null;
            }
        };
    }
}

注意在feign远程调用你是会将字符串转LocalDataTime,需要在CoursePublish 类中LocalDateTime的属性上边添加如下代码:

@JsonFormat(shape = JsonFormat.Shape.STRING,pattern = "yyyy-MM-dd HH:mm:ss")

边写边测,看一下远程调用是否成功:

@SpringBootTest
public class FeignClientTest {

  @Autowired
 ContentServiceClient contentServiceClient;

  @Test
  public void testContentServiceClient(){
	   CoursePublish coursepublish = contentServiceClient.getCoursepublish(18L);
	   Assertions.assertNotNull(coursepublish);
  }
}

3、添加选课接口

请求参数为课程id。再定义Vo:

@Data
@ToString
public class XcChooseCourseVo extends XcChooseCourse {

    //学习资格,[{"code":"702001","desc":"正常学习"},{"code":"702002","desc":"没有选课或选课后没有支付"},{"code":"702003","desc":"已过期需要申请续期或重新支付"}]
    public String learnStatus;

}

接口定义:

@Slf4j
@RestController
public class MyCourseTablesController {


    @ApiOperation("添加选课")
    @PostMapping("/choosecourse/{courseId}")
    public XcChooseCourseVo addChooseCourse(@PathVariable("courseId") Long courseId)  {
        
    }

}

定义Service接口:

public interface MyCourseTablesService {

    /**
 * @description 添加选课
 * @param userId 用户id
 * @param courseId 课程id
*/
 public XcChooseCourseVo addChooseCourse(String userId, Long courseId);

}

写实现类:(先用注释写逻辑,再将注释中的一部分抽成单独的方法来调用

@Slf4j
@Service
public class MyCourseTablesServiceImpl implements MyCourseTablesService {

    @Autowired
    XcChooseCourseMapper xcChooseCourseMapper;

    @Autowired
    XcCourseTablesMapper xcCourseTablesMapper;

    @Autowired
    ContentServiceClient contentServiceClient;

    @Autowired
    MyCourseTablesService myCourseTablesService;
    
    @Autowired
    MyCourseTablesServiceImpl currentProxy;
	
	@Transactional
    @Override
    public XcChooseCourseVo addChooseCourse(String userId,Long courseId) {
        //查询课程信息
        CoursePublish coursepublish = contentServiceClient.getCoursepublish(courseId);
        
		//课程收费标准
        String charge = coursepublish.getCharge();
        //选课记录
        XcChooseCourse chooseCourse = null;
        if("201000".equals(charge)){//课程免费
            //添加免费课程(抽成方法)
            chooseCourse  = addFreeCoruse(userId, coursepublish);
            //添加到我的课程表(抽成方法)
            XcCourseTables xcCourseTables = addCourseTabls(chooseCourse);
        }else{
            //添加收费课程(抽成方法)
            chooseCourse  = addChargeCoruse(userId, coursepublish);
        }
        //获取学习资格
        ...
        return null;
    }


	//接下来写单独抽出来的方法
	//添加免费课程,免费课程加入选课记录表、我的课程表
	public XcChooseCourse addFreeCoruse(String userId, CoursePublish coursepublish) {
	    return null;
	}

	//添加收费课程
	public XcChooseCourse addChargeCoruse(String userId,CoursePublish coursepublish){
	
	    return null;
	}
	//添加到我的课程表
	public XcCourseTables addCourseTabls(XcChooseCourse xcChooseCourse){
	   return null;
	}
}


对上面单独抽出来的方法进行完善:

//添加免费课程,免费课程加入选课记录表、我的课程表
public XcChooseCourse addFreeCoruse(String userId, CoursePublish coursepublish) {
    //查询选课记录表是否存在免费的且选课成功的订单
    LambdaQueryWrapper<XcChooseCourse> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper = queryWrapper.eq(XcChooseCourse::getUserId, userId)
            .eq(XcChooseCourse::getCourseId, coursepublish.getId())
            .eq(XcChooseCourse::getOrderType, "700001")//免费课程
            .eq(XcChooseCourse::getStatus, "701001");//选课成功
    List<XcChooseCourse> xcChooseCourses = xcChooseCourseMapper.selectList(queryWrapper);
    if (xcChooseCourses != null && xcChooseCourses.size()>0) {
    	//查到则直接返回这条记录,不用再添加
        return xcChooseCourses.get(0);
    }
    //添加选课记录信息
    XcChooseCourse xcChooseCourse = new XcChooseCourse();
    xcChooseCourse.setCourseId(coursepublish.getId());
    xcChooseCourse.setCourseName(coursepublish.getName());
    xcChooseCourse.setCoursePrice(0f);//免费课程价格为0
    xcChooseCourse.setUserId(userId);
    xcChooseCourse.setCompanyId(coursepublish.getCompanyId());
    xcChooseCourse.setOrderType("700001");//免费课程
    xcChooseCourse.setCreateDate(LocalDateTime.now());
    xcChooseCourse.setStatus("701001");//选课成功

    xcChooseCourse.setValidDays(365);//免费课程默认365
    xcChooseCourse.setValidtimeStart(LocalDateTime.now());
    xcChooseCourse.setValidtimeEnd(LocalDateTime.now().plusDays(365));
    xcChooseCourseMapper.insert(xcChooseCourse);
    int result = xcChooseCourseMapper.insert(xcChooseCourse);
    if(result <= 0){
    	MyException.cast("添加信息到课程记录表失败!");
    }
    return xcChooseCourse;

}

注意上面的LocalDateTime.now().plusDays(365)。接下来完善添加收费课程到选课记录表:


//添加收费课程
public XcChooseCourse addChargeCoruse(String userId,CoursePublish coursepublish){

    //如果存在待支付交易记录直接返回
    LambdaQueryWrapper<XcChooseCourse> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper = queryWrapper.eq(XcChooseCourse::getUserId, userId)
            .eq(XcChooseCourse::getCourseId, coursepublish.getId())
            .eq(XcChooseCourse::getOrderType, "700002")//收费订单
            .eq(XcChooseCourse::getStatus, "701002");//待支付
    List<XcChooseCourse> xcChooseCourses = xcChooseCourseMapper.selectList(queryWrapper);
    if (xcChooseCourses != null && xcChooseCourses.size()>0) {
        return xcChooseCourses.get(0);
    }

    XcChooseCourse xcChooseCourse = new XcChooseCourse();
    xcChooseCourse.setCourseId(coursepublish.getId());
    xcChooseCourse.setCourseName(coursepublish.getName());
    xcChooseCourse.setCoursePrice(coursepublish.getPrice());
    xcChooseCourse.setUserId(userId);
    xcChooseCourse.setCompanyId(coursepublish.getCompanyId());
    xcChooseCourse.setOrderType("700002");//收费课程
    xcChooseCourse.setCreateDate(LocalDateTime.now());
    xcChooseCourse.setStatus("701002");//待支付

    xcChooseCourse.setValidDays(coursepublish.getValidDays());
    xcChooseCourse.setValidtimeStart(LocalDateTime.now());
    xcChooseCourse.setValidtimeEnd(LocalDateTime.now().plusDays(coursepublish.getValidDays()));
    xcChooseCourseMapper.insert(xcChooseCourse);
    return xcChooseCourse;
}

完善添加到课程表:

public XcCourseTables addCourseTabls(XcChooseCourse xcChooseCourse){
    //选课记录完成且未过期可以添加课程到课程表
    String status = xcChooseCourse.getStatus();
    if (!"701001".equals(status)){
       MyException.cast("选课未成功,无法添加到课程表");
    }
    //查询我的课程表,同一个课程id和同一个userid
    XcCourseTables xcCourseTables = getXcCourseTables(xcChooseCourse.getUserId(), xcChooseCourse.getCourseId());
    if(xcCourseTables!=null){
         return xcCourseTables;
    }
    XcCourseTables xcCourseTablesNew = new XcCourseTables();
    xcCourseTablesNew.setChooseCourseId(xcChooseCourse.getId());
    xcCourseTablesNew.setUserId(xcChooseCourse.getUserId());
    xcCourseTablesNew.setCourseId(xcChooseCourse.getCourseId());
    xcCourseTablesNew.setCompanyId(xcChooseCourse.getCompanyId());
    xcCourseTablesNew.setCourseName(xcChooseCourse.getCourseName());
    xcCourseTablesNew.setCreateDate(LocalDateTime.now());
    xcCourseTablesNew.setValidtimeStart(xcChooseCourse.getValidtimeStart());
    xcCourseTablesNew.setValidtimeEnd(xcChooseCourse.getValidtimeEnd());
    xcCourseTablesNew.setCourseType(xcChooseCourse.getOrderType());
    xcCourseTablesNew.setCourseType(xcChooseCourse.getOrderType());
    int result = xcCourseTablesMapper.insert(xcCourseTablesNew);
	if(result <= 0){
		MyException.cast("添加信息到课程表失败!"):
	}
    return xcCourseTablesNew;

}

/**
 * @description 根据课程和用户查询我的课程表中某一门课程
 * @param userId
 * @param courseId
*/
public XcCourseTables getXcCourseTables(String userId,Long courseId){
    XcCourseTables xcCourseTables = xcCourseTablesMapper.selectOne(new LambdaQueryWrapper<XcCourseTables>().eq(XcCourseTables::getUserId, userId).eq(XcCourseTables::getCourseId, courseId));
    return xcCourseTables;

}
    

完善获取学习资格的代码。定义获取学习资格的接口方法:

public interface MyCourseTablesService {

    public XcChooseCourseVo addChooseCourse(String userId, Long courseId);
    /**
     * @description 判断学习资格
     * @param userId
     * @param courseId
     * @return XcCourseTablesDto 学习资格状态 [{"code":"702001","desc":"正常学习"},{"code":"702002","desc":"没有选课或选课后没有支付"},{"code":"702003","desc":"已过期需要申请续期或重新支付"}]
     */
    public XcCourseTablesVo getLearningStatus(String userId, Long courseId);
}

写实现类:


/**
 * @description 判断学习资格
 * @param userId
 * @param courseId
 * @return XcCourseTablesVo 学习资格状态 [{"code":"702001","desc":"正常学习"},{"code":"702002","desc":"没有选课或选课后没有支付"},{"code":"702003","desc":"已过期需要申请续期或重新支付"}]
 * 呃这里就set一个code,没get到为啥单独定义个Vo,直接return String也行
*/
public XcCourseTablesVo getLearningStatus(String userId, Long courseId){
    //调用上面定义的方法,查询我的课程表
    XcCourseTables xcCourseTables = getXcCourseTables(userId, courseId);
    if(xcCourseTables==null){
        XcCourseTablesVo xcCourseTablesVo = new XcCourseTablesVo();
        //没有选课或选课后没有支付
        xcCourseTablesVo.setLearnStatus("702002");
        return xcCourseTablesVo;
    }
    XcCourseTablesVo xcCourseTablesVo = new XcCourseTablesVo();
    BeanUtils.copyProperties(xcCourseTables,xcCourseTablesVo);
    //是否过期,true过期,false未过期
    boolean isExpires = xcCourseTables.getValidtimeEnd().isBefore(LocalDateTime.now());
    if(!isExpires){
        //正常学习
        xcCourseTablesVo.setLearnStatus("702001");
       return xcCourseTablesVo;

    }else{
        //已过期
        xcCourseTablesVo.setLearnStatus("702003");
        return xcCourseTablesVo;
    }

}

.isBefore(LocalDateTime.now())来判断是否过期。各个方法都写完了,最后在service层中调用,并完善controller

4、完善controller

@Autowired
MyCourseTablesService courseTablesService;

@ApiOperation("添加选课")
@PostMapping("/choosecourse/{courseId}")
public XcChooseCourseVo addChooseCourse(@PathVariable("courseId") Long courseId) {
    //自定义工具类获取当前登录用户
    SecurityUtil.XcUser user = SecurityUtil.getUser();
    if(user == null){
       MyException.cast("请登录后继续选课");
    }
    String userId = user.getId();
    return  courseTablesService.addChooseCourse(userId, courseId);

}


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/451977.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

每日一个小技巧:1招教你手机消除笔怎么用

在日常生活中&#xff0c;我们经常需要在手机上进行编辑和涂改&#xff0c;但是由于各种原因&#xff0c;我们可能会做出错误或者不满意的修改。这时候&#xff0c;消除笔就派上用场了。消除笔可以帮助我们在不影响其他内容的前提下&#xff0c;对错误或者不满意的修改进行撤销…

java实现大气无风环境污染物扩散模拟

一、扩散公式整理 二、编写java代码实现 String strJson InterpolationUtils.calGaussPlumePoints0(z,height,q,lon,lat, size,scale,airStable); return strJson.replaceAll("NaN","0").replaceAll("Infinity",String.valueOf(q)); String st…

【机器学习】P23 决策树、熵和信息增益

决策树、熵与信息增益 决策树熵信息增益Python 与 决策树 决策树 决策树&#xff08;Decision Tree&#xff09; 是一种基于树形结构的分类算法&#xff0c;它通过一系列的询问&#xff08;也称为测试或判定条件&#xff09;来判断一个数据实例属于哪个类别。 以一个案例贯穿…

Go | 一分钟掌握Go | 2 - 集成开发工具

作者&#xff1a;Mars酱 声明&#xff1a;本文章由Mars酱编写&#xff0c;部分内容来源于网络&#xff0c;如有疑问请联系本人。 转载&#xff1a;欢迎转载&#xff0c;转载前先请联系我&#xff01; 工具介绍 编码是一门传统手艺活&#xff0c;手艺好不好很重要&#xff0c;器…

高精度气象模拟软件WRF实践技术

【原文链接】&#xff1a;高精度气象模拟软件WRF(Weather Research Forecasting)实践技术及案例应用https://mp.weixin.qq.com/s?__bizMzU5NTkyMzcxNw&mid2247538149&idx3&sn3890c3b29f34bcb07678a9dd4b9947b2&chksmfe68938fc91f1a99bbced2113b09cad822711e7f…

开发者必读!常用的二维码生成器 API 推荐

引言 二维码是一种能够存储信息的图形码&#xff0c;它在现代社会中扮演着越来越重要的角色。生成二维码的过程通常需要使用二维码生成器&#xff0c;而现在有很多二维码生成器 API 可以供开发者使用。 在本文中&#xff0c;我们将讨论二维码生成器 API 的工作原理、应用场景…

CHAPTER 6: 《DESIGN A KEY-VALUE STORE》 第6章 《设计一个键值存储》

CHAPTER 6: DESIGN A KEY-VALUE STORE 键值存储(也称为键值数据库)是一种非关系数据库。每一个唯一标识符存储为与其关联值的键。这种数据配对称为“键-值”对。 在一个键-值对中&#xff0c;键必须是唯一的&#xff0c;与该键相关联的值可以是通过密钥访问。键可以是纯文本或…

编译原理个人作业--第五章——基于 编译原理 国防工业出版社 第三版

1 文法 G 1 G_1 G1​为 E → E T ∣ T T → T ∗ F ∣ F F → ( E ) ∣ i E\rightarrow ET|T\\ T\rightarrow T*F|F\\ F\rightarrow(E)|i E→ET∣TT→T∗F∣FF→(E)∣i 请证明 E T ∗ F ET*F ET∗F是他的一个句型(课本写的是ET*T感觉是印错了)&#xff0c;指出它的所有短语…

这份最新阿里、腾讯、华为、字节等大厂的薪资和职级对比,你看过没?

互联网大厂新入职员工各职级薪资对应表(技术线)~ 最新阿里、腾讯、华为、字节跳动等大厂的薪资和职级对比 上面的表格不排除有很极端的收入情况&#xff0c;但至少能囊括一部分同职级的收入。这个表是“技术线”新入职员工的职级和薪资情况&#xff0c;非技术线(如产品、运营、…

pbootcms自动配图出图插件

pbootcms文章无图自动出图配图插件的优点 1、提高文章的可读性和吸引力&#xff1a;插入图片可以丰富文章的内容和形式&#xff0c;增强读者的阅读体验和吸引力&#xff0c;提高文章的点击率和转化率。 2、节省时间和精力&#xff1a;手动添加图片需要花费大量时间和精力去寻找…

【LeetCode: 1416. 恢复数组 | 暴力递归=>记忆化搜索=>动态规划 】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…

【花雕学AI】超级提问模型大全!150个ChatGPT神奇示例,让你的聊天更有趣!

引言 你是否想要成为聊天高手&#xff0c;让你的对话更加有趣和深入&#xff1f;你是否想要掌握一些超级提问模型&#xff0c;让你的聊天更加有创意和挑战&#xff1f;你是否想要借助人工智能的力量&#xff0c;生成一些适合你的超级提问模型&#xff1f; 如果你的答案是肯定…

浅谈新能源电动汽与汽车传感器充电桩的影响

安科瑞 徐浩竣 江苏安科瑞电器制造有限公司 zx acrelxhj 摘要&#xff1a;随着我国能源战略发展以及低碳行动的实施&#xff0c;电动汽车已逐步广泛应用&#xff0c;而电动汽车的应用非常符合当今社会对环保意识的要求&#xff0c;以及有效节省化石燃料的消耗。由于其无污染…

Flutter+YesAPI 快速构建零运维的APP

前言 移动互联网经过多年的发展&#xff0c;已经进入一个成熟的阶段&#xff0c;几乎每个公司都有自己的移动应用程序或移动网站。随着5G技术的不断发展&#xff0c;也带来了更高效的数据传输速度和更稳定的网络连接&#xff0c;这使得更多的应用程序和服务能够在互联网上运行&…

NFT介绍及监管规则

什么是NFT NFT是Non-Fungible Token&#xff08;非同质化代币&#xff09;的缩写。 NFT是“Non-Fungible Token”的缩写&#xff0c;即非同质化代币。不同于FT&#xff08;Fungible Token&#xff0c;同质化代币&#xff09;&#xff0c;每一个NFT都是独一无二且不可相互替代的…

cmake管理子程序,lib库和so库应用实践

cmake在管理大型项目时经常被用到&#xff0c;本文以简单程序演示来说明camke管理项目应用&#xff0c;其中包括主程序&#xff0c;子程序&#xff0c;so库程序&#xff0c;lib程序。 目录 1.程序目录结构 2.编译执行 3.清除临时文件 4.完整代码 1.程序目录结构 ├── bu…

【PWN刷题__ret2text】[BJDCTF 2020]babystack

新手上路~低速慢行~ 目录 前言 1. checksec 2. IDA 反汇编 3. payload编写 4. exp编写 5. pwntools用法 前言 作为pwn新手&#xff0c;尽可能在刷题中&#xff0c;记录、学习一些通用的知识点&#xff0c;因此wp是少不了的。 本题是一道简单的ret2text 1. checksec 没有…

6.2 龙格—库塔法

学习目标&#xff1a; 学习龙格-库塔法的具体明确的学习目标可以有以下几点&#xff1a; 理解龙格-库塔法的基本思想和原理&#xff1a;我们应该了解龙格-库塔法的数值求解思想和数值误差的概念&#xff0c;包括截断误差和稳定性等基本概念&#xff0c;并且要熟悉龙格-库塔法的…

大学生无线耳机怎么选?内行推荐四款高性价比蓝牙耳机

随着蓝牙耳机的使用频率越来越高&#xff0c;大学生成为了蓝牙耳机的主要用户群体之一。最近看到很多网友问&#xff0c;大学生无线耳机怎么选&#xff1f;针对这个问题&#xff0c;我来给大家推荐几款高性价比蓝牙耳机&#xff0c;一起来看看吧。 一、南卡小音舱Lite2蓝牙耳机…

MIMO-OFDM系统中信道估计的快速谐波搜索技术(Matlab代码实现)

目录 &#x1f4a5;1 概述 &#x1f4da;2 运行结果 &#x1f389;3 参考文献 &#x1f468;‍&#x1f4bb;4 Matlab代码 &#x1f4a5;1 概述 目前,由OFDM技术与空时编码技术相融合而成的MIMO-OFDM技术已经引起了通信领域的广泛关注和研究.在无线通信系统中,MIMO-OFDM技…