苍穹外卖学习文档
软件开发整体介绍
软件开发流程
需求分析
需求规格说明书、产品原型
设计
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相关配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
/**
* 通过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;
}
|
-
设置静态资源映射,否则接口文档页面无法访问
1
2
3
4
5
6
7
8
9
|
/**
* 设置静态资源映射
* @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来封装数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
/**
* 新增员工
* @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
1
2
3
4
5
|
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>${pagehelper}</version>
</dependency>
|
功能测试
可以通过接口文档进行测试,也可以进行前后端联调测试。
代码完善
最后操作时间需要修改成年月日

解决方式:
启用禁用员工账号
需求分析和设计
产品原型

业务规则:
- 可以对状态为“启用”的员工账号进行“禁用”操作
- 可以对状态为“禁用”的员工账号进行“启用”操作
- 状态为“禁用”的员工账号不能登录系统
接口设计


代码开发
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
/**
* 启用/禁用员工账号
* @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>
|
功能测试
编辑员工
需求分析和设计
产品原型


编辑员工功能涉及到两个接口:




代码开发
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
/**
* 编辑员工信息
* @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结构数据库。
- 基于内存存储,读写性能高
- 适合存储热点数据(热点商品、资讯、新闻)
- 企业应用广泛
官网:https://redis.io/
中文网:https://www.redis.net.cn/
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客户端很多,常用的几种:
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方法发送请求
入门案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
|
/**
* 测试通过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();
}
|
微信小程序开发
介绍
准备工作
入门案例
操作步骤:
微信登录
导入小程序代码
微信登录流程
官网: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 提供了一层抽象,底层可以切换不同的缓存实现,例如:

常用注解:

在启动类上添加@EnableCaching
注解
1
2
3
4
5
6
7
8
9
|
@Slf4j
@SpringBootApplication
@EnableCaching //开启缓存注解功能
public class CacheDemoApplication {
public static void main(String[] args) {
SpringApplication.run(CacheDemoApplication.class,args);
log.info("项目启动成功...");
}
}
|
在controller上使用@Cacheable、@Cacheput、@CacheEvict
注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
@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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
/**
* 历史订单查询
*
* @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
1
2
3
4
5
6
7
8
|
/**
* 用户端订单分页查询
* @param page
* @param pageSize
* @param status
* @return
*/
PageResult pageQuery4User(int page, int pageSize, Integer status);
|
1.2.3 OrderServiceImpl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
/**
* 用户端订单分页查询
*
* @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
1
2
3
4
5
|
/**
* 分页条件查询并按下单时间排序
* @param ordersPageQueryDTO
*/
Page<Orders> pageQuery(OrdersPageQueryDTO ordersPageQueryDTO);
|
1.2.5 OrderMapper.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
<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
1
2
3
4
5
6
7
|
/**
* 根据订单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
1
2
3
4
5
6
7
8
9
10
11
12
|
/**
* 查询订单详情
*
* @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
1
2
3
4
5
6
|
/**
* 查询订单详情
* @param id
* @return
*/
OrderVO details(Long id);
|
2.2.3 OrderServiceImpl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
/**
* 查询订单详情
*
* @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
1
2
3
4
5
6
|
/**
* 根据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
1
2
3
4
5
6
7
8
9
10
11
|
/**
* 用户取消订单
*
* @return
*/
@PutMapping("/cancel/{id}")
@ApiOperation("取消订单")
public Result cancel(@PathVariable("id") Long id) throws Exception {
orderService.userCancelById(id);
return Result.success();
}
|
3.2.2 OrderService
1
2
3
4
5
|
/**
* 用户取消订单
* @param id
*/
void userCancelById(Long id) throws Exception;
|
3.2.3 OrderServiceImpl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
/**
* 用户取消订单
*
* @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
1
2
3
4
5
6
7
8
9
10
11
12
|
/**
* 再来一单
*
* @param id
* @return
*/
@PostMapping("/repetition/{id}")
@ApiOperation("再来一单")
public Result repetition(@PathVariable Long id) {
orderService.repetition(id);
return Result.success();
}
|
4.2.2 OrderService
1
2
3
4
5
6
|
/**
* 再来一单
*
* @param id
*/
void repetition(Long id);
|
4.2.3 OrderServiceImpl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
/**
* 再来一单
*
* @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);
}
|
1
2
3
4
5
6
|
/**
* 批量插入购物车数据
*
* @param shoppingCartList
*/
void insertBatch(List<ShoppingCart> shoppingCartList);
|
1
2
3
4
5
6
7
8
|
<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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
/**
* 订单管理
*/
@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
1
2
3
4
5
6
|
/**
* 条件搜索订单
* @param ordersPageQueryDTO
* @return
*/
PageResult conditionSearch(OrdersPageQueryDTO ordersPageQueryDTO);
|
1.2.3 OrderServiceImpl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
|
/**
* 订单搜索
*
* @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
1
2
3
4
5
6
7
8
9
10
11
|
/**
* 各个状态的订单数量统计
*
* @return
*/
@GetMapping("/statistics")
@ApiOperation("各个状态的订单数量统计")
public Result<OrderStatisticsVO> statistics() {
OrderStatisticsVO orderStatisticsVO = orderService.statistics();
return Result.success(orderStatisticsVO);
}
|
2.2.2 OrderService
1
2
3
4
5
|
/**
* 各个状态的订单数量统计
* @return
*/
OrderStatisticsVO statistics();
|
2.2.3 OrderServiceImpl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
/**
* 各个状态的订单数量统计
*
* @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
1
2
3
4
5
6
|
/**
* 根据状态统计订单数量
* @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
1
2
3
4
5
6
7
8
9
10
11
12
|
/**
* 订单详情
*
* @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
1
2
3
4
5
6
7
8
9
10
11
|
/**
* 接单
*
* @return
*/
@PutMapping("/confirm")
@ApiOperation("接单")
public Result confirm(@RequestBody OrdersConfirmDTO ordersConfirmDTO) {
orderService.confirm(ordersConfirmDTO);
return Result.success();
}
|
4.2.2 OrderService
1
2
3
4
5
6
|
/**
* 接单
*
* @param ordersConfirmDTO
*/
void confirm(OrdersConfirmDTO ordersConfirmDTO);
|
4.2.3 OrderServiceImpl
1
2
3
4
5
6
7
8
9
10
11
12
13
|
/**
* 接单
*
* @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
1
2
3
4
5
6
7
8
9
10
11
|
/**
* 拒单
*
* @return
*/
@PutMapping("/rejection")
@ApiOperation("拒单")
public Result rejection(@RequestBody OrdersRejectionDTO ordersRejectionDTO) throws Exception {
orderService.rejection(ordersRejectionDTO);
return Result.success();
}
|
5.2.2 OrderService
1
2
3
4
5
6
|
/**
* 拒单
*
* @param ordersRejectionDTO
*/
void rejection(OrdersRejectionDTO ordersRejectionDTO) throws Exception;
|
5.2.3 OrderServiceImpl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
/**
* 拒单
*
* @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
1
2
3
4
5
6
7
8
9
10
11
|
/**
* 取消订单
*
* @return
*/
@PutMapping("/cancel")
@ApiOperation("取消订单")
public Result cancel(@RequestBody OrdersCancelDTO ordersCancelDTO) throws Exception {
orderService.cancel(ordersCancelDTO);
return Result.success();
}
|
6.2.2 OrderService
1
2
3
4
5
6
|
/**
* 商家取消订单
*
* @param ordersCancelDTO
*/
void cancel(OrdersCancelDTO ordersCancelDTO) throws Exception;
|
6.2.3 OrderServiceImpl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
/**
* 取消订单
*
* @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
1
2
3
4
5
6
7
8
9
10
11
|
/**
* 派送订单
*
* @return
*/
@PutMapping("/delivery/{id}")
@ApiOperation("派送订单")
public Result delivery(@PathVariable("id") Long id) {
orderService.delivery(id);
return Result.success();
}
|
7.2.2 OrderService
1
2
3
4
5
6
|
/**
* 派送订单
*
* @param id
*/
void delivery(Long id);
|
7.2.3 OrderServiceImpl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
/**
* 派送订单
*
* @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
1
2
3
4
5
6
7
8
9
10
11
|
/**
* 完成订单
*
* @return
*/
@PutMapping("/complete/{id}")
@ApiOperation("完成订单")
public Result complete(@PathVariable("id") Long id) {
orderService.complete(id);
return Result.success();
}
|
8.2.2 OrderService
1
2
3
4
5
6
|
/**
* 完成订单
*
* @param id
*/
void complete(Long id);
|
8.2.3 OrderServiceImpl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
/**
* 完成订单
*
* @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://passport.baidu.com/v2/?reg&tt=1671699340600&overseas=&gid=CF954C2-A3D2-417F-9FE6-B0F249ED7E33&tpl=pp&u=https%3A%2F%2Flbsyun.baidu.com%2Findex.php%3Ftitle%3D首页
登录百度地图开放平台: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,注入上面的配置项:
1
2
3
4
5
|
@Value("${sky.shop.address}")
private String shopAddress;
@Value("${sky.baidu.ak}")
private String ak;
|
在OrderServiceImpl中提供校验方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
/**
* 检查客户的收货地址是否超出配送范围
* @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坐标:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
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文件下载到客户端浏览器