环境搭建
课前知识
- 不同类的作用
| 名称 | 说明 |
| — | — |
| Entity | 实体,通常和数据库中的表对应 |
| DTO | 数据传输对象,通常用于程序中各层之间的数据传递 |
| VO | 视图对象,为前端展示数据提供的对象 |
| POJO | 普通的java对象,只有属性和对应的set、get方法 |
整体结构
环境搭建
后端环境搭建
后端基于maven进行项目构建,并且分模块开发
前端环境
nginx
Git版本管理
详情查看瑞吉外卖项目总结
数据库
前后端联调
后端的初始工程已经实现了登录功能,直接进行前后端的联调即可。
前端发送的请求,是如何请求到后端服务的?
前端请求地址:http://localhost/api/employee/login
后端请求地址:http://localhost:8080/admin/employee/login
nginx反向代理,就是将前端发送的动态请求有nginx转发到后端服务器。
nginx反向代理的好处
- 提高访问速度
- 进行负载均衡
- 保障后端服务的安全
所谓负载均衡,是把大量的请求按照我们指定的方式均衡的分配给集群中的每台服务器。
nginx反向代理的配置方式
nginx负载均衡的配置方式
负载均衡的策略
完善登录功能
问题:员工表的密码是铭文存储。安全性低。
思路:
- 将密码加密后存储,提高安全性
- 使用MD5加密方式对明文进行加密
实现:
- 修改数据库中明文密码,改为MD5加密后的密文
- 修改Java代码,前端提交的密码进行MD5加密后再跟数据库中密码比对
注意:
调用md5加密时,调用用DigestUtils工具类
导入接口文档
前后端分离开发流程
Swagger
介绍
使用Swagger只需按照它的规范去定义接口及接口相关的信息,就可以做到生成接口文档,以及在线接口调试页面。
Knife4j是Java MVC框架集成Swagger生成Api文档的增强解决方案
使用方式
- 导入Knife4j的maven坐标
- 在配置类中加入Knife4j相关配置
@Bean
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/");
}
常用注解
通过注解可以控制生成的接口文档,使接口文档拥有更好的可读性。常用注解如下:
示例:
放在类上的:@Api(tags = “员工相关接口”) tags属性代表描述此类的作用
放在方法上:@ApiOperation(value = “员工登录”)
员工模块
新增员工
需求分析和设计
原型图:
接口设计
本项目约定:
- 管理端发出的请求,统一使用/admin作为前缀
- 用户端发出的请求,统一使用//user作为前缀
数据库设计
代码开发
根据新增员工接口设计对应的DTO:
注意:当前端提交的数据和实体类对应的属性差别较大时,建议使用DTO来封装数据
注意事项
- 前端传输的数据用DTO来接收,但是保存的时候最好转为实体类来保存。DTO类和实体类里面的属性字段名一致时,使用对象拷贝功能。
测试
在使用Swagger测试时,因为有token的全局拦截器,所以需要设置全局参数,携带token
代码完善
程序存在的问题:
- 录入的用户名已经存在,抛出异常后没有处理
- 新增员工时,创建人id和修改人id设置了固定值
解决方法:
- 处理异常时,在全局异常处理器定义方法,用来捕捉异常,然后截取异常信息,根据提示返回不同的结果。
@ExceptionHandler
public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
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);
}
}
- 解决第二个问题,需要动态获取登录用户的id
在实现这个功能之前,先看看jwt的工作流程
在生成token时,已经设置了employee的id
后续的请求中,前端会携带JWT令牌,通过JWT令牌可以解析出当前登录用户的id
那么解析出员工的登录id后,如何传递给Service的save方法呢?
注意:客户端的每一次请求,调用的各种方法都是一个独立的线程。这时就可以使用ThreadLocal。
ThreadLocal并不是一个Thread,而是Thread的局部变量。
Threadlocal为每一个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。
也就是说,在线程整个生命周期内,就可以共享这份存储空间。可以再拦截器的地方,把用户id存入存储空间,
当直行道service中时,获取id
Threadlocal的常用方法:
- public void set(T value)设置当前线程的线程局部变量的值
- public T get() 返回当前线程锁对应的线程局部变量的值
- public void remove() 移除当前线程的局部变量
注意:
在使用ThreadLocal时通常会封装为工具类
员工的分页查询
需求分析和设计
产品原型
接口设计
代码实现
根据分页查询接口设计对应的DTO
后面的所有的分页查询,都会统一封装成PageResult对象。
员工信息分页查询后返回前端的对象类型为:Result
mybatis在分页查询时,需要用到pageHelper插件
底层基于拦截器动态的拼接sql
public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
//开始分页查询
PageHelper.startPage(employeePageQueryDTO.getPage(),employeePageQueryDTO.getPageSize());
Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);
long total = page.getTotal();
List<Employee> records = page.getResult();
return new PageResult(total,records);
}
注意:
查询到的结果封装到了page对象里面,因为返回的是PageResult类型,从page里面取出属性赋值给PageResult
功能测试
操作时间数据格式不能正常显示
代码完善
解决方式:
- 在属性上加入注解,对日期进行格式化
只能处理单个属性
- 在WebMvcConfiguration中扩展SpringMVC的消息转换器,统一对日期类型进行格式化处理
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
//创建一个消息转换器对象
MappingJackson2CborHttpMessageConverter messageConverter = new MappingJackson2CborHttpMessageConverter();
//需要为消息转换器设置一个对象转换器,对象转换器可以将java对象序列化为Json数据
messageConverter.setObjectMapper(new JacksonObjectMapper());
//将自己的消息转换器加入容器中。0代表优先级
converters.add(0,messageConverter);
}
启用禁用员工账号
需求分析设计
业务规则:
- 可以对状态为“启用”的员工账号进行“禁用操作”
- 可以对状态为“禁用”的员工账号进行“启用操作”
- 状态为“禁用”的员工账号不能登录系统
接口设计
代码开发
public void startOrStop(Integer status, Long id) {
//
Employee employee = new Employee();
employee.setStatus(status);
employee.setId(id);
employeeMapper.update(employee);
}
注意:
此处更新做了通用功能
编辑员工
需求分析和设计
原型图
编辑员工功能涉及两个接口
- 根据id查询员工信息
- 编辑用户信息
代码开发
根据id查询员工
编辑用户信息
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删除分类
- 修改分类
- 禁用启用分类
- 根据类型查询分类
数据库表
菜品模块
公共字段自动填充
问题分析
业务表中的公共字段:
问题:代码冗余,不便于维护。
解决思路
- 自定义AutoFill注解,用于标识需要进行公共字段填充的方法
- 自定义切面类AutoFillAspect,统一拦截加入了AutoFill注解的方法,通过反射为公共字段赋值。
- 在Mapper的方法上加入AutoFill的注解
技术点:枚举、注解、Aop、反射
代码实现
- 自定义AutoFill注解,用于标识需要进行公共字段填充的方法
@Target(ElementType.METHOD)//次注解添加的位置为方法
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
//数据库操作类型:UPDATE INSERT
OperationType value();
}
- 自定义切面类AutoFillAspect,统一拦截加入了AutoFill注解的方法,通过反射为公共字段赋值。
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
/**
* 切入点
*/
@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
public void aotoFillPointCut(){}
/**
* 前置通知,在通知中进行公共字段的赋值
*/
@Before("aotoFillPointCut()")
public void autoFill(JoinPoint joinPoint){
log.info("开始进行公共字段的自动填充...");
//获取到当前被拦截的方法上的数据库操作类型
MethodSignature signature = (MethodSignature)joinPoint.getSignature();//方法签名对象
AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);//获得方法上的注解
OperationType type = autoFill.value();//获得数据库的操作类型
//获取到当前被拦截方法的参数--实体对象
Object[] args = joinPoint.getArgs();
if (args == null || args.length ==0){
return;
}
Object entity = args[0];
//准备赋值的数据
LocalDateTime now = LocalDateTime.now();
Long currentId = BaseContext.getCurrentId();
//根据当前不同的操作类型,为对应的属性通过反射来赋值
if (type == OperationType.INSERT){
//为4个公共字段赋值
try {
Method setCreateTime = entity.getClass().getDeclaredMethod("setCreateTime", LocalDateTime.class);
Method setUpdateTime = entity.getClass().getDeclaredMethod("setUpdateTime", LocalDateTime.class);
Method setCreateUser = entity.getClass().getDeclaredMethod("setCreateUser", Long.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod("setUpdateUser", Long.class);
//通过反射为对象属性赋值
setCreateTime.invoke(entity,now);
setCreateUser.invoke(entity,currentId);
setUpdateTime.invoke(entity,now);
setUpdateUser.invoke(entity,currentId);
} catch (Exception e) {
throw new RuntimeException(e);
}
}else if (type == OperationType.UPDATE){
//为两个公共字段赋值
try {
Method setUpdateTime = entity.getClass().getDeclaredMethod("setUpdateTime", LocalDateTime.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod("setUpdateUser", Long.class);
//通过反射为对象属性赋值
setUpdateTime.invoke(entity,now);
setUpdateUser.invoke(entity,currentId);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
新增菜品
需求分析和设计
产品原型
业务规则:
- 菜品的名称必须是唯一的
- 菜品必须属于某个分类下,不能单独存在
- 新增菜品时,可以根据情况选择菜品的口味
- 每个菜品必须对应一个图片
接口设计
- 根据类型查询分类(已完成)
- 文件上传
- 新增菜品
数据库设计
代码开发
文件上传
配置阿里云,详情可见阿里云OSS服务笔记
这里遇到一个问题,阿里云的key失效,重新创建。
@PostMapping("/upload")
@ApiOperation("文件上传")
public Result<String> upload(MultipartFile file){
log.info("文件上传:{}",file);
try {
//原始文件名
String originalFilename = file.getOriginalFilename();
//截取原始文件名的后缀 dfdfdf.png
String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
//构造新文件名称
String objectName = UUID.randomUUID().toString() + extension;
//文件的请求路径
String filePath = aliOssUtil.upload(file.getBytes(), objectName);
return Result.success(filePath);
} catch (IOException e) {
log.error("文件上传失败:{}", e);
}
return Result.error(MessageConstant.UPLOAD_FAILED);
}
工具类
@Data
@AllArgsConstructor
@Slf4j
public class AliOssUtil {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
/**
* 文件上传
*
* @param bytes
* @param objectName
* @return
*/
public String upload(byte[] bytes, String objectName) {
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
ClientBuilderConfiguration configuration = new ClientBuilderConfiguration();
configuration.setSupportCname(false);
try {
// 创建PutObject请求。
ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes));
} catch (OSSException oe) {
System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
System.out.println("Error Message:" + oe.getErrorMessage());
System.out.println("Error Code:" + oe.getErrorCode());
System.out.println("Request ID:" + oe.getRequestId());
System.out.println("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
System.out.println("Caught an ClientException, which means the client encountered "
+ "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network.");
System.out.println("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
//文件访问路径规则 https://BucketName.Endpoint/ObjectName
StringBuilder stringBuilder = new StringBuilder("https://");
stringBuilder
.append(bucketName)
.append(".")
.append(endpoint)
.append("/")
.append(objectName);
log.info("文件上传到:{}", stringBuilder.toString());
return stringBuilder.toString();
}
}
配置类
@Configuration
@Slf4j
public class OssConfiguration {
@Bean
@ConditionalOnMissingBean //保证spring容器中只有一个对象
public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties){
log.info("开始创建阿里云文件上传工具类对象:{}",aliOssProperties);
return new AliOssUtil(aliOssProperties.getEndpoint(),
aliOssProperties.getAccessKeyId(),
aliOssProperties.getAccessKeySecret(),
aliOssProperties.getBucketName());
}
}
新增菜品
插入口味数据批量插入,使用了foreach,collection属性代表要遍历的集合,item属性代表遍历后每个元素的名称,separator代表分隔符。
注意:因为存储菜品口味数据时,需要获取插入的菜品的id,所以在插入语句时加入了useGeneratedKey属性,意味着在插入数据成功后,获取插入数据的主键。keyProperty属性代表,返回主键的值赋值给那个属性
@Transactional
public void saveWithFlavor(DishDTO dishDTO) {
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO,dish);
//向菜品表插入1条数据
dishMapper.insert(dish);
//获取insert语句生成的主键值
Long dishId = dish.getId();
List<DishFlavor> flavors = dishDTO.getFlavors();
if (flavors != null && flavors.size() > 0){
flavors.forEach(dishFlavor -> {
dishFlavor.setDishId(dishId);
});
//向口味表插入n条数据
dishFlavorMapper.insertBatch(flavors);
}
}
菜品的分页查询
需求分析和设计
原型图
业务规则
- 根据页码展示菜品信息
- 每页展示10条数据
- 分页查询时可以根据需要输入菜品名称、菜品分类、菜品状态查询
接口设计
代码开发
根据菜品分页查询接口定义设计对应的VO
删除菜品
需求分析和设计
产品原型:
业务规则
- 可以一次删除一个菜品,也可以批量删除菜品
- 起售中的菜品不能删除
- 被套餐关联的菜品不能删除
- 删除菜品后,关联的口味数据也要删除掉
接口设计
数据库
代码开发
前端传上来的数据为字符串,后端使用@RequestParam注解,自动封装为List类型。
@Transactional
public void deleteBatch(List<Long> ids) {
//判断当前菜品是否能够删除--是否存在起售中的菜品
for (Long id : ids) {
Dish dish = dishMapper.getById(id);
if (dish.getStatus() == StatusConstant.ENABLE){
throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
}
}
//判断当前菜品是否能够删除--是否被套餐关联了
List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(ids);
if (setmealIds != null && setmealIds.size() > 0){
throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
}
//删除菜品表中的菜品数据
for (Long id : ids) {
dishMapper.deleteById(id);
//删除菜品关联的口味数据
dishFlavorMapper.deleteByDishId(id);
}
}
修改菜品
需求分析和设计
产品原型图
接口设计
- 根据id查询菜品
- 根据类型实现分类(已经实现)
- 文件上传(已实现)
- 修改菜品
代码开发
根据id查询菜品和对应的口味数据,用于数据回显
public DishVO getByIdWithFlavor(Long id) {
//根据id查询菜品数据
Dish dish = dishMapper.getById(id);
//根据菜品id查询口味数据
List<DishFlavor> dishFlavors = dishFlavorMapper.getByDishId(id);
//将查询到的数据封装到VO
DishVO dishVO = new DishVO();
BeanUtils.copyProperties(dish,dishVO);
dishVO.setFlavors(dishFlavors);
return dishVO;
}
根据id修改菜品的基本信息和口味信息
public void updateWithFlavor(DishDTO dishDTO) {
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO,dish);
//修改菜品的基本信息
dishMapper.update(dish);
//删除原有的口味数据
dishFlavorMapper.deleteByDishId(dish.getId());
//重新插入口味数据
List<DishFlavor> flavors = dishDTO.getFlavors();
if (flavors != null && flavors.size() > 0){
flavors.forEach(dishFlavor -> {
dishFlavor.setDishId(dish.getId());
});
//向口味表插入n条数据
dishFlavorMapper.insertBatch(flavors);
}
}
套餐模块
新增套餐
需求分析设计
产品原型:
业务规则
- 套餐名称唯一
- 套餐必须属于某个分类
- 套餐必须包含菜品
- 名称、分类、价格、图片为必填项
- 添加菜品窗口需要根据分类类型来展示菜品
- 新增的套餐默认为停售状态
接口设计
接口设计(共涉及到4个接口):
- 根据类型查询分类(已完成)
- 根据分类id查询菜品
- 图片上传(已完成)
- 新增套餐
数据库设计
setmeal表为套餐表,用于存储套餐的信息。具体表结构如下:
setmeal_dish表为套餐菜品关系表,用于存储套餐和菜品的关联关系。具体表结构如下:
代码实现
套餐分页查询
需求分析和设计
产品原型:
业务规则:
- 根据页码进行分页展示
- 每页展示10条数据
- 可以根据需要,按照套餐名称、分类、售卖状态进行查询
接口设计
删除套餐
需求分析和设计
产品原型
业务规则
- 可以一次删除一个套餐,也可以批量删除套餐
- 起售中的套餐不能删除
接口设计
修改套餐
需求分析和设计
产品原型:
接口设计
- 根据id查询套餐
- 根据类型查询分类(已完成)
- 根据分类id查询菜品(已完成)
- 图片上传(已完成)
- 修改套餐
店铺营业状态设计
需求分析和设计
产品原型
接口设计
- 设置营业状态
- 管理端查询营业状态
- 用户端查询营业状态
本项目约定:
- 管理端发出的请求,前缀为/admin
- 用户端发出的请求,前缀为/user
代码实现
设置营业状态
public static final String KEY = "SHOP_STATUS";
@PutMapping("/{status}")
@ApiOperation("设置店铺状态")
public Result setStatus(@PathVariable Integer status){
log.info("设置店铺的营业状态为:{}",status == 1?"营业中" : "打样中");
redisTemplate.opsForValue().set(KEY,status);
return Result.success();
}
查询店铺状态
@GetMapping("/status")
@ApiOperation("获取店铺的营业状态")
public Result<Integer> getStatus(){
Integer status = (Integer) redisTemplate.opsForValue().get(KEY);
log.info("获取到店铺的营业状态为:{}",status == 1?"营业中" : "打样中");
return Result.success(status);
}
用户端开发
HttpClient
HttpClient可以用来提供高效的、最新的、功能丰富的支持HTTP协议的客户端编程工具包,并且它支持HTTP协议最新的版本和建议
使用HttpClient需要导入maven坐标
HttpClient核心API
- HttpClient
- HttpClients
- CloseableHttpClient
- HttpGet
- HttpPost
发送请求的步骤
- 创建HttpClient对象
- 创建Http请求对象
- 调用HttpClient的execute方法发送请求
Demo
@Test
public void testGet() throws Exception{
//创建HttpClient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
//创建请求对象
HttpGet httpGet = new HttpGet("http://localhost:8080/user/shop/status");
//发送对象
CloseableHttpResponse response = httpClient.execute(httpGet);
//获取服务端返回的状态码
int statusCode = response.getStatusLine().getStatusCode();
System.out.println("statusCode = " + statusCode);
//获取服务端返回的数据
HttpEntity entity = response.getEntity();
String body = EntityUtils.toString(entity);
System.out.println("服务端返回的数据为:"+body);
//关闭资源
response.close();
httpClient.close();
}
/**
* 通过HttpClient发送Post方式请求
*/
@Test
public void testPost() throws Exception{
//创建HttpClient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
//创建请求对象
HttpPost httpPost = new HttpPost("http://localhost:8080/admin/employee/login");
JSONObject jsonObject = new JSONObject();
jsonObject.put("username","admin");
jsonObject.put("password","123456");
StringEntity entity = new StringEntity(jsonObject.toString());
//指定编码方式
entity.setContentEncoding("utf-8");
//数据格式
entity.setContentType("application/json");
httpPost.setEntity(entity);
//发送对象
CloseableHttpResponse response = httpClient.execute(httpPost);
//获取服务端返回的状态码
int statusCode = response.getStatusLine().getStatusCode();
System.out.println("statusCode = " + statusCode);
//获取服务端返回的数据
HttpEntity entity1 = response.getEntity();
String body = EntityUtils.toString(entity1);
System.out.println("body = " + body);
//关闭资源
response.close();
httpClient.close();
}
HttpClientUtil
package com.sky.utils;
import com.alibaba.fastjson.JSONObject;
import org.apache.http.NameValuePair;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Http工具类
*/
public class HttpClientUtil {
static final int TIMEOUT_MSEC = 5 * 1000;
/**
* 发送GET方式请求
* @param url
* @param paramMap
* @return
*/
public static String doGet(String url,Map<String,String> paramMap){
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
String result = "";
CloseableHttpResponse response = null;
try{
URIBuilder builder = new URIBuilder(url);
if(paramMap != null){
for (String key : paramMap.keySet()) {
builder.addParameter(key,paramMap.get(key));
}
}
URI uri = builder.build();
//创建GET请求
HttpGet httpGet = new HttpGet(uri);
//发送请求
response = httpClient.execute(httpGet);
//判断响应状态
if(response.getStatusLine().getStatusCode() == 200){
result = EntityUtils.toString(response.getEntity(),"UTF-8");
}
}catch (Exception e){
e.printStackTrace();
}finally {
try {
response.close();
httpClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return result;
}
/**
* 发送POST方式请求
* @param url
* @param paramMap
* @return
* @throws IOException
*/
public static String doPost(String url, Map<String, String> paramMap) throws IOException {
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
String resultString = "";
try {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(url);
// 创建参数列表
if (paramMap != null) {
List<NameValuePair> paramList = new ArrayList();
for (Map.Entry<String, String> param : paramMap.entrySet()) {
paramList.add(new BasicNameValuePair(param.getKey(), param.getValue()));
}
// 模拟表单
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList);
httpPost.setEntity(entity);
}
httpPost.setConfig(builderRequestConfig());
// 执行http请求
response = httpClient.execute(httpPost);
resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
} catch (Exception e) {
throw e;
} finally {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return resultString;
}
/**
* 发送POST方式请求
* @param url
* @param paramMap
* @return
* @throws IOException
*/
public static String doPost4Json(String url, Map<String, String> paramMap) throws IOException {
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
String resultString = "";
try {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(url);
if (paramMap != null) {
//构造json格式数据
JSONObject jsonObject = new JSONObject();
for (Map.Entry<String, String> param : paramMap.entrySet()) {
jsonObject.put(param.getKey(),param.getValue());
}
StringEntity entity = new StringEntity(jsonObject.toString(),"utf-8");
//设置请求编码
entity.setContentEncoding("utf-8");
//设置数据类型
entity.setContentType("application/json");
httpPost.setEntity(entity);
}
httpPost.setConfig(builderRequestConfig());
// 执行http请求
response = httpClient.execute(httpPost);
resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
} catch (Exception e) {
throw e;
} finally {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return resultString;
}
private static RequestConfig builderRequestConfig() {
return RequestConfig.custom()
.setConnectTimeout(TIMEOUT_MSEC)
.setConnectionRequestTimeout(TIMEOUT_MSEC)
.setSocketTimeout(TIMEOUT_MSEC).build();
}
}
微信小程序开发
appid: wxee238e2b899fa140
秘钥:
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAymwi9lEzud6xnTfy9QDg+SnBM/tYxr2Rfd8r/NnrZwBSF/Il
fzapULO0tQiSguJxkkpbxyGWn42uvHijwGQcYpSQK42Zx/v/uu1hwyKfCAwXBqNM
aQ1DSNQjElokLAcZXYvw2pBZxCSIg6rstSayhJB6apODzNFlfuuHoaUQ/3x2LwLn
v/R1oIbZAUpj5FPSSt5B4j8GkQHks/MDXFLZbsaP/g7ftO2NO9yIC75vS1+NsG0U
3mkOHH+OQcgBbdDcdnX3cD4FPMPC57XeoJqpGlsaB8M4wuhWiIcGPJ7GePaFjXjQ
E8X5P/7/GtlB3nHbWNKMf/79mfQRHgTW331bLwIDAQABAoIBAHtEEKJY3pvwfc6U
tJH4IAf7IlkqVWxpIkE5dwr6FXza2CMW5Tirq72mQYQAnV+wz6JbP7pqvNOaru68
tFNDY3mwqH6tMxlyXs+RIPh4i8mVuyvio3RRjEi8TkMtu1tIU20A/AHM560m5i7w
FkdWMl3BsZpXDGrclOcOPMD1yP0a0zEAxtw5b/ZfjUpcg4Ve6rsLz2feRnfbe+wX
VOEMwYZbcaL3+QuW/gFEgHjPmYUNlKVLFVAxDaDPcLuooLd/zVoetS4Xasf6RKK9
NClm93peNhU78iPCbuVyEGwZFA+JOeTcK2HPsnhq9lwN8bUTt6XYugzfcadYObyt
fRwsQEkCgYEA700LePZl+eVb6QitbZBtG+oVOOx7DBtqulrw8Xj4pB67FAfhGtur
WbRKId98QP7ODosdd0a/5Pdz8AvJlRUyzPnnDL6J60OZ2Ghnt4zZlT4Og95po9Ao
j6YG05jFikKGL96Ya7dLEXJ30NQi8ggBxQm8keXRPmsg5YMfOiZu0zUCgYEA2IxH
1Yrt0n1dwYYMvLz+v0tqMT1Dv5xrF5gZs5DCqo0tFx8gKdsMS69tzJLNP0LyIMZq
TYYR0oeCSnljwIy564bfX4L4KT5w5wH4uUZR0tpYPtLwUobqiEkYlW/CRRsSgxkO
kA69uxyUJwpa9O4Nu+RBO7GWuyfpJW2t02p2fVMCgYEA0/LLXIHwZFuPAmGbKdWn
rfewgFCD039dEl2F1nosz0AgtmccK6uwoq0ak9Hbvb1xSFRS7tgNEoFRgKUQEClM
a1xUFmeUxHmFg7VFV7864AKs3INb0amGo2SL21IOdJzjOPbQzWb06CkYki+yG+iX
mKs9B4QjxkMgSefO+rQbqJkCgYEAwvizKMqYzH9B7h+C0mCcQaJmL4VIvXnZVnoF
Eg8RprvL0Fie/fCSxoZiJuI6WG4vUWE0jy5aV6LYpbNcJB7QuwZJklZ6l6/3uZal
2jM9MsqSz6Xe6X8+JY0izFG+qbfxWAY0fXI4VAMsRWZmdMbtqLGgJl4EJ+iMW72/
122w168CgYBm4agQVxzhqFNBw2Uiw3JTmOJDLoeauA8bKdw8VppQlF97IF8Ngt+J
H2/EYrn4lG/W0NHEyUHo7kY9LLZNyRzGUhx6PPUKQU0EnUN4p2b/tTSQbQzUkYoj
1Rq4n0AV02nik+NXLJeAOrBTovAU1vdKNaKy/YIbXdiKucY9nqVFUQ==
-----END RSA PRIVATE KEY-----
入门案例
目录结构
一个小程序页面由四个文件组成
微信登录
流程
接口设计
数据库设计
代码开发
Controller层:
@PostMapping("/login")
@ApiOperation("微信登录")
public Result<UserLoginVO> login(@RequestBody UserLoginDTO userLoginDTO){
log.info("微信用户登录:{}",userLoginDTO.getCode());
//微信登录
User user = userService.wxLogin(userLoginDTO);
//为微信用户生成jwt令牌
Map<String,Object> claims = new HashMap<>();
claims.put(JwtClaimsConstant.USER_ID,user.getId());
String token = JwtUtil.createJWT(jwtProperties.getUserSecretKey(), jwtProperties.getUserTtl(), claims);
UserLoginVO userLoginVO = UserLoginVO.builder()
.id(user.getId())
.openid(user.getOpenid())
.token(token)
.build();
return Result.success(userLoginVO);
}
Service层:
@Service
@Slf4j
public class UserServiceImpl implements UserService {
@Autowired
private WeChatProperties weChatProperties;
@Autowired
private UserMapper userMapper;
//微信服务接口地址
public static final String WX_LOGIN = "https://api.weixin.qq.com/sns/jscode2session";
/**
* 微信登录
* @param userLoginDTO
* @return
*/
public User wxLogin(UserLoginDTO userLoginDTO) {
//调用微信接口服务,获取当前微信用户的oppenId
String openid = getOpenid(userLoginDTO);
//判断oppenid是否为空,如果为空标识登录失败,抛出业务异常
if (openid == null){
throw new LoginFailedException(MessageConstant.LOGIN_FAILED);
}
//判断当前用户是否为新用户
User user = userMapper.getByOpenid(openid);
//如果是新用户,自动完成注册
if (user == null){
user = User.builder()
.openid(openid)
.createTime(LocalDateTime.now())
.build();
userMapper.insert(user);
}
//返回用户对象
return user;
}
/**
* 调用微信接口服务,获取微信的opnid
* @param userLoginDTO
* @return
*/
private String getOpenid(UserLoginDTO userLoginDTO) {
Map<String,String> map = new HashMap<>();
map.put("appid",weChatProperties.getAppid());
map.put("secret",weChatProperties.getSecret());
map.put("js_code", userLoginDTO.getCode());
map.put("grant_type","authorization_code");
String json = HttpClientUtil.doGet(WX_LOGIN, map);
JSONObject jsonObject = JSON.parseObject(json);
String openid = jsonObject.getString("openid");
return openid;
}
功能测试
导入商品浏览功能代码
接口设计
- 查询分类
- 根据分类id查询菜品
- 根据分类id查询套餐
- 根据套餐id查询包含的菜品