计算机系统应用教程网站

网站首页 > 技术文章 正文

自营性电商项目④ 自营式电商平台的主要优势在于

btikc 2024-09-25 15:18:29 技术文章 29 ℃ 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
  ]
)

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

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表