功能权限代码
- 从代码分层开始
- 分层设计是什么?有什么好处?
- 分层设计带来的好处
- 项目分层的目的
- 阿里分层建议
- DDD分层
- 代码编写
- 实体类
- Mapper层
- 结构设计
- Service层
- 结构设计
- Controller层
- 结构设计
- 规范及设计
- 遵循Restful API
- 遵循领域模型规约
- 对象拷贝
- 统一接口返回结果
源码地址
从代码分层开始
分层设计是什么?有什么好处?
分层设计将软件划分成若干层,每一层只解决一部分问题,通过所有层的协作来完成整体的目标。一个复杂的问题通过分解成一系统子问题,这样就有效的降低了每个子问题的规模与复杂度
分层设计带来的好处
- 降低了系统软件的复杂度。将一个复杂问题通过分解,分而治之。
- 功能的复用和封装
项目分层的目的
- 保证多成员参与的项目保持视觉一致性;
- 迭代与交接可以更加无缝;
- 减少设计出错率;
- 提升团队工作效率;
- 直接目的是约束设计行为,最终目的是确保设计合理统一
阿里分层建议
根据业务架构实践,结合业界分层规范与流行技术框架分析,推荐分层结构如图所示,默认上层依赖于下层,箭头关系表示可直接依赖,如:开放 API 层可以依赖于 Web 层(Controller 层),也可以直接依赖于 Service 层,依此类推
- 开放 API 层:可直接封装 Service 接口暴露成 RPC 接口;通过 Web 封装成 http 接口;网关控制层等。
- 终端显示层:各个端的模板渲染并执行显示的层。当前主要是 velocity 渲染,JS 渲染,JSP 渲染,移动端展示等。
- Web层:主要是对访问控制进行转发,各类基本参数校验,或者不复用的业务简单处理等。
- Service 层:相对具体的业务逻辑服务层。
- Manager 层:通用业务处理层,它有如下特征:
1) 对第三方平台封装的层,预处理返回结果及转化异常信息,适配上层接口。
2) 对 Service 层通用能力的下沉,如缓存方案、中间件通用处理。
3) 与 DAO 层交互,对多个 DAO 的组合复用。 - DAO 层:数据访问层,与底层 MySQL、Oracle、Hbase、OB 等进行数据交互。
- 第三方服务:包括其它部门 RPC 服务接口,基础平台,其它公司的 HTTP 接口,如淘宝开放平台、支付宝付款服务、高德地图服务等。
- 外部数据接口:外部(应用)数据存储服务提供的接口,多见于数据迁移场景中。
DDD分层
DDD(Domain-Driven Design,领域驱动设计)分层结构是一种在软件开发中广泛使用的架构模式,它旨在通过分层来降低系统的复杂度,提高代码的可维护性和可扩展性。DDD分层结构通常包括以下几个主要层次:
1. 用户接口层(User Interface Layer)
职责:负责向用户显示信息和解释用户指令。这里的用户可以是终端用户、程序、自动化测试或批处理脚本等。在微服务架构中,用户接口层常常通过API网关与前端应用和后端微服务进行交互。
特点:用户接口层主要负责接收用户请求,并将处理结果返回给用户。它通常不包含业务逻辑,而是将请求转发给应用层进行处理。
2. 应用层(Application Layer)
职责:主要负责服务的组合和编排,实现业务用例的执行顺序以及结果的拼装。它调用领域层的功能来处理用户请求,并将处理结果返回给用户接口层。
特点:应用层是微服务之间的交互通道,可以调用其他微服务的应用服务,完成微服务之间的服务组合和编排。它应该保持轻量,不包含复杂的业务逻辑,而是通过调用领域服务来完成业务操作。
3. 领域层(Domain Layer)
职责:包含业务概念、规则、领域模型等,是DDD分层架构的核心。它负责处理系统的核心业务逻辑,并通过各种校验手段保证业务的正确性。
特点:领域层包含实体(Entity)、值对象(Value Object)、聚合(Aggregate)、聚合根(Aggregate Root)和领域服务(Domain Service)等概念。实体和领域服务共同实现业务逻辑,其中实体会采用充血模型来实现所有与之相关的业务功能。领域服务则用于处理跨实体的业务逻辑。
4. 基础层(Infrastructure Layer)
职责:为其他各层提供通用的技术和基础服务,包括数据库访问、消息中间件、文件存储、缓存等。它封装了与底层技术相关的细节,使得上层应用可以更加专注于业务逻辑的实现。
特点:基础层采用依赖倒置设计,封装基础资源服务,实现应用层、领域层与基础层的解耦。这样可以降低外部资源变化对应用的影响,提高系统的稳定性和可维护性。
代码编写
现在开始写权限模块相关代码,如果系统代码结构以及相关类结构都已经固定,那么后续根据表结构可以用代码生成器生成基础代码,现在我们先设计基础代码结构。
实体类
- 把公共属性抽离出来创建一个实体公共父类BaseEntity,因为我们每个实体对象(与表映射)都会有
create_by
(创建人)、create_time
(创建时间)、update_by
(更新人)、update_time
(更新人)四个字段,同时由于系统设计成多租户架构,因此很多业务表通过tenant_id
来区分,但并不是所有表都需要这个属性,所以单独创建一个公共父类TenantEntity
。 - 类似这种所有服务都会用到的公共类,最好的方法是创建一个
common
模块,这样其它服务需要这些类就可以直接引用这个模块
①创建一个公共模块tps-common
②打开项目tps-common
,删除所有文件,只保留build.gradle
文件以及包,然后右击tps-common
模块创建tps-common-core
模块,结构如下图所示,同时在项目settings.gradle
添加tps-common
及tps-common-core
,然后gradle重新reload
rootProject.name = 'tps-cloud'
//系统模块
include 'tps-system'
include 'tps-common'
//common模块下子模块
include 'tps-common:tps-common-core'
include 'tps-common:tps-common-mybatis'
//系统模块下子模块
include 'tps-system:tps-system-api'
include 'tps-system:tps-system-biz'
③ 在tps-common-core
下创建entity包,然后创建BaseEntity
、TenantEntity
类
package com.tps.cloud.entity;
import java.io.Serializable;
import java.time.LocalDateTime;
public abstract class BaseEntity implements Serializable {
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 最后更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 创建者id
*/
@TableField(fill = FieldFill.INSERT, jdbcType = JdbcType.VARCHAR)
private String createBy;
/**
* 更新者id
*/
@TableField(fill = FieldFill.INSERT_UPDATE, jdbcType = JdbcType.VARCHAR)
private String updateBy;
/**
* 是否删除 0否 1是
*/
@TableLogic
private Boolean deleted;
}
package com.tps.cloud.entity;
/**
* 租户基类
*/
public abstract class TenantEntity extends BaseEntity {
/**
* 多租户id
*/
private Long tenantId;
}
配置@TableField(fill =***)属性需要注入MetaObjectHandler ,这里我们自定义填充规则
/**
* mybatis plus 统一配置
*/
@AutoConfiguration
public class MybatisAutoConfiguration implements WebMvcConfigurer {
/**
* 审计字段自动填充
* @return {@link MybatisPlusMetaObjectHandler}
*/
@Bean
public MybatisPlusMetaObjectHandler mybatisPlusMetaObjectHandler() {
return new MybatisPlusMetaObjectHandler();
}
}
/**
* MybatisPlus 自动填充配置
*/
public class MybatisPlusMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
LocalDateTime now = LocalDateTime.now();
fillValIfNullByName("createTime", now, metaObject, true);
fillValIfNullByName("updateTime", now, metaObject, true);
}
@Override
public void updateFill(MetaObject metaObject) {
fillValIfNullByName("updateTime", LocalDateTime.now(), metaObject, true);
}
/**
* 填充值,先判断是否有手动设置,优先手动设置的值,例如:job必须手动设置
* @param fieldName 属性名
* @param fieldVal 属性值
* @param metaObject MetaObject
* @param isCover 是否覆盖原有值,避免更新操作手动入参
*/
private static void fillValIfNullByName(String fieldName, Object fieldVal, MetaObject metaObject, boolean isCover) {
// 0. 如果填充值为空
if (fieldVal == null) {
return;
}
// 1. 没有 set 方法
if (!metaObject.hasSetter(fieldName)) {
return;
}
// 2. 如果用户有手动设置的值
Object userSetValue = metaObject.getValue(fieldName);
String setValueStr = StrUtil.str(userSetValue, Charset.defaultCharset());
if (StrUtil.isNotBlank(setValueStr) && !isCover) {
return;
}
// 3. field 类型相同时设置
Class<?> getterType = metaObject.getGetterType(fieldName);
if (ClassUtils.isAssignableValue(getterType, fieldVal)) {
metaObject.setValue(fieldName, fieldVal);
}
}
}
注意@AutoConfiguration
需要在resource新增org.springframework.boot.autoconfigure.AutoConfiguration.imports文件,并且配置com.tps.cloud.handler.MybatisPlusMetaObjectHandler,具体原理可以百度
1.目前时间类型属性推荐LocalDateTime,相较于Date有以下优点:
- 功能丰富的API:LocalDateTime提供了丰富的方法来处理日期和时间,包括时区、日期时间计算、格式化等,这使得它在处理日期和时间方面具有更高的灵活性和便利性。
- 不可变性:LocalDateTime是不可变的,一旦创建,其值不会改变。这种特性有助于避免在多线程环境中出现并发问题,提高了代码的健壮性和可靠性。
- 更好的日期时间分离:与Date类相比,LocalDateTime允许更好地分离日期和时间信息,使得处理仅涉及日期或时间的情况更加简单。
- 时区支持:LocalDateTime可以与时区信息一起使用,更好地处理跨时区问题,而Date类默认不包含时区信息,其实际值会受到系统默认时区的影响。
- 更直观的操作:LocalDateTime的方法和操作更直观,更易于理解和使用,相比之下,Date类的API设计较为繁琐,使用起来不够直观
2.
@TableField
作用:@TableField注解主要用于标记实体类中的字段与数据库表中的列的映射关系。通过该注解,可以指定实体类中的
字段与数据库表中的列名进行映射,以及控制字段在SQL操作中的行为。
属性表
属性 类型 描述 value String 数据库字段名 exist boolean exist = false 表示该属性不是数据库字段,新增等使用bean的时候,mybatis-plus就会忽略这个,不会报错 condition String 预处理 where 实体查询比较条件,有值设置则按设置的值为准,没有则为默认全局的 %s=#{%s}。@TableField(condition = SqlCondition.LIKE)输出SQL为: select 表 where name LIEK CONCAT(‘%’,值,‘%’) update String 预处理 update set 部分注入,例如:当在age字段上注解update=“%s+1” 表示更新时会 set age=age+1 (该属性优先级高于 el 属性) insertStrategy FieldStrategy Mybatis-plus insert对字段的操作 updateStrategy FieldStrategy Mybatis-plus update对字段的操作 whereStrategy FieldStrategy Mybatis-plus where条件对字段的操作 fill FieldFill 字段自动填充策略 select boolean @TableField(select = false) 查询时,则不返回该字段的值 。 keepGlobalFormat boolean 是否保持使用全局的 format 进行处理 jdbcType JdbcType JDBC 类型 (该默认值不代表会按照该值生效) typeHandler Class<? extends TypeHandler> 类型处理器 (该默认值不代表会按照该值生效) numericScale String 指定小数点后保留的位数
FieldStrategy
枚举值
值 描述 IGNORED 忽略判断,该字段值不论是什么,都进行更新 NOT_NULL (默认)不为null则更新,也就是字段值为null则不生成到sql中不更新该字段,如果字段值为""(空字符串)也是会更新的 NOT_EMPTY 不为空则更新,注意该字段值为null或为""(空字符串)都不会更新 DEFAULT 默追随全局配置,和IGNORED 中的配置保持一致 NEVER 不做更新操作,该字段值不论是什么,都不进行更新
FieldFill
枚举值
值 描述 DEFAULT 默认不处理 INSERT 插入时填充字段 UPDATE 更新时填充字段 INSERT_UPDATE 插入和更新时填充字段 应用场景
- 映射数据库表字段名与实体类属性名不一致的情况。通过设置value属性,可以灵活地指定属性对应的数据库表字段名,避免在查询和更新操作时发生字段名不匹配的错误。
- 设置插入和更新时需要忽略的字段。通过设置insert和update属性,可以灵活地控制是否插入或者更新某个属性,避免不必要的数据库操作。
- 根据条件查询指定的字段。通过设置condition属性,可以指定查询时的条件,只有满足条件的数据才会查询到该字段,提高查询性能。
3.
@TableLogic
作用:@TableLogic:表示逻辑删除注解,在字段上加上这个注解再执行BaseMapper的删除方法时,删除方法就会变成修改。
例:
实体类:
@TableLogic
private Integer del;
Service层:
调用BaseMapper的deleteById(id);
执行是效果:
加@TableLogic的情况下
走 Update 表名 set 加注解的列=值 where del=值
不加@TableLogic的情况下
走delete from 表名 where del=值
@TableLogic注解参数
value = “” 默认的原值
delval = “” 删除后的值
@TableLogic(value=“原值”,delval=“改值”)
④ 关于实体类的Getter/Setter,有两种方式,一种是lombok生成,一种是通过IDEA快捷键直接生成,这里我们选择使用lombok,首先需要在项目根目录build.gradle
引入lombok,由于后面很多模块存在共同引用,所以我们使用allprojects
标签,同时添加buildscript标签,在buildscript标签中我们通过在ext可以管理jar版本,就相当于在maven通过properties管理版本号
buildscript {
ext {
springBootVersion = '3.3.1'
lombokVersion = '1.18.34'
mybatisPlusVersion = '3.5.7'
mybatisPlusJoinVersion = '1.4.13'
mysqlVersion = '8.0.23'
springValidationVersion = '3.3.2'
hutoolVersion='5.8.29'
}
// 指定插件的仓库
repositories {
maven { url 'https://maven.aliyun.com/repository/public/' }
maven { url 'https://mirrors.cloud.tencent.com/nexus/repository/maven-public/' }
mavenCentral()
}
dependencies {
classpath "org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}"
}
}
group = 'com.tps.cloud'
version = '0.0.1-SNAPSHOT'
allprojects {
apply plugin: 'java'
apply plugin: 'io.spring.dependency-management'
apply plugin: 'org.springframework.boot'
repositories {
maven { url 'https://maven.aliyun.com/repository/public/' }
maven { url 'https://mirrors.cloud.tencent.com/nexus/repository/maven-public/' }
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
//构建Web应用程序
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
//Mybatis-plus依赖
implementation "com.baomidou:mybatis-plus-spring-boot3-starter:${mybatisPlusVersion}"
//MyBatis 联表查询
implementation "com.github.yulichang:mybatis-plus-join-boot-starter:${mybatisPlusJoinVersion}"
//mysql驱动
implementation "mysql:mysql-connector-java:${mysqlVersion}"
//hutool工具包
implementation "cn.hutool:hutool-core:${hutoolVersion}"
//lombok引入
compileOnly "org.projectlombok:lombok:${lombokVersion}"
annotationProcessor "org.projectlombok:lombok:${lombokVersion}"
}
}
同时对模块多余文件进行删除,删除后项目结构展示如下:
其次IDEA启用 Annotation processor以及安装lombok插件
⑤然后在实体类BaseEntity
、TenantEntity
上添加@Data、@EqualsAndHashCode注解
@Data
public abstract class BaseEntity implements Serializable {
省略...
}
@Data
@EqualsAndHashCode(callSuper=true)
public abstract class TenantEntity extends BaseEntity {
省略...
}
@EqualsAndHashCode(callSuper = true)
是Lombok注解之一,用于自动生成equals(Object other)和hashCode()方法。
当我们使用该注解时,Lombok会自动为我们生成equals(Object other)和hashCode()方法的实现代码。其中,callSuper属性设置为true表示要调用父类的equals和hashCode方法,以确保在多层继承结构中也能正确比较对象的相等性
⑥ 在tps-system-biz
模块下entity
包中创建SystemUser、SystemRole、SystemMenu、SystemDept、SystemPost、SystemRoleMenu、SystemTenant、SystemTenantPackage、SystemUserPost、SystemUserRole,这些实体类根据实际是否需要租户id继承不同父类
1.
@TableName
在实体类上指定,指定实体类和数据库表的映射关系。当实体类的类名在转成小写后和数据库表名相同时,可以不指定该注解
2.@TableId
用于注释主键。mybatisplus默认主键的名字是id,如果表中不叫id而是叫uid或者userid的话,就需要标识主键
同时mybatisplus默认主键的生成策略为雪花算法,而要修改这个主键策略也是要通过@TableId来实现。
@TableId
注解的属性值包括如下:
属性 类型 是否必须 默认值 描述 value String 否 “” 数据库字段名称 type Enum 否 IdType.NONE 主键类型
IdType
值的描述:
IdType值 描述 AUTO 数据库 ID自增 NONE 未设置主键类型。默认雪花算法 INPUT 插入前自己设置主键值 ASSIGN_ID 雪花算法。默认策略 ASSIGN_UUID 没有中划线的UUID
部门对象
package com.tps.cloud.system.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.tps.cloud.entity.TenantEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* 部门表(SystemDept)实体类
*
* @author zyn
* @since 2024-07-29 15:00:46
*/
@TableName("system_dept")
@Data
@EqualsAndHashCode(callSuper = true)
public class SystemDept extends TenantEntity implements Serializable {
private static final long serialVersionUID = 620694442588067381L;
/**
* 部门id
*/
@TableId
private Long id;
/**
* 部门名称
*/
private String name;
/**
* 父部门id
*/
private Long parentId;
/**
* 显示顺序
*/
private Integer sort;
/**
* 负责人id
*/
private Long leaderUserId;
/**
* 联系电话
*/
private String phone;
/**
* 邮箱
*/
private String email;
/**
* 部门状态(0正常 1停用)
*/
private Integer status;
}
菜单权限对象
package com.tps.cloud.system.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.tps.cloud.entity.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* 菜单权限表(SystemMenu)
*/
@TableName("system_menu")
@Data
@EqualsAndHashCode(callSuper = true)
public class SystemMenu extends BaseEntity{
private static final long serialVersionUID = 999093587954270863L;
/**
* 菜单ID
*/
@TableId
private Long id;
/**
* 菜单名称
*/
private String name;
/**
* 权限标识
*/
private String permission;
/**
* 菜单类型
*/
private Integer type;
/**
* 显示顺序
*/
private Integer sort;
/**
* 父菜单ID
*/
private Long parentId;
/**
* 路由地址
*/
private String path;
/**
* 菜单图标
*/
private String icon;
/**
* 菜单状态
*/
private Integer status;
/**
* 是否可见
*/
private Boolean visible;
/**
* 是否缓存
*/
private Boolean keepAlive;
/**
* 是否总是显示
*/
private Boolean alwaysShow;
}
部门对象
package com.tps.cloud.system.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.tps.cloud.entity.TenantEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 部门表(SystemDept)
*/
@TableName("system_dept")
@Data
@EqualsAndHashCode(callSuper = true)
public class SystemDept extends TenantEntity {
private static final long serialVersionUID = 620694442588067381L;
/**
* 部门id
*/
@TableId
private Long id;
/**
* 部门名称
*/
private String name;
/**
* 父部门id
*/
private Long parentId;
/**
* 显示顺序
*/
private Integer sort;
/**
* 负责人id
*/
private Long leaderUserId;
/**
* 联系电话
*/
private String phone;
/**
* 邮箱
*/
private String email;
/**
* 部门状态(0正常 1停用)
*/
private Integer status;
}
岗位对象
package com.tps.cloud.system.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.tps.cloud.entity.TenantEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 岗位信息表(SystemPost)
*/
@TableName("system_post")
@Data
@EqualsAndHashCode(callSuper = true)
public class SystemPost extends TenantEntity {
private static final long serialVersionUID = -66365300876267680L;
/**
* 岗位id
*/
@TableId
private Long id;
/**
* 岗位编码
*/
private String code;
/**
* 岗位名称
*/
private String name;
/**
* 显示顺序
*/
private Integer sort;
/**
* 状态(0正常 1停用)
*/
private Integer status;
/**
* 备注
*/
private String remark;
}
角色对象
package com.tps.cloud.system.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.tps.cloud.entity.TenantEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 角色信息表(SystemRole)
*/
@TableName("system_role")
@Data
@EqualsAndHashCode(callSuper = true)
public class SystemRole extends TenantEntity {
private static final long serialVersionUID = 304249783530732202L;
/**
* 角色id
*/
@TableId
private Long id;
/**
* 角色名称
*/
private String name;
/**
* 角色标识
*/
private String code;
/**
* 显示顺序
*/
private Integer sort;
/**
* 角色状态(0正常 1停用)
*/
private Integer status;
/**
* 角色类型
*/
private Integer type;
/**
* 备注
*/
private String remark;
}
角色菜单关联
package com.tps.cloud.system.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.tps.cloud.entity.TenantEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.util.Date;
/**
* 角色和菜单关联表(SystemRoleMenu)
*/
@TableName("system_role_menu")
@Data
@EqualsAndHashCode(callSuper = true)
public class SystemRoleMenu extends TenantEntity {
private static final long serialVersionUID = 694647751117838041L;
/**
* 关联id
*/
@TableId
private Long id;
/**
* 角色id
*/
private Long roleId;
/**
* 菜单id
*/
private Long menuId;
}
租户对象
package com.tps.cloud.system.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.tps.cloud.entity.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
* 租户表(SystemTenant)
*/
@TableName("system_tenant")
@Data
@EqualsAndHashCode(callSuper = true)
public class SystemTenant extends BaseEntity {
private static final long serialVersionUID = -77073153072486330L;
/**
* 租户id
*/
@TableId
private Long id;
/**
* 租户名
*/
private String name;
/**
* 联系人的用户编号
*/
private Long contactUserId;
/**
* 联系人
*/
private String contactName;
/**
* 联系手机
*/
private String contactMobile;
/**
* 租户状态(0正常 1停用)
*/
private Integer status;
/**
* 绑定域名
*/
private String website;
/**
* 套餐编号
*/
private Long packageId;
/**
* 开始时间
*/
private LocalDateTime startTime;
/**
* 结束时间
*/
private LocalDateTime endTime;
/**
* 账号限额
*/
private Integer accountCount;
}
租户套餐表
package com.tps.cloud.system.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.tps.cloud.entity.TenantEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 租户套餐表(SystemTenantPackage)
*/
@TableName("system_tenant_package")
@Data
@EqualsAndHashCode(callSuper = true)
public class SystemTenantPackage extends TenantEntity {
private static final long serialVersionUID = 974574673435548734L;
/**
* 套餐id
*/
@TableId
private Long id;
/**
* 套餐名
*/
private String name;
/**
* 租户状态(0正常 1停用)
*/
private Integer status;
/**
* 备注
*/
private String remark;
/**
* 关联的菜单编号
*/
private String menuIds;
}
用户信息表
package com.tps.cloud.system.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.tps.cloud.entity.TenantEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
* 用户信息表(SystemUser)
*/
@TableName("system_user")
@Data
@EqualsAndHashCode(callSuper = true)
public class SystemUser extends TenantEntity {
private static final long serialVersionUID = -22571814685261082L;
/**
* 用户id
*/
@TableId
private Long id;
/**
* 用户账号
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 用户昵称
*/
private String nickname;
/**
* 备注
*/
private String remark;
/**
* 部门id
*/
private Long deptId;
/**
* 用户邮箱
*/
private String email;
/**
* 手机号码
*/
private String mobile;
/**
* 用户性别
*/
private Integer sex;
/**
* 头像地址
*/
private String avatar;
/**
* 帐号状态(0正常 1停用)
*/
private Integer status;
/**
* 最后登录IP
*/
private String loginIp;
/**
* 最后登录时间
*/
private LocalDateTime loginDate;
}
用户岗位表
package com.tps.cloud.system.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.tps.cloud.entity.TenantEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
import java.io.Serializable;
/**
* 用户岗位表(SystemUserPost)
*/
@TableName("system_user")
@Data
@EqualsAndHashCode(callSuper = true)
public class SystemUserPost extends TenantEntity {
private static final long serialVersionUID = 662558938338965975L;
/**
* 关联id
*/
@TableId
private Long id;
/**
* 用户id
*/
private Long userId;
/**
* 岗位id
*/
private Long postId;
}
用户和角色关联对象
package com.tps.cloud.system.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.tps.cloud.entity.TenantEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 用户和角色关联表(SystemUserRole)
*/
@TableName("system_user")
@Data
@EqualsAndHashCode(callSuper = true)
public class SystemUserRole extends TenantEntity {
private static final long serialVersionUID = -56990064264317914L;
/**
* 关联id
*/
@TableId
private Long id;
/**
* 用户id
*/
private Long userId;
/**
* 角色id
*/
private Long roleId;
}
⑦ 创建业务模块的Service、ServiceImpl、Controller、Mapper,具体接口以及业务逻辑这里就不展示,因为实际代码逻辑需要根据需求来编写,这里主要是对每一层需要注意的点进行强调。
Mapper层
结构设计
MyBatis-Plus-Join
MyBatis-Plus
首先了解一下上面两个工具就知道mapper应该怎么写,此层有三种用法,我们采用第三种自定义mapper,方便后续拓展
1.继承BaseMapper
BaseMapper 是 Mybatis-Plus 提供的一个通用 Mapper 接口,它封装了一系列常用的数据库操作方法,包括增、删、改、查等。通过继承 BaseMapper,开发者可以快速地对数据库进行操作,而无需编写繁琐的 SQL 语句。
@Mapper
public interface SystemUserMapper extends BaseMapper<SystemUser> {
}
2.继承MPJBaseMapper
支持联表查询
@Mapper
public interface SystemUserMapper extends MPJBaseMapper<SystemDept> {
}
3.自定义Mapper
通过自定义Mapper扩展自定义功能
public interface CustomBaseMapper<T> extends MPJBaseMapper<T> {
}
@Mapper
public interface SystemUserMapper extends MPJBaseMapper<SystemDept> {
}
服务模块启动类注意添加@MapperScan 用于扫描映射器接口
@SpringBootApplication
@MapperScan("com.tps.cloud.system.mapper")
public class TpsSystemBizApplication {
public static void main(String[] args) {
SpringApplication.run(TpsSystemBizApplication.class, args);
}
}
@Mapper
定义和用途
在MyBatis框架中,@Mapper是一个标记接口,用于标记一个接口作为映射器接口。映射器接口是MyBatis的核心概念之一,它将Java方法>与SQL语句关联起来。每个映射器接口方法对应于一个SQL语句,该SQL语句在映射器XML文件或使用@Select, @Update, @Insert, >@Delete等注解在接口方法中直接定义。
当你在接口上使用@Mapper注解,MyBatis会知道这个接口是一个映射器接口,并将它注册到MyBatis的配置中。然后,你可以从MyBatis的SqlSession中获取这个映射器接口的实例,并调用其方法来执行SQL语句
@MapperScan
定义和用途
@MapperScan 是 MyBatis-Spring 集成包提供的一个注解,用于自动扫描指定包下的所有映射器接口,并将它们注册到 Spring 上下文中。这样,你就可以像其他 Spring 组件一样,通过 @Autowired 注解来注入映射器接口。
使用 @MapperScan 可以避免手动为每个映射器接口创建一个映射器 bean。这在你有大量映射器接口时特别有用。
@Mapper
注解与@MapperScan
注解的区别
@Mapper 和 @MapperScan 是 MyBatis 和 MyBatis-Spring 提供的两个注解,它们都用于处理映射器接口,但它们的用途和工作方式有所不同。
1.@Mapper
:这是 MyBatis 提供的注解,用于将一个接口标记为映射器接口。你需要在每个映射器接口上使用这个注解。然后,你可以通过 MyBatis 的 SqlSession 获取映射器接口的实例,并调用其方法来执行 SQL 语句。
@Mapper 注解针对的是一个一个的类,相当于是一个一个 Mapper.xml 文件。而一个接口一个接口的使用 @Mapper,太麻烦了,于是 @MapperScan 就应用而生了。@MapperScan 配置一个或多个包路径,自动的扫描这些包路径下的类,自动的为它们生成代理类。
2.@MapperScan
:这是 MyBatis-Spring 提供的注解,用于自动扫描指定包下的所有映射器接口,并将它们注册到 Spring 上下文中。这样,你就可以像其他 Spring 组件一样,通过 @Autowired 注解来注入映射器接口。你只需要在一个配置类上使用这个注解,而不是在每个映射器接口上使用。
当使用了 @MapperScan 注解,将会生成 MapperFactoryBean, 如果没有标注 @MapperScan 也就是没有 MapperFactoryBean 的实例,就走 @Import 里面的配置,具体可以在 AutoConfiguredMapperScannerRegistrar 和 MybatisAutoConfiguration 类中查看源代码进行分析。
3.当同时使用这两个注解时,应确保@MapperScan配置的包包含了所有需要被代理的接口。如果某些接口只在@Mapper上被标记,而没有在@MapperScan的扫描范围内,那么这些接口将不会被代理,可能会导致运行时错误。
最佳实践是在可能的情况下尽量依赖@MapperScan进行批量扫描,除非有特殊需求才在个别接口上使用@Mapper注解。
Service层
结构设计
此处框架我们选择第二种方式,如果需要封装自己的service公共方法可以采用第三种设计。
1.普通写法
public interface SystemUserService {
}
@Service
@AllArgsConstructor
public class SystemUserServiceImpl implements SystemUserService {
private final SystemUserMapper systemUserMapper;
}
2.使用Mybatis-Plus的IService
IService 是 MyBatis-Plus 提供的一个通用 Service 层接口,它封装了常见的 CRUD 操作,包括插入、删除、查询和分页等。通过继承 IService 接口,可以快速实现对数据库的基本操作,同时保持代码的简洁性和可维护性。
public interface SystemUserService extends IService<SystemUser> {
}
@Service
@AllArgsConstructor
public class SystemUserServiceImpl extends ServiceImpl<SystemUserMapper, SystemUser> implements SystemUserService {
}
3.自定义Service公共接口
仿照Mybatis-Plus的结构自己封装一些公共方法,比如增删改查,同时还可以封装导入导出公共方法,这样更具有拓展性。
public interface BaseService<T> extends IService<T>{
}
public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> extends ServiceImpl<M, T> implements BaseService<T> {
@Autowired
protected M baseMapper;
}
public interface SystemUserService extends BaseService<SystemUser> {
}
@Service
@AllArgsConstructor
public class SystemUserServiceImpl extends BaseServiceImpl<SystemUserMapper, SystemUser> implements SystemUserService {
}
@Service
作用:
@Service注解用于告诉Spring容器,被注解的类是一个服务类。它是Spring框架中的一个组件扫描注解,用于自动扫描并创建实例,以便在应用程序中使用。
与@Component的关系:
@Service注解实际上是@Component注解的一个特例。@Component用于通用的组件标记,而@Service用于服务层的类。它们的区别在于语义上的不同,但从功能上来说是一样的,即告诉Spring要将这个类纳入管理并创建其实例。
Controller层
结构设计
本框架采用第二种方式,第一种有点过度设计的味道,主要因为多数模块的增删改查总是存在一些不同的地方,采用共同方法需要覆盖继承下来的方法,导致代码臃肿
1.封装公共控制类
通过在BaseController封装增删改查导入导出方法,这样每个模块的控制类增删改查以及封装的公共接口就不需要再各个控制类再写一遍。
public interface BaseController<T extends BaseDTO,E extends BaseEntity>{
}
public abstract class AbstractBaseController<T extends BaseDTO, E extends BaseEntity> extends BaseController<T, E>{
}
@RequestMapping("/system/user")
public interface SystemUserController extends BaseController<SystemUserDto, SystemUser> {
}
@RestController
public class SystemUserControllerImpl extends AbstractBaseController<SystemUserDto, SystemUser> implements InterfaceAppController {
}
2.普通方式
@RestController
@AllArgsConstructor
@RequestMapping("/system/user")
public class SystemUserController {
private final SystemUserService systemUserService;
}
@RestController
@RestController
是 Spring Framework 中的注解,是@Controller
注解的一个变体。与 @Controller 注解不同,@RestController
注解用于标识一个类是 RESTful 风格的控制器组件,它结合了@Controller
和@ResponseBody
的功能,使得处理请求并返回数据更加方便.
具体来说,@RestController
注解用于标记一个类,表明该类是一个控制器,并且其下的方法都将返回数据作为响应。使用@RestController
注解时,不再需要在方法上添加@ResponseBody
注解,因为@RestController
默认将所有方法的返回值自动序列化为响应体。
**@RequestMapping
**
@RequestMapping
是一个用来处理请求地址到处理器 controller 功能方法映射规则的注解,这个注解会将 HTTP 请求映射到 MVC 和 REST 控制器的处理方法 controller 上,可用于类或方法上。注解在类上,表示类中的所有响应请求的方法都是以该地址作为父路径(模块路径)
属性名 描述 value(path) 指定请求的实际访问地址,默认@RequestMapping(“url”)的值url即为value的值。指定的地址可以是 URI Template 模式。 method 指定请求的method类型,主要有 GET、POST、DELETE、PUT等; params 指定request中必须包含某些参数值,包含才让该方法处理请求。 headers 指定request中必须包含某些指定的header值,包含才能让该方法处理请求。 consumes 指定处理请求之后返回数据类型,例如application/json,text/html等。 produces 指定返回的内容类型,当且仅当request请求头中的(Accept)类型中包含该指定类型才返回; 控制层下还会用到以下注解
@PostMapping
它是@RequestMapping(method = RequestMethod.POST)
的缩写。它用于处理HTTP POST请求的方法,只能标注在方法上。使用@PostMapping
注解的方法将仅响应POST请求
@GetMapping
它是@RequestMapping(method = RequestMethod.GET)
的缩写。它用于处理HTTP GET请求的方法,也只能标注在方法上。使用@GetMapping
注解的方法将仅响应GET请求
规范及设计
遵循Restful API
网络上一堆解释的,大家自己了解一下
Restful API 接口规范详解
搞懂 API 和 RESTful API
遵循领域模型规约
DO(Data Object):与数据库表结构一一对应,通过DAO层向上传输数据源对象。
DTO(Data Transfer Object):数据传输对象,Service或Manager向外传输的对象。
BO(Business Object):业务对象。由Service层输出的封装业务逻辑的对象。
AO(Application Object):应用对象。在Web层与Service层之间抽象的复用对象模型,极为贴近展示层,复用度不高。
VO(View Object):显示层对象,通常是Web向模板渲染引擎层传输的对象。
Query:数据查询对象,各层接收上层的查询请求。注意超过2个参数的查询封装,禁止使用Map类来传输
具体详情可以百度
对象拷贝
如果遵循领域模型规范,在代码中经常遇到DTO转DO以及DO转VO的情况,以前为了图方便,所有对象转换都使用BeanUtils.copyProperties
或者使用hutool的BeanUtils进行转换,本框架复杂对象转换建议使用MapStruct
(官网),简单使用BeanUtils.copyProperties。
@RestController
@AllArgsConstructor
@RequestMapping("/system/user")
public class SystemUserController {
private final SystemUserService systemUserService;
@PostMapping("/save")
public Result<Long> save(@RequestBody SystemUserDto systemUserDto) {
systemUserService.save(systemUserDto);
return Result.ok();
}
}
@Service
@AllArgsConstructor
public class SystemUserServiceImpl extends ServiceImpl<SystemUserMapper, SystemUser> implements SystemUserService {
@Override
public Long save(SystemUserDto systemUserDto) {
SystemUser user=new SystemUser();
//此处将DTO转化为DO
BeanUtils.copyProperties(systemUserDto,user);
super.save(user);
return user.getId();
}
}
为啥不建议用BeanUtils.copyProperties拷贝数据
再见 BeanUtils!对比 12 种 Bean 自动映射工具,就它性能最拉跨
统一接口返回结果
前端接口请求后台端,后端将返回结果统一封装。提高交互的规范性及通用性,也提高了前后端联调效率。前端根据规范格式返回结构体进行统一映射处理,就避免一个接口一个返回格式的问题
通常来说,一个统一返回结果类会包含以下几个基本属性:
- code:响应状态码。一般来说,我们会规定1代表操作成功,0代表操作失败。
- msg:响应的消息,用于简单描述本次请求的结果或错误信息。
- data:返回的数据。这个字段通常在请求成功,并且需要返回额外数据时使用。
包含以下方法: - 成功返回(无参)
- 成功返回(数据)
- 成功返回(返回信息 + 数据)
- 失败返回(无参)
- 失败返回(返回信息)
- 失败返回(数据)
- 失败返回(返回信息 + 数据)
- 全参数方法
package com.tps.cloud.response;
import lombok.*;
import lombok.experimental.Accessors;
import java.io.Serializable;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class Result<T> implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 返回码
*/
private int code;
/**
* 返回信息
*/
private String msg;
/**
* 返回数据
*/
private T data;
/**
* 成功返回(无参)
* @return
* @param <T>
*/
public static <T> Result<T> ok() {
return restResult(null, ResultCode.SUCCESS.getCode(), null);
}
/**
* 成功返回(数据)
* @param data
* @return
* @param <T>
*/
public static <T> Result<T> ok(T data) {
return restResult(data, ResultCode.SUCCESS.getCode(), null);
}
/**
* 成功返回(返回信息 + 数据)
* @param data
* @param msg
* @return
* @param <T>
*/
public static <T> Result<T> ok(T data, String msg) {
return restResult(data, ResultCode.SUCCESS.getCode(), msg);
}
/**
* 失败返回(无参)
* @return
* @param <T>
*/
public static <T> Result<T> failed() {
return restResult(null, ResultCode.ERROR.getCode(), null);
}
/**
* 失败返回(返回信息)
* @param msg
* @return
* @param <T>
*/
public static <T> Result<T> failed(String msg) {
return restResult(null, ResultCode.ERROR.getCode(), msg);
}
/**
* 失败返回(数据)
* @param data
* @return
* @param <T>
*/
public static <T> Result<T> failed(T data) {
return restResult(data, ResultCode.ERROR.getCode(), null);
}
/**
* 失败返回(返回信息 + 数据)
* @param data
* @param msg
* @return
* @param <T>
*/
public static <T> Result<T> failed(T data, String msg) {
return restResult(data, ResultCode.ERROR.getCode(), msg);
}
/**
* 全参数方法
* @param data
* @param code
* @param msg
* @return
* @param <T>
*/
public static <T> Result<T> restResult(T data, int code, String msg) {
Result<T> apiResult = new Result<>();
apiResult.setCode(code);
apiResult.setData(data);
apiResult.setMsg(msg);
return apiResult;
}
}
package com.tps.cloud.response;
import lombok.Getter;
@Getter
public enum ResultCode {
/* 成功状态码 */
SUCCESS(200, "成功"),
/* 错误状态码 */
NOT_FOUND(404, "请求的资源不存在"),
ERROR(500, "服务器内部错误"),
PARAMETER_EXCEPTION(501, "请求参数校验异常");
private final Integer code;
private final String message;
ResultCode(Integer code, String message) {
this.code = code;
this.message = message;
}
}