毕设项目<<基于微信小程序的餐馆外卖系统的设计后端>>(开发记录(一))
文章预览:
视频传送带 视频传送带
准备工作
-
角色分工
-
软件环境
-
技术选型
后端环境
- 项目结构:
JWT
JSON Web Token(JSON Web令牌):是一个开放标准(rfc7519),它定义了一种紧凑的、自包含的方式,用于在各方之间以JSON对象安全地传输信息。是一个数字签名,生成的信息是可以验证并被信任的,jwt可以使用秘密〈使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。通过JSON形式作为Web应用中的令牌,用于在各方之间安全地将信息作为JSON对象传输。在数据传输过程中还可以完成数据加密、签名等相关处理。JWT就是一个字符串,由三部分构成:Header(头部)、Payload(数据)、Signature(签名)JWE
(规范文件RFC 7516
):JSON Web Encryption
,表示基于JSON
数据结构的加密内容,加密机制对任意八位字节序列进行加密、提供完整性保护和提高破解难度,
-
JWT作用:
- 授权:一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由,服务和资源。它的开销很小并且可以在不同的域中使用。如:单点登录
-
- 信息交换:在各方之间安全地传输信息。JWT可进行签名(如使用公钥/私钥对),因此可确保发件人。由于签名是使用标头和有效负载计算的,因此还可验证内容是否被篡改。
-
JWT认证
-
认证流程
- 前端通过Web表单将自己的用户名和密码发送到后端的接口。该过程一般是HTTP的POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探
- 后端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT(Token)。
- 后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStorage(浏览器本地缓存)或sessionStorage(session缓存)上,退出登录时前端删除保存的JWT即可
- 前端在每次请求时将JWT放入HTTP的Header中的Authorization位。(解决XSS和XSRF问题)HEADER
- 后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确﹔检查Token是否过期;检查Token的接收方是否是自己(可选)
- 验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果
-
JWT优点
- 简洁(Compact):可以通过URL,POST参数或者在HTTP header发送,数据量小,传输速度也很快
- 自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库
- Token是以JSON加密的形式保存在客户端,所以JWT是跨语言的,原则上任何web形式都支持。
- 不需要在服务端保存会话信息,特别适用于分布式微服务
Nginx反向代理
- 正向代理: 如果把局域网外的 Internet 想象成一个巨大的资源库,则局域网中的客户端要访问 Internet,则需要通过代理服务器来访问,这种代理服务就称为正向代理。下面是正向代理的原理图。
- 反向代理:反向代理就是代理服务端,是客户感受不到的
-
作用:
- 可以防止外网对内网服务器的恶性攻击、缓存以减少服务器的压力和访问安全控制
- 提高访问速度
- 进行负载均衡,将用户请求分配给多个服务器
-
listen 80; server_name localhost; location /api/ { proxy_pass http://localhost:8080/admin/; # 反向代理 #proxy_pass http://webservers/admin/; # 负载均衡 }```
-
-
通过knife4j生成接口文档,增强使用swagger进行集成测试
- 导入knife4j的maven坐标
-
<groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-spring-boot-starter</artifactId> <version>${knife4j}</version> </dependency>```
-
- 通过knife4j生成接口文档(WebMvcConfiguration)
-
public Docket docket() { 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; }```
- 设置静态资源映射,否则接口文档页面无法访问
protected void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/"); registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/"); }
-
- 导入knife4j的maven坐标
-
登录功能完善
- 将密码加密后存储,提高安全性
- 使用MD5加密方式对,明文密码加密
MD5即Message-Digest Algorithm 5(信息-摘要算法5),用于确保信息传输完整一致。是计算机广泛使用的杂凑算法之一(又译摘要算法、哈希算法),主流编程语言普遍已有MD5实现
- 具有以下特点
- 压缩性:任意长度的数据,算出的MD5值长度都是固定的
- 容易计算:从原数据计算出MD5值很容易
- 抗修改性:对原数据进行任何改动,哪怕只修改1个字节,所得到的MD5值都有很大区别
- 强抗碰撞:已知原数据和其MD5值,想找到一个具有相同MD5值的数据(即伪造数据)是非常困难的
- java实现:
password = DigestUtils.md5DigestAsHex(password.getBytes());
员工管理
新增员工
需求分析和设计
产品原型
要求和限制
- 账号必须唯一
- 手机号为合法11位
- 身份证号的校验:主要是长度(18位)
- 新增默认初始密码为123456
- 请求方式为post,使用Json格式提交
接口设计
约定:
- 管理端发出的请求,统一使用/admin作为前缀
- 用户端发出的请求,统一使用/user作为前缀
数据库设计
employee表
代码开发
- 当前端提交的数据和实体类中对应的属性差别比较大时,推荐使用DTO来封装数据
[[VO、BO、PO、DO、DTO的区别]]
功能测试
测试方式:
- 通过接口文档来测试
- 通过前后端联调测试
- 由于开发阶段前端和后端是并行开发的,后端完成某个功能后,此时前端对应的功能可能还没有完成,导致无法进行前后端联调测试,所以在开发阶段,后端测试主要以接口文档测试为主
代码完善
存在问题
录入的用户名已存在时,抛异常后无处理
全局捕获异常关键字,根据空格分割,提取用户名,然后提升信息
@ExceptionHandler
public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
//Duplicate entry 'zhangsan' for key 'employee.idx_username'
String message = ex.getMessage();
if(message.contains("Duplicate entry")){
String [L|400] split = message.split(" ");
String username = split[2];
//String msg = username + "已存在";
String msg = username + MessageConstant.ALREALY_EXISTS;
return Result.error(msg);
}else {
return Result.error(MessageConstant.UNKNOWN_ERROR);
}
}
新增员工时,创建人id和修改人id设置成固定值
- ThreadLocal 技术
- 本地线程变量,
ThreadLocal
中填充的的是当前线程的变量,该变量对其他线程而言是封闭且隔离的,ThreadLocal
为变量在每个线程中创建了一个副本,这样每个线程都可以访问自己内部的副本变量 - 每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来
- 每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就_不存在多线程间共享的问题
- ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收
- 方法
public void set(T value)
设置当前线程局部变量值public T get()
返回当前程所对应的线程局部变量的值public void remove()
移除当前线程的线程局部变量的值
- 封装在sky-common/src/main/context/BaseContext中
- 本地线程变量,
- 鉴于每一次请求(提交)调用一个线程,可以使用ThreadLocal技术解决动态获取当前用户id的问题
- 实现:
- 在拦截器中通过
BaseContext.setCurrentId()
获取 - 在save方法中通过
BaseContext.getCurrentId()
传入
- 在拦截器中通过
分类管理
产品原型
- 根据页码展示员工信息
- 每页10条数据
- 分页查询时可以根据需要,输入员工姓名进行查询
需求分析和设计
代码开发
根据分页查询接口设计对应的DTO
调用my-batis框架插件实现分类查询
pom文件引入依赖
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>${pagehelper}</version>
</dependency>
mysql 动态查询
<select id="pageQuery" resultType="com.sky.entity.Employee">
select * from employee
<where>
<if test ="name!=null and name !=''">
and name like concat('%',#{name},'%')
</if>
</where>
order by create_time desc
</select>
代码完善
最后操作时间的完善
-
处理方式一
- 在属性上加注解,对日期进行格式化
-
private LocalDateTime updateTime;```
- 对比:接口文档:
-
在
WebMvcConfiguration
中扩展SpringMVC
消息转换器,统一对日期类型进行格式化处理
/**
* 扩展SpringMVC消息转换器
* @param converters
*/
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器");
//创建消息转换器对象
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
//需要为消息转换器设置对象转换器,对象转换器可以将Java对象序列化为json数据
converter.setObjectMapper(new JacksonObjectMapper());
//将自己的消息转换器加入到容器中,且优先使用
converters.add(0,converter);
}
启用禁用员工账号
需求分析和设计
产品原型
业务规则
- 可以对状态为“启用”的员工账号进行“禁用”操作
- 可以对状态为“禁用”的员工账号进行“启用”操作
- 状态为“禁用”的员工账号不能登录系统
接口设计
代码开发
@PostMapping("/status/{status}")
@ApiOperation("启用禁用员工账号")
public Result startOrStop(@PathVariable Integer status, Long id) {
log.info("启用禁用员工账号:{},{}",status,id);
employeeService.startOrStop(status,id);
return Result.success();
}
编辑员工
接口设计
- 根据id查询员工信息
- 编辑员工信息
代码开发
/**
* 根据id查询员工信息
* @param id
* @return
*/
public Employee getById(Long id) {
Employee employee = employeeMapper.getById(id);
employee.setPassword("****");
return employee;
}
/**
* 编辑员工信息
* @param employeeDTO
*/
public void update(EmployeeDTO employeeDTO) {
Employee employee = new Employee();
BeanUtils.copyProperties(employeeDTO,employee);
employee.setUpdateTime(LocalDateTime.now());
employee.setUpdateUser(BaseContext.getCurrentId());
employeeMapper.update(employee);
}
导入分类模块功能
功能分析
- 新增菜品
- 新增套餐分类
业务规则
- 分类名称必须是唯一的
- 分类按照类型可以分为菜品分类和套餐分类
- 新添加的分类状态默认为“禁用”
需求分析和接口设计
接口设计
- 新增分类
- 分类分页查询
- 根据id删除分类
- 修改分类
- 启用禁用分类
- 根据类型查询分类
数据库设计
category表
具体思路同员工和管理