# 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端-用户相关 | 微信登录 |