百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分类 > 正文

自营性电商项目④

ztj100 2025-05-11 19:44 17 浏览 0 评论

类别管理--根据id查询类别详情--持久层

13.1. 规划SQL语句

本次需要执行的SQL语句大致是:

select * from pms_category where id=?

关于字段列表,应该包括:

id, name, parent_id, depth, keywords, sort, icon, enable, is_parent, is_display

13.2. 抽象方法(可能需要创建VO类)

csmall-pojo的根包下的vo包下创建CategoryDetailsVO类,封装以上设计的字段对应的属性:

import lombok.Data;
import java.io.Serializable;

@Data
public class CategoryDetailsVO implements Serializable {
  private Long id;
  private String name;
  private Long parentId;
  private Integer depth;
  private String keywords;
  private Integer sort;
  private String icon;
  private Integer enable;
  private Integer isParent;
  private Integer isDisplay;
}

CategoryMapper接口中添加:

CategoryDetailsVO getDetailsById(Long id);

13.3. 在XML中配置SQL

CategoryMapper.xml中添加配置:

<!-- CategoryDetailsVO getDetailsById(Long id); -->
<select id="getDetailsById" resultMap="DetailsResultMap">
  select
  <include refid="DetailsQueryFields"/>
    from
  pms_category
  where
  id=#{id}
</select>

<sql id="DetailsQueryFields">
  <if test="true">
      id, name, parent_id, depth, keywords,
      sort, icon, enable, is_parent, is_display
  </if>
</sql>

<resultMap id="DetailsResultMap" type="cn.celinf.csmall.pojo.vo.CategoryDetailsVO">
    <id column="id" property="id" />
    <result column="name" property="name" />
    <result column="parent_id" property="parentId" />
    <result column="depth" property="depth" />
    <result column="keywords" property="keywords" />
    <result column="sort" property="sort" />
    <result column="icon" property="icon" />
    <result column="enable" property="enable" />
    <result column="is_parent" property="isParent" />
    <result column="is_display" property="isDisplay" />
</resultMap>

13.4. 测试

@Test
@Sql({"classpath:truncate.sql", "classpath:insert_data.sql"})
public void testGetDetailsByIdSuccessfully() {
  // 测试数据
  Long id = 1L;
  // 断言不会抛出异常
  assertDoesNotThrow(() -> {
    // 执行查询
    Object category = mapper.getDetailsById(id);
  // 断言查询结果不为null
  assertNotNull(category);
});
}

@Test
@Sql({"classpath:truncate.sql"})
public void testGetDetailsByIdFailBecauseNotFound() {
  // 测试数据
  Long id = -1L;
  // 断言不会抛出异常
  assertDoesNotThrow(() -> {
    // 执行查询
    Object category = mapper.getDetailsById(id);
  // 断言查询结果为null
  assertNull(category);
});
}

14. 类别管理--根据id查询类别详情--业务逻辑层

14.1. 接口和抽象方法

ICategoryService中添加:

CategoryDetailsVO getDetailsById(Long id);

14.2. 实现

CategoryServiceImpl中执行查询并返回。

14.3. 测试

@Test
@Sql({"classpath:truncate.sql", "classpath:insert_data.sql"})
public void testGetDetailsByIdSuccessfully() {
    // 测试数据
    Long id = 1L;
    // 断言不抛出异常
    assertDoesNotThrow(() -> {
        service.getDetailsById(id);
    });
}

@Test
@Sql({"classpath:truncate.sql"})
public void testGetDetailsByIdFailBecauseNotFound() {
    // 测试数据
    Long id = -1L;
    // 断言抛出异常
    assertThrows(ServiceException.class, () -> {
        service.getDetailsById(id);
    });
}

15. 类别管理--根据id查询类别详情--控制器层

CategoryController中添加:

@GetMapping("/{id}")
public JsonResult<CategoryDetailsVO> getDetailsById(@PathVariable Long id) {
  CategoryDetailsVO category = categoryService.getDetailsById(id);
  return JsonResult.ok(category);
}

CategoryControllerTests中测试:

@Test
@Sql({"classpath:truncate.sql", "classpath:insert_data.sql"})
public void testGetDetailsByIdSuccessfully() throws Exception {
  // 准备测试数据,注意:此次没有提交必要的name属性值
  String id = "1";
  // 请求路径,不需要写协议、服务器主机和端口号
  String url = "/categories/" + id;
  // 执行测试
  // 以下代码相对比较固定
  mockMvc.perform( // 执行发出请求
    MockMvcRequestBuilders.get(url) // 根据请求方式决定调用的方法
    .contentType(MediaType.APPLICATION_FORM_URLENCODED) // 请求数据的文档类型,例如:application/json; charset=utf-8
    .accept(MediaType.APPLICATION_JSON)) // 接收的响应结果的文档类型,注意:perform()方法到此结束
    .andExpect( // 预判结果,类似断言
    MockMvcResultMatchers
    .jsonPath("state") // 预判响应的JSON结果中将有名为state的属性
    .value(State.OK.getValue())) // 预判响应的JSON结果中名为state的属性的值,注意:andExpect()方法到此结束
    .andDo( // 需要执行某任务
    MockMvcResultHandlers.print()); // 打印日志
}

@Test
@Sql({"classpath:truncate.sql", "classpath:insert_data.sql"})
public void testGetDetailsByIdFailBecauseNotFound() throws Exception {
  // 准备测试数据,注意:此次没有提交必要的name属性值
  String id = "9999999999";
  // 请求路径,不需要写协议、服务器主机和端口号
  String url = "/categories/" + id;
  // 执行测试
  // 以下代码相对比较固定
  mockMvc.perform( // 执行发出请求
    MockMvcRequestBuilders.get(url) // 根据请求方式决定调用的方法
    .contentType(MediaType.APPLICATION_FORM_URLENCODED) // 请求数据的文档类型,例如:application/json; charset=utf-8
    .accept(MediaType.APPLICATION_JSON)) // 接收的响应结果的文档类型,注意:perform()方法到此结束
    .andExpect( // 预判结果,类似断言
    MockMvcResultMatchers
    .jsonPath("state") // 预判响应的JSON结果中将有名为state的属性
    .value(State.ERR_CATEGORY_NOT_FOUND.getValue())) // 预判响应的JSON结果中名为state的属性的值,注意:andExpect()方法到此结束
    .andDo( // 需要执行某任务
    MockMvcResultHandlers.print()); // 打印日志
}

管理员相关数据表

管理员及权限的管理,涉及的数据表有:

-- 数据库:mall_ams

-- 权限表:创建数据表
drop table if exists ams_permission;
create table ams_permission (
    id bigint unsigned auto_increment,
    name varchar(50) default null comment '名称',
    value varchar(255) default null comment '值',
    description varchar(255) default null comment '描述',
    sort tinyint unsigned default 0 comment '自定义排序序号',
    gmt_create datetime default null comment '数据创建时间',
    gmt_modified datetime default null comment '数据最后修改时间',
    primary key (id)
) comment '权限表' charset utf8mb4;

-- 权限表:插入测试数据
insert into ams_permission (name, value, description) values
('商品-商品管理-读取', '/pms/product/read', '读取商品数据,含列表、详情、查询等'),
('商品-商品管理-编辑', '/pms/product/update', '修改商品数据'),
('商品-商品管理-删除', '/pms/product/delete', '删除商品数据'),
('后台管理-管理员-读取', '/ams/admin/read', '读取管理员数据,含列表、详情、查询等'),
('后台管理-管理员-编辑', '/ams/admin/update', '编辑管理员数据'),
('后台管理-管理员-删除', '/ams/admin/delete', '删除管理员数据');

-- 角色表:创建数据表
drop table if exists ams_role;
create table ams_role (
    id bigint unsigned auto_increment,
    name varchar(50) default null comment '名称',
    description varchar(255) default null comment '描述',
    sort tinyint unsigned default 0 comment '自定义排序序号',
    gmt_create datetime default null comment '数据创建时间',
    gmt_modified datetime default null comment '数据最后修改时间',
    primary key (id)
) comment '角色表' charset utf8mb4;

-- 角色表:插入测试数据
insert into ams_role (name) values
    ('超级管理员'), ('系统管理员'), ('商品管理员'), ('订单管理员');

