# Arrived **Repository Path**: Cililin/Arrived ## Basic Information - **Project Name**: Arrived - **Description**: 外卖平台——到了么 - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2024-08-17 - **Last Updated**: 2024-08-26 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## 到了么外卖后端管理接口开发 ### ### 技术栈 #### Swagger:生成接口文档、在线接口调试 ##### 使用: 1.引入依赖 ```xml com.github.xiaoymin knife4j-spring-boot-starter ``` 2.配置 ```java /** * 通过knife4j生成接口文档 * * @return */ @Bean public Docket docketAdmin() { ApiInfo apiInfo = new ApiInfoBuilder() .title("到了么项目接口文档") .version("1.0") .description("到了么项目接口文档") .build(); Docket docket = new Docket(DocumentationType.SWAGGER_2) .groupName("管理端接口") .apiInfo(apiInfo) .select() .apis(RequestHandlerSelectors.basePackage("com.arrived.controller.admin")) .paths(PathSelectors.any()) .build(); return docket; } @Bean public Docket docketUser() { ApiInfo apiInfo = new ApiInfoBuilder() .title("到了么项目接口文档") .version("1.0") .description("到了么项目接口文档") .build(); Docket docket = new Docket(DocumentationType.SWAGGER_2) .groupName("用户端接口") .apiInfo(apiInfo) .select() .apis(RequestHandlerSelectors.basePackage("com.arrived.controller.user")) .paths(PathSelectors.any()) .build(); return docket; } ``` 3.设置映射 ```java /** * 注册自定义拦截器 * * @param registry */ protected void addInterceptors(InterceptorRegistry registry) { //log.info("开始注册自定义拦截器..."); registry.addInterceptor(jwtTokenAdminInterceptor) .addPathPatterns("/admin/**") .excludePathPatterns("/admin/employee/login"); registry.addInterceptor(jwtTokenUserInterceptor) .addPathPatterns("/user/**") .excludePathPatterns("/user/user/login") .excludePathPatterns("/user/shop/status"); } ``` #### Spring Data Redis:Spring对Redis底层开发进行高度封装 ##### 使用: 1.导入依赖坐标 ```xml org.springframework.boot spring-boot-starter-data-redis ``` 2.配置Redis数据源 ```yml arrived: redis: host: localhost port: 6379 password: 123456 ``` 3.编写配置类,创建RedisTemplate对象 ```java @Configuration @Slf4j public class RedisConfiguration { @Bean public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { //log.info("开始创建redis模板对象..."); RedisTemplate redisTemplate = new RedisTemplate(); // 设置redis连接工厂对象 redisTemplate.setConnectionFactory(redisConnectionFactory); //log.info("redisTemplate初始化成功..."); // 设置redis key的序列化器 redisTemplate.setKeySerializer(new StringRedisSerializer()); return redisTemplate; } } ``` 4.通过对象操作Redis ```java @RestController("userShopController") @RequestMapping("/user/shop") @Slf4j @Api(tags = "店铺相关接口") public class ShopController { public static final String KEY = "SHOP_STATUS"; @Autowired private RedisTemplate redisTemplate; /** * 查询店铺的营业状态 * * @return */ @GetMapping("/status") @ApiOperation("查询店铺的营业状态") public Result getStatus() { Integer status = (Integer) redisTemplate.opsForValue().get(KEY); //log.info("查询店铺的营业状态为:{}", status == 1 ? "营业中" : "打烊中"); return Result.success(status); } } ``` #### Spring Cache:框架,实现注解缓存 ##### 使用: 1.导入依赖 ```xml org.springframework.boot spring-boot-starter-cache ``` 2.常用注解 | 注解 | 说明 | | -------------- | ------------------------------------------------------------ | | @EnableCaching | 开启缓存注解功能,通常加在启动类上 | | @Cacheable | 在方法执行前先查询缓存中是否有数据,如果有数据,则直接返回缓存数据;如果没有缓存数据,调用方法并将方法返回值放到缓存中 | | @CachePut | 将方法的返回值放到缓存中 | | @CacheEvict | 将一条或多条数据从内存中删除 | 3.添加EnableCahching注解 ```java @SpringBootApplication @Slf4j @EnableCaching //开启缓存注解功能 public class ArrivedApplication { public static void main(String[] args) { SpringApplication.run(ArrivedApplication.class, args); } } ``` 4.编写业务代码,添加`@Cacheable(cacheNames = "setmealCache", key = "#categoryId")` ```java @RestController("userSetmealController") @RequestMapping("/user/setmeal") @Api(tags = "C端-套餐浏览接口") public class SetmealController { @Autowired private SetmealService setmealService; /** * 条件查询 * * @param categoryId * @return */ @GetMapping("/list") @ApiOperation("根据分类id查询套餐") @Cacheable(cacheNames = "setmealCache", key = "#categoryId") public Result> list(Long categoryId) { Setmeal setmeal = new Setmeal(); setmeal.setCategoryId(categoryId); setmeal.setStatus(StatusConstant.ENABLE); List list = setmealService.list(setmeal); return Result.success(list); } } ``` 5.添加`@CacheEvict(cacheNames = "setmealCache",allEntries = true)` ```java @RestController @RequestMapping("/admin/setmeal") @Api(tags = "套餐相关接口") @Slf4j public class SetmealController { @Autowired private SetmealService setmealService; @PostMapping @ApiOperation("新增套餐") @CacheEvict(cacheNames = "setmealCache",key = "#setmealDTO.categoryId") public Result save(@RequestBody SetmealDTO setmealDTO) { setmealService.saveWithDish(setmealDTO); return Result.success(); } /** * 批量删除套餐 * * @param ids * @return */ @DeleteMapping @ApiOperation("套餐批量删除") @CacheEvict(cacheNames = "setmealCache",allEntries = true) public Result delete(@RequestParam List ids) { setmealService.deleteBatch(ids); return Result.success(); } /** * 修改套餐 * * @param setmealDTO * @return */ @PutMapping @ApiOperation("修改套餐") @CacheEvict(cacheNames = "setmealCache",allEntries = true) public Result update(@RequestBody SetmealDTO setmealDTO) { setmealService.update(setmealDTO); return Result.success(); } /** * 套餐起售停售 * * @param status * @param id * @return */ @PostMapping("/status/{status}") @ApiOperation("套餐起售停售") @CacheEvict(cacheNames = "setmealCache",allEntries = true) public Result startOrStop(@PathVariable Integer status, Long id) { setmealService.startOrStop(status, id); return Result.success(); } } ``` #### Spring Task:Spring框架提供任务调度工具,按照约定时间自动执行某个代码逻辑 ##### 使用: 1.引入依赖:spring-context 2.添加`@EnableScheduling`注解,开启任务调度 ```java @SpringBootApplication @EnableScheduling //开启定时任务功能 public class ArrivedApplication { public static void main(String[] args) { SpringApplication.run(ArrivedApplication.class, args); log.info("server started"); } } ``` 3,自定义定时任务类 ```java /** * 定时任务类,定时处理订单状态 */ @Component @Slf4j public class OrderTask { @Autowired private OrderMapper orderMapper; /** * 处理超时订单 */ @Scheduled(cron = "0 * * * * ? ") // 每分钟执行一次 public void orderTimeOutProcessTask() { //log.info("开始处理超时订单"); LocalDateTime time = LocalDateTime.now().plusMinutes(-15); List ordersList = orderMapper.getByStatusAndOrderTime(Orders.PENDING_PAYMENT, time); if (ordersList != null && !ordersList.isEmpty()) { for (Orders orders : ordersList) { orders.setStatus(Orders.CANCELLED); orders.setCancelReason("订单超时,自动取消"); orders.setCancelTime(LocalDateTime.now()); orderMapper.update(orders); } } } /** * 处理派送中订单 */ @Scheduled(cron = "0 0 1 * * ? ") // 每天凌晨1点执行一次 public void orderDeliveryProcessTask() { //log.info("开始处理派送中订单:{}", LocalDateTime.now()); LocalDateTime time = LocalDateTime.now().plusMinutes(-60); List ordersList = orderMapper.getByStatusAndOrderTime(Orders.DELIVERY_IN_PROGRESS, time); if (ordersList != null && !ordersList.isEmpty()) { for (Orders orders : ordersList) { orders.setStatus(Orders.COMPLETED); orderMapper.update(orders); } } } } ``` #### WebSocket:基于TCP的网络协议。实现浏览器与服务器全双工通信——只需要完成一次握手即可创建持久性连接,并双向数据传输 ##### 区别 | HTTP协议 | WebSocket协议 | | -------------------------- | ------------- | | 短链接 | 长连接 | | 单向通信,基于请求响应模式 | 双向通信 | 共同点:底层都是TCP连接 ##### 使用 1.引入依赖坐标 ```xml org.springframework.boot spring-boot-starter-websocket ``` 2.编写WebSocket服务端组件WebSocketServer,用于和客户端通信 ```java /** * WebSocket服务 */ @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(); } } } } ``` 3.配置并注册其组件 ```java /** * WebSocket配置类,用于注册WebSocket的Bean */ @Configuration public class WebSocketConfiguration { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } } ``` #### Apache POI:处理MS各种文件的开源项目。对文件进行读写操作 ```java @Service @Slf4j public class ReportServiceImpl implements ReportService { @Autowired private OrderMapper orderMapper; @Autowired private UserMapper userMapper; @Autowired private WorkspaceService workspaceService; /** * 统计指定时间区间内的营业额数据 * * @param begin * @param end * @return */ @Override public TurnoverReportVO getTurnoverStatistics(LocalDate begin, LocalDate end) { //当前集合用于存放从begin到end范围内的每天的日期 List dateList = new ArrayList<>(); dateList.add(begin); while (!begin.equals(end)) { //日期计算,计算指定日期的后一天对应的的日期 begin = begin.plusDays(1); dateList.add(begin); } //存放每天的营业额 List turnoverList = new ArrayList<>(); for (LocalDate date : dateList) { //查询date日期对应的营业额数据,营业额指:状态为“已完成”的订单总金额 LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN); LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX); Map map = new HashMap(); map.put("begin", beginTime); map.put("end", endTime); map.put("status", Orders.COMPLETED); Double turnover = orderMapper.sumByMap(map); turnover = turnover == null ? 0.0 : turnover; turnoverList.add(turnover); } //封装返回结果 return TurnoverReportVO .builder() .dateList(StringUtils.join(dateList, ",")) .turnoverList(StringUtils.join(turnoverList, ",")) .build(); } /** * 统计指定时间区间内的用户数据 * * @param begin * @param end * @return */ @Override public UserReportVO getUserStatistics(LocalDate begin, LocalDate end) { //当前集合用于存放从begin到end范围内的每天的日期 List dateList = new ArrayList<>(); dateList.add(begin); while (!begin.equals(end)) { //日期计算,计算指定日期的后一天对应的的日期 begin = begin.plusDays(1); dateList.add(begin); } //存放每天的新增用户数量 List newUserList = new ArrayList<>(); //存放每天的总用户数量 List totalUserList = new ArrayList<>(); for (LocalDate date : dateList) { LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN); LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX); Map map = new HashMap(); map.put("end", endTime); //总用户数量 Integer totalUser = userMapper.countByMap(map); map.put("begin", beginTime); //新增用户数量 Integer newUser = userMapper.countByMap(map); totalUserList.add(totalUser); newUserList.add(newUser); } //封装结果数据 return UserReportVO .builder() .dateList(StringUtils.join(dateList, ",")) .totalUserList(StringUtils.join(totalUserList, ",")) .newUserList(StringUtils.join(newUserList, ",")) .build(); } /** * 统计指定时间区间内的订单数据 * * @param begin * @param end * @return */ @Override public OrderReportVO getOrderStatistics(LocalDate begin, LocalDate end) { //当前集合用于存放从begin到end范围内的每天的日期 List dateList = new ArrayList<>(); dateList.add(begin); while (!begin.equals(end)) { //日期计算,计算指定日期的后一天对应的的日期 begin = begin.plusDays(1); dateList.add(begin); } //存放每天的订单总数 List orderCountList = new ArrayList<>(); //存放每天的有效订单数 List validOrderCountList = new ArrayList<>(); //遍历dateList集合,查询每天的有效订单数和订单总数 for (LocalDate date : dateList) { //查询每天的订单总数 LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN); LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX); Integer orderCount = getOrderCount(beginTime, endTime, null); //查询每天的有效订单数 Integer validOrderCount = getOrderCount(beginTime, endTime, Orders.COMPLETED); orderCountList.add(orderCount); validOrderCountList.add(validOrderCount); } //计算时间区间内的订单总数 Integer totalOrderCount = orderCountList.stream().reduce(Integer::sum).get(); //计算时间区间内的有效订单数量 Integer validOrderCount = validOrderCountList.stream().reduce(Integer::sum).get(); Double orderCompletionRate = 0.0; if (totalOrderCount != 0) { //计算订单完成率 orderCompletionRate = validOrderCount.doubleValue() / totalOrderCount; } return OrderReportVO.builder() .dateList(StringUtils.join(dateList, ",")) .orderCountList(StringUtils.join(orderCountList, ",")) .validOrderCountList(StringUtils.join(validOrderCountList, ",")) .totalOrderCount(totalOrderCount) .validOrderCount(validOrderCount) .orderCompletionRate(orderCompletionRate) .build(); } /** * 统计指定时间区间内的销量前10 * * @param begin * @param end * @return */ @Override public SalesTop10ReportVO getSalesTop10(LocalDate begin, LocalDate end) { LocalDateTime beginTime = LocalDateTime.of(begin, LocalTime.MIN); LocalDateTime endTime = LocalDateTime.of(end, LocalTime.MAX); List salesTop10 = orderMapper.getSalesTop10(beginTime, endTime); List names = salesTop10.stream().map(GoodsSalesDTO::getName).collect(Collectors.toList()); String nameList = StringUtils.join(names, ","); List numbers = salesTop10.stream().map(GoodsSalesDTO::getNumber).collect(Collectors.toList()); String numberList = StringUtils.join(numbers, ","); return SalesTop10ReportVO .builder() .nameList(nameList) .numberList(numberList) .build(); } /** * 导出运营数据报表 * * @param response */ @Override public void exportBusinessData(HttpServletResponse response) { //1. 查询数据库,获取营业数据---查询最近30天的运营数据 LocalDate dateBegin = LocalDate.now().minusDays(30); LocalDate dateEnd = LocalDate.now().minusDays(1); //查询概览数据 BusinessDataVO businessDataVO = workspaceService.getBusinessData(LocalDateTime.of(dateBegin, LocalTime.MIN), LocalDateTime.of(dateEnd, LocalTime.MAX)); //2. 通过POI将数据写入到Excel文件中 InputStream in = this.getClass().getClassLoader().getResourceAsStream("template/运营数据报表模板.xlsx"); try { //基于模板文件创建一个新的Excel文件 XSSFWorkbook excel = new XSSFWorkbook(in); //获取表格文件的Sheet页 XSSFSheet sheet = excel.getSheet("Sheet1"); //填充数据--时间 sheet.getRow(1).getCell(1).setCellValue("时间:" + dateBegin + "至" + dateEnd); //获得第4行 XSSFRow row = sheet.getRow(3); row.getCell(2).setCellValue(businessDataVO.getTurnover()); row.getCell(4).setCellValue(businessDataVO.getOrderCompletionRate()); row.getCell(6).setCellValue(businessDataVO.getNewUsers()); //获得第5行 row = sheet.getRow(4); row.getCell(2).setCellValue(businessDataVO.getValidOrderCount()); row.getCell(4).setCellValue(businessDataVO.getUnitPrice()); //填充明细数据 for (int i = 0; i < 30; i++) { LocalDate date = dateBegin.plusDays(i); //查询某一天的营业数据 BusinessDataVO businessData = workspaceService.getBusinessData(LocalDateTime.of(date, LocalTime.MIN), LocalDateTime.of(date, LocalTime.MAX)); //获得某一行 row = sheet.getRow(7 + i); row.getCell(1).setCellValue(date.toString()); row.getCell(2).setCellValue(businessData.getTurnover()); row.getCell(3).setCellValue(businessData.getValidOrderCount()); row.getCell(4).setCellValue(businessData.getOrderCompletionRate()); row.getCell(5).setCellValue(businessData.getUnitPrice()); row.getCell(6).setCellValue(businessData.getNewUsers()); } //3. 通过输出流将Excel文件下载到客户端浏览器 ServletOutputStream out = response.getOutputStream(); excel.write(out); //关闭资源 out.close(); excel.close(); } catch (IOException e) { e.printStackTrace(); } } private Integer getOrderCount(LocalDateTime beginTime, LocalDateTime endTime, Integer status) { Map map = new HashMap(); map.put("begin", beginTime); map.put("end", endTime); map.put("status", status); return orderMapper.countByMap(map); } } ``` ### 业务功能 #### 管理端 | 功能模块 | 具体接口 | | ------------ | ------------------------------------------------------------ | | 分类相关 | 新增分类、删除分类、修改分类、分类分页查询、启用禁用分类、根据类型查询分类 | | 通用 | 文件上传 | | 菜品相关 | 新增菜品、菜品批量删除、修改菜品、菜品分页查询、菜品起售停售、根据id查询菜品、根据分类id查询菜品 | | 员工相关 | 员工登录、员工退出、新增员工、启用禁用员工账号、修改员工信息、员工分页查询、根据id查询员工信息 | | 订单管理 | 订单搜索、接单、拒单、取消订单、派送订单、完成订单、查询订单详情、各个状态的订单数量统计 | | 店铺相关 | 设置店铺的营业状态、查询店铺的营业状态 | | 套餐相关 | 新增套餐、修改套餐、套餐批量删除、套餐起售停售、套餐分页查询、根据id查询套餐 | | 工作台相关 | 工作台今日数据查询、查询订单管理数据、查询菜品总览、查询套餐总览 | | 数据统计相关 | 营业额统计、用户统计、订单统计、销量排名top10、导出运营数据报表 | #### 用户端 | 功能模块 | 具体接口 | | -------------- | ------------------------------------------------------------ | | C端-地址簿 | 查询当前登录用户的所有地址信息、新增地址、根据id查询地址、根据id修改地址、设置默认地址、根据id删除地址、查询默认地址 | | C端-分类 | 查询分类 | | C端-菜品浏览 | 根据分类id查询菜品 | | C端-订单相关 | 用户下单、订单支付、历史订单查询、查询订单详情、取消订单、再来一单、客户催单 | | C端-套餐浏览 | 根据分类id查询套餐、根据套餐id查询包含的菜品列表 | | 店铺相关 | 查询店铺的营业状态 | | C端-购物车相关 | 添加购物车中一个商品、删除购物车中一个商品、查询购物车商品、情况购物车商品 | | C端-用户相关 | 微信登录 |