自营性电商项目④_自营式电商平台缺点

boyanx2个月前技术教程13

类别管理--根据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
  ]
)

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

标签: superadmin

相关文章

SpringBoot 使用 Sa-Token 完成路由拦截鉴权

一、需求分析在前文,我们详细的讲述了在 Sa-Token 如何使用注解进行权限认证,注解鉴权虽然方便,却并不适合所有鉴权场景。假设有如下需求:项目中所有接口均需要登录认证校验,只有 “登录接口” 本身...

MYSQL存储引擎InnoDB(八十):InnoDB静态数据加密

InnoDB支持独立表空间、通用表空间、mysql系统表空间、重做日志和撤消日志的静态数据加密。从 MySQL 8.0.16 开始,还支持为模式和通用表空间设置加密默认值,这允许 DBA 控制在这些模...

【开源】基于 Vue 和 Gin 开发的前后端分离的开源框架

项目介绍Gin-vue-admin是一个基于vue和gin开发的全栈前后端分离的开发基础平台,旨在快速搭建中小型项目。拥有jwt鉴权,动态路由,动态菜单,casbin鉴权,表单生成器,代码生成器等功能...

Django干货知识分享,Admin管理工具

您好,欢迎来到这里学习Django框架,关注我会持续更新内容哦~如果本文对您有帮助,还请您点赞转发哦,谢谢!创建超级管理员python3 manage.py createsuperuser User...

NetBox 4.4.1 自动化安装脚本,一键安装

复制下面的内容,centos 保存为install.sh,设置执行权限,./install.sh*******************************#!/bin/bash# NetBox 4...

大数据Hadoop之——Hadoop HDFS多目录磁盘扩展与数据平衡实战操作

一、概述hdfs 需要存写大量文件,有时磁盘会成为整个集群的性能瓶颈,所以需要优化 hdfs 存取速度,将数据目录配置多磁盘,既可以提高并发存取的速度,还可以解决一块磁盘空间不够的问题。Hadoop...

发表评论    

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。