Featured image of post 苍穹外卖学习笔记

苍穹外卖学习笔记

苍穹外卖学习笔记

苍穹外卖学习文档

软件开发整体介绍

软件开发流程

需求分析

需求规格说明书、产品原型

设计

UI设计、数据库设计、接口设计

编码

项目代码、单元测试

测试

测试用例、测试报告

上线运维

软件环境安装、配置

角色分工

  • 项目经理

    对整体项目负责,任务分配、把控进度

  • 产品经理

    进行需求调研。输出需求调研文档、产品原型等

  • UI设计师

    根据产品模型输出界面效果图

  • 架构师

    项目整体架构设计、技术选型等

  • 开发工程师

    代码实现

  • 测试工程师

    编写测试用例,输出测试报告

  • 运维工程师

    软件环境搭建、项目上线

软件环境

开发环境

开发人员在开发阶段使用的环境,一般外部用户无法访问

测试环境

专门给测试人员使用的环境,用于测试项目,一般外部用户无法访问

生产环境

即线上环境,正式提供对外服务的环境

苍穹外卖项目介绍

项目介绍

定位:专门为餐饮企业定制的一款软件产品

功能架构:

image-20240729110900376

产品原型

用于展示项目的业务功能

技术选型

展示项目中使用到的技术框架和中间件等

image-20240729112434033

开发环境搭建

前端环境搭建

整体结构

image-20240729112806737

通过Nginx代理

image-20240729113442408

后端环境搭建

熟悉项目结构

image-20240729113352626

sky-common子模块

image-20240729113639122

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

image-20240729144242952

image-20240729144255067

sky-server子模块

存放的是 配置文件、配置类、拦截器、controller、service、mapper、启动类等

image-20240729144646481

使用Git进行版本控制

  • 创建Git本地仓库
  • 创建Git远程仓库
  • 将本地文件推送到Git远程仓库

数据库环境搭建

image-20240729150543025

前后端联调

image-20240729151226183

Nginx🆕

反向代理,就是让前端发送的动态请求由Nginx转发到后端服务器

image-20240729154056277

Nginx反向代理的好处

  • 提高访问速度

  • 进行负载均衡

    就是把大量的请求按照我们指定的方式均衡的分配给集群中的每台服务器

  • 保证后端服务安全

Nginx反向代理的配置方式

image-20240729154703167image-20240729154857411

Nginx负载均衡的配置方式

image-20240729154950918

Nginx负载均衡策略

image-20240729155222125

轮询:平均接收到请求

完善登录功能

  1. 修改数据库中的明文密码,改为MD5加密后的密文
  2. 修改Java代码,前端提交的密码进行MD5加密后再跟数据库中密码比对

image-20240729170539456

image-20240729170715948

导入接口文档

前后端分离开发流程

image-20240729171913958

操作步骤

image-20240729172149205

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

image-20240729173254713

Swagger

介绍

image-20240729173436192

使用方式

  1. 导入knife4j的maven坐标

  2. 在配置类中加入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;
        }
    
  3. 设置静态资源映射,否则接口文档页面无法访问

    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/");
        }
    

常用注解

通过注解可以控制生成的接口文档,使接口文档拥有更好的可读性,常用注解如下:

image-20240729190727324

员工管理、分类管理

员工管理界面

image-20240730093858032

分类管理界面

image-20240730093917566

新增员工

需求分析和设计

产品原型

image-20240730094312236

接口设计

image-20240730094713856

image-20240730094757928

本项目约定:

  • 管理端发出的请求,统一使用/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设置为了固定值

image-20240730110517800

image-20240730110748941

image-20240730110758581

解析出登录员工id后,如何传递给Service的save方法?

image-20240730111602354

员工分页查询

需求分析和设计

产品原型

image-20240730112519920

业务规则:

  • 根据每页展示员工信息
  • 每页展示10条数据
  • 分页查询时可以根据需要,输入员工姓名进行查询