-- 角色权限关联表:创建数据表
drop table if exists ams_role_permission;
create table ams_role_permission (
    id bigint unsigned auto_increment,
    role_id bigint unsigned default null comment '角色id',
    permission_id bigint unsigned default null comment '权限id',
    gmt_create datetime default null comment '数据创建时间',
    gmt_modified datetime default null comment '数据最后修改时间',
    primary key (id)
) comment '角色权限关联表' charset utf8mb4;

-- 角色权限关联表:插入测试数据
insert into ams_role_permission (role_id, permission_id) values
    (1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6),
    (2, 1), (2, 2), (2, 3), (2, 4), (2, 5), (2, 6),
    (3, 1), (3, 2), (3, 3);

-- 管理员表:创建数据表
drop table if exists ams_admin;
create table ams_admin (
    id bigint unsigned auto_increment,
    username varchar(50) default null unique comment '用户名',
    password char(64) default null comment '密码(密文)',
    nickname varchar(50) default null comment '昵称',
    avatar varchar(255) default null comment '头像URL',
    phone varchar(50) default null unique comment '手机号码',
    email varchar(50) default null unique comment '电子邮箱',
    description varchar(255) default null comment '描述',
    is_enable tinyint unsigned default 0 comment '是否启用,1=启用,0=未启用',
    last_login_ip varchar(50) default null comment '最后登录IP地址(冗余)',
    login_count int unsigned default 0 comment '累计登录次数(冗余)',
    gmt_last_login datetime default null comment '最后登录时间(冗余)',
    gmt_create datetime default null comment '数据创建时间',
    gmt_modified datetime default null comment '数据最后修改时间',
    primary key (id)
) comment '管理员表' charset utf8mb4;

-- 管理员表:插入测试数据
insert into ams_admin (username, password, nickname, email, description, is_enable) values
    ('root', '1234', 'root', 'root@celinf.cn', '最高管理员', 1),
    ('super_admin', '1234', 'administrator', 'admin@celinf.cn', '超级管理员', 1),
    ('nobody', '1234', '无名', 'liucs@celinf.cn', null, 0);

-- 管理员角色关联表:创建数据表
drop table if exists ams_admin_role;
create table ams_admin_role (
    id bigint unsigned auto_increment,
    admin_id bigint unsigned default null comment '管理员id',
    role_id bigint unsigned default null comment '角色id',
    gmt_create datetime default null comment '数据创建时间',
    gmt_modified datetime default null comment '数据最后修改时间',
    primary key (id)
) comment '管理员角色关联表' charset utf8mb4;

-- 管理员角色关联表:插入测试数据
insert into ams_admin_role (admin_id, role_id) values
    (1, 1), (1, 2), (1, 3), (2, 2), (2, 3), (2, 4), (3, 3);

-- 查询示例:查询id=1的管理员的权限
select distinct ams_permission.value from ams_permission
left join ams_role_permission on ams_role_permission.permission_id=ams_permission.id
left join ams_role on ams_role_permission.role_id=ams_role.id
left join ams_admin_role on ams_admin_role.role_id=ams_role.id
left join ams_admin on ams_admin_role.admin_id=ams_admin.id
where ams_admin.id=1
order by ams_permission.value;

-- 管理员登录日志表:创建数据表
drop table if exists ams_login_log;
create table ams_login_log (
    id bigint unsigned auto_increment,
    admin_id bigint unsigned default null comment '管理员id',
    username varchar(50) default null comment '管理员用户名(冗余)',
    nickname varchar(50) default null comment '管理员昵称(冗余)',
    ip varchar(50) default null comment '登录IP地址',
    user_agent varchar(255) default null comment '浏览器内核',
    gmt_login datetime default null comment '登录时间',
    gmt_create datetime default null comment '数据创建时间',
    gmt_modified datetime default null comment '数据最后修改时间',
    primary key (id)
) comment '管理员登录日志表' charset utf8mb4;

