目录
Swagger能生成接口文档,方便后端调试
使用方式
导入maven坐标
- <dependency>
- <groupId>com.github.xiaoymingroupId>
- <artifactId>knife4j-spring-boot-starterartifactId>
- <version>3.0.2version>
- dependency>
创建配置类
- @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;
- }
图中扫描了controller包,之后会自动生成接口文档
添加静态资源映射
- protected void addResourceHandlers(ResourceHandlerRegistry registry) {
- registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
- registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
- }
完成后访问页面即可,按图中操作既是访问/doc.html
注解
注解的作用是在swagger生成的接口文档中添加相应的说明,随手添加养成好习惯
创建类往线程中添加键值对存储当前操作者的id,前端的每一次请求都会开启一个线程
- public class BaseContext {
-
- public static ThreadLocal
threadLocal = new ThreadLocal<>(); -
- public static void setCurrentId(Long id) {
- threadLocal.set(id);
- }
-
- public static Long getCurrentId() {
- return threadLocal.get();
- }
-
- public static void removeCurrentId() {
- threadLocal.remove();
- }
-
- }
在员工类中对dto和其他属性进行封装,BeanUtils.copyProperties(a,b)能将a对象中成员值传递到b对象的同名成员中
- public static Employee create(EmployeeDTO employeeDTO,Long creator)
- {
- Employee employee=new Employee();
- BeanUtils.copyProperties(employeeDTO,employee);
- employee.setStatus(StatusConstant.ENABLE);
- employee.setCreateTime(LocalDateTime.now());
- employee.setUpdateTime(LocalDateTime.now());
- employee.setCreateUser(creator);
- employee.setUpdateUser(creator);
- employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));
- return employee;
- }
用户名在数据库里设置为unique,当重复时抛出SQLIntegrityConstraintViolationException异常,对前端返回
-
- @ExceptionHandler
- public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
- String message=ex.getMessage();
- if(message.contains("Duplicate entry")) return Result.error("用户名已存在");
- return Result.error("未知错误");
- }
-
-
在service层使用PageHelper插件,该插件类似于拦截器,在sql语句之后自动添加limit,故不要加分号,同时,在此之前会进行对数据库的一次count()询问,得到total
PageHelper依赖
-
com.github.pagehelper -
pagehelper-spring-boot-starter
此时,返回的值中日期时间会以数组形式返回给前端
在WebMvcConfiguration中重写实现json转换器对传回前端的日期等数据进行格式转化
- protected void extendMessageConverters(List
> converters) { - MappingJackson2HttpMessageConverter converter=new MappingJackson2HttpMessageConverter();
- converter.setObjectMapper(new JacksonObjectMapper());
- converters.add(0,converter);//往转换器集合中添加自己的转换器
- }
重启服务器后得到正确格式
为员工设置状态,限制其登录,本质上就是修改操作update
将update使用
"update"> - update sky_take_out.employee
-
- <if test="name!=null and name!='' ">name=#{name},if>
- <if test="username!=null and username!='' ">username=#{username},if>
- <if test="password!=null and password!='' ">passord=#{password},if>
- <if test="phone!=null and phone!='' ">phone=#{phone},if>
- <if test="sex!=null and sex!='' ">sex=#{sex},if>
- <if test="idNumber!=null and idNumber!='' ">id_number=#{idNumber},if>
- <if test="status!=null">status=#{status},if>
- <if test="createTime!=null">create_time=#{createTime},if>
- <if test="updateTime!=null">update_time=#{updateTime},if>
- <if test="createUser!=null">create_user=#{createUser},if>
- <if test="updateUser!=null">update_user=#{updateUser},if>
-
- where id=#{id}
-
前端需要数据回显,先进行id查询员工的操作,然后在修改
我们利用上面提到的update更新即可,记得更新修改者和修改时间,同上单线程内的当前id已经放入context了,直接取出来就行了
- @Override
- public void updateEmployee(EmployeeDTO employeeDTO) {
- Employee employee=new Employee();
- BeanUtils.copyProperties(employeeDTO,employee);
- employee.setUpdateUser(BaseContext.getCurrentId());
- employee.setUpdateTime(LocalDateTime.now());
- employeeMapper.update(employee);
- }
分类接口和员工接口操作类似,不再赘述
发现随着项目的复杂化,在创建修改数据是会产生大量冗余的代码来设置创建修改的信息,所以考虑采用AOP和注解的方式实现自动填充信息
自定义注解AutoFill
- @Target(ElementType.METHOD)
- @Retention(RetentionPolicy.RUNTIME)
- public @interface AutoFill {
- //设置操作类型,update 和 insert
- OperationType value();
- }
这里我们在需要填充信息的方法上添加注解,在切面设置Before方法时通过下面的代码就能获得添加的注解的value从而执行不同的填充操作,然后利用反射对mapper的参数填充信息
- //获得加了注解的方法对象
- MethodSignature signature=(MethodSignature) joinPoint.getSignature();
- //通过方法对象获得该方法上的注解对象
- AutoFill autoFill=signature.getMethod().getAnnotation(AutoFill.class);
- //获得注解的枚举值
- OperationType operationType=autoFill.value();
我们可以把项目中所上传保存的图片上传到阿里云保存,先开通阿里云的对象存储oss服务
创建bucket并获取自己的AccessKey的id和secret,在meaven中导入依赖,创建上传工具类后即可,创建controller以接受前端传来的请求
安装方式参考官方文档
工具类代码如下
- @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);
-
- 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();
- }
- }
菜品除了自身单一的属性之外,还有和口味的一对多的关系,这里将口味抽离出来作为单独的对象,自建一个数据库表,所以在添加菜品时应先将口味除外的属性添加到Dish表,并拿到回显的主键,然后将设置好对应关系的口味插入DishFlavor表中
- @Override
- public Result save(DishDTO dishDTO) {
- Dish dish=new Dish();
- BeanUtils.copyProperties(dishDTO,dish);
- dish.setStatus(StatusConstant.ENABLE);
- List
flavors=dishDTO.getFlavors(); -
- if(dishMapper.check(dish.getName())!=0)
- {return Result.error("菜品已存在");}
-
- dishMapper.save(dish);
- Long id=dish.getId();
- if(flavors!=null && !flavors.isEmpty()){
- flavors.forEach(dishFlavor -> {
- dishFlavor.setDishId(id);
- });
- }
- dishFlavorMapper.saveBatch(flavors);
- return Result.success();
- }
dish表中只含有category_id,没有菜品分类的名字,这里使用多表查询,为c的项目起别名来映射到对象
- select d.*,c.name as categoryName from dish d left outer join category c on d.category_id=c.id
-
- <if test="categoryId != null">and d.category_id=#{categoryId}if>
- <if test="name !=null and name!='' ">and d.name like concat('%',#{name},'%')if>
- <if test="status !=null ">and d.status = #{status}if>
-
- order by d.create_time desc
删除单个和多个菜品共用一个方法即可,删除菜品时得删两个表,菜品表和口味表,在售的菜品不能删,还包含在套餐里的菜品也不能删
- @Override
- public Result deleteBatch(List
ids) { - //在售不能删
- Long countSell=dishMapper.numberOfSell(ids);
- //套餐关联不能删
- Long countSetMeal=setMealDishMapper.numberOfDishId(ids);
-
- if(countSell!=0||countSetMeal!=0)
- {
- return Result.error("有菜品在售或被套餐绑定!");
- }
- dishMapper.deleteBatch(ids);
- dishFlavorMapper.deleteByDishId(ids);
- return Result.success("删除成功");
- }
修改菜品时口味表也许会变,得先删除原先的口味表,然后新设置口味表插入
- @Override
- @Transactional
- public void update(DishDTO dishDTO) {
- Dish dish=new Dish();
- BeanUtils.copyProperties(dishDTO,dish);
-
- List
dishId=new ArrayList<>();//复用方法删除口味表 - dishId.add(dishDTO.getId());
- dishFlavorMapper.deleteByDishId(dishId);
-
- List
flavors=dishDTO.getFlavors();//为dish设置口味表 - Long id=dishDTO.getId();
- if(flavors!=null && !flavors.isEmpty()){
- flavors.forEach(dishFlavor -> {
- dishFlavor.setDishId(id);
- });
- }
- dishFlavorMapper.saveBatch(flavors);
- System.out.println("OK");
- dishMapper.update(dish);
- }
店铺状态查询需要频繁查询,内容又简单,适合使用redis来实现
Redis在spring-boot中的使用方法
1.导入maven坐标
-
org.springframework.boot -
spring-boot-starter-data-redis -
2.在配置文件中配置redis
- redis:
- password: 123456 //密码
- host: localhost //地址
- port: 6379 //端口
- database: 0 //使用几号库
3.添加配置类,导入序列化器
- @Configuration
- @Slf4j
- public class RedisConfiguration {
- @Bean
- public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
- log.info("创建redis模板对象");
- RedisTemplate redisTemplate=new RedisTemplate();
- redisTemplate.setConnectionFactory(redisConnectionFactory);
- redisTemplate.setKeySerializer(new StringRedisSerializer());
- return redisTemplate;
- }
- }
然后在代码里通过redisTemplate对象得到的如ValueOperations的方法就可使用redis
营业端代码
- private final String KEY="SHOP_STATUS";
- @PutMapping("/{status}")
- @ApiOperation("设置营业状态")
- public Result setShopStatus(@PathVariable Integer status)
- {
- log.info("设置营业状态:{}",status==1?"营业中":"打样中");
- ValueOperations valueOperations=redisTemplate.opsForValue();
- valueOperations.set(KEY,status);
- return Result.success();
- }
-
- @GetMapping("/status")
- @ApiOperation("查询营业状态")
- public Result
getShopStatus() - {
- ValueOperations valueOperations= redisTemplate.opsForValue();
- Integer status= (Integer) valueOperations.get(KEY);
- log.info("营业状态:{}",status==1?"营业中":"打样中");
- return Result.success(status);
- }
微信小程序登录的官方流程图如下
小程序先发出携带js_code的登录请求,服务端接收后将小程序的id和密钥连同用户的js_code发送给微信接口服务,微信接口服务返回用户的openid给服务端,服务端此时可以通过此id来注册登录用户
向微信接口服务发送请求时可以使用Httpclient,此处用工具类包装
- private String WX_LOGIN="https://api.weixin.qq.com/sns/jscode2session";
- private String getOpenid(String code){
- Map
mp=new HashMap<>(); - mp.put("appid", weChatProperties.getAppid());
- mp.put("secret",weChatProperties.getSecret());
- mp.put("js_code",code);
- mp.put("grant_type","authorization_code");
- String json=HttpClientUtil.doGet(WX_LOGIN,mp);
- JSONObject jsonObject= JSON.parseObject(json);
- return jsonObject.getString("openid");
- }
同时注意要再设置一个拦截器拦截来自用户端的请求
后端代码与管理端大同小异,不再赘述,根据需求文档编写代码
但是,在用户端访问时,可能出现频繁请求访问的情况,使用redis缓存来减轻数据库压力,再通过分类查询菜品后,将该分类id作为key,将得到的数据放入redis缓存中,下次用户查询若redis中已经存在该数据,就不用查询数据库了
- String key="dish_"+categoryId;
- List
list = (List) redisTemplate.opsForValue().get(key); - if(list!=null && !list.isEmpty())
- {
- return Result.success(list);
- }
为了保持数据的一致性,管理端在更新修改菜品时,应该清除redis中相关的缓存
在修改操作返回前执行清除缓存操作
- private void cleanCache(String pattern)
- {
- Set keys=redisTemplate.keys(pattern);
- redisTemplate.delete(keys);
- }
补全套餐接口,注意没写根据id获得套餐信息(数据回显)之前无法正常测试修改套餐接口,(明明前端已经有套餐id了还要使用回显的id),代码重复度高,略
spring_cache使用方法
导入坐标
-
-
org.springframework.boot -
spring-boot-starter-cache -
在启动类上加上@EnableCaching注解开启缓存代理,会自动判断你使用的缓存来进行管理
在需要管理的方法上加上对应注解即可
@Cacheable(cacheNames="xxx",key="#yyy")
将在缓存中添加key为xxx::yyy,value为方法返回值的键值对(#代表取值而不是字符串)
@CacheEvict(cacheNames="xxx",allentries=true)
删除所有xxx::的缓存,也可将后面替换为key来精确删除
单独开表实现购物车功能,购物车表里每一项都代表着某人的购物车里的某个菜,在添加时先查询是否存在,如果存在就修改数量,不存在就从其他表里拿出必要信息补全并添加,删除同理,删除单个时要先判断菜品的数量来决定修改还是删除
添加代码如下
- @Override
- public void add(ShoppingCartDTO shoppingCartDTO) {
- ShoppingCart shoppingCart=new ShoppingCart();
- BeanUtils.copyProperties(shoppingCartDTO,shoppingCart);
- shoppingCart.setUserId(BaseContext.getCurrentId());
- List
list=shoppingCartMapper.list(shoppingCart); - if(list!=null && !list.isEmpty())
- {
- ShoppingCart res=list.get(0);
- res.setNumber(res.getNumber()+1);
- shoppingCartMapper.update(res);
- return ;
- }
-
- if(shoppingCart.getDishId()!=null)
- {
- Long dishId=shoppingCart.getDishId();
- Dish dish= new Dish();
- dish.setId(dishId);
- List
res=dishMapper.list(dish); - dish=res.get(0);
- shoppingCart.setImage(dish.getImage());
- shoppingCart.setName(dish.getName());
- shoppingCart.setAmount(dish.getPrice());
- }
- else
- {
- Long setmealId=shoppingCart.getSetmealId();
- Setmeal setmeal=setmealMapper.selectById(setmealId);
- shoppingCart.setImage(setmeal.getImage());
- shoppingCart.setName(setmeal.getName());
- shoppingCart.setAmount(setmeal.getPrice());
- }
- shoppingCart.setNumber(1);
- shoppingCart.setCreateTime(LocalDateTime.now());
- shoppingCartMapper.add(shoppingCart);
- }
删除代码如下
- @Override
- public void delete(ShoppingCartDTO shoppingCartDTO) {
- ShoppingCart shoppingCart=new ShoppingCart();
- BeanUtils.copyProperties(shoppingCartDTO,shoppingCart);
- shoppingCart.setUserId(BaseContext.getCurrentId());
-
- List
list=shoppingCartMapper.list(shoppingCart); - ShoppingCart res=list.get(0);
- if(res.getNumber()>1)
- {
- res.setNumber(res.getNumber()-1);
- shoppingCartMapper.update(res);
- }
- else
- {
- shoppingCartMapper.delete(shoppingCart);
- }
- }
订单提交到后端时,先处理下数据,处理没有地址,购物车为空的情况(前端已经完成过,以防万一),然后将前端传过来的数据补全,添加到订单表(order)和订单详情表(order_detail),最后注意清空用户的购物车表(shoppingcart)
个人认证的小程序没法使用支付功能,这里跳过支付,只要用户点击支付就算支付成功,代码如下
- @Override
- public OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) {
- Long userId=BaseContext.getCurrentId();
- User user=userMapper.getByid(userId);
-
- JSONObject jsonObject = new JSONObject();
- jsonObject.put("code", "ORDERPAID");
-
- OrderPaymentVO vo = jsonObject.toJavaObject(OrderPaymentVO.class);
- vo.setPackageStr(jsonObject.getString("package"));//返回前端的对象
-
- Orders orders=new Orders();
- orders.setNumber(ordersPaymentDTO.getOrderNumber());
- orders.setUserId(userId);
-
- Orders ordersDB = orderMapper.select(orders);//根据userid和订单号查询指定订单
-
- orders.setId(ordersDB.getId());
- orders.setStatus(Orders.TO_BE_CONFIRMED);
- orders.setPayStatus(Orders.PAID);
- orders.setPayMethod(ordersPaymentDTO.getPayMethod());
- orders.setCheckoutTime(LocalDateTime.now());
-
- orderMapper.update(orders);//更新为支付状态
- return vo;
- }
微信小程序支付官方流程图
使用方法
导入坐标(在整合包中已经包含)
在启动类上添加@EnableScheduling开启定时任务
创建定时任务类,在方法上添加@Scheduled(cron = "参数")注解
在项目启动后,就会按照cron所表达的内容定时执行该方法
- @Scheduled(cron = "0 * * * * ?")
- public void timeOutOrder()
- {
- log.info("处理超时未支付订单:{}",LocalDateTime.now());
- LocalDateTime time=LocalDateTime.now().plusMinutes(-15);
- List
list=orderMapper.wrongOrder(Orders.PENDING_PAYMENT,time); - if(list==null||list.isEmpty()) return;
-
- for(Orders orders:list)
- {
- orders.setStatus(Orders.CANCELLED);
- orders.setCancelReason("支付超时");
- orders.setCancelTime(LocalDateTime.now());
- orderMapper.update(orders);
- }
- }
-
-
- @Select("select * from orders where status=#{status} and order_time<#{orderTime}")
- List
wrongOrder(Integer status, LocalDateTime orderTime);
与http短连接协议不同,ws协议是持续链接协议,建立了全双工通道,可以实时相互发送消息,项目里包装使用,代码如下
- @Component
- @ServerEndpoint("/ws/{sid}")
- public class WebSocketServer {
-
- //存放会话对象
- private static Map
sessionMap = new HashMap(); -
- /**
- * 连接建立成功调用的方法
- */
- @OnOpen
- public void onOpen(Session session, @PathParam("sid") String sid) {
- System.out.println("客户端:" + sid + "建立连接");
- sessionMap.put(sid, session);
- }
-
- /**
- * 收到客户端消息后调用的方法
- *
- * @param message 客户端发送过来的消息
- */
- @OnMessage
- public void onMessage(String message, @PathParam("sid") String sid) {
- System.out.println("收到来自客户端:" + sid + "的信息:" + message);
- }
-
- /**
- * 连接关闭调用的方法
- *
- * @param sid
- */
- @OnClose
- public void onClose(@PathParam("sid") String sid) {
- System.out.println("连接断开:" + sid);
- sessionMap.remove(sid);
- }
-
- /**
- * 群发
- *
- * @param message
- */
- public void sendToAllClient(String message) {
- Collection
sessions = sessionMap.values(); - for (Session session : sessions) {
- try {
- //服务器向客户端发送消息
- session.getBasicRemote().sendText(message);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
-
- }
由于无法实现支付,新订单提醒放在了暂定的实现方法后面,正常应放在支付成功,微信服务端返回调用的函数中
催单提醒时,先判断订单号和订单状态,存在相应的订单才向客户端发送催单
此为前端技术,引入js文件后,按照指定格式填写数据就能简单地在页面生成一个表格
因此,后端的任务根据接口文档就知道返回数据列表即可
先将前端转过来的日期范围落实成一个日期的列表,然后根据列表的内容去查询每天的营业额,再把每天的营业额放入另一个列表,最后将两个列表封装返回给前端
- @Override
- public TurnoverReportVO turnoverStatistics(LocalDate begin, LocalDate end) {
- List
datelist=new ArrayList<>(); - datelist.add(begin);
- while(!begin.equals(end))
- {
- begin=begin.plusDays(1);
- datelist.add(begin);
- }
- List
incomelist=new ArrayList<>(); - for(LocalDate date:datelist)
- {
- LocalDateTime beginTime=LocalDateTime.of(date, LocalTime.MIN);
- LocalDateTime endTime=LocalDateTime.of(date, LocalTime.MAX);
- Map mp=new HashMap<>();
- mp.put("begin",beginTime);
- mp.put("end",endTime);
- mp.put("status",Orders.COMPLETED);
- Double income=orderMapper.sumByMap(mp);
- incomelist.add(income==null?0.0:income);
- }
- return TurnoverReportVO.builder()
- .dateList(org.apache.commons.lang.StringUtils.join(datelist,","))
- .turnoverList(StringUtils.join(incomelist,","))
- .build();
- }
通过上述制表技术,前端要求给出日期始末,返回相应的数据
将拿到的日期先得到一个日期的List,再根据每一个日期在数据库中相应的数据
将得到的数据List转化为字符串(此处可用stream流),封装之后返回给前端
按着计划写下来最后就差订单管理板块,根据接口文档写即可,注意在put的请求时,这里接收参数都是用dto或path,单独接收id会出错(传过来的就是对象)
至此项目基本完成