接口设计

image-20240730165845560

image-20240730165859053

代码开发

image-20240730170022373

image-20240730170135049

员工信息分页查询后端返回的对象类型为: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>

功能测试

可以通过接口文档进行测试,也可以进行前后端联调测试。

代码完善

最后操作时间需要修改成年月日

image-20240730232027270

解决方式:

  • 方式一:在属性上加入注解,对日期进行格式化

    1
    2
    
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime
    
  • 方式二:在Webconfiguration中扩展SpringMvc的消息转换器,统一对日期类型进行格式化处理

    image-20240730232329779

启用禁用员工账号

需求分析和设计

产品原型

image-20240731104555917

业务规则:

  • 可以对状态为“启用”的员工账号进行“禁用”操作
  • 可以对状态为“禁用”的员工账号进行“启用”操作
  • 状态为“禁用”的员工账号不能登录系统

接口设计

image-20240731104815608

image-20240731104830116

代码开发

 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>

功能测试

编辑员工

需求分析和设计

产品原型

image-20240731111326199

image-20240731111347506

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

  • 根据id查询员工信息

image-20240731111621824

image-20240731111636727

  • 编辑员工信息

image-20240731111731219

image-20240731111739536

代码开发

 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);
    }

功能测试

导入分类模块功能代码

需求分析和设计

产品原型

image-20240731123112488

业务规则:

  • 分类名称必须是唯一
  • 分类按照类型可以分为菜品分类套餐分类
  • 新添加的分类状态默认为禁用

接口设计:

  • 新增分类
  • 分类分页查询
  • 根据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

代码导入

功能测试

菜品管理

公共字段自动填充🌟

问题分析

业务表中的公共字段:

image-20240731125716319

image-20240731125807535

image-20240731135526156

问题:代码冗余、不便于后期维护

实现思路

image-20240731140015889

  • 自定义注解AutoFill,用于标识需要进行公共字段自动填充的方法
  • 自定义切面类AutoFillAspect,统一拦截加入了AutoFill注解的方法,通过反射为公共字段赋值
  • 在Mapper的方法上加入AutoFill注解

代码开发

功能测试

新增菜品

需求分析和设计

产品原型:

image-20240731145308435

业务规则:

  • 菜品名称必须是唯一的
  • 菜品必须属于某个分类下,不能单独存在
  • 新增菜品时可以根据情况选择菜品的口味
  • 每个菜品必须对应一张图片

接口设计:

  • 根据类型查询分类(已完成)

    image-20240731150003051

    image-20240731150103453

  • 文件上传

    image-20240731150121623

  • 新增菜品

    image-20240731150457335

    image-20240731150512177

数据库设计(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) 口味值

代码开发

开发文件上传接口:

image-20240731151934082

image-20240731155431636

功能测试

菜品分页查询

需求分析和设计

产品原型

image-20240801092032347

业务规则

  • 根据页码展示菜品信息
  • 每页展示10条数据
  • 分页查询时可以根据需要输入菜品名称、菜品分类、菜品状态进行查询

接口设计

image-20240801092241079

image-20240801092310661

代码开发

根据菜品分页查询接口定义设计对应的DTO:

image-20240801092610922

根据菜品分页查询接口定义设计对应的VO:

image-20240801092637618

功能测试

删除菜品

需求分析和设计

产品原型

image-20240801095549599

业务规则:

  • 可以一次删除一个菜品,也可以批量删除菜品
  • 起售中的菜品不能删除
  • 被套餐关联的菜品不能删除
  • 删除菜品后,关联的口味数据也需要删除掉

接口设计:

image-20240801095951713

数据库设计:

image-20240801100043756

代码开发

功能测试

修改菜品

需求分析和设计

产品原型

image-20240801104243735

接口设计:

  • 根据id查询商品

    image-20240801104528223

    image-20240801104545075

  • 根据类型查询分类(已实现)

  • 文件上传(已实现)

  • 修改商品

