自营性电商项目④
ztj100 2025-05-11 19:44 3 浏览 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-service和csmall-admin-webapi这2个子模块(与csmall-product的2个子模块类似),然后,尽量在csmall-admin-webapi中实现以上查询功能:
public interface AdminMapper {
AdminLoginVO findByUsername(String username);
}
最后,关于Key的使用,通常建议使用冒号区分多层次,类似URL的设计方式,例如:
- 类别列表的Key:categories:list或categories
- 某个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`)中实现此查询功能,需要:
- - [`csmall-passport`] 添加数据库编程的相关依赖
- - `mysql-connector-java`
- - `mybatis-spring-boot-starter`
- - `durid` / `druid-spring-boot-starter`
- - [`csmall-passport`] 添加连接数据库的配置信息
- - [`csmall-passport`] 创建`MybatisConfiguration`配置类,用于配置`@MapperScan`
- - [`csmall-passport`] 在配置文件中配置`mybatis.mapper-locations`属性,以指定XML文件的位置
- - [`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
]
)
学习记录,如有侵权请联系删除
相关推荐
-
- SpringBoot如何实现优雅的参数校验
-
平常业务中肯定少不了校验,如果我们把大量的校验代码夹杂到业务中,肯定是不优雅的,对于一些简单的校验,我们可以使用java为我们提供的api进行处理,同时对于一些...
-
2025-05-11 19:46 ztj100
- Java中的空指针怎么处理?
-
#暑期创作大赛#Java程序员工作中遇到最多的错误就是空指针异常,无论你多么细心,一不留神就从代码的某个地方冒出NullPointerException,令人头疼。...
- 一坨一坨 if/else 参数校验,被 SpringBoot 参数校验组件整干净了
-
来源:https://mp.weixin.qq.com/s/ZVOiT-_C3f-g7aj3760Q-g...
- 用了这两款插件,同事再也不说我代码写的烂了
-
同事:你的代码写的不行啊,不够规范啊。我:我写的代码怎么可能不规范,不要胡说。于是同事打开我的IDEA,安装了一个插件,然后执行了一下,规范不规范,看报告吧。这可怎么是好,这玩意竟然给我挑出来这么...
- SpringBoot中6种拦截器使用场景
-
SpringBoot中6种拦截器使用场景,下面是思维导图详细总结一、拦截器基础...
- 用注解进行参数校验,spring validation介绍、使用、实现原理分析
-
springvalidation是什么在平时的需求开发中,经常会有参数校验的需求,比如一个接收用户注册请求的接口,要校验用户传入的用户名不能为空、用户名长度不超过20个字符、传入的手机号是合法的手机...
- 快速上手:SpringBoot自定义请求参数校验
-
作者:UncleChen来源:http://unclechen.github.io/最近在工作中遇到写一些API,这些API的请求参数非常多,嵌套也非常复杂,如果参数的校验代码全部都手动去实现,写起来...
- 分布式微服务架构组件
-
1、服务发现-Nacos服务发现、配置管理、服务治理及管理,同类产品还有ZooKeeper、Eureka、Consulhttps://nacos.io/zh-cn/docs/what-is-nacos...
- 优雅的参数校验,告别冗余if-else
-
一、参数校验简介...
- Spring Boot断言深度指南:用断言机制为代码构筑健壮防线
-
在SpringBoot开发中,断言(Assert)如同代码的"体检医生",能在上线前精准捕捉业务逻辑漏洞。本文将结合企业级实践,解析如何通过断言机制实现代码自检、异常预警与性能优化三...
- 如何在项目中优雅的校验参数
-
本文看点前言验证数据是贯穿所有应用程序层(从表示层到持久层)的常见任务。通常在每一层实现相同的验证逻辑,这既费时又容易出错。为了避免重复这些验证,开发人员经常将验证逻辑直接捆绑到域模型中,将域类与验证...
- SpingBoot项目使用@Validated和@Valid参数校验
-
一、什么是参数校验?我们在后端开发中,经常遇到的一个问题就是入参校验。简单来说就是对一个方法入参的参数进行校验,看是否符合我们的要求。比如入参要求是一个金额,你前端没做限制,用户随便过来一个负数,或者...
- 28个验证注解,通过业务案例让你精通Java数据校验(收藏篇)
-
在现代软件开发中,数据验证是确保应用程序健壮性和可靠性的关键环节。JavaBeanValidation(JSR380)作为一个功能强大的规范,为我们提供了一套全面的注解工具集,这些注解能够帮...
- Springboot @NotBlank参数校验失效汇总
-
有时候明明一个微服务里的@Validated和@NotBlank用的好好的,但就是另一个里不能用,这时候问题是最不好排查的,下面列举了各种失效情况的汇总,供各位参考:1、版本问题springbo...
- 这可能是最全面的Spring面试八股文了
-
Spring是什么?Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架。...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- idea eval reset (50)
- vue dispatch (70)
- update canceled (42)
- order by asc (53)
- spring gateway (67)
- 简单代码编程 贪吃蛇 (40)
- transforms.resize (33)
- redisson trylock (35)
- 卸载node (35)
- np.reshape (33)
- torch.arange (34)
- node卸载 (33)
- npm 源 (35)
- vue3 deep (35)
- win10 ssh (35)
- exceptionininitializererror (33)
- vue foreach (34)
- idea设置编码为utf8 (35)
- vue 数组添加元素 (34)
- std find (34)
- tablefield注解用途 (35)
- python str转json (34)
- java websocket客户端 (34)
- tensor.view (34)
- java jackson (34)