源码下载链接:xxlinx/sky-take-out
1. 环境搭建
技术选型
后端项目结构
sky-take-out | maven父工程,统一管理依赖版本,聚合其他子模块 |
sky-common | 子模块,存放公共类,例如:工具类、常量类、异常类等 |
sky-pojo | 子模块,存放实体类、VO、DTO等 |
sky-server | 子模块,配置文件、Controller、Service、Mapper等 |
sky-common
存放的是一些公共类,可以供其他模块使用
sky-pojo
存放的是一些 entity、DTO、VO
sky-server
存放的是 配置文件、配置类、拦截器、controller、service、mapper、启动类等
数据库
参考数据库设计文档
前后端联调
前端发送的请求,是如何请求到后端服务的?
前端请求地址:http://localhost/api/employee/login
后端接口地址:http://localhost:8080/admin/employee/login
nginx 反向代理
就是将前端发送的动态请求由 nginx 转发到后端服务器
nginx 反向代理的好处:
- 提高访问速度,nginx可以进行缓存
- 进行负载均衡,针对分布式系统
- 保证后端服务安全,不会对外公开自己的服务调用接口
配置方式
在文件 nginx.conf
反向代理的配置方式:
server{
listen 80;
server_name localhost;
location /api/ {
proxy_pass http://localhost:8080/admin/; #反向代理
}
}
nginx 负载均衡的配置方式:
upstream webservers{
server 192.168.100.128:8080;
server 192.168.100.129:8080;
}
server{
listen 80;
server_name localhost;
location /api/ {
proxy_pass http://webservers/admin/; #负载均衡 默认为轮询
}
}
2. 登录功能
员工表中的密码是明文存储,安全性太低,采用 MD5 加密格式
拦截器配置
需求:在调用用户登录接口的时候不需要进行 jwtToken 认证,其他接口都需要进行认证
拦截器对动态方法进行拦截
登录Controller
对于新登录的用户,生成一个 jwt 令牌
@PostMapping("/login")
@ApiOperation(value = "员工登录")
public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) {
log.info("员工登录:{}", employeeLoginDTO);
Employee employee = employeeService.login(employeeLoginDTO);
//登录成功后,生成jwt令牌
Map<String, Object> claims = new HashMap<>();
claims.put(JwtClaimsConstant.EMP_ID, employee.getId());
String token = JwtUtil.createJWT(
jwtProperties.getAdminSecretKey(),
jwtProperties.getAdminTtl(),
claims);
EmployeeLoginVO employeeLoginVO = EmployeeLoginVO.builder()
.id(employee.getId())
.userName(employee.getUsername())
.name(employee.getName())
.token(token)
.build();
return Result.success(employeeLoginVO);
}
登录service
因为数据库里存的是进行 md5 加密后的信息,在进行密码对比的时候,需要将前端传入的明文密码转为 md5 后再进行对比
3. Swagger
Knife4j 是为Java MVC框架集成Swagger生成Api文档的增强解决方案
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.2</version>
</dependency>
使用方式
1. 导入 knife4j 的maven坐标
2. 在配置类中加入 knife4j 相关配置
WebMvcConfiguration.java
@Bean
public Docket docket() {
log.info("准备生产接口文档");
ApiInfo apiInfo = new ApiInfoBuilder()
.title("苍穹外卖项目接口文档")
.version("2.0")
.description("苍穹外卖项目接口文档")
.build();
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo)
.select()
.apis(RequestHandlerSelectors.basePackage("com.sky.controller"))
.paths(PathSelectors.any())
.build();
return docket;
}
3. 设置静态资源映射,否则接口文档页面无法访问
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
log.info("开始设置静态资源映射");
registry.addResourceHandler("/doc.html").
addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").
addResourceLocations("classpath:/META-INF/resources/webjars/");
}
4. 访问
接口文档访问路径为 http://ip:port/doc.html
常用注解
注解 | 说明 |
@Api | 用在类上,例如Controller,表示对类的说明 |
@ApiModel | 用在类上,例如entity、DTO、VO |
@ApiModelProperty | 用在属性上,描述属性信息 |
@ApiOperation | 用在方法上,例如Controller的方法,说明方法的用途、作用 |
4. 员工管理
开发都是采用三层结构(MVC模式),具体查看源码
代码开发:
1. 设计接受前端传入的 DTO
2. 在 Controller 定义执行方法
3. 在 Service,ServiceImpl 中进行实现方法逻辑
4. 在 Mapper 层进行对数据库的调用查询
5. Controller 返回前端需要的数据类型
新增员工
正常采用三层结构实现
@PostMapping
@ApiOperation("员工新增")
public Result save(@RequestBody EmployeeDTO employeeDTO) {
log.info("新增员工:{}", employeeDTO);
// 新增员工业务方法
employeeService.save(employeeDTO);
return Result.success();
}
程序存在问题
1. 在出现同样的 username 的时候,系统会报错;
该异常应被全局异常处理器处理
@ExceptionHandler
public Result exceptionHandler(SQLIntegrityConstraintViolationException ex) {
//错误内容: Duplicate entry 'zhangsan' for key 'idx_username'
final String message = ex.getMessage();
if(message.contains("Duplicate entry")) {
String[] split = message.split(" ");
String username = split[2];
String msg = username + MessageConstant.ALREADY_EXISTS;
return Result.error(msg);
}else {
return Result.error(MessageConstant.UNKNOWN_ERROR);
}
}
2. 当前登录用户的 id 如何存储
使用 ThreadLocal,是一个线程的局部变量,为每个线程单独提供一份存储空间,具有线程隔离效果,只有在线程内才能获取到对应的值,线程外则不能访问
在 sky-common 中已经封装为 BaseContext 类
员工分页查询
分页查询使用使用 mybatis 的分页插件 PageHelper 来简化分页代码的开发。
底层基于 mybatis 的拦截器实现,在 sql 语句后面进行拼接 limit
代码完善
日期时间在前端的显示结果不是我们想要的
解决方法:
1. 在属性上加入注解,对日期进行格式化
可以实现日期的序列化,但是只能实现这一个属性的序列化。 不推荐
2. 在 WebMvcConfiguration 中扩展Spring MVC的消息转换器,统一对日期类型进行格式化
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
//自己创建一个消息转换器
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
// 为消息转换器 设置一个对象转换器,可以将java对象序列化为json数据
converter.setObjectMapper(new JacksonObjectMapper());
//将自己的消息转换器加入到容器里, 默认是放在最后一个
// 0 -> 就是把这个消息转换器放在前面
converters.add(0, converter);
}
启用禁用员工账号
需要一个 update 数据库的方法
在 EmployeeMapper.xml 编写 SQL
<update id="update" parameterType="Employee">
update employee
<set>
<if test="name != null and name != ''" >name = #{name},</if>
<if test="username != null and username != ''" >username = #{username},</if>
<if test="sex != null and sex != ''" >sex = #{sex},</if>
<if test="password != null and password != ''" >password = #{password},</if>
<if test="phone != null and phone != ''" >phone = #{phone},</if>
<if test="idNumber != null and idNumber != ''" >id_number = #{idNumber},</if>
<if test="status != null">status = #{status},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>
<if test="updateUser != null">update_user = #{updateUser},</if>
</set>
where id = #{id}
</update>
编辑员工信息
需要一个查询员工信息的接口,和上一个的修改员工信息的接口
5. 分类模块功能
思路与员工管理的一致,做着基本的crud
注:
在删除分类的时候,要求其下面没有挂载任何内容才可以
6. 公共字段自动填充
针对业务表里的公共字段进行维护
序号 | 字段名 | 含义 | 数据类型 |
1 | create_time | 创建时间 | datetime |
2 | create_user | 创建人id | bigint |
3 | update_time | 修改时间 | datetime |
4 | update_user | 修改人id | bigint |
实现思路
技术:注解、AOP、反射
思路:
1.自定义注解 AutoFill,用于标识需要进行公共字段自动填充的方法
2.自定义切面类 AutoFillAspect,统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值
3.在 Mapper 的方法上加入 AutoFill 注解
开发
自定义注解 AutoFill
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
//数据库操作类型: update insert
OperationType value();
}
自定义切面类 AutoFillAspect
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
/**
* 指定切入点
*/
@Pointcut("execution(* com.sky.mapper.*.*(..)) &&
@annotation(com.sky.annotation.AutoFill)")
public void autoFillPointCut(){}
/**
* 前置通知,在通知中进行公共字段的赋值
*/
@Before("autoFillPointCut()")
public void autoFill(JoinPoint joinPoint) {
log.info("开始进行公共字段的填充");
// 在这里实现逻辑
}
}
实现逻辑:
1. 获取到当前被拦截到的方法的数据库操作类型
//获取到方法签名对象
MethodSignature signature = (MethodSignature)joinPoint.getSignature();
//获取方法上的注解对象
AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);
// 获取到数据库的操作类型
OperationType operationType = autoFill.value();
2. 获取到被拦截的方法的参数(实体对象) 这里约定: 参数里面的实体对象为第一个参数
Object[] args = joinPoint.getArgs();
if(args == null || args.length == 0) {
return;
}
Object entity = args[0];
3. 获取赋值的数据
LocalDateTime now = LocalDateTime.now();
Long currentId = BaseContext.getCurrentId();
4. 通过反射来赋值
if(operationType == OperationType.INSERT) {
//给4个公共字段赋值
try {
// 获取创建时间方法
Method setCreateTime =
entity.getClass()
.getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME,LocalDateTime.class);
// 获取创建人方法
Method setCreateUser =
entity.getClass()
.getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
// 获取修改时间方法
Method setUpdateTime =
entity.getClass()
.getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
// 获取修改人方法
Method setUpdateUser =
entity.getClass()
.getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
// 赋值操作
// 创建时间赋值
setCreateTime.invoke(entity, now);
// 创建人赋值
setCreateUser.invoke(entity, currentId);
// 修改时间赋值
setUpdateTime.invoke(entity, now);
// 修改人赋值
setUpdateUser.invoke(entity, currentId);
} catch (Exception e) {
e.printStackTrace();
}
}else if(operationType == OperationType.UPDATE) {
//给2个公共字段赋值
try {
// 获取修改时间方法
Method setUpdateTime =
entity.getClass()
.getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
// 获取修改人方法
Method setUpdateUser =
entity.getClass()
.getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
// 赋值操作
// 修改时间赋值
setUpdateTime.invoke(entity, now);
// 修改人赋值
setUpdateUser.invoke(entity, currentId);
} catch (Exception e) {
throw new RuntimeException(e);
}
}else {
// 既不是新增又不是修改
throw new RuntimeException(MessageConstant.UNKNOWN_ERROR);
}
5. 在Mapper接口的方法上加入 AutoFill 注解
@AutoFill(OperationType.INSERT
@AutoFill(OperationType.UPDATE)
6. 去掉业务层这个重复的代码