image-20240801104643083

image-20240801104652405

代码开发

功能测试

店铺营业状态设置

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根据分数升序排序,没有重复元素

各种数据类型的特点

image-20240801214152710

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 获取哈希表中所有值

image-20240801220851970

列表操作命令

Redis列表是简单的字符串列表,按照插入顺序排序,常用命令:

  • LPUSH key value1 [value2] 将一个或多个值插入到列表头部
  • LRANGE key start stop 获取列表指定范围内的元素
  • RPOP key 移除并获取列表最后一个元素
  • LLEN key 获取列表长度

image-20240802105339863

集合操作命令

Redis set 是 String 类型的无序集合。集合成员是唯一的,集合中不能出现重复的数据,常用命令:

  • SADD key member1 [member2] 向集合添加一个或多个成员
  • SMEMBERS key 返回集合中所有的成员
  • SCARD key 获取集合的成员数
  • SINTER key1 [key2] 返回给定的所有集合的交集
  • SUNION key1 [key2] 返回所有给定集合的并集
  • SREM key member1 [member2] 删除集合中一个或多个成员

image-20240802110520705

有序集合操作命令

Redis的有序集合是String类型元素的集合,且不允许有重复成员。每个元素都会关联一个double类型的分数。常用命令:

  • ZADD key score1 member1 [score2 member2] 向有序集合添加一个或多个成员
  • ZRANGE key start stop [WITHSCORES] 通过索引区间返回有序集合中指定区间内的成员
  • ZINCRBY key increment member 有序集合中对指定成员的分数加上增量increment
  • ZREM key member [member… ] 移除有序集合中的一个或多个成员

image-20240802112534431

通用命令

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使用方式

操作步骤:

  1. 导入Spring Data Redis 的Maven坐标

    image-20240802142232095

  2. 配置Redis数据源

    image-20240802142243974

  3. 编写配置类,创建RedisTemplate对象

    image-20240802142326934

  4. 通过RedisTemplate对象操作Redis

店铺营业状态

需求分析和设计

产品原型

image-20240802152705228

image-20240802152717725

接口设计:

  • 设置营业状态
  • 管理端查询营业状态
  • 用户端查询营业状态

本项目约定:

  • 管理端发出的请求,统一使用/admin作为前缀
  • 用户端发出的请求,统一使用/user作为前缀

image-20240802153330198

image-20240802153450159

营业状态数据存储方式:基于Redis的字符串来进行存储

image-20240802153802698

代码开发

功能测试

微信登录、商品浏览

image-20240803162559100

HttpClient🆕

介绍

HttpClient 是Apache Jakarta Common下的子项目,可以用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本和建议。

image-20240803162925521

核心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();
    }

微信小程序开发

介绍

准备工作

入门案例

操作步骤:

  • 了解微信小程序目录结构

    小程序包含一个描述整体程序的 app 和多个描述各自页面的 page。一个小程序主体部分由三个文件组成,必须放在项目的根目录,如下:

    image-20240803173706418

    一个小程序页面由四个文件组成:

    image-20240803174029789

  • 编写小程序代码

  • 编译小程序

微信登录

导入小程序代码

微信登录流程

官网:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html

image-20240803190028917

需求分析和设计

产品原型:

image-20240803190237210

业务规则:

  • 基于微信登录实现小程序的登录功能
  • 如果是新用户需要自动完成注册

接口设计:

image-20240803190529128

数据库设计(user表):

image-20240803190714040

代码开发

配置微信登录所需配置项:

image-20240803195925333

配置为微信用户生成jwt令牌时使用的配置项:

image-20240803195959210

功能测试

导入商品浏览功能代码

需求分析和设计

产品原型:

image-20240803210624703

接口设计:

  • 查询分类

    image-20240803211011751

  • 根据分类id查询菜品

    image-20240803211227483

  • 根据分类id查询套餐

    image-20240803211314972

  • 根据套餐id查询包含的菜品

    image-20240803211603333

代码导入

功能测试

缓存商品、购物车

image-20240804144536747

缓存菜品

问题说明

用户端小程序展示的菜品数据都是通过查询数据库获得,如果用户端访问量比较大,数据库访问压力随之增大。

image-20240804145328503

实现思路

通过Redis来缓存菜品数据,减少数据库查询操作。

image-20240804145449960

缓存逻辑分析:

  • 每个分类下的菜品保存一份缓存数据

    image-20240804145939082

  • 数据库中菜品数据有变更时清理缓存数据

代码开发

修改管理端接口 DishController 的相关方法,加入清理缓存的逻辑,需要改造的方法:

  • 新增菜品
  • 修改菜品
  • 批量删除菜品
  • 起售、停售菜品

功能测试

缓存套餐

Spring Cache⭐

Spring Cache 是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。

Spring Cache 提供了一层抽象,底层可以切换不同的缓存实现,例如:

  • EHCache
  • Caffeine
  • Redis

image-20240804160032385

常用注解:

image-20240804160124679

在启动类上添加@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注解

image-20240804165233104

 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注解

代码开发

功能测试

添加购物车

需求分析和设计

产品原型:

image-20240805103352522

接口设计:

  • 请求方式:POST
  • 请求路径:/user/shoppingCart/add
  • 请求参数:套餐id、菜品id、口味
  • 返回结果:code、data、msg

image-20240805103704404

数据库设计:

  • 作用:暂时存放所选商品的地方
  • 选的什么商品
  • 每个商品买了几个
  • 不同用户的购物车需要区分开

image-20240805103944991

代码开发

功能测试

查看购物车

需求分析和设计

产品原型:

image-20240805141318029

接口设计:

image-20240805141335472

代码开发

功能测试

清空购物车

需求分析和设计

产品原型:

image-20240805142137852

接口设计:

image-20240805142244369

功能测试

代码开发

用户下单、订单支付

导入地址簿功能代码

需求分析和设计

产品原型:

image-20240806104116944

业务功能:

  • 查询地址列表
  • 新增地址
  • 修改地址
  • 删除地址
  • 设置默认地址
  • 查询默认地址

接口设计:

  • 新增地址
  • 查询当前登录用户的所有地址信息
  • 查询默认地址
  • 根据id删除地址
  • 根据id修改地址
  • 根据id查询地址
  • 设置默认地址

image-20240806104551525

image-20240806104707457

image-20240806104800655

image-20240806104825323

image-20240806104850674

image-20240806104911791

image-20240806104942648