-- 管理员登录日志表:插入测试数据
insert into ams_login_log (admin_id, username, nickname, ip, user_agent, gmt_login) values
    (1, 'root', 'root', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Safari/605.1.15', DATE_SUB(NOW(), interval 1 day)),
    (2, 'root', 'root', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Safari/605.1.15', DATE_SUB(NOW(), interval 12 hour)),
    (3, 'root', 'root', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Safari/605.1.15', NOW());

-- 查看数据表结构
desc ams_permission; desc ams_role; desc ams_role_permission; desc ams_admin; desc ams_admin_role; desc ams_login_log;

当某个管理员尝试登录时,必须实现”根据用户名查询此管理员的信息,至少包括id、密码、权限“,需要执行的SQL语句大致是:

-- 管理员表 admin
-- 角色表 role
-- 管理员与角色关联表 admin_role (admin_id, role_id)
-- 权限表 permission
-- 角色与权限关联表 role_permission (role_id, permission_id)
-- 【根据用户名查询管理员,且必须查出对应的权限】
select
    ams_admin.id,
    ams_admin.username,
    ams_admin.password,
    ams_admin.is_enable,
    ams_permission.value
from ams_admin
left join ams_admin_role on ams_admin.id = ams_admin_role.admin_id
left join ams_role_permission on ams_admin_role.role_id = ams_role_permission.role_id
left join ams_permission on ams_role_permission.permission_id = ams_permission.id
where username='root';

接下来,在根项目中创建csmall-admin模块(与csmall-product类似),并在其下创建csmall-admin-servicecsmall-admin-webapi这2个子模块(与csmall-product的2个子模块类似),然后,尽量在csmall-admin-webapi中实现以上查询功能:

public interface AdminMapper {
    AdminLoginVO findByUsername(String username);
}

最后,关于Key的使用,通常建议使用冒号区分多层次,类似URL的设计方式,例如:

  • 类别列表的Key:categories:listcategories
  • 某个id(9000)对应的类别的Key:categories:item:9000

关于用户身份认证与授权

Spring Security是用于解决认证与授权的框架

在根项目下创建新的`csmall-passport`子模块,最基础的依赖项包括`spring-boot-starter-web`与`spring-boot-starter-security`(为避免默认存在的测试类出错,应该保留测试的依赖项`spring-boot-starter-test`),完整的`csmall-passwort`的`pom.xml`为:

<dependencies>
  <!-- Spring Boot Web:支持Spring MVC -->
  <dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <!-- Spring Boot Security:处理认证与授权 -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
  </dependency>
  <!-- Spring Boot Test:测试 -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
  </dependency>
</dependencies>

调整完成后,即可启动项目,在启动的日志中,可以看到类似以下内容:

Using generated security password: 2abb9119-b5bb-4de9-8584-9f893e4a5a92

Spring Security有默认登录的账号和密码(以上提示的值),密码是随机的,每次启动项目都会不同。

Spring Security默认要求所有的请求都是必须先登录才允许的访问,可以使用默认的用户名`user`和自动生成的随机密码来登录。在测试登录时,在浏览器访问当前主机的任意网址都可以(包括不存在的资源),会自动跳转到登录页(是由Spring Security提供的,默认的URL是:http://localhost:8080/login),当登录成功后,会自动跳转到此前访问的URL(跳转登录页之前的URL),另外,还可以通过 http://localhost:8080/logout 退出登录。

Spring Security的依赖项中包括了Bcrypt算法的工具类,Bcrypt是一款非常优秀的密码加密工具,适用于对需要存储下来的密码进行加密处理。

import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

public class BcryptPasswordEncoderTests {
  
  private BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

  @Test
  public void testEncode() {
    // 原文相同的情况,每次加密得到的密文都不同
    for (int i = 0; i < 10; i++) {
      String rawPassword = "123456";
      String encodedPassword = passwordEncoder.encode(rawPassword);
      System.out.println("rawPassword = " + rawPassword);
      System.out.println("encodedPassword = " + encodedPassword);
    }
    // rawPassword = 123456
    // encodedPassword = $2a$10$HWuJ9WgPazrwg9.isaae4u7XdP7ohH7LetDwdlTWuPC4ZAvG.Uc7W
    // encodedPassword = $2a$10$rOwgZMpDvZ3Kn7CxHWiEbeC6bQMGtfX.VYc9DCzx9BxkWymX6FbrS
  }
  @Test
  public void testMatches() {
    String rawPassword = "123456";
    String encodedPassword = "$2a$10$hI4wweFOGJ7FMduSmCjNBexbKFOjYMWl8hkug0n0k1LNR5vEyhhMW";
    boolean matchResult = passwordEncoder.matches(rawPassword, encodedPassword);
    System.out.println("match result : " + matchResult);
  }
}

如果要使得Spring Security能使用数据库中的信息(数据库中的用户名与密码)来验证用户身份(认证),首先,必须实现“根据用户名查询此用户的登录信息(应该包括权限信息)”的查询功能,要实现此查询,需要执行的SQL语句大致是:

select
	ams_admin.id,
  ams_admin.username,
  ams_admin.password,
  ams_admin.is_enable,
  ams_permission.value
from ams_admin
  left join ams_admin_role on ams_admin.id = ams_admin_role.admin_id
  left join ams_role_permission on ams_admin_role.role_id = ams_role_permission.role_id
  left join ams_permission on ams_role_permission.permission_id = ams_permission.id
where username='root';

要在当前模块(`csmall-passport`)中实现此查询功能,需要:

  1. - [`csmall-passport`] 添加数据库编程的相关依赖
  2. - `mysql-connector-java`
  3. - `mybatis-spring-boot-starter`
  4. - `durid` / `druid-spring-boot-starter`
  5. - [`csmall-passport`] 添加连接数据库的配置信息
  6. - [`csmall-passport`] 创建`MybatisConfiguration`配置类,用于配置`@MapperScan`
  7. - [`csmall-passport`] 在配置文件中配置`mybatis.mapper-locations`属性,以指定XML文件的位置
  8. - [`csmall-pojo`] 创建`AdminLoginVO`类
@Data
public class AdminLoginVO implements Serializable {
  private Long id;
  private String username;
  private String password;
  private Integer isEnable;
  private List<String> permissions;
}

- [`csmall-passport`] 在`pom.xml`中添加对`csmall-pojo`的依赖

- [`csmall-passport`] 在`src/main/java`下的`cn.celinf.csmall.passport`包下创建`mapper.AdminMapper.java`接口

- [`csmall-passport`] 在接口中添加抽象方法:

AdminLoginVO getLoginInfoByUsername(String username);

- 在`src/main/resources`下创建`mapper`文件夹,并在此文件夹下粘贴得到`AdminMapper.xml`

- 在`AdminMapper.xml`中配置以上抽象方法映射的SQL查询:

<mapper namespace="cn.celinf.csmall.passport.mapper.AdminMapper">
<!-- AdminLoginVO getLoginInfoByUsername(String username); -->
<select id="getLoginInfoByUsername" resultMap="LoginInfoResultMap">
select
<include refid="LoginInfoQueryFields" />
from ams_admin
left join ams_admin_role
on ams_admin.id = ams_admin_role.admin_id
left join ams_role_permission
on ams_admin_role.role_id = ams_role_permission.role_id
left join ams_permission
on ams_role_permission.permission_id = ams_permission.id
where username=#{username}
</select>

<sql id="LoginInfoQueryFields">
<if test="true">
ams_admin.id,
ams_admin.username,
ams_admin.password,
ams_admin.is_enable,
ams_permission.value
</if>
</sql>

<resultMap id="LoginInfoResultMap" type="cn.celinf.csmall.pojo.vo.AdminLoginVO">
<id column="id" property="id" />
<result column="username" property="username" />
<result column="password" property="password" />
<result column="is_enable" property="isEnable" />
<collection property="permissions" ofType="java.lang.String">
<!-- 以下配置类似在Java中执行 new String("/pms/product/read") -->
<constructor>
<arg column="value" />
</constructor>
</collection>
</resultMap>
</mapper>

- 完成后,还应该编写并执行测试

根据有效的用户名查询出的结果例如:

AdminLoginVO(
  id=1,
  username=root,
  password=1234,
  isEnable=1,
  permissions=[
    /pms/product/read,
    /pms/product/update,
    /pms/product/delete,
    /ams/admin/read,
    /ams/admin/update,
    /ams/admin/delete
  ]
)

学习记录,如有侵权请联系删除

相关推荐

Win10预览版10532已知问题汇总(微软win11正式版已知问题一览)

IT之家讯微软已向Insider用户推送了Win10预览版10532更新,本次更新对右键菜单、《Windows反馈》应用以及Edge浏览器进行了改进。除此之外还包含一些Bug,汇总如下,有意升级Wi...

Gabe Aul正测试Win10 Mobile 10532,Insider用户还需等

IT之家讯本月中旬微软向Insider用户推送了Win10Mobile预览版10512,该版本修复了一些Bug,增强了系统稳定性,但依然存在一些问题。今天,微软Insider项目负责人GabeAu...

微软开始推送Win10预览版10532快速版更新

8月28日消息,刚才,微软推送了Win10Build10532快速版,修复了之前的Bug,并带来了三项改进。主要来说,这次的更新改进了右键菜单的UI,使其更具Modern风格(见上图)。此外,更新...

Win10预览版10532更新内容大全(windows10更新预览版)

IT之家讯今天凌晨微软向Insider用户推送了Win10预览版10532快速版更新,本次更新主要带来了三处改进,汇总如下:o改进右键菜单,外观更加Modern。这是基于网友要求界面一致的反馈做出...

无法升级Win10预览版10532?也许Hyper-V在搞鬼

根据IT之家网友的反映,安装了微软虚拟机Hyper-V的Win10预览版用户无法成功升级Build10532版本,安装过程中会被要求回滚系统。很多朋友在尝试关闭虚拟机之后重启安装程序,结果仍然无法顺...

Win10预览版10532界面兴起“酷黑”风潮

Win10预览版10532的界面改动还是较为明显的,主要体现在右键菜单上面。总体来看,该版本的右键菜单间距更宽,视觉上更大气,操作上更便于触控。具体来说,任务栏右键菜单的变化最为明显。除了增加选项的宽...

Win10预览版10532上手图集(windows10预览版下载)

IT之家讯8月28日,微软今天推送了Win10预览版10532快速版更新,在该版本中,微软主要是加强细节上调整,并且主要是增强Edge浏览器性能等。在Windows10预览版10532中,微软改进了...

Win10预览版10532上手视频亮点演示

IT之家讯8月28日消息,今天凌晨微软向WindowsInsider快速通道用户推送了Win10预览版10532。在Windows10预览版10532中,微软改进了右键菜单,外观更加现代化。另外还...

第二篇 前端框架Vue.js(vue前端框架技术)

前端三大核心是网页开发的基础,Vue则是基于它们构建的“生产力工具”。通俗理解就是HTML是化妆的工具如眉笔,CSS是化妆品如口红,JavaScript是化妆后的互动,而Vue就是化妆助手。有了化妆工...

基于SpringBoot + vue2实现的旅游推荐管理系统

项目描述...

基于Vue以及iView组件的后端管理UI模板——iview-admin

介绍iView-admin是一套后端管理界面模板,基于Vue2.0,iView(现在为ViewUI)组件是一套完整的基于Vue的高质量组件库,虽然Github上有一套非常火的基于ElementUI...

别再说你会SPA开发了,这5个核心你真的搞懂了吗?

前言此spa非彼spa,不是你所熟知的spa。你所熟知的spa作者肯定是没有你熟悉的。我们这里指的是在前端开发中的一种模型,叫作单页应用程序,顾名思义,就是整个项目只有一个页面,而页面中的内容是动态的...

React.js Top20面试题(react.js中文官网)

概述作为React开发者,对框架的关键概念和原则有扎实的理解是很重要的。考虑到这一点,我整理了一份包含20个重要问题的清单,每个React开发者都应该知道,无论他们是在面试工作还是只是想提高技能。...

美媒:特朗普签署行政令后,FBI又发现约2400份、总计超14000页涉肯尼迪遇刺案文件

来源:环球时报新媒体1月23日特朗普下令公布肯尼迪遇刺案相关机密文件图源:美媒综合福克斯新闻网和Axios网站10日报道,在总统特朗普签署行政令,要求公布“肯尼迪遇刺案”相关政府机密文件之后,美国...

2021 年 Node.js 开发人员学习路线图

Node.js自发布以来,已成为业界重要破局者之一。Uber、Medium、PayPal和沃尔玛等大型企业,纷纷将技术栈转向Node.js。Node.js支持开发功能强大的应用,例如实时追踪...

取消回复欢迎 发表评论: