苍穹外卖项目--学习笔记
苍穹外卖学习文档
代码仓库:https://github.com/xu1feng/sky-take-out
软件开发整体介绍
软件开发流程
需求分析
需求规格说明书、产品原型
设计
UI设计、数据库设计、接口设计
编码
项目代码、单元测试
测试
测试用例、测试报告
上线运维
软件环境安装、配置
角色分工
-
项目经理
对整体项目负责,任务分配、把控进度
-
产品经理
进行需求调研。输出需求调研文档、产品原型等
-
UI设计师
根据产品模型输出界面效果图
-
架构师
项目整体架构设计、技术选型等
-
开发工程师
代码实现
-
测试工程师
编写测试用例,输出测试报告
-
运维工程师
软件环境搭建、项目上线
软件环境
开发环境
开发人员在开发阶段使用的环境,一般外部用户无法访问
测试环境
专门给测试人员使用的环境,用于测试项目,一般外部用户无法访问
生产环境
即线上环境,正式提供对外服务的环境
苍穹外卖项目介绍
项目介绍
定位:专门为餐饮企业定制的一款软件产品
功能架构:

产品原型
用于展示项目的业务功能
技术选型
展示项目中使用到的技术框架和中间件等
开发环境搭建
前端环境搭建
整体结构

通过Nginx代理

后端环境搭建
熟悉项目结构

sky-common子模块
- constant:常量类
- context:项目上下文相关
- enumeration:枚举类
- exception:自定义异常类
- json:处理json转换
- properties:springboot配置属性类,把配置文件中的配置项封装成对象
- result:后端返回的结果
- utils:工具类
sky-pojo子模块

sky-server子模块
存放的是 配置文件、配置类、拦截器、controller、service、mapper、启动类等
使用Git进行版本控制
- 创建Git本地仓库
- 创建Git远程仓库
- 将本地文件推送到Git远程仓库
数据库环境搭建

前后端联调
Nginx🆕
反向代理,就是让前端发送的动态请求由Nginx转发到后端服务器
Nginx反向代理的好处
-
提高访问速度
-
进行负载均衡
就是把大量的请求按照我们指定的方式均衡的分配给集群中的每台服务器
-
保证后端服务安全
Nginx反向代理的配置方式


Nginx负载均衡的配置方式

Nginx负载均衡策略

轮询:平均接收到请求
完善登录功能
- 修改数据库中的明文密码,改为MD5加密后的密文
- 修改Java代码,前端提交的密码进行MD5加密后再跟数据库中密码比对

导入接口文档
前后端分离开发流程

操作步骤

这里YApi可换成ApiPost,导入数据选择YApi即可

Swagger
介绍
使用方式
-
导入knife4j的maven坐标
-
在配置类中加入knife4j相关配置
/** * 通过knife4j生成接口文档 * @return */ @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; } -
设置静态资源映射,否则接口文档页面无法访问
/** * 设置静态资源映射 * @param registry */ protected void addResourceHandlers(ResourceHandlerRegistry registry) { log.info("开始设置静态资源映射..."); registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/"); registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/"); }
常用注解
通过注解可以控制生成的接口文档,使接口文档拥有更好的可读性,常用注解如下:
员工管理、分类管理
员工管理界面
分类管理界面
新增员工
需求分析和设计
产品原型

接口设计
本项目约定:
- 管理端发出的请求,统一使用
/admin
作为前缀 - 用户端发出的请求,统一使用
/user
作为前缀
数据库设计
employee表为员工表,用于存储商家内部的员工信息。具体表结构如下:
字段名 | 数据类型 | 说明 | 备注 |
---|---|---|---|
id | bigint | 主键 | 自增⭐ |
name | varchar(32) | 姓名 | |
username | varchar(32) | 用户名 | 唯一⭐ |
password | varchar(64) | 密码 | |
phone | varchar(11) | 手机号 | |
sex | varchar(2) | 性别 | |
id_number | varchar(18) | 身份证号 | |
status | int | 账号状态 | 1正常 0锁定⭐ |
create_time | datetime | 创建时间 | |
update_time | datetime | 最后修改时间 | |
create_user | bigint | 创建人id | |
update_user | bigint | 最后修改人id |
代码开发
根据新增员工接口设计对应的DTO
注意:当前端提交的数据和实体类中对应的属性差别比较大时,建议使用DTO来封装数据
/** * 新增员工 * @param employeeDTO */ public void save(EmployeeDTO employeeDTO) { Employee employee = new Employee(); //对象属性拷贝,属性名必须一致 BeanUtils.copyProperties(employeeDTO, employee); //设置账号的状态,默认正常状态,1表示正常,0表示锁定 employee.setStatus(StatusConstant.ENABLE); //设置密码,默认密码为123456 employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes())); //设置当前记录的创建时间和修改时间 employee.setCreateTime(LocalDateTime.now()); employee.setUpdateTime(LocalDateTime.now()); //设置当前记录的创建人id和修改人id // TODO 后期需要改为当前登录的用户id employee.setCreateUser(10L); employee.setUpdateUser(10L); employeeMapper.insert(employee); }
功能测试
- 通过Swagger接口文档测试
- 通过前后端联调测试
注意:由于开发阶段前端和后端是并行开发的,后端完成某个功能后,此时前端对应的功能可能还没有开发完成,导致无法进行前后端联调测试。所以在开发阶段,后端测试主要以接口文档测试为主。
代码完善
程序存在的问题:
- 录入的用户名已存在,抛出异常后没有处理
- 新增员工时,创建人id和修改人id设置为了固定值

解析出登录员工id后,如何传递给Service的save方法?
员工分页查询
需求分析和设计
产品原型
业务规则:
- 根据每页展示员工信息
- 每页展示10条数据
- 分页查询时可以根据需要,输入员工姓名进行查询
接口设计
代码开发
员工信息分页查询后端返回的对象类型为:Result<PageResult>
mybatis提供的分页查询框架pagehelper
<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>${pagehelper}</version> </dependency>
功能测试
可以通过接口文档进行测试,也可以进行前后端联调测试。
代码完善
最后操作时间需要修改成年月日
解决方式:
-
方式一:在属性上加入注解,对日期进行格式化
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime updateTime -
方式二:在Webconfiguration中扩展SpringMvc的消息转换器,统一对日期类型进行格式化处理
启用禁用员工账号
需求分析和设计
产品原型
业务规则:
- 可以对状态为“启用”的员工账号进行“禁用”操作
- 可以对状态为“禁用”的员工账号进行“启用”操作
- 状态为“禁用”的员工账号不能登录系统
接口设计
代码开发
/** * 启用/禁用员工账号 * @param status * @param id * @return */ @PostMapping("status/{status}") @ApiOperation("启用/禁用员工账号") public Result startOrStop(@PathVariable("status") Integer status, Long id) { log.info("启用/禁用员工账号:{}, {}", status, id); employeeService.startOrStop(status, id); return Result.success(); }
/** * 启用/禁用员工账号 * @param status * @param id */ public void startOrStop(Integer status, Long id) { // update employee set status = ? where id = ? /*Employee employee = new Employee(); employee.setStatus(status); employee.setId(id);*/ Employee employee = Employee.builder() .status(status) .id(id) .build(); employeeMapper.update(employee); }
<update id="update" parameterType="Employee"> update employee <set> <if test="name != null">name = #{name},</if> <if test="username != null">username = #{username},</if> <if test="password != null">password = #{password},</if> <if test="phone != null">phone = #{phone},</if> <if test="sex != null">sex = #{sex},</if> <if test="idNumber != null">id_Number = #{idNumber},</if> <if test="updateTime != null">update_Time = #{updateTime},</if> <if test="updateUser != null">update_User = #{updateUser},</if> <if test="status != null">status = #{status},</if> </set> where id = #{id} </update>
功能测试
编辑员工
需求分析和设计
产品原型
编辑员工功能涉及到两个接口:
- 根据id查询员工信息
- 编辑员工信息
代码开发
/** * 编辑员工信息 * @param employeeDTO * @return */ @PutMapping @ApiOperation("编辑员工信息") public Result update(@RequestBody EmployeeDTO employeeDTO) { log.info("编辑员工信息: {}", employeeDTO); employeeService.update(employeeDTO); return Result.success(); }
/** * 编辑员工信息 * @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表):
category表为分类表,用于存储商品的分类信息。具体表结构如下
字段名 | 数据类型 | 说明 | 备注 |
---|---|---|---|
id | bigint | 主键 | 自增 |
name | varchar(32) | 分类名称 | 唯一 |
type | int | 分类类型 | 1菜品分类 2套餐分类 |
sort | int | 排序字段 | 用于分类数据的排序 |
status | int | 状态 | 1启用 0禁用 |
create_time | datetime | 创建时间 | |
update_time | datetime | 最后修改时间 | |
create_user | bigint | 创建人id | |
update_user | bigint | 最后修改人id |
代码导入
功能测试
菜品管理
公共字段自动填充🌟
问题分析
业务表中的公共字段:
问题:代码冗余、不便于后期维护
实现思路
- 自定义注解
AutoFill
,用于标识需要进行公共字段自动填充的方法 - 自定义切面类
AutoFillAspect
,统一拦截加入了AutoFill
注解的方法,通过反射为公共字段赋值 - 在Mapper的方法上加入
AutoFill
注解
代码开发
功能测试
新增菜品
需求分析和设计
产品原型:
业务规则:
- 菜品名称必须是唯一的
- 菜品必须属于某个分类下,不能单独存在
- 新增菜品时可以根据情况选择菜品的口味
- 每个菜品必须对应一张图片
接口设计:
-
根据类型查询分类(已完成)
-
文件上传
-
新增菜品
数据库设计(dish菜品表和dish_flavor口味表):
dish表为菜品表,用于存储菜品的信息。具体表结构如下
字段名 | 数据类型 | 说明 | 备注 |
---|---|---|---|
id | bigint | 主键 | 自增 |
name | varchar(32) | 菜品名称 | 唯一 |
category_id | bigint | 分类id | 逻辑外键 |
price | decimal(10,2) | 菜品价格 | |
image | varchar(255) | 图片路径 | |
description | varchar(255) | 菜品描述 | |
status | int | 售卖状态 | 1起售 0停售 |
create_time | datetime | 创建时间 | |
update_time | datetime | 最后修改时间 | |
create_user | bigint | 创建人id | |
update_user | bigint | 最后修改人id |
dish_flavor表为菜品口味表,用于存储菜品的口味信息。具体表结构如下
字段名 | 数据类型 | 说明 | 备注 |
---|---|---|---|
id | bigint | 主键 | 自增 |
dish_id | bigint | 菜品id | 逻辑外键 |
name | varchar(32) | 口味名称 | |
value | varchar(255) | 口味值 |
代码开发
开发文件上传接口:
功能测试
菜品分页查询
需求分析和设计
产品原型
业务规则
- 根据页码展示菜品信息
- 每页展示10条数据
- 分页查询时可以根据需要输入菜品名称、菜品分类、菜品状态进行查询
接口设计
代码开发
根据菜品分页查询接口定义设计对应的DTO:
根据菜品分页查询接口定义设计对应的VO:
功能测试
删除菜品
需求分析和设计
产品原型
业务规则:
- 可以一次删除一个菜品,也可以批量删除菜品
- 起售中的菜品不能删除
- 被套餐关联的菜品不能删除
- 删除菜品后,关联的口味数据也需要删除掉
接口设计:
数据库设计:
代码开发
功能测试
修改菜品
需求分析和设计
产品原型
接口设计:
-
根据id查询商品
-
根据类型查询分类(已实现)
-
文件上传(已实现)
-
修改商品
代码开发
功能测试
店铺营业状态设置
Redis入门
Redis简介
Redis是一个基于内存的key-value结构数据库。
- 基于内存存储,读写性能高
- 适合存储热点数据(热点商品、资讯、新闻)
- 企业应用广泛
Redis下载与安装
Redis服务启动与停止
Redis数据类型
5种常用数据类型介绍
Redis存储的是key-value结构的数据,其中key是字符串类型,value有5种常用的数据类型:
-
字符串 string
普通字符串,Redis中最简单的数据类型
-
哈希 hash
也叫散列,类似于Java中的HashMap结构
-
列表 list
按照插入顺序排序,可以有重复元素,类似于Java中的LinkedList
-
集合 set
无序集合,没有重复元素,类似于Java中的HashSet
-
有序集合 sorted set/zset
集合中每个元素关联一个分数(score),Redis根据分数升序排序,没有重复元素
各种数据类型的特点

Redis常用命令
字符串操作命令
- SET key value 设置指定key的值
- GET key 获取指定key的值
- SETEX key seconds value 设置指定key的值,并将key的过期时间设为seconds秒 ---> 短信验证码
- SETNX key value 只有在key不存在时设置key的值
哈希操作命令
Redis hash 是一个String类型的 field 和 value 的映射表,hash特别适合用于存储对象,常用命令:
- HSET key field value 将哈希表 key 中的字段 field 的值为 value
- HGET key field 获取存储在哈希表中指定字段的值
- HDEL key field 删除存储在哈希表中的指定字段
- HKEYS key 获取哈希表中所有字段
- HVALS key 获取哈希表中所有值
列表操作命令
Redis列表是简单的字符串列表,按照插入顺序排序,常用命令:
- LPUSH key value1 [value2] 将一个或多个值插入到列表头部
- LRANGE key start stop 获取列表指定范围内的元素
- RPOP key 移除并获取列表最后一个元素
- LLEN key 获取列表长度
集合操作命令
Redis set 是 String 类型的无序集合。集合成员是唯一的,集合中不能出现重复的数据,常用命令:
- SADD key member1 [member2] 向集合添加一个或多个成员
- SMEMBERS key 返回集合中所有的成员
- SCARD key 获取集合的成员数
- SINTER key1 [key2] 返回给定的所有集合的交集
- SUNION key1 [key2] 返回所有给定集合的并集
- SREM key member1 [member2] 删除集合中一个或多个成员
有序集合操作命令
Redis的有序集合是String类型元素的集合,且不允许有重复成员。每个元素都会关联一个double类型的分数。常用命令:
- ZADD key score1 member1 [score2 member2] 向有序集合添加一个或多个成员
- ZRANGE key start stop [WITHSCORES] 通过索引区间返回有序集合中指定区间内的成员
- ZINCRBY key increment member 有序集合中对指定成员的分数加上增量increment
- ZREM key member [member... ] 移除有序集合中的一个或多个成员
通用命令
Redis的通用命令是不分数据类型的,都可以使用的命令:
- KEYS pattern 查找所有符合给定模式(pattern)的key
- EXISTS key 检查给定key是否存在
- TYPE key 返回key所存储的值的类型
- DEL key 该命令用于在key存在时删除key
在Java中操作Redis
Redis的Java客户端
Redis的Java客户端很多,常用的几种:
-
Jedis
-
Lettuce
-
Spring Data Redis
是Spring的一部分,对Redis底层开发包进行了高度封装。在Spring项目中,可以使用Spring Data Redis来简化操作。
Spring Data Redis使用方式
操作步骤:
-
导入Spring Data Redis 的Maven坐标
-
配置Redis数据源
-
编写配置类,创建RedisTemplate对象
-
通过RedisTemplate对象操作Redis
店铺营业状态
需求分析和设计
产品原型
接口设计:
- 设置营业状态
- 管理端查询营业状态
- 用户端查询营业状态
本项目约定:
- 管理端发出的请求,统一使用
/admin
作为前缀 - 用户端发出的请求,统一使用
/user
作为前缀
营业状态数据存储方式:基于Redis的字符串来进行存储
代码开发
功能测试
微信登录、商品浏览
HttpClient🆕
介绍
HttpClient 是Apache Jakarta Common下的子项目,可以用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本和建议。
核心API:
- HttpClient
- HttpClients
- CloseableHttpClient
- HttpGet
- HttpPost
发送请求步骤:
- 创建HttpClient对象
- 创建Http请求对象 ---> HttpGet/HttpPost
- 调用HttpClient的execute方法发送请求
入门案例
/** * 测试通过HttpClient发送GET方式请求 */ @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); 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); HttpEntity entity1 = response.getEntity(); String body = EntityUtils.toString(entity1); System.out.println("响应数据为:" + body); //关闭资源 response.close(); httpClient.close(); }
微信小程序开发
介绍
准备工作
入门案例
操作步骤:
-
了解微信小程序目录结构
小程序包含一个描述整体程序的 app 和多个描述各自页面的 page。一个小程序主体部分由三个文件组成,必须放在项目的根目录,如下:
一个小程序页面由四个文件组成:
-
编写小程序代码
-
编译小程序
微信登录
导入小程序代码
微信登录流程
官网:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html
需求分析和设计
产品原型:
业务规则:
- 基于微信登录实现小程序的登录功能
- 如果是新用户需要自动完成注册
接口设计:
数据库设计(user表):
代码开发
配置微信登录所需配置项:
配置为微信用户生成jwt令牌时使用的配置项:
功能测试
导入商品浏览功能代码
需求分析和设计
产品原型:
接口设计:
-
查询分类
-
根据分类id查询菜品
-
根据分类id查询套餐
-
根据套餐id查询包含的菜品
代码导入
功能测试
缓存商品、购物车
缓存菜品
问题说明
用户端小程序展示的菜品数据都是通过查询数据库获得,如果用户端访问量比较大,数据库访问压力随之增大。
实现思路
通过Redis来缓存菜品数据,减少数据库查询操作。
缓存逻辑分析:
-
每个分类下的菜品保存一份缓存数据
-
数据库中菜品数据有变更时清理缓存数据
代码开发
修改管理端接口 DishController 的相关方法,加入清理缓存的逻辑,需要改造的方法:
- 新增菜品
- 修改菜品
- 批量删除菜品
- 起售、停售菜品
功能测试
缓存套餐
Spring Cache⭐
Spring Cache 是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。
Spring Cache 提供了一层抽象,底层可以切换不同的缓存实现,例如:
- EHCache
- Caffeine
- Redis
常用注解:
在启动类上添加@EnableCaching
注解
@Slf4j @SpringBootApplication @EnableCaching //开启缓存注解功能 public class CacheDemoApplication { public static void main(String[] args) { SpringApplication.run(CacheDemoApplication.class,args); log.info("项目启动成功..."); } }
在controller上使用@Cacheable、@Cacheput、@CacheEvict
注解
@RestController @RequestMapping("/user") @Slf4j public class UserController { @Autowired private UserMapper userMapper; @PostMapping @CachePut(cacheNames = "userCache", key = "#user.id") //如果使用SpringCache缓存数据,key的生成:userCache::2 public User save(@RequestBody User user){ userMapper.insert(user); return user; } @DeleteMapping @CacheEvict(cacheNames = "userCache", key = "#id") public void deleteById(Long id){ userMapper.deleteById(id); } @DeleteMapping("/delAll") @CacheEvict(cacheNames = "userCache", allEntries = true) //删除userCache下的所有缓存 public void deleteAll(){ userMapper.deleteAll(); } @GetMapping @Cacheable(cacheNames = "userCache", key = "#id") //key的生成:userCache::10 public User getById(Long id){ User user = userMapper.getById(id); return user; } }
实现思路
具体的实现思路如下:
- 导入SpringCache和Redis相关Maven坐标
- 在启动类上加入@EnableCache注解,开启缓存注解功能
- 在用户端接口SetmealController的list方法上加入@Cacheable注解
- 在管理端接口SetmealController的save、delete、update、startOrStop等方法上加入@CacheEvict注解
代码开发
功能测试
添加购物车
需求分析和设计
产品原型:
接口设计:
- 请求方式:POST
- 请求路径:/user/shoppingCart/add
- 请求参数:套餐id、菜品id、口味
- 返回结果:code、data、msg
数据库设计:
- 作用:暂时存放所选商品的地方
- 选的什么商品
- 每个商品买了几个
- 不同用户的购物车需要区分开
代码开发
功能测试
查看购物车
需求分析和设计
产品原型:
接口设计:
代码开发
功能测试
清空购物车
需求分析和设计
产品原型:
接口设计:
功能测试
代码开发
用户下单、订单支付
导入地址簿功能代码
需求分析和设计
产品原型:
业务功能:
- 查询地址列表
- 新增地址
- 修改地址
- 删除地址
- 设置默认地址
- 查询默认地址
接口设计:
- 新增地址
- 查询当前登录用户的所有地址信息
- 查询默认地址
- 根据id删除地址
- 根据id修改地址
- 根据id查询地址
- 设置默认地址
数据库设计(address_book表):
address_book表为地址表,用于存储C端用户的收货地址信息。具体表结构如下:
字段名 | 数据类型 | 说明 | 备注 |
---|---|---|---|
id | bigint | 主键 | 自增 |
user_id | bigint | 用户id | 逻辑外键 |
consignee | varchar(50) | 收货人 | |
sex | varchar(2) | 性别 | |
phone | varchar(11) | 手机号 | |
province_code | varchar(12) | 省份编码 | |
province_name | varchar(32) | 省份名称 | |
city_code | varchar(12) | 城市编码 | |
city_name | varchar(32) | 城市名称 | |
district_code | varchar(12) | 区县编码 | |
district_name | varchar(32) | 区县名称 | |
detail | varchar(200) | 详细地址信息 | 具体到门牌号 |
label | varchar(100) | 标签 | 公司、家、学校 |
is_default | tinyint(1) | 是否默认地址 | 1是 0否 |
代码导入
功能测试
用户下单
需求分析和设计
用户下单业务说明:
在电商系统中,用户是通过下单的方式通知商家,用户已经购买了商品,需要商家进行备货和发货。
用户下单后会产生订单相关数据,订单数据需要能够体现如下信息:
用户点餐业务流程:
接口设计(分析):
接口设计:
数据库(orders表、order_deatail表)设计:

orders表为订单表,用于存储C端用户的订单数据。具体表结构如下:
字段名 | 数据类型 | 说明 | 备注 |
---|---|---|---|
id | bigint | 主键 | 自增 |
number | varchar(50) | 订单号 | |
status | int | 订单状态 | 1待付款 2待接单 3已接单 4派送中 5已完成 6已取消 |
user_id | bigint | 用户id | 逻辑外键 |
address_book_id | bigint | 地址id | 逻辑外键 |
order_time | datetime | 下单时间 | |
checkout_time | datetime | 付款时间 | |
pay_method | int | 支付方式 | 1微信支付 2支付宝支付 |
pay_status | tinyint | 支付状态 | 0未支付 1已支付 2退款 |
amount | decimal(10,2) | 订单金额 | |
remark | varchar(100) | 备注信息 | |
phone | varchar(11) | 手机号 | |
address | varchar(255) | 详细地址信息 | |
user_name | varchar(32) | 用户姓名 | |
consignee | varchar(32) | 收货人 | |
cancel_reason | varchar(255) | 订单取消原因 | |
rejection_reason | varchar(255) | 拒单原因 | |
cancel_time | datetime | 订单取消时间 | |
estimated_delivery_time | datetime | 预计送达时间 | |
delivery_status | tinyint | 配送状态 | 1立即送出 0选择具体时间 |
delivery_time | datetime | 送达时间 | |
pack_amount | int | 打包费 | |
tableware_number | int | 餐具数量 | |
tableware_status | tinyint | 餐具数量状态 | 1按餐量提供 0选择具体数量 |
order_detail表为订单明细表,用于存储C端用户的订单明细数据。具体表结构如下:
字段名 | 数据类型 | 说明 | 备注 |
---|---|---|---|
id | bigint | 主键 | 自增 |
name | varchar(32) | 商品名称 | |
image | varchar(255) | 商品图片路径 | |
order_id | bigint | 订单id | 逻辑外键 |
dish_id | bigint | 菜品id | 逻辑外键 |
setmeal_id | bigint | 套餐id | 逻辑外键 |
dish_flavor | varchar(50) | 菜品口味 | |
number | int | 商品数量 | |
amount | decimal(10,2) | 商品单价 |
代码开发
根据用户下单接口的参数设计DTO:
根据用户下单接口的返回结果设计VO:
功能测试
订单支付
微信支付介绍
微信支付产品:
参考:https://pay.weixin.qq.com/static/product/product_index.shtm
微信支付接入流程:
微信小程序支付时序图:
JSAPI下单:商户系统调用该接口在微信支付服务后台生成预支付交易单
微信小程序调起支付:通过JSAPI下单接口获取到发起支付的必要参数prepay_id,然后使用微信支付提供的小程序方法调起小程序支付
微信支付准备工作
微信小程序支付时序图:
获取微信支付平台证书、商户私钥文件
获取临时域名:支付成功后微信服务通过该域名回调我们的程序
代码导入
微信支付相关配置:
功能测试
用户端历史订单模块
1. 查询历史订单
1.1 需求分析和设计
产品原型:
业务规则
- 分页查询历史订单
- 可以根据订单状态查询
- 展示订单数据时,需要展示的数据包括:下单时间、订单状态、订单金额、订单明细(商品名称、图片)
接口设计:参见接口文档
1.2 代码实现
1.2.1 user/OrderController
/** * 历史订单查询 * * @param page * @param pageSize * @param status 订单状态 1待付款 2待接单 3已接单 4派送中 5已完成 6已取消 * @return */ @GetMapping("/historyOrders") @ApiOperation("历史订单查询") public Result<PageResult> page(int page, int pageSize, Integer status) { PageResult pageResult = orderService.pageQuery4User(page, pageSize, status); return Result.success(pageResult); }
1.2.2 OrderService
/** * 用户端订单分页查询 * @param page * @param pageSize * @param status * @return */ PageResult pageQuery4User(int page, int pageSize, Integer status);
1.2.3 OrderServiceImpl
/** * 用户端订单分页查询 * * @param pageNum * @param pageSize * @param status * @return */ public PageResult pageQuery4User(int pageNum, int pageSize, Integer status) { // 设置分页 PageHelper.startPage(pageNum, pageSize); OrdersPageQueryDTO ordersPageQueryDTO = new OrdersPageQueryDTO(); ordersPageQueryDTO.setUserId(BaseContext.getCurrentId()); ordersPageQueryDTO.setStatus(status); // 分页条件查询 Page<Orders> page = orderMapper.pageQuery(ordersPageQueryDTO); List<OrderVO> list = new ArrayList(); // 查询出订单明细,并封装入OrderVO进行响应 if (page != null && page.getTotal() > 0) { for (Orders orders : page) { Long orderId = orders.getId();// 订单id // 查询订单明细 List<OrderDetail> orderDetails = orderDetailMapper.getByOrderId(orderId); OrderVO orderVO = new OrderVO(); BeanUtils.copyProperties(orders, orderVO); orderVO.setOrderDetailList(orderDetails); list.add(orderVO); } } return new PageResult(page.getTotal(), list); }
1.2.4 OrderMapper
/** * 分页条件查询并按下单时间排序 * @param ordersPageQueryDTO */ Page<Orders> pageQuery(OrdersPageQueryDTO ordersPageQueryDTO);
1.2.5 OrderMapper.xml
<select id="pageQuery" resultType="Orders"> select * from orders <where> <if test="number != null and number!=''"> and number like concat('%',#{number},'%') </if> <if test="phone != null and phone!=''"> and phone like concat('%',#{phone},'%') </if> <if test="userId != null"> and user_id = #{userId} </if> <if test="status != null"> and status = #{status} </if> <if test="beginTime != null"> and order_time >= #{beginTime} </if> <if test="endTime != null"> and order_time <= #{endTime} </if> </where> order by order_time desc </select>
1.2.6 OrderDetailMapper
/** * 根据订单id查询订单明细 * @param orderId * @return */ @Select("select * from order_detail where order_id = #{orderId}") List<OrderDetail> getByOrderId(Long orderId);
1.3 功能测试
略
2. 查询订单详情
2.1 需求分析和设计
产品原型:
接口设计:参见接口文档
2.2 代码实现
2.2.1 user/OrderController
/** * 查询订单详情 * * @param id * @return */ @GetMapping("/orderDetail/{id}") @ApiOperation("查询订单详情") public Result<OrderVO> details(@PathVariable("id") Long id) { OrderVO orderVO = orderService.details(id); return Result.success(orderVO); }
2.2.2 OrderService
/** * 查询订单详情 * @param id * @return */ OrderVO details(Long id);
2.2.3 OrderServiceImpl
/** * 查询订单详情 * * @param id * @return */ public OrderVO details(Long id) { // 根据id查询订单 Orders orders = orderMapper.getById(id); // 查询该订单对应的菜品/套餐明细 List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(orders.getId()); // 将该订单及其详情封装到OrderVO并返回 OrderVO orderVO = new OrderVO(); BeanUtils.copyProperties(orders, orderVO); orderVO.setOrderDetailList(orderDetailList); return orderVO; }
2.2.4 OrderMapper
/** * 根据id查询订单 * @param id */ @Select("select * from orders where id=#{id}") Orders getById(Long id);
2.3 功能测试
略
3. 取消订单
3.1 需求分析和设计
产品原型:
业务规则:
- 待支付和待接单状态下,用户可直接取消订单
- 商家已接单状态下,用户取消订单需电话沟通商家
- 派送中状态下,用户取消订单需电话沟通商家
- 如果在待接单状态下取消订单,需要给用户退款
- 取消订单后需要将订单状态修改为“已取消”
接口设计:参见接口文档
3.2 代码实现
3.2.1 user/OrderController
/** * 用户取消订单 * * @return */ @PutMapping("/cancel/{id}") @ApiOperation("取消订单") public Result cancel(@PathVariable("id") Long id) throws Exception { orderService.userCancelById(id); return Result.success(); }
3.2.2 OrderService
/** * 用户取消订单 * @param id */ void userCancelById(Long id) throws Exception;
3.2.3 OrderServiceImpl
/** * 用户取消订单 * * @param id */ public void userCancelById(Long id) throws Exception { // 根据id查询订单 Orders ordersDB = orderMapper.getById(id); // 校验订单是否存在 if (ordersDB == null) { throw new OrderBusinessException(MessageConstant.ORDER_NOT_FOUND); } //订单状态 1待付款 2待接单 3已接单 4派送中 5已完成 6已取消 if (ordersDB.getStatus() > 2) { throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR); } Orders orders = new Orders(); orders.setId(ordersDB.getId()); // 订单处于待接单状态下取消,需要进行退款 if (ordersDB.getStatus().equals(Orders.TO_BE_CONFIRMED)) { //调用微信支付退款接口 weChatPayUtil.refund( ordersDB.getNumber(), //商户订单号 ordersDB.getNumber(), //商户退款单号 new BigDecimal(0.01),//退款金额,单位 元 new BigDecimal(0.01));//原订单金额 //支付状态修改为 退款 orders.setPayStatus(Orders.REFUND); } // 更新订单状态、取消原因、取消时间 orders.setStatus(Orders.CANCELLED); orders.setCancelReason("用户取消"); orders.setCancelTime(LocalDateTime.now()); orderMapper.update(orders); }
3.3 功能测试
略
4. 再来一单
4.1 需求分析和设计
产品原型:
接口设计:参见接口文档
业务规则:
- 再来一单就是将原订单中的商品重新加入到购物车中
4.2 代码实现
4.2.1 user/OrderController
/** * 再来一单 * * @param id * @return */ @PostMapping("/repetition/{id}") @ApiOperation("再来一单") public Result repetition(@PathVariable Long id) { orderService.repetition(id); return Result.success(); }
4.2.2 OrderService
/** * 再来一单 * * @param id */ void repetition(Long id);
4.2.3 OrderServiceImpl
/** * 再来一单 * * @param id */ public void repetition(Long id) { // 查询当前用户id Long userId = BaseContext.getCurrentId(); // 根据订单id查询当前订单详情 List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(id); // 将订单详情对象转换为购物车对象 List<ShoppingCart> shoppingCartList = orderDetailList.stream().map(x -> { ShoppingCart shoppingCart = new ShoppingCart(); // 将原订单详情里面的菜品信息重新复制到购物车对象中 BeanUtils.copyProperties(x, shoppingCart, "id"); shoppingCart.setUserId(userId); shoppingCart.setCreateTime(LocalDateTime.now()); return shoppingCart; }).collect(Collectors.toList()); // 将购物车对象批量添加到数据库 shoppingCartMapper.insertBatch(shoppingCartList); }
4.2.4 ShoppingCartMapper
/** * 批量插入购物车数据 * * @param shoppingCartList */ void insertBatch(List<ShoppingCart> shoppingCartList);
4.2.5 ShoppingCartMapper.xml
<insert id="insertBatch" parameterType="list"> insert into shopping_cart (name, image, user_id, dish_id, setmeal_id, dish_flavor, number, amount, create_time) values <foreach collection="shoppingCartList" item="sc" separator=","> (#{sc.name},#{sc.image},#{sc.userId},#{sc.dishId},#{sc.setmealId},#{sc.dishFlavor},#{sc.number},#{sc.amount},#{sc.createTime}) </foreach> </insert>
4.3 功能测试
略
商家端订单管理模块
1. 订单搜索
1.1 需求分析和设计
产品原型:
业务规则:
- 输入订单号/手机号进行搜索,支持模糊搜索
- 根据订单状态进行筛选
- 下单时间进行时间筛选
- 搜索内容为空,提示未找到相关订单
- 搜索结果页,展示包含搜索关键词的内容
- 分页展示搜索到的订单数据
接口设计:参见接口文档
1.2 代码实现
1.2.1 admin/OrderController
在admin包下创建OrderController
/** * 订单管理 */ @RestController("adminOrderController") @RequestMapping("/admin/order") @Slf4j @Api(tags = "订单管理接口") public class OrderController { @Autowired private OrderService orderService; /** * 订单搜索 * * @param ordersPageQueryDTO * @return */ @GetMapping("/conditionSearch") @ApiOperation("订单搜索") public Result<PageResult> conditionSearch(OrdersPageQueryDTO ordersPageQueryDTO) { PageResult pageResult = orderService.conditionSearch(ordersPageQueryDTO); return Result.success(pageResult); } }
1.2.2 OrderService
/** * 条件搜索订单 * @param ordersPageQueryDTO * @return */ PageResult conditionSearch(OrdersPageQueryDTO ordersPageQueryDTO);
1.2.3 OrderServiceImpl
/** * 订单搜索 * * @param ordersPageQueryDTO * @return */ public PageResult conditionSearch(OrdersPageQueryDTO ordersPageQueryDTO) { PageHelper.startPage(ordersPageQueryDTO.getPage(), ordersPageQueryDTO.getPageSize()); Page<Orders> page = orderMapper.pageQuery(ordersPageQueryDTO); // 部分订单状态,需要额外返回订单菜品信息,将Orders转化为OrderVO List<OrderVO> orderVOList = getOrderVOList(page); return new PageResult(page.getTotal(), orderVOList); } private List<OrderVO> getOrderVOList(Page<Orders> page) { // 需要返回订单菜品信息,自定义OrderVO响应结果 List<OrderVO> orderVOList = new ArrayList<>(); List<Orders> ordersList = page.getResult(); if (!CollectionUtils.isEmpty(ordersList)) { for (Orders orders : ordersList) { // 将共同字段复制到OrderVO OrderVO orderVO = new OrderVO(); BeanUtils.copyProperties(orders, orderVO); String orderDishes = getOrderDishesStr(orders); // 将订单菜品信息封装到orderVO中,并添加到orderVOList orderVO.setOrderDishes(orderDishes); orderVOList.add(orderVO); } } return orderVOList; } /** * 根据订单id获取菜品信息字符串 * * @param orders * @return */ private String getOrderDishesStr(Orders orders) { // 查询订单菜品详情信息(订单中的菜品和数量) List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(orders.getId()); // 将每一条订单菜品信息拼接为字符串(格式:宫保鸡丁*3;) List<String> orderDishList = orderDetailList.stream().map(x -> { String orderDish = x.getName() + "*" + x.getNumber() + ";"; return orderDish; }).collect(Collectors.toList()); // 将该订单对应的所有菜品信息拼接在一起 return String.join("", orderDishList); }
1.3 功能测试
略
2. 各个状态的订单数量统计
2.1 需求分析和设计
产品原型:
接口设计:参见接口文档
2.2 代码实现
2.2.1 admin/OrderController
/** * 各个状态的订单数量统计 * * @return */ @GetMapping("/statistics") @ApiOperation("各个状态的订单数量统计") public Result<OrderStatisticsVO> statistics() { OrderStatisticsVO orderStatisticsVO = orderService.statistics(); return Result.success(orderStatisticsVO); }
2.2.2 OrderService
/** * 各个状态的订单数量统计 * @return */ OrderStatisticsVO statistics();
2.2.3 OrderServiceImpl
/** * 各个状态的订单数量统计 * * @return */ public OrderStatisticsVO statistics() { // 根据状态,分别查询出待接单、待派送、派送中的订单数量 Integer toBeConfirmed = orderMapper.countStatus(Orders.TO_BE_CONFIRMED); Integer confirmed = orderMapper.countStatus(Orders.CONFIRMED); Integer deliveryInProgress = orderMapper.countStatus(Orders.DELIVERY_IN_PROGRESS); // 将查询出的数据封装到orderStatisticsVO中响应 OrderStatisticsVO orderStatisticsVO = new OrderStatisticsVO(); orderStatisticsVO.setToBeConfirmed(toBeConfirmed); orderStatisticsVO.setConfirmed(confirmed); orderStatisticsVO.setDeliveryInProgress(deliveryInProgress); return orderStatisticsVO; }
2.2.4 OrderMapper
/** * 根据状态统计订单数量 * @param status */ @Select("select count(id) from orders where status = #{status}") Integer countStatus(Integer status);
2.3 功能测试
略
3. 查询订单详情
3.1 需求分析和设计
产品原型:
业务规则:
- 订单详情页面需要展示订单基本信息(状态、订单号、下单时间、收货人、电话、收货地址、金额等)
- 订单详情页面需要展示订单明细数据(商品名称、数量、单价)
接口设计:参见接口文档
3.2 代码实现
3.2.1 admin/OrderController
/** * 订单详情 * * @param id * @return */ @GetMapping("/details/{id}") @ApiOperation("查询订单详情") public Result<OrderVO> details(@PathVariable("id") Long id) { OrderVO orderVO = orderService.details(id); return Result.success(orderVO); }
3.3 功能测试
略
4. 接单
4.1 需求分析和设计
产品原型:
业务规则:
- 商家接单其实就是将订单的状态修改为“已接单”
接口设计:参见接口文档
4.2 代码实现
4.2.1 admin/OrderController
/** * 接单 * * @return */ @PutMapping("/confirm") @ApiOperation("接单") public Result confirm(@RequestBody OrdersConfirmDTO ordersConfirmDTO) { orderService.confirm(ordersConfirmDTO); return Result.success(); }
4.2.2 OrderService
/** * 接单 * * @param ordersConfirmDTO */ void confirm(OrdersConfirmDTO ordersConfirmDTO);
4.2.3 OrderServiceImpl
/** * 接单 * * @param ordersConfirmDTO */ public void confirm(OrdersConfirmDTO ordersConfirmDTO) { Orders orders = Orders.builder() .id(ordersConfirmDTO.getId()) .status(Orders.CONFIRMED) .build(); orderMapper.update(orders); }
4.3 功能测试
略
5. 拒单
5.1 需求分析和设计
产品原型:
业务规则:
- 商家拒单其实就是将订单状态修改为“已取消”
- 只有订单处于“待接单”状态时可以执行拒单操作
- 商家拒单时需要指定拒单原因
- 商家拒单时,如果用户已经完成了支付,需要为用户退款
接口设计:参见接口文档
5.2 代码实现
5.2.1 admin/OrderController
/** * 拒单 * * @return */ @PutMapping("/rejection") @ApiOperation("拒单") public Result rejection(@RequestBody OrdersRejectionDTO ordersRejectionDTO) throws Exception { orderService.rejection(ordersRejectionDTO); return Result.success(); }
5.2.2 OrderService
/** * 拒单 * * @param ordersRejectionDTO */ void rejection(OrdersRejectionDTO ordersRejectionDTO) throws Exception;
5.2.3 OrderServiceImpl
/** * 拒单 * * @param ordersRejectionDTO */ public void rejection(OrdersRejectionDTO ordersRejectionDTO) throws Exception { // 根据id查询订单 Orders ordersDB = orderMapper.getById(ordersRejectionDTO.getId()); // 订单只有存在且状态为2(待接单)才可以拒单 if (ordersDB == null || !ordersDB.getStatus().equals(Orders.TO_BE_CONFIRMED)) { throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR); } //支付状态 Integer payStatus = ordersDB.getPayStatus(); if (payStatus == Orders.PAID) { //用户已支付,需要退款 String refund = weChatPayUtil.refund( ordersDB.getNumber(), ordersDB.getNumber(), new BigDecimal(0.01), new BigDecimal(0.01)); log.info("申请退款:{}", refund); } // 拒单需要退款,根据订单id更新订单状态、拒单原因、取消时间 Orders orders = new Orders(); orders.setId(ordersDB.getId()); orders.setStatus(Orders.CANCELLED); orders.setRejectionReason(ordersRejectionDTO.getRejectionReason()); orders.setCancelTime(LocalDateTime.now()); orderMapper.update(orders); }
5.3 功能测试
略
6. 取消订单
6.1 需求分析和设计
产品原型:
业务规则:
- 取消订单其实就是将订单状态修改为“已取消”
- 商家取消订单时需要指定取消原因
- 商家取消订单时,如果用户已经完成了支付,需要为用户退款
接口设计:参见接口文档
6.2 代码实现
6.2.1 admin/OrderController
/** * 取消订单 * * @return */ @PutMapping("/cancel") @ApiOperation("取消订单") public Result cancel(@RequestBody OrdersCancelDTO ordersCancelDTO) throws Exception { orderService.cancel(ordersCancelDTO); return Result.success(); }
6.2.2 OrderService
/** * 商家取消订单 * * @param ordersCancelDTO */ void cancel(OrdersCancelDTO ordersCancelDTO) throws Exception;
6.2.3 OrderServiceImpl
/** * 取消订单 * * @param ordersCancelDTO */ public void cancel(OrdersCancelDTO ordersCancelDTO) throws Exception { // 根据id查询订单 Orders ordersDB = orderMapper.getById(ordersCancelDTO.getId()); //支付状态 Integer payStatus = ordersDB.getPayStatus(); if (payStatus == 1) { //用户已支付,需要退款 String refund = weChatPayUtil.refund( ordersDB.getNumber(), ordersDB.getNumber(), new BigDecimal(0.01), new BigDecimal(0.01)); log.info("申请退款:{}", refund); } // 管理端取消订单需要退款,根据订单id更新订单状态、取消原因、取消时间 Orders orders = new Orders(); orders.setId(ordersCancelDTO.getId()); orders.setStatus(Orders.CANCELLED); orders.setCancelReason(ordersCancelDTO.getCancelReason()); orders.setCancelTime(LocalDateTime.now()); orderMapper.update(orders); }
6.3 功能测试
略
7. 派送订单
7.1 需求分析和设计
产品原型:
业务规则:
- 派送订单其实就是将订单状态修改为“派送中”
- 只有状态为“待派送”的订单可以执行派送订单操作
接口设计:参见接口文档
7.2 代码实现
7.2.1 admin/OrderController
/** * 派送订单 * * @return */ @PutMapping("/delivery/{id}") @ApiOperation("派送订单") public Result delivery(@PathVariable("id") Long id) { orderService.delivery(id); return Result.success(); }
7.2.2 OrderService
/** * 派送订单 * * @param id */ void delivery(Long id);
7.2.3 OrderServiceImpl
/** * 派送订单 * * @param id */ public void delivery(Long id) { // 根据id查询订单 Orders ordersDB = orderMapper.getById(id); // 校验订单是否存在,并且状态为3 if (ordersDB == null || !ordersDB.getStatus().equals(Orders.CONFIRMED)) { throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR); } Orders orders = new Orders(); orders.setId(ordersDB.getId()); // 更新订单状态,状态转为派送中 orders.setStatus(Orders.DELIVERY_IN_PROGRESS); orderMapper.update(orders); }
7.3 功能测试
略
8. 完成订单
8.1 需求分析和设计
产品原型:
业务规则:
- 完成订单其实就是将订单状态修改为“已完成”
- 只有状态为“派送中”的订单可以执行订单完成操作
接口设计:参见接口文档
8.2 代码实现
8.2.1 admin/OrderController
/** * 完成订单 * * @return */ @PutMapping("/complete/{id}") @ApiOperation("完成订单") public Result complete(@PathVariable("id") Long id) { orderService.complete(id); return Result.success(); }
8.2.2 OrderService
/** * 完成订单 * * @param id */ void complete(Long id);
8.2.3 OrderServiceImpl
/** * 完成订单 * * @param id */ public void complete(Long id) { // 根据id查询订单 Orders ordersDB = orderMapper.getById(id); // 校验订单是否存在,并且状态为4 if (ordersDB == null || !ordersDB.getStatus().equals(Orders.DELIVERY_IN_PROGRESS)) { throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR); } Orders orders = new Orders(); orders.setId(ordersDB.getId()); // 更新订单状态,状态转为完成 orders.setStatus(Orders.COMPLETED); orders.setDeliveryTime(LocalDateTime.now()); orderMapper.update(orders); }
8.3 功能测试
略
校验收货地址是否超出配送范围
1. 环境准备
登录百度地图开放平台:https://lbsyun.baidu.com/
进入控制台,创建应用,获取AK:
相关接口:
https://lbsyun.baidu.com/index.php?title=webapi/guide/webservice-geocoding
https://lbsyun.baidu.com/index.php?title=webapi/directionlite-v1
2. 代码开发
2.1 application.yml
配置外卖商家店铺地址和百度地图的AK:
2.2 OrderServiceImpl
改造OrderServiceImpl,注入上面的配置项:
@Value("${sky.shop.address}") private String shopAddress; @Value("${sky.baidu.ak}") private String ak;
在OrderServiceImpl中提供校验方法:
/** * 检查客户的收货地址是否超出配送范围 * @param address */ private void checkOutOfRange(String address) { Map map = new HashMap(); map.put("address",shopAddress); map.put("output","json"); map.put("ak",ak); //获取店铺的经纬度坐标 String shopCoordinate = HttpClientUtil.doGet("https://api.map.baidu.com/geocoding/v3", map); JSONObject jsonObject = JSON.parseObject(shopCoordinate); if(!jsonObject.getString("status").equals("0")){ throw new OrderBusinessException("店铺地址解析失败"); } //数据解析 JSONObject location = jsonObject.getJSONObject("result").getJSONObject("location"); String lat = location.getString("lat"); String lng = location.getString("lng"); //店铺经纬度坐标 String shopLngLat = lat + "," + lng; map.put("address",address); //获取用户收货地址的经纬度坐标 String userCoordinate = HttpClientUtil.doGet("https://api.map.baidu.com/geocoding/v3", map); jsonObject = JSON.parseObject(userCoordinate); if(!jsonObject.getString("status").equals("0")){ throw new OrderBusinessException("收货地址解析失败"); } //数据解析 location = jsonObject.getJSONObject("result").getJSONObject("location"); lat = location.getString("lat"); lng = location.getString("lng"); //用户收货地址经纬度坐标 String userLngLat = lat + "," + lng; map.put("origin",shopLngLat); map.put("destination",userLngLat); map.put("steps_info","0"); //路线规划 String json = HttpClientUtil.doGet("https://api.map.baidu.com/directionlite/v1/driving", map); jsonObject = JSON.parseObject(json); if(!jsonObject.getString("status").equals("0")){ throw new OrderBusinessException("配送路线规划失败"); } //数据解析 JSONObject result = jsonObject.getJSONObject("result"); JSONArray jsonArray = (JSONArray) result.get("routes"); Integer distance = (Integer) ((JSONObject) jsonArray.get(0)).get("distance"); if(distance > 5000){ //配送距离超过5000米 throw new OrderBusinessException("超出配送范围"); } }
在OrderServiceImpl的submitOrder方法中调用上面的校验方法:
订单状态定时处理、来单提醒和客户催单
Spring Task
Spring Task 是Spring框架提供的任务调度工具,可以按照约定的时间自动执行某个代码逻辑。
定位:定时任务框架
作用:定时自动执行某段Java代码
介绍
应用场景:
- 信用卡每月还卡提醒
- 银行贷款每月还款提醒
- 火车票售票系统处理未支付订单
- 入职纪念日为用户发送通知
cron表达式
cron表达式其实就是一个字符串,通过cron表达式可以定义任务触发的时间
构成规则:分为6或7个域,由空格分隔开,每个域代表一个含义
每个域的含义分别为:秒、分钟、小时、日、月、周、年(可选) 周 和 日互斥,只能选择其一,另外一个选择?
2022年10月12日上午9点整 对应的cron表达式: 0 0 9 12 10 ?2022
入门案例
Spring Task使用步骤:
- 导入Maven坐标 spring-context
- 启动类添加注解
@EnableScheduling
开启任务调度 - 自定义定时任务类
订单状态定时处理
需求分析和设计
用户下单后可能存在的情况:
- 下单未支付,订单一直处于“未支付”状态
- 用户收货后管理端未点击完成按钮,订单一直处于“派送中”状态
对于上面两种情况需要通过定时任务来修改订单状态,具体逻辑为:
- 通过定时任务每分钟检查一次是否存在支付超时订单(下单后超过15分钟仍未支付则判定为支付超时订单),如果存在则修改订单状态为“已取消”
- 通过定时任务每天凌晨1点检查一次是否存在“派送中”的订单,如果存在则修改订单状态为“已完成”
代码开发
功能测试
WebSocket
介绍
WebSocket 是基于 TCP 的一种新的网络协议。它实现了浏览器与服务器全双工通信——浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。
HTTP协议和WebSocket协议对比:
- HTTP是短连接
- WebSocket是长连接
- HTTP通信是单向的,基于请求响应模式
- WebSocket支持双向通信
- HTTP和WebSocket底层都是TCP连接
应用场景
- 视频弹幕
- 网页聊天
- 体育实况更新
- 股票基金报价实时更新
效果展示:
入门案例
实现步骤:
- 直接使用websocket.html页面作为WebSocket客户端
- 导入WebSocket的maven坐标
- 导入WebSocket服务端组建WebSocketServer,用于和客户端通信
- 导入配置类WebSocketConfiguration,注册WebSocket的服务端组件
- 导入定时任务类WebSocketTask,定时向客户端推送数据
来单提醒
需求分析和设计
用户下单并且支付成功后需要第一时间通知外卖商家。通知的形式有如下两种:
- 语音播报
- 弹出提示框
设计:
- 通过WebSocket实现管理端页面和服务端页面保持长连接状态
- 当客户支付后,调用WebSocket的相关API实现服务端向管理端推送消息
- 管理端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报
- 约定服务端发送给客户端浏览器的数据格式为JSON,字段包括:type、orderid、content
- type 为消息类型,1为来单提醒,2为客户催单
- orderid 为订单id
- content 为消息内容
代码开发
功能测试
客户催单
需求分析和设计
用户在小程序中点击催单按钮后,需要第一时间通知外卖商家。通知的形式有如下两种:
- 语音播报
- 弹出提示框
设计:
- 通过WebSocket实现管理端页面和服务端页面保持长连接状态
- 当用户点击催单按钮后,调用WebSocket的相关API实现服务端向管理端推送消息
- 管理端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报
- 约定服务端发送给客户端浏览器的数据格式为JSON,字段包括:type、orderid、content
- type 为消息类型,1为来单提醒,2为客户催单
- orderid 为订单id
- content 为消息内容
接口设计:
代码开发
功能测试
数据统计-图形报表
Apache Echarts
介绍
Apache Echarts是一款基于Javascript 的数据可视化图表库,提供直观,生动,可交互,可个性化定制的数据可视化图表。
官网地址:https://echarts.apache.org/zh/index.html
效果展示:
入门案例
总结:使用Echarts,重点在于研究当前图表所需的数据格式。通常是需要后端提供符合格式要求的动态数据,然后响应给前端来展示图表。
营业额统计
需求分析和设计
产品原型:
业务规则:
- 营业额指订单状态为已完成的订单金额合计
- 基于可视化报表的折线图展示营业额数据,X轴为日期,Y轴为营业额
- 根据时间选择区间,展示每天的营业额数据
接口设计:
代码开发
根据接口定义设计对应的VO:
功能测试
用户统计
需求分析和设计
产品原型:
业务规则:
- 基于可视化报表的折线图展示用户数据,x轴为日期,y轴为用户数
- 根据时间选择区间,展示每天的用户总量和新增用户量数据
接口设计:
代码开发
根据用户统计接口的返回结果设计VO:
功能测试
订单统计
需求分析和设计
产品原型:
业务规则:
- 有效订单指状态为“已完成”的订单
- 基于可视化报表的折线图展示订单数据,X轴为日期,Y轴为订单数量
- 根据时间选择区间,展示每天的订单总数和有效订单数
- 展示所选时间区间内的有效订单数、总订单数、订单完成率,订单完成率=有效订单数/总订单数 * 100%
接口设计:
代码开发
根据订单统计接口的返回结果设计VO:
功能测试
销量排行Top10
需求分析和设计
产品原型:
业务规则:
- 根据时间选择区间,展示销量前10的商品(包括菜品和套餐)
- 基于可视化报表的柱状图降序展示商品销量
- 此处的销量为商品销售的份数
接口设计:
代码开发
根据销量排名接口的返回结果设计VO:
功能测试
数据统计-Excel报表
工作台
需求分析和设计
工作台是系统运营的数据看板,并提供快捷操作入口,可以有效提高商家的工作效率。
工作台展示的数据:
- 今日数据
- 订单管理
- 菜品总览
- 套餐总览
- 订单信息
名词解释:
- 营业额:已完成订单的总金额
- 有效订单:已完成订单的数量
- 订单完成率:有效订单数、总订单数 * 100
- 平均客单价:营业额 / 有效订单数
- 新增用户:新增用户的数量
接口设计:
- 今日数据接口
- 订单管理接口
- 菜品总览接口
- 套餐总览接口
-
订单搜索(已完成)
-
各个状态的订单数量统计(已完成)
代码导入
功能测试
Apache POI
介绍
Apache POl是一个处理Miscrosoft Office各种文件格式的开源项目。简单来说就是,我们可以使用 POl 在 Java 程序中对Miscrosoft Office各种文件进行读写操作。
一般情况下,POI都是用于操作Excel文件。
Apache POI 的应用场景:
- 银行网银系统导出交易明细
- 各种业务系统导出Excel报表
- 批量导入业务数据
入门案例
Apache POI的maven坐标:
public class POITest { /** * 通过POI创建Excel文件并且写入文件内容 */ public static void write() throws Exception{ //在内存中创建一个Excel文件 XSSFWorkbook excel = new XSSFWorkbook(); //在Excel文件中创建一个sheet页 XSSFSheet sheet = excel.createSheet("info"); //在sheet页中创建行对象,rownum编号从0开始 XSSFRow row = sheet.createRow(1); //创建单元格,并且写入文件内容 row.createCell(1).setCellValue("姓名"); row.createCell(2).setCellValue("城市"); //创建一个新行 row = sheet.createRow(2); row.createCell(1).setCellValue("张三"); row.createCell(2).setCellValue("北京"); row = sheet.createRow(3); row.createCell(1).setCellValue("李四"); row.createCell(2).setCellValue("南京"); //通过输出流将内存中的Excel文件写入磁盘 FileOutputStream out = new FileOutputStream(new File("D:\\Projects\\info.xlsx")); excel.write(out); //关闭资源 out.close(); excel.close(); } public static void main(String[] args) throws Exception { write(); } }
导出运营数据Excel报表
需求分析和设计
产品原型:
导出的Excel报表格式:
业务规则:
- 导出Excel形式的报表文件
- 导出最近30天的运营数据
接口设计:
注意:当前接口没有返回数据,因为报表导出功能本质上是文件下载,服务端会通过输出流将Excel文件下载到客户端浏览器
代码开发
实现步骤:
-
设计Excel模板文件
-
查询近30天的运营数据
-
将查询到的运营数据写入模板文件
-
通过输出流将Excel文件下载到客户端浏览器
功能测试
本文作者:指尖下的代码i
本文链接:https://www.cnblogs.com/xu1feng/p/18338988
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步