数据库设计(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否

代码导入

功能测试

用户下单

需求分析和设计

用户下单业务说明:

在电商系统中,用户是通过下单的方式通知商家,用户已经购买了商品,需要商家进行备货和发货。

用户下单后会产生订单相关数据,订单数据需要能够体现如下信息:

image-20240806110653045

用户点餐业务流程:

image-20240806111027821

接口设计(分析):

image-20240806111556225

接口设计:

image-20240806112030256

数据库(orders表、order_deatail表)设计:

image-20240806112510967

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:

image-20240806113153733

根据用户下单接口的返回结果设计VO:

image-20240806113257161

功能测试

订单支付

微信支付介绍

微信支付产品:

image-20240807105329028

参考:https://pay.weixin.qq.com/static/product/product_index.shtm

微信支付接入流程:

image-20240807105618734

微信小程序支付时序图:

image-20240807105835487

image-20240807110117500

image-20240807110516758

JSAPI下单:商户系统调用该接口在微信支付服务后台生成预支付交易单

image-20240807111108288

微信小程序调起支付:通过JSAPI下单接口获取到发起支付的必要参数prepay_id,然后使用微信支付提供的小程序方法调起小程序支付

image-20240807111615882

微信支付准备工作

微信小程序支付时序图:

image-20240807112213944

获取微信支付平台证书、商户私钥文件

image-20240807112249948

获取临时域名:支付成功后微信服务通过该域名回调我们的程序

image-20240807112441385

代码导入

微信支付相关配置:

image-20240807113505769

功能测试

用户端历史订单模块

1. 查询历史订单

1.1 需求分析和设计

产品原型:

image-20221128092537535

业务规则

  • 分页查询历史订单
  • 可以根据订单状态查询
  • 展示订单数据时,需要展示的数据包括:下单时间、订单状态、订单金额、订单明细(商品名称、图片)

接口设计:参见接口文档

image-20221128103222657

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 &gt;= #{beginTime}
            </if>
            <if test="endTime != null">
                and order_time &lt;= #{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 需求分析和设计

产品原型:

image-20221128102144294

接口设计:参见接口文档

image-20221128142142811

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 需求分析和设计

产品原型:

image-20221128145444268

业务规则:

  • 待支付和待接单状态下,用户可直接取消订单
  • 商家已接单状态下,用户取消订单需电话沟通商家
  • 派送中状态下,用户取消订单需电话沟通商家
  • 如果在待接单状态下取消订单,需要给用户退款
  • 取消订单后需要将订单状态修改为“已取消”

接口设计:参见接口文档

image-20221128164410852

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 需求分析和设计

产品原型:

image-20221128173238656

接口设计:参见接口文档

image-20221128173350467

业务规则:

  • 再来一单就是将原订单中的商品重新加入到购物车中

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);
    }
4.2.4 ShoppingCartMapper
1
2
3
4
5
6
    /**
     * 批量插入购物车数据
     *
     * @param shoppingCartList
     */
    void insertBatch(List<ShoppingCart> shoppingCartList);
4.2.5 ShoppingCartMapper.xml
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 需求分析和设计

产品原型:

image-20221129092023177

image-20221129114035570

image-20221129114054664

image-20221129114116492

image-20221129114132956

image-20221129114151055

业务规则:

  • 输入订单号/手机号进行搜索,支持模糊搜索
  • 根据订单状态进行筛选
  • 下单时间进行时间筛选
  • 搜索内容为空,提示未找到相关订单
  • 搜索结果页,展示包含搜索关键词的内容
  • 分页展示搜索到的订单数据

接口设计:参见接口文档

image-20221129092552620

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 需求分析和设计

产品原型:

image-20221129095804419

接口设计:参见接口文档

image-20221129095912896

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 需求分析和设计

产品原型:

image-20221129101712084

业务规则:

  • 订单详情页面需要展示订单基本信息(状态、订单号、下单时间、收货人、电话、收货地址、金额等)
  • 订单详情页面需要展示订单明细数据(商品名称、数量、单价)

接口设计:参见接口文档

image-20221129101035374

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 需求分析和设计

产品原型:

image-20221129105142623

image-20221129105116285

业务规则:

  • 商家接单其实就是将订单的状态修改为“已接单”

接口设计:参见接口文档

image-20221129105313172

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 需求分析和设计

产品原型:

image-20221129110358976

image-20221129110428390

业务规则:

  • 商家拒单其实就是将订单状态修改为“已取消”
  • 只有订单处于“待接单”状态时可以执行拒单操作
  • 商家拒单时需要指定拒单原因
  • 商家拒单时,如果用户已经完成了支付,需要为用户退款

接口设计:参见接口文档

image-20221129110725031

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 需求分析和设计

产品原型:

image-20221129111402099

业务规则:

  • 取消订单其实就是将订单状态修改为“已取消”
  • 商家取消订单时需要指定取消原因
  • 商家取消订单时,如果用户已经完成了支付,需要为用户退款

接口设计:参见接口文档

image-20221129112201836

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 需求分析和设计

产品原型:

image-20221129113257201

业务规则:

  • 派送订单其实就是将订单状态修改为“派送中”
  • 只有状态为“待派送”的订单可以执行派送订单操作

接口设计:参见接口文档

image-20221129113449124

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 需求分析和设计

产品原型:

image-20221129112554051

业务规则:

  • 完成订单其实就是将订单状态修改为“已完成”
  • 只有状态为“派送中”的订单可以执行订单完成操作

接口设计:参见接口文档

image-20221129113622784

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:

image-20221222170049729

image-20221222170256927

相关接口:

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:

image-20221222170819582

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方法中调用上面的校验方法:

image-20240807202040336

订单状态定时处理、来单提醒和客户催单

Spring Task

Spring Task 是Spring框架提供的任务调度工具,可以按照约定的时间自动执行某个代码逻辑。

定位:定时任务框架

作用:定时自动执行某段Java代码

介绍

应用场景:

  • 信用卡每月还卡提醒
  • 银行贷款每月还款提醒
  • 火车票售票系统处理未支付订单
  • 入职纪念日为用户发送通知

cron表达式

cron表达式其实就是一个字符串,通过cron表达式可以定义任务触发的时间

构成规则:分为6或7个域,由空格分隔开,每个域代表一个含义

每个域的含义分别为:秒、分钟、小时、日、月、周、年(可选) 周 和 日互斥,只能选择其一,另外一个选择?

2022年10月12日上午9点整 对应的cron表达式: 0 0 9 12 10 ?2022

image-20240808105440295

入门案例

Spring Task使用步骤:

  1. 导入Maven坐标 spring-context
  2. 启动类添加注解@EnableScheduling开启任务调度
  3. 自定义定时任务类

订单状态定时处理

需求分析和设计

用户下单后可能存在的情况:

  • 下单未支付,订单一直处于“未支付”状态
  • 用户收货后管理端未点击完成按钮,订单一直处于“派送中”状态

对于上面两种情况需要通过定时任务来修改订单状态,具体逻辑为:

  • 通过定时任务每分钟检查一次是否存在支付超时订单(下单后超过15分钟仍未支付则判定为支付超时订单),如果存在则修改订单状态为“已取消”
  • 通过定时任务每天凌晨1点检查一次是否存在“派送中”的订单,如果存在则修改订单状态为“已完成”

代码开发

功能测试

WebSocket

介绍

WebSocket 是基于 TCP 的一种新的网络协议。它实现了浏览器与服务器全双工通信——浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。

image-20240808214104004

image-20240808214121713

HTTP协议和WebSocket协议对比:

  • HTTP是短连接
  • WebSocket是长连接
  • HTTP通信是单向的,基于请求响应模式
  • WebSocket支持双向通信
  • HTTP和WebSocket底层都是TCP连接

应用场景

  • 视频弹幕
  • 网页聊天
  • 体育实况更新
  • 股票基金报价实时更新

效果展示:

image-20240808220107304

入门案例

image-20240808220251814

实现步骤:

  1. 直接使用websocket.html页面作为WebSocket客户端
  2. 导入WebSocket的maven坐标
  3. 导入WebSocket服务端组建WebSocketServer,用于和客户端通信
  4. 导入配置类WebSocketConfiguration,注册WebSocket的服务端组件
  5. 导入定时任务类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 为消息内容

接口设计:

image-20240808231835680

代码开发

功能测试

数据统计-图形报表

image-20240809102537649

Apache Echarts

介绍

Apache Echarts是一款基于Javascript 的数据可视化图表库,提供直观,生动,可交互,可个性化定制的数据可视化图表。

官网地址:https://echarts.apache.org/zh/index.html

image-20240809103022101

效果展示:

image-20240809103034606

image-20240809103045974

image-20240809103052856

入门案例

总结:使用Echarts,重点在于研究当前图表所需的数据格式。通常是需要后端提供符合格式要求的动态数据,然后响应给前端来展示图表。

营业额统计

需求分析和设计

产品原型:

image-20240809104723921

业务规则:

  • 营业额指订单状态为已完成的订单金额合计
  • 基于可视化报表的折线图展示营业额数据,X轴为日期,Y轴为营业额
  • 根据时间选择区间,展示每天的营业额数据

接口设计:

image-20240809112638378

代码开发

根据接口定义设计对应的VO:

image-20240809112924190

功能测试

用户统计

需求分析和设计

产品原型:

image-20240809150429471

业务规则:

  • 基于可视化报表的折线图展示用户数据,x轴为日期,y轴为用户数
  • 根据时间选择区间,展示每天的用户总量和新增用户量数据

接口设计:

image-20240809150912031

代码开发

根据用户统计接口的返回结果设计VO:

image-20240809151111119

功能测试

订单统计

需求分析和设计

产品原型:

image-20240809155455580

业务规则:

  • 有效订单指状态为“已完成”的订单
  • 基于可视化报表的折线图展示订单数据,X轴为日期,Y轴为订单数量
  • 根据时间选择区间,展示每天的订单总数和有效订单数
  • 展示所选时间区间内的有效订单数、总订单数、订单完成率,订单完成率=有效订单数/总订单数 * 100%

接口设计:

image-20240809160632211

代码开发

根据订单统计接口的返回结果设计VO:

image-20240809160816506

功能测试

销量排行Top10

需求分析和设计

产品原型:

image-20240809195731336

业务规则:

  • 根据时间选择区间,展示销量前10的商品(包括菜品和套餐)
  • 基于可视化报表的柱状图降序展示商品销量
  • 此处的销量为商品销售的份数

接口设计:

image-20240809200216011

代码开发

根据销量排名接口的返回结果设计VO:

image-20240809200340210

功能测试

数据统计-Excel报表

工作台

需求分析和设计

工作台是系统运营的数据看板,并提供快捷操作入口,可以有效提高商家的工作效率。

工作台展示的数据:

  • 今日数据
  • 订单管理
  • 菜品总览
  • 套餐总览
  • 订单信息

image-20240810092849178

名词解释:

  • 营业额:已完成订单的总金额
  • 有效订单:已完成订单的数量
  • 订单完成率:有效订单数、总订单数 * 100
  • 平均客单价:营业额 / 有效订单数
  • 新增用户:新增用户的数量

接口设计:

  • 今日数据接口

image-20240810094048891

  • 订单管理接口

image-20240810094135128

  • 菜品总览接口

image-20240810094210253

  • 套餐总览接口

image-20240810094229500

  • 订单搜索(已完成)
  • 各个状态的订单数量统计(已完成)

代码导入

功能测试

Apache POI

介绍

Apache POl是一个处理Miscrosoft Office各种文件格式的开源项目。简单来说就是,我们可以使用 POl 在 Java 程序中对Miscrosoft Office各种文件进行读写操作。

一般情况下,POI都是用于操作Excel文件。

image-20240810105131175

Apache POI 的应用场景:

  • 银行网银系统导出交易明细
  • 各种业务系统导出Excel报表
  • 批量导入业务数据

入门案例

Apache POI的maven坐标:

image-20240810105839892

 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报表

需求分析和设计

产品原型:

image-20240810113900669

导出的Excel报表格式:

image-20240810113957432

业务规则:

  • 导出Excel形式的报表文件
  • 导出最近30天的运营数据

接口设计:

image-20240811093337607

注意:当前接口没有返回数据,因为报表导出功能本质上是文件下载,服务端会通过输出流将Excel文件下载到客户端浏览器

代码开发

实现步骤:

  1. 设计Excel模板文件

    image-20240811093833403

  2. 查询近30天的运营数据

  3. 将查询到的运营数据写入模板文件

  4. 通过输出流将Excel文件下载到客户端浏览器

NovaBryan的博客
使用 Hugo 构建
主题 StackJimmy 设计