admin管理员组

文章数量:1543684

简介:这是一个完整外卖项目,包括前端页面设计、后端逻辑代码和数据库,可以作为大学生毕业季的项目。小编在这里详细展示了编码步骤、注意事项、相关BUG解决方案以及最最重要的就是源代码和部署过程

        (备注:项目来源于B站的黑马课程,如果时间允许还是建议同学们可以认真的从头到尾去学习,这样对于开发来说会有更好的掌握)

目录

1、软件开发整体介绍

1.1 软件开发流程

1.2 角色分工

1.3 软件环境

2、苍穹外卖项目介绍

2.1 项目介绍

2.2 产品原型

2.3 技术选型

3、开发环境搭建

3.1 前端环境搭建

3.2 后端环境搭建

3.2.1 后端环境搭建—使用Git进行版本控制

3.2.2 后端环境搭建—数据库环境搭建

3.2.3 后端环境搭建—前后端联调

3.3 完善登录功能

4、导入接口文档

4.1 前后端分离开发流程

4.2 操作步骤

5、Swagger

5.1 Swagger介绍

5.2 使用方式

5.3 常用注解

6、员工管理

6.1 新增员工

6.1.1 需求分析和设计

6.1.2 代码开发

6.1. 3 功能测试

6.1.4 完善代码

6.2 员工分页查询

6.2.1 需求分析和设计

6.2.2 代码开发

6.3 启用禁用员工账号

6.3.1 需求分析和设计

6.3.2 代码开发

6.4 编辑员工

6.5 分类模块

7、公共字段自动填充

7.1 问题分析

 7.2 实现思路 

7.3 代码开发

8、新增菜品

8.1 需求分析和设计

8.2 代码开发

9、菜品分页查询

9.1 需求分析和设计

9.2 代码开发

10、删除菜品

10.1 需求分析和设计

10.2 代码开发

11、修改菜品

11.1 需求分析和设计

11.2 代码开发

14、清空购物车

14.1 需求分析和设计

14.2 代码开发

15、导入地址簿

15.1 需求分析和设计

15.2 代码开发

15.2.1 查询当前登录用户的所有地址信息

15.2.2 新增地址

15.2.3 设置默认地址

15.2.4 查询默认地址

16、用户下单

16.1 需求分析和设计

16.2 代码开发

17、订单支付

17.1 需求分析和设计

17.2 微信支付准备工作

17.2.1 如何保障支付过程安全性

17.2.2 微信后台如何调用商户系统

17.3 代码开发

17.3.1 代码导入

17.3.2 微信支付工具类

17.3.3 支付成功进行回调

18、订单状态定时更新

18.1 需求分析和设计

18.2 代码开发

19、来单提醒

19.1 需求分析和设计

19.2 代码开发

20、客户催单

20.1 需求分析和设计

20.2 代码开发

21、营业额统计

21.1 需求分析和设计

21.2 代码开发

22、用户统计

22.1 需求分析和设计

22.2 代码开发

23、订单统计

23.1 需求分析和设计

23.2 代码开发

24、销量排名

24.1 需求分析和设计

24.2 代码开发

25、工作台

25.1 需求分析和设计

25.2 代码开发

26、总结


1、软件开发整体介绍

1.1 软件开发流程

图 1.1-1 软件开发流程

1.2 角色分工

  • 项目经理:对整个项目负责、任务分配、把控进度
  • 产品经理:进行需求调研,输出需求调研文档、产品原型等
  • UI设计师:根据产品原型输出界面效果图
  • 架构师:项目整体架构设计、技术选型
  • 开发工程师:代码实现(小编的身份)
  • 测试工程师:编写测试用例、输出测试报告
  • 运维工程师:软件环境搭建、项目上线

1.3 软件环境

图 1.3-1 软件环境
上图是开发人员、测试人员以及在最后上线提供服务的服务器的介绍。

2、苍穹外卖项目介绍

2.1 项目介绍

图 2.1-1 功能架构
上图是本外卖项目的功能架构在管理端、用户端各个业务功能模块

2.2 产品原型

图 2.2-1 产品原型
上图中展示的是产品原型,展示了各个业务具体的功能模块具有那些功能和对应开发标准,开发者根据产品原型进行编码实现对应的功能。

2.3 技术选型

图 2.3-1 技术选型
上图中重要说明了,开发当前外卖系统需要使用的技术框架、中间件等。其中用户层、网关层、应用层、数据层各自用到哪些技术都已标注

3、开发环境搭建

3.1 前端环境搭建

图 3.1-1 前端环境搭建

在这里我是将前端项目运行在 nginx 中,这里读者可以去官网下载,也可以在直接通过百度网盘直接下载:

网盘地址:nginx下载  提取码:6666

注意:nginx不能运行在中文目录中,所以在安装时不能出现中文目录

           启动 nginx ,点击nginx.exe启动,运行端口号为80

3.2 后端环境搭建

3.2.1 后端环境搭建—使用Git进行版本控制

使用Git进行项目代码的版本控制,具体操作:

  • 创建Git本地仓库
  • 创建Git远程仓库
  • 将本地文件推送到远程Git仓库
3.2.2 后端环境搭建—数据库环境搭建
CREATE DATABASE  IF NOT EXISTS `sky_take_out` ;
USE `sky_take_out`;

DROP TABLE IF EXISTS `address_book`;
CREATE TABLE `address_book` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  `user_id` bigint NOT NULL COMMENT '用户id',
  `consignee` varchar(50) COLLATE utf8_bin DEFAULT NULL COMMENT '收货人',
  `sex` varchar(2) COLLATE utf8_bin DEFAULT NULL COMMENT '性别',
  `phone` varchar(11) COLLATE utf8_bin NOT NULL COMMENT '手机号',
  `province_code` varchar(12) CHARACTER SET utf8mb4  DEFAULT NULL COMMENT '省级区划编号',
  `province_name` varchar(32) CHARACTER SET utf8mb4  DEFAULT NULL COMMENT '省级名称',
  `city_code` varchar(12) CHARACTER SET utf8mb4  DEFAULT NULL COMMENT '市级区划编号',
  `city_name` varchar(32) CHARACTER SET utf8mb4  DEFAULT NULL COMMENT '市级名称',
  `district_code` varchar(12) CHARACTER SET utf8mb4  DEFAULT NULL COMMENT '区级区划编号',
  `district_name` varchar(32) CHARACTER SET utf8mb4  DEFAULT NULL COMMENT '区级名称',
  `detail` varchar(200) CHARACTER SET utf8mb4  DEFAULT NULL COMMENT '详细地址',
  `label` varchar(100) CHARACTER SET utf8mb4  DEFAULT NULL COMMENT '标签',
  `is_default` tinyint(1) NOT NULL DEFAULT '0' COMMENT '默认 0 否 1是',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin COMMENT='地址簿';

DROP TABLE IF EXISTS `category`;
CREATE TABLE `category` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  `type` int DEFAULT NULL COMMENT '类型   1 菜品分类 2 套餐分类',
  `name` varchar(32) COLLATE utf8_bin NOT NULL COMMENT '分类名称',
  `sort` int NOT NULL DEFAULT '0' COMMENT '顺序',
  `status` int DEFAULT NULL COMMENT '分类状态 0:禁用,1:启用',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `create_user` bigint DEFAULT NULL COMMENT '创建人',
  `update_user` bigint DEFAULT NULL COMMENT '修改人',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_category_name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=23 DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin COMMENT='菜品及套餐分类';

INSERT INTO `category` VALUES (11,1,'酒水饮料',10,1,'2022-06-09 22:09:18','2022-06-09 22:09:18',1,1);
INSERT INTO `category` VALUES (12,1,'传统主食',9,1,'2022-06-09 22:09:32','2022-06-09 22:18:53',1,1);
INSERT INTO `category` VALUES (13,2,'人气套餐',12,1,'2022-06-09 22:11:38','2022-06-10 11:04:40',1,1);
INSERT INTO `category` VALUES (15,2,'商务套餐',13,1,'2022-06-09 22:14:10','2022-06-10 11:04:48',1,1);
INSERT INTO `category` VALUES (16,1,'蜀味烤鱼',4,1,'2022-06-09 22:15:37','2022-08-31 14:27:25',1,1);
INSERT INTO `category` VALUES (17,1,'蜀味牛蛙',5,1,'2022-06-09 22:16:14','2022-08-31 14:39:44',1,1);
INSERT INTO `category` VALUES (18,1,'特色蒸菜',6,1,'2022-06-09 22:17:42','2022-06-09 22:17:42',1,1);
INSERT INTO `category` VALUES (19,1,'新鲜时蔬',7,1,'2022-06-09 22:18:12','2022-06-09 22:18:28',1,1);
INSERT INTO `category` VALUES (20,1,'水煮鱼',8,1,'2022-06-09 22:22:29','2022-06-09 22:23:45',1,1);
INSERT INTO `category` VALUES (21,1,'汤类',11,1,'2022-06-10 10:51:47','2022-06-10 10:51:47',1,1);

DROP TABLE IF EXISTS `dish`;
CREATE TABLE `dish` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` varchar(32) COLLATE utf8_bin NOT NULL COMMENT '菜品名称',
  `category_id` bigint NOT NULL COMMENT '菜品分类id',
  `price` decimal(10,2) DEFAULT NULL COMMENT '菜品价格',
  `image` varchar(255) COLLATE utf8_bin DEFAULT NULL COMMENT '图片',
  `description` varchar(255) COLLATE utf8_bin DEFAULT NULL COMMENT '描述信息',
  `status` int DEFAULT '1' COMMENT '0 停售 1 起售',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `create_user` bigint DEFAULT NULL COMMENT '创建人',
  `update_user` bigint DEFAULT NULL COMMENT '修改人',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_dish_name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=70 DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin COMMENT='菜品';

INSERT INTO `dish` VALUES (46,'王老吉',11,6.00,'https://sky-itcast.oss-cn-beijing.aliyuncs/41bfcacf-7ad4-4927-8b26-df366553a94c.png','',1,'2022-06-09 22:40:47','2022-06-09 22:40:47',1,1);
INSERT INTO `dish` VALUES (47,'北冰洋',11,4.00,'https://sky-itcast.oss-cn-beijing.aliyuncs/4451d4be-89a2-4939-9c69-3a87151cb979.png','还是小时候的味道',1,'2022-06-10 09:18:49','2022-06-10 09:18:49',1,1);
INSERT INTO `dish` VALUES (48,'雪花啤酒',11,4.00,'https://sky-itcast.oss-cn-beijing.aliyuncs/bf8cbfc1-04d2-40e8-9826-061ee41ab87c.png','',1,'2022-06-10 09:22:54','2022-06-10 09:22:54',1,1);
INSERT INTO `dish` VALUES (49,'米饭',12,2.00,'https://sky-itcast.oss-cn-beijing.aliyuncs/76752350-2121-44d2-b477-10791c23a8ec.png','精选五常大米',1,'2022-06-10 09:30:17','2022-06-10 09:30:17',1,1);
INSERT INTO `dish` VALUES (50,'馒头',12,1.00,'https://sky-itcast.oss-cn-beijing.aliyuncs/475cc599-8661-4899-8f9e-121dd8ef7d02.png','优质面粉',1,'2022-06-10 09:34:28','2022-06-10 09:34:28',1,1);
INSERT INTO `dish` VALUES (51,'老坛酸菜鱼',20,56.00,'https://sky-itcast.oss-cn-beijing.aliyuncs/4a9cefba-6a74-467e-9fde-6e687ea725d7.png','原料:汤,草鱼,酸菜',1,'2022-06-10 09:40:51','2022-06-10 09:40:51',1,1);
INSERT INTO `dish` VALUES (52,'经典酸菜鮰鱼',20,66.00,'https://sky-itcast.oss-cn-beijing.aliyuncs/5260ff39-986c-4a97-8850-2ec8c7583efc.png','原料:酸菜,江团,鮰鱼',1,'2022-06-10 09:46:02','2022-06-10 09:46:02',1,1);
INSERT INTO `dish` VALUES (53,'蜀味水煮草鱼',20,38.00,'https://sky-itcast.oss-cn-beijing.aliyuncs/a6953d5a-4c18-4b30-9319-4926ee77261f.png','原料:草鱼,汤',1,'2022-06-10 09:48:37','2022-06-10 09:48:37',1,1);
INSERT INTO `dish` VALUES (54,'清炒小油菜',19,18.00,'https://sky-itcast.oss-cn-beijing.aliyuncs/3613d38e-5614-41c2-90ed-ff175bf50716.png','原料:小油菜',1,'2022-06-10 09:51:46','2022-06-10 09:51:46',1,1);
INSERT INTO `dish` VALUES (55,'蒜蓉娃娃菜',19,18.00,'https://sky-itcast.oss-cn-beijing.aliyuncs/4879ed66-3860-4b28-ba14-306ac025fdec.png','原料:蒜,娃娃菜',1,'2022-06-10 09:53:37','2022-06-10 09:53:37',1,1);
INSERT INTO `dish` VALUES (56,'清炒西兰花',19,18.00,'https://sky-itcast.oss-cn-beijing.aliyuncs/e9ec4ba4-4b22-4fc8-9be0-4946e6aeb937.png','原料:西兰花',1,'2022-06-10 09:55:44','2022-06-10 09:55:44',1,1);
INSERT INTO `dish` VALUES (57,'炝炒圆白菜',19,18.00,'https://sky-itcast.oss-cn-beijing.aliyuncs/22f59feb-0d44-430e-a6cd-6a49f27453ca.png','原料:圆白菜',1,'2022-06-10 09:58:35','2022-06-10 09:58:35',1,1);
INSERT INTO `dish` VALUES (58,'清蒸鲈鱼',18,98.00,'https://sky-itcast.oss-cn-beijing.aliyuncs/c18b5c67-3b71-466c-a75a-e63c6449f21c.png','原料:鲈鱼',1,'2022-06-10 10:12:28','2022-06-10 10:12:28',1,1);
INSERT INTO `dish` VALUES (59,'东坡肘子',18,138.00,'https://sky-itcast.oss-cn-beijing.aliyuncs/a80a4b8c-c93e-4f43-ac8a-856b0d5cc451.png','原料:猪肘棒',1,'2022-06-10 10:24:03','2022-06-10 10:24:03',1,1);
INSERT INTO `dish` VALUES (60,'梅菜扣肉',18,58.00,'https://sky-itcast.oss-cn-beijing.aliyuncs/6080b118-e30a-4577-aab4-45042e3f88be.png','原料:猪肉,梅菜',1,'2022-06-10 10:26:03','2022-06-10 10:26:03',1,1);
INSERT INTO `dish` VALUES (61,'剁椒鱼头',18,66.00,'https://sky-itcast.oss-cn-beijing.aliyuncs/13da832f-ef2c-484d-8370-5934a1045a06.png','原料:鲢鱼,剁椒',1,'2022-06-10 10:28:54','2022-06-10 10:28:54',1,1);
INSERT INTO `dish` VALUES (62,'金汤酸菜牛蛙',17,88.00,'https://sky-itcast.oss-cn-beijing.aliyuncs/7694a5d8-7938-4e9d-8b9e-2075983a2e38.png','原料:鲜活牛蛙,酸菜',1,'2022-06-10 10:33:05','2022-06-10 10:33:05',1,1);
INSERT INTO `dish` VALUES (63,'香锅牛蛙',17,88.00,'https://sky-itcast.oss-cn-beijing.aliyuncs/f5ac8455-4793-450c-97ba-173795c34626.png','配料:鲜活牛蛙,莲藕,青笋',1,'2022-06-10 10:35:40','2022-06-10 10:35:40',1,1);
INSERT INTO `dish` VALUES (64,'馋嘴牛蛙',17,88.00,'https://sky-itcast.oss-cn-beijing.aliyuncs/7a55b845-1f2b-41fa-9486-76d187ee9ee1.png','配料:鲜活牛蛙,丝瓜,黄豆芽',1,'2022-06-10 10:37:52','2022-06-10 10:37:52',1,1);
INSERT INTO `dish` VALUES (65,'草鱼2斤',16,68.00,'https://sky-itcast.oss-cn-beijing.aliyuncs/b544d3ba-a1ae-4d20-a860-81cb5dec9e03.png','原料:草鱼,黄豆芽,莲藕',1,'2022-06-10 10:41:08','2022-06-10 10:41:08',1,1);
INSERT INTO `dish` VALUES (66,'江团鱼2斤',16,119.00,'https://sky-itcast.oss-cn-beijing.aliyuncs/a101a1e9-8f8b-47b2-afa4-1abd47ea0a87.png','配料:江团鱼,黄豆芽,莲藕',1,'2022-06-10 10:42:42','2022-06-10 10:42:42',1,1);
INSERT INTO `dish` VALUES (67,'鮰鱼2斤',16,72.00,'https://sky-itcast.oss-cn-beijing.aliyuncs/8cfcc576-4b66-4a09-ac68-ad5b273c2590.png','原料:鮰鱼,黄豆芽,莲藕',1,'2022-06-10 10:43:56','2022-06-10 10:43:56',1,1);
INSERT INTO `dish` VALUES (68,'鸡蛋汤',21,4.00,'https://sky-itcast.oss-cn-beijing.aliyuncs/c09a0ee8-9d19-428d-81b9-746221824113.png','配料:鸡蛋,紫菜',1,'2022-06-10 10:54:25','2022-06-10 10:54:25',1,1);
INSERT INTO `dish` VALUES (69,'平菇豆腐汤',21,6.00,'https://sky-itcast.oss-cn-beijing.aliyuncs/16d0a3d6-2253-4cfc-9b49-bf7bd9eb2ad2.png','配料:豆腐,平菇',1,'2022-06-10 10:55:02','2022-06-10 10:55:02',1,1);

DROP TABLE IF EXISTS `dish_flavor`;
CREATE TABLE `dish_flavor` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  `dish_id` bigint NOT NULL COMMENT '菜品',
  `name` varchar(32) COLLATE utf8_bin DEFAULT NULL COMMENT '口味名称',
  `value` varchar(255) COLLATE utf8_bin DEFAULT NULL COMMENT '口味数据list',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=104 DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin COMMENT='菜品口味关系表';

INSERT INTO `dish_flavor` VALUES (40,10,'甜味','[\"无糖\",\"少糖\",\"半糖\",\"多糖\",\"全糖\"]');
INSERT INTO `dish_flavor` VALUES (41,7,'忌口','[\"不要葱\",\"不要蒜\",\"不要香菜\",\"不要辣\"]');
INSERT INTO `dish_flavor` VALUES (42,7,'温度','[\"热饮\",\"常温\",\"去冰\",\"少冰\",\"多冰\"]');
INSERT INTO `dish_flavor` VALUES (45,6,'忌口','[\"不要葱\",\"不要蒜\",\"不要香菜\",\"不要辣\"]');
INSERT INTO `dish_flavor` VALUES (46,6,'辣度','[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]');
INSERT INTO `dish_flavor` VALUES (47,5,'辣度','[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]');
INSERT INTO `dish_flavor` VALUES (48,5,'甜味','[\"无糖\",\"少糖\",\"半糖\",\"多糖\",\"全糖\"]');
INSERT INTO `dish_flavor` VALUES (49,2,'甜味','[\"无糖\",\"少糖\",\"半糖\",\"多糖\",\"全糖\"]');
INSERT INTO `dish_flavor` VALUES (50,4,'甜味','[\"无糖\",\"少糖\",\"半糖\",\"多糖\",\"全糖\"]');
INSERT INTO `dish_flavor` VALUES (51,3,'甜味','[\"无糖\",\"少糖\",\"半糖\",\"多糖\",\"全糖\"]');
INSERT INTO `dish_flavor` VALUES (52,3,'忌口','[\"不要葱\",\"不要蒜\",\"不要香菜\",\"不要辣\"]');
INSERT INTO `dish_flavor` VALUES (86,52,'忌口','[\"不要葱\",\"不要蒜\",\"不要香菜\",\"不要辣\"]');
INSERT INTO `dish_flavor` VALUES (87,52,'辣度','[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]');
INSERT INTO `dish_flavor` VALUES (88,51,'忌口','[\"不要葱\",\"不要蒜\",\"不要香菜\",\"不要辣\"]');
INSERT INTO `dish_flavor` VALUES (89,51,'辣度','[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]');
INSERT INTO `dish_flavor` VALUES (92,53,'忌口','[\"不要葱\",\"不要蒜\",\"不要香菜\",\"不要辣\"]');
INSERT INTO `dish_flavor` VALUES (93,53,'辣度','[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]');
INSERT INTO `dish_flavor` VALUES (94,54,'忌口','[\"不要葱\",\"不要蒜\",\"不要香菜\"]');
INSERT INTO `dish_flavor` VALUES (95,56,'忌口','[\"不要葱\",\"不要蒜\",\"不要香菜\",\"不要辣\"]');
INSERT INTO `dish_flavor` VALUES (96,57,'忌口','[\"不要葱\",\"不要蒜\",\"不要香菜\",\"不要辣\"]');
INSERT INTO `dish_flavor` VALUES (97,60,'忌口','[\"不要葱\",\"不要蒜\",\"不要香菜\",\"不要辣\"]');
INSERT INTO `dish_flavor` VALUES (101,66,'辣度','[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]');
INSERT INTO `dish_flavor` VALUES (102,67,'辣度','[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]');
INSERT INTO `dish_flavor` VALUES (103,65,'辣度','[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]');

DROP TABLE IF EXISTS `employee`;
CREATE TABLE `employee` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` varchar(32) COLLATE utf8_bin NOT NULL COMMENT '姓名',
  `username` varchar(32) COLLATE utf8_bin NOT NULL COMMENT '用户名',
  `password` varchar(64) COLLATE utf8_bin NOT NULL COMMENT '密码',
  `phone` varchar(11) COLLATE utf8_bin NOT NULL COMMENT '手机号',
  `sex` varchar(2) COLLATE utf8_bin NOT NULL COMMENT '性别',
  `id_number` varchar(18) COLLATE utf8_bin NOT NULL COMMENT '身份证号',
  `status` int NOT NULL DEFAULT '1' COMMENT '状态 0:禁用,1:启用',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `create_user` bigint DEFAULT NULL COMMENT '创建人',
  `update_user` bigint DEFAULT NULL COMMENT '修改人',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin COMMENT='员工信息';

INSERT INTO `employee` VALUES (1,'管理员','admin','123456','13812312312','1','110101199001010047',1,'2022-02-15 15:51:20','2022-02-17 09:16:20',10,1);

DROP TABLE IF EXISTS `order_detail`;
CREATE TABLE `order_detail` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` varchar(32) COLLATE utf8_bin DEFAULT NULL COMMENT '名字',
  `image` varchar(255) COLLATE utf8_bin DEFAULT NULL COMMENT '图片',
  `order_id` bigint NOT NULL COMMENT '订单id',
  `dish_id` bigint DEFAULT NULL COMMENT '菜品id',
  `setmeal_id` bigint DEFAULT NULL COMMENT '套餐id',
  `dish_flavor` varchar(50) COLLATE utf8_bin DEFAULT NULL COMMENT '口味',
  `number` int NOT NULL DEFAULT '1' COMMENT '数量',
  `amount` decimal(10,2) NOT NULL COMMENT '金额',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin COMMENT='订单明细表';

DROP TABLE IF EXISTS `orders`;
CREATE TABLE `orders` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  `number` varchar(50) COLLATE utf8_bin DEFAULT NULL COMMENT '订单号',
  `status` int NOT NULL DEFAULT '1' COMMENT '订单状态 1待付款 2待接单 3已接单 4派送中 5已完成 6已取消 7退款',
  `user_id` bigint NOT NULL COMMENT '下单用户',
  `address_book_id` bigint NOT NULL COMMENT '地址id',
  `order_time` datetime NOT NULL COMMENT '下单时间',
  `checkout_time` datetime DEFAULT NULL COMMENT '结账时间',
  `pay_method` int NOT NULL DEFAULT '1' COMMENT '支付方式 1微信,2支付宝',
  `pay_status` tinyint NOT NULL DEFAULT '0' COMMENT '支付状态 0未支付 1已支付 2退款',
  `amount` decimal(10,2) NOT NULL COMMENT '实收金额',
  `remark` varchar(100) COLLATE utf8_bin DEFAULT NULL COMMENT '备注',
  `phone` varchar(11) COLLATE utf8_bin DEFAULT NULL COMMENT '手机号',
  `address` varchar(255) COLLATE utf8_bin DEFAULT NULL COMMENT '地址',
  `user_name` varchar(32) COLLATE utf8_bin DEFAULT NULL COMMENT '用户名称',
  `consignee` varchar(32) COLLATE utf8_bin DEFAULT NULL COMMENT '收货人',
  `cancel_reason` varchar(255) COLLATE utf8_bin DEFAULT NULL COMMENT '订单取消原因',
  `rejection_reason` varchar(255) COLLATE utf8_bin DEFAULT NULL COMMENT '订单拒绝原因',
  `cancel_time` datetime DEFAULT NULL COMMENT '订单取消时间',
  `estimated_delivery_time` datetime DEFAULT NULL COMMENT '预计送达时间',
  `delivery_status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '配送状态  1立即送出  0选择具体时间',
  `delivery_time` datetime DEFAULT NULL COMMENT '送达时间',
  `pack_amount` int DEFAULT NULL COMMENT '打包费',
  `tableware_number` int DEFAULT NULL COMMENT '餐具数量',
  `tableware_status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '餐具数量状态  1按餐量提供  0选择具体数量',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin COMMENT='订单表';

DROP TABLE IF EXISTS `setmeal`;
CREATE TABLE `setmeal` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  `category_id` bigint NOT NULL COMMENT '菜品分类id',
  `name` varchar(32) COLLATE utf8_bin NOT NULL COMMENT '套餐名称',
  `price` decimal(10,2) NOT NULL COMMENT '套餐价格',
  `status` int DEFAULT '1' COMMENT '售卖状态 0:停售 1:起售',
  `description` varchar(255) COLLATE utf8_bin DEFAULT NULL COMMENT '描述信息',
  `image` varchar(255) COLLATE utf8_bin DEFAULT NULL COMMENT '图片',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `create_user` bigint DEFAULT NULL COMMENT '创建人',
  `update_user` bigint DEFAULT NULL COMMENT '修改人',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_setmeal_name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=32 DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin COMMENT='套餐';

DROP TABLE IF EXISTS `setmeal_dish`;
CREATE TABLE `setmeal_dish` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  `setmeal_id` bigint DEFAULT NULL COMMENT '套餐id',
  `dish_id` bigint DEFAULT NULL COMMENT '菜品id',
  `name` varchar(32) COLLATE utf8_bin DEFAULT NULL COMMENT '菜品名称 (冗余字段)',
  `price` decimal(10,2) DEFAULT NULL COMMENT '菜品单价(冗余字段)',
  `copies` int DEFAULT NULL COMMENT '菜品份数',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=47 DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin COMMENT='套餐菜品关系';

DROP TABLE IF EXISTS `shopping_cart`;
CREATE TABLE `shopping_cart` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` varchar(32) COLLATE utf8_bin DEFAULT NULL COMMENT '商品名称',
  `image` varchar(255) COLLATE utf8_bin DEFAULT NULL COMMENT '图片',
  `user_id` bigint NOT NULL COMMENT '主键',
  `dish_id` bigint DEFAULT NULL COMMENT '菜品id',
  `setmeal_id` bigint DEFAULT NULL COMMENT '套餐id',
  `dish_flavor` varchar(50) COLLATE utf8_bin DEFAULT NULL COMMENT '口味',
  `number` int NOT NULL DEFAULT '1' COMMENT '数量',
  `amount` decimal(10,2) NOT NULL COMMENT '金额',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin COMMENT='购物车';

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  `openid` varchar(45) COLLATE utf8_bin DEFAULT NULL COMMENT '微信用户唯一标识',
  `name` varchar(32) COLLATE utf8_bin DEFAULT NULL COMMENT '姓名',
  `phone` varchar(11) COLLATE utf8_bin DEFAULT NULL COMMENT '手机号',
  `sex` varchar(2) COLLATE utf8_bin DEFAULT NULL COMMENT '性别',
  `id_number` varchar(18) COLLATE utf8_bin DEFAULT NULL COMMENT '身份证号',
  `avatar` varchar(500) COLLATE utf8_bin DEFAULT NULL COMMENT '头像',
  `create_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin COMMENT='用户信息';
## 数据库设计文档

| 序号 | 数据表名      | 中文名称       |
| ---- | ------------- | -------------- |
| 1    | employee      | 员工表         |
| 2    | category      | 分类表         |
| 3    | dish          | 菜品表         |
| 4    | dish_flavor   | 菜品口味表     |
| 5    | setmeal       | 套餐表         |
| 6    | setmeal_dish  | 套餐菜品关系表 |
| 7    | user          | 用户表         |
| 8    | address_book  | 地址表         |
| 9    | shopping_cart | 购物车表       |
| 10   | orders        | 订单表         |
| 11   | order_detail  | 订单明细表     |

### 1. employee

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

### 2. 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 |                      |

### 3. dish

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

### 4. dish_flavor

dish_flavor表为菜品口味表,用于存储菜品的口味信息。具体表结构如下:

| 字段名  | 数据类型     | 说明     | 备注     |
| ------- | ------------ | -------- | -------- |
| id      | bigint       | 主键     | 自增     |
| dish_id | bigint       | 菜品id   | 逻辑外键 |
| name    | varchar(32)  | 口味名称 |          |
| value   | varchar(255) | 口味值   |          |

### 5. setmeal

setmeal表为套餐表,用于存储套餐的信息。具体表结构如下:

| 字段名      | 数据类型      | 说明         | 备注        |
| ----------- | ------------- | ------------ | ----------- |
| 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 |             |

### 6. setmeal_dish

setmeal_dish表为套餐菜品关系表,用于存储套餐和菜品的关联关系。具体表结构如下:

| 字段名     | 数据类型      | 说明     | 备注     |
| ---------- | ------------- | -------- | -------- |
| id         | bigint        | 主键     | 自增     |
| setmeal_id | bigint        | 套餐id   | 逻辑外键 |
| dish_id    | bigint        | 菜品id   | 逻辑外键 |
| name       | varchar(32)   | 菜品名称 | 冗余字段 |
| price      | decimal(10,2) | 菜品单价 | 冗余字段 |
| copies     | int           | 菜品份数 |          |

### 7. user

user表为用户表,用于存储C端用户的信息。具体表结构如下:

| 字段名      | 数据类型     | 说明               | 备注 |
| ----------- | ------------ | ------------------ | ---- |
| id          | bigint       | 主键               | 自增 |
| openid      | varchar(45)  | 微信用户的唯一标识 |      |
| name        | varchar(32)  | 用户姓名           |      |
| phone       | varchar(11)  | 手机号             |      |
| sex         | varchar(2)   | 性别               |      |
| id_number   | varchar(18)  | 身份证号           |      |
| avatar      | varchar(500) | 微信用户头像路径   |      |
| create_time | datetime     | 注册时间           |      |

### 8. 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否        |

### 9. shopping_cart

shopping_cart表为购物车表,用于存储C端用户的购物车信息。具体表结构如下:

| 字段名      | 数据类型      | 说明         | 备注     |
| ----------- | ------------- | ------------ | -------- |
| id          | bigint        | 主键         | 自增     |
| name        | varchar(32)   | 商品名称     |          |
| image       | varchar(255)  | 商品图片路径 |          |
| user_id     | bigint        | 用户id       | 逻辑外键 |
| dish_id     | bigint        | 菜品id       | 逻辑外键 |
| setmeal_id  | bigint        | 套餐id       | 逻辑外键 |
| dish_flavor | varchar(50)   | 菜品口味     |          |
| number      | int           | 商品数量     |          |
| amount      | decimal(10,2) | 商品单价     |          |
| create_time | datetime      | 创建时间     |          |

### 10. orders

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选择具体数量                      |

### 11. order_detail

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) | 商品单价     |          |

3.2.3 后端环境搭建—前后端联调
图 3.2.3-1 联调过程图

当后端的初始工程已经实现了登录功能,直接进行前后端联调测试即可。

图 3.2.3-2 nginx反向代理

  • Nginx是一个高性能的HTTP和反向代理服务器,同时也提供了IMAP/POP3/SMTP服务。
  • 我们知道,Web服务器是用来对外共享资源的 ,类似于我们以前学习的Tomcat一样,是Web项目的部署容器。
  • 而IMAP/POP3/SMTP这三者,则是一种邮件协议,分别是邮件的发送和接收协议,所以Nginx还可以作为一个邮件服务器来使用。
  • 综上所述,Nginx就是一个服务器,这个服务器既可以当成邮件服务器,也可以当成Web服务器来使用。 

3.3 完善登录功能

图 3.3-1 完善登录功能

原数据中,账号密码是明文存储的,安全性比较低,这里就需要对其进行加密存储,所以要进行如下操作。

  • 修改数据库中明文密码,改为MD5加密后的密文
  • 修改Java代码,将前端提交的密码进行MD5加密后和数据库中的密码进行比对

4、导入接口文档

4.1 前后端分离开发流程

图 4.1-1 前后端开发流程
图中我们看到,在定制接口中定义规范,前后端开发人员就可以进行并行开发,之后走联调、提测等。

4.2 操作步骤

这边建议的是使用 YApi 作为开发项目接口管理器,使用起来简便、快捷。

YApi官网:http://yapi.mglicai/

5、Swagger

5.1 Swagger介绍

  • 使用Swagger你只需要按照它的规范去定义接口以及接口相关的信息,就可以做到生成接口文档,以及在线接口调试页面。
  • Knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案,使用这个方案,只需要在项目的pom.xml文件中导入对应依赖就可以。

5.2 使用方式

下面是使用Swagger的步骤:

  1. 导入Knife4j的maven坐标
  2. 在配置类中添加Knife4j相关配置
  3. 设置静态资源映射,否则接口文档页面无法访问

以上三个步骤中都配有相对应的代码,

5.3 常用注解

图 5.3-1 Swagger常用注解
上图中是Swagger常用注解,通过注解可以控制生成的接口文档,使接口文档拥有更好的可读性

6、员工管理

6.1 新增员工

6.1.1 需求分析和设计
图 6.1.1-1 新增员工需求分析和设计

新增员工:

  • 用户名
  • 身份证号
  • 手机号码
  • 性别
以上就是新增员工需求分析和设计,即需要新增用户提供以上信息完成用户注册
6.1.2 代码开发
图 6.1.2-1 Controller层代码
上图中,展示的是用户登录的控制层代码,用以接收客户端传来的登录信息
6.1. 3 功能测试

功能测试方式:

  • 通过接口文档测试:启动项目后,在浏览器端输入 http://localhost:8080/doc.html 进入Swagger在线接口文档进行调试
  • 通过前后端联调测试:需要开启前端项目,进行测试。
6.1.4 完善代码

当管理端注册新用户时,新用户登记信息中有一个当前操作用户ID属性,但是这个属性无法直接赋值,所以这就需要一个新的技术点 ThreadLocal 

ThreadLocal 为每一个线程提供一份单独的存储空间,具有线程隔离的效果,只有在线程内才能获取到相对应的值,线程外不能访问。

图 6.1.4-1 新增员工
在上图中,我们看到新增管理端用户,在登记信息中需要提供当前操作员工的ID,通过 ThreadLoad 实现
图 6.1.4-2 ThreadLocal常用方法
在上图中,我们看到了 ThreadLoad 实常见方法,通过这几个方法实现在登记信息中需要提供当前操作员工的ID这一需求

6.2 员工分页查询

6.2.1 需求分析和设计
图 6.2.1-1 产品原型
上图中,我们看到数据展示规则;首先是根据页码展示员工信息,其次是每页展示10条数据,最后是分页查询时可以根据需要,输入员工姓名进行查询
6.2.2 代码开发
图 6.2.2-1 代码开发
上图中,我们看到前端页面在地址栏传递了name、page、pageSize三个参数,后端使用了EmployeePageQueryDTO 这个属性类接收
图 6.2.2-2 PageResult对象
上图中,说明了后面所有的分页查询,统一封装成PageResult对象

6.3 启用禁用员工账号

6.3.1 需求分析和设计
图 6.3.1 员工账号数据库设计图

业务规则:

  • 可以对状态为 “启用” 的员工账号进行 “禁用” 操作
  • 可以对状态为 “禁用” 的员工账号进行 “启用” 操作
  • 状态为 “禁用” 的员工账号不能登录系统
6.3.2 代码开发
图 6.3.2-1 启用、禁用员工账号Controller层代码
图中展示的是启用、禁用员工账号 Controller 层代码,主要是通过接收前端传递的 (Integer status, Long id)参数,去调用 Service 层代码。
图 6.3.2-2 启用、禁用员工账号Service层代码

图中展示的是启用、禁用员工账号 Service 层代码,主要是通过接收Controller传递的 (Integer status, Long id)参数,去调用 Mapper 层代码,实现修改数据库表中的员工数据
图 6.3.2-3 启用、禁用员工账号Mapper层代码
图中展示的是启用、禁用员工账号 Mapper层代码,主要是通过接收Service 传递的 (Employee employee)参数,去调用 Mapper 层代码,实现修改数据库表中的员工数据

6.4 编辑员工

6.4.1 需求分析和设计

图 6.4.1 编辑员工需求分析和设计

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

  • 根据id查询员工信息
  • 修改员工信息

6.4.2 代码开发

图 6.4.2-1 编辑员工Controller层代码
上图中展示的是编辑员工Controller层代码,接收前端传递的参数(Long id)从而调用Service层代码查询 
图 6.4.2-2 编辑员工Service层代码
上图中展示的是编辑员工Service层代码,接收Controller层传递的参数(Long id)从而调用Mapper层代码查询
图 6.4.2-3 编辑员工Mapper层代码
上图中展示的是编辑员工Mapper层代码,接收Service层传递的参数(Long id)进而通过数据库进行查询

6.5 分类模块

6.5.1 需求分析和设计

图 6.5.1 需求分析和设计

业务规则:

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

6.5.2 代码开发

package com.sky.controller.admin;

import com.sky.dto.CategoryDTO;
import com.sky.dto.CategoryPageQueryDTO;
import com.sky.entity.Category;
import com.sky.result.PageResult;
import com.sky.result.Result;
import com.sky.service.CategoryService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;

/**
 * 分类管理
 */
@RestController
@RequestMapping("/admin/category")
@Api(tags = "分类相关接口")
@Slf4j
public class CategoryController {

    @Autowired
    private CategoryService categoryService;

    /**
     * 新增分类
     * @param categoryDTO
     * @return
     */
    @PostMapping
    @ApiOperation("新增分类")
    public Result<String> save(@RequestBody CategoryDTO categoryDTO){
        log.info("新增分类:{}", categoryDTO);
        categoryService.save(categoryDTO);
        return Result.success();
    }

    /**
     * 分类分页查询
     * @param categoryPageQueryDTO
     * @return
     */
    @GetMapping("/page")
    @ApiOperation("分类分页查询")
    public Result<PageResult> page(CategoryPageQueryDTO categoryPageQueryDTO){
        log.info("分页查询:{}", categoryPageQueryDTO);
        PageResult pageResult = categoryService.pageQuery(categoryPageQueryDTO);
        return Result.success(pageResult);
    }

    /**
     * 删除分类
     * @param id
     * @return
     */
    @DeleteMapping
    @ApiOperation("删除分类")
    public Result<String> deleteById(Long id){
        log.info("删除分类:{}", id);
        categoryService.deleteById(id);
        return Result.success();
    }

    /**
     * 修改分类
     * @param categoryDTO
     * @return
     */
    @PutMapping
    @ApiOperation("修改分类")
    public Result<String> update(@RequestBody CategoryDTO categoryDTO){
        categoryService.update(categoryDTO);
        return Result.success();
    }

    /**
     * 启用、禁用分类
     * @param status
     * @param id
     * @return
     */
    @PostMapping("/status/{status}")
    @ApiOperation("启用禁用分类")
    public Result<String> startOrStop(@PathVariable("status") Integer status, Long id){
        categoryService.startOrStop(status,id);
        return Result.success();
    }

    /**
     * 根据类型查询分类
     * @param type
     * @return
     */
    @GetMapping("/list")
    @ApiOperation("根据类型查询分类")
    public Result<List<Category>> list(Integer type){
        List<Category> list = categoryService.list(type);
        return Result.success(list);
    }
}
图 6.5.2-2 Service层代码

代码太多,这里就不展示了,在源码中我做了详细的标记。

这部分代码详情对应的分别是源码的 “DishServiceImpl”类、 “SetmealServiceImpl”类、“CategoryServiceImpl”类。

源码地址是在我的百度网盘中:https://pan.baidu/s/1t1NRbEaML2UiX5qS8JkKcw  提取码:6666 
图 6.5.2-3 Mapper层代码

上图中展示的就是分类模块的Mapper层代码,即通过直接查询数据库获取相关数据。

7、公共字段自动填充

7.1 问题分析

图 7.1-1 公共字段问题分析

上图中,我们看到当数据库中进行插入、修改操作时,会频繁使用图中的四个字段,如果我们对相对应的数据表进行常规的Set赋值、Get获取值,会出现大量的代码冗余

 7.2 实现思路 

图 7.1-2 公共字段实现思路
上图中,是提供了解决 “图 7.1-1 公共字段问题分析” 中代码冗余的方案 

7.3 代码开发

图 7.3-1 创建枚举类
图中枚举类代码,用于指定数据库操作的类型
图 7.3-2 创建自定义注解
图中代码为创建自定义注解,用于表示需要进行AOP注入的方法

图 7.3-3 创建AOP切面方程

package com.sky.aspect;

import com.sky.annotation.AutoFill;
import com.sky.constant.AutoFillConstant;
import com.sky.context.BaseContext;
import com.sky.enumeration.OperationType;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.time.LocalDateTime;

/**
 * 自定义切面,实现公共字段自动填充逻辑
 */

// 注解说明此类为切面类
@Aspect
// 将此类的对象交给容器处理,用以实现自动自动注入
@Component
@Slf4j
public class AutoFillAspect {

    /**
     * 切入点
     */
    @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
    public void autoFillPointCut(){}

    /**
     * 前置通知,在通知中进行公共字段的赋值
     */
    @Before("autoFillPointCut()")
    public void autoFill(JoinPoint joinPoint){
        log.info("开始进行公共字段自动填充...");

        // 获取当前被拦截方法的数据库操作类型
        MethodSignature signature = (MethodSignature)joinPoint.getSignature(); //获取方法签名对象
        AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class); //获取方法上的注解
        OperationType operationType = autoFill.value(); //获取注解的操作类型

        // 获取被拦截方法的参数--实体类
        Object[] args = joinPoint.getArgs(); //获取添加自定义注解方法的所有参数
        if(args == null || args.length ==0){
            return;
        }
        Object entity = args[0];


        // 准备需要赋值的数据
        LocalDateTime localDateTime = LocalDateTime.now();
        Long currentId = BaseContext.getCurrentId();

        // 根据当前不同的操作类型,为对应的属性来赋值
        if (operationType == OperationType.INSERT){
            //为4个公共字段赋值
            try {
                Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
                Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
                Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);

                //通过反射技术为相应属性赋值
                setCreateTime.invoke(entity, localDateTime);
                setCreateUser.invoke(entity, currentId);
                setUpdateTime.invoke(entity, localDateTime);
                setUpdateUser.invoke(entity, currentId);
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else if (operationType == OperationType.UPDATE){
            //为4个公共字段赋值
            try {
                Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);

                //通过反射技术为相应属性赋值
                setUpdateTime.invoke(entity, localDateTime);
                setUpdateUser.invoke(entity, currentId);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

图中的代码是AOP切面类,通过反射方法为属性赋值。

8、新增菜品

8.1 需求分析和设计

图 8.1-1 需求分析和设计

业务规则:

  • 菜品名称必须是唯一的
  • 菜品必须属于某个分类下面,不能单独存在
  • 新增菜品时可以根据情况选择菜品的口味
  • 每个菜品必须对应一张图片
图 8.1-2 数据库设计
上图中,是针对菜品表、菜品口味表的数据库设计,其中 dish 菜品表中的category_id是逻辑外键(通过java代码来指定主键是那张数据表),其中 dish_flavor 口味表中的 dish_id 是逻辑外键(通过java代码来指定主键是那张数据表)。

8.2 代码开发

图 8.2-1 Controller层代码
上图中定义了控制层代码,用以接收前端传来的请求、参数,并且将结果返回给前端
图 8.2-2 Service层代码
上图中定义了Service层代码,用以实现新增菜品和对应的口味功能
图 8.2-3 Mapper层代码

上图中定义了Mapper层代码,用以实现与数据库进行交互

9、菜品分页查询

9.1 需求分析和设计

图 9.1-1 需求分析和设计

业务规则:

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

9.2 代码开发

图 9.2-1 Controller层代码
上图中定义了控制层代码,用以接收前端传来的请求、参数,并且将结果返回给前端
图 9.2-2 Service层代码
上图中定义了Service层代码,用以实现新增菜品和对应的口味功能
图 9.2-3 Mapper层代码

上图中定义了Mapper层代码,用以实现与数据库进行交互

10、删除菜品

10.1 需求分析和设计

图 10.1-1 需求分析和设计

业务规则:

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

10.2 代码开发

图 10.2-1 Controller层代码
图中定义了控制层代码,用以接收前端传来的请求、参数,并且将结果返回给前端
图 10.2-2 Service层代码
上图中定义了Service层代码,用以实现批量删除菜品的功能
图 10.2-3 Mapper层代码
上图中定义了Mapper层代码,用以实现与数据库进行交互

11、修改菜品

11.1 需求分析和设计

11.1-1 需求分析和设计

接口设计:

  • 根据id查询菜品
  • 根据类型查询分类
  • 文件上传
  • 修改菜品
以上就是修改菜品的相关接口设计

11.2 代码开发

图 11.2-1 Controller层代码
图中定义了控制层代码,用以接收前端传来的请求、参数,并且将结果返回给前端
图 11.2-2 Service层代码
上图中定义了Service层代码,用以实现修改菜品的功能
图 11.2-3 Mapper层代码
上图中定义了Mapper层代码,用以实现与数据库进行交互

14、清空购物车

14.1 需求分析和设计

14.1-1 需求分析图

接口设计:

  • 用户端清空购物车商品

项目约定

  • 用户端 发出请求统一使用user 作为前提
图中展示的是查看购物车的相关接口设计

14.2 代码开发

14.2-1 Controller层代码
图中红框标记的便是项目中清空购物车 Controller 层代码,用于接受前端发送过来的请求,并且将结果反馈出去。

15、导入地址簿

15.1 需求分析和设计

15.1-1 需求分析图

接口设计:

数据库表设计:

图中展示的是导入地址簿的相关接口设计和数据库表的设计。

15.2 代码开发

15.2.1 查询当前登录用户的所有地址信息
15.2.1-1 Controller层代码
 
图中红框定义了 “查询当前登录用户的所有地址信息 ”  Controller层代码,用以接收前端传来的查询当前登录用户的所有地址信息请求,并且以 List 集合的方式返回
15.2.2 新增地址
15.2.2-1 Controller层代码
图中红框定义了 “新增地址 ”  Controller层代码,用以接收前端传来的新增地址信息请求
15.2.2-2 Service层代码
图中红框标记的便是项目中 “新增地址 ” Service 层代码,执行相应的逻辑代码。
15.2.2-3 Mapper层代码
图中红框标记的便是项目中 “新增地址 ” Mapper 层代码,执行相应的动态SQL语句。
15.2.3 设置默认地址
15.2.3-1 Controller层代码
图中红框定义了 “设置默认地址”  Controller层代码,用以接收前端传来的设置默认地址请求
15.2.4 查询默认地址
15.2.4-1 Controller层代码
图中红框定义了 “查询默认地址”  Controller层代码,用以接收前端传来的查询默认地址请求

16、用户下单

16.1 需求分析和设计

16.1-1 需求分析和设计图

接口设计:                                                                    

        接口参数如下

  • 地址簿id
  • 配送状态
  • 打包费
  • 总金额
  • 备注
  • 餐具数量

        接口返回数据

  • 下单时间
  • 订单总金额
  • 订单号
  • 订单id
图中展示的是用户下单的相关接口设计以及接口参数的设计。
16.1-2 数据库设计
图中展示的是用户下单数据库设计,包含订单表、订单明细表。

16.2 代码开发

16.2-1 Controller层代码

17、订单支付

17.1 需求分析和设计

17.1-1 微信支付介绍

图中展示的是微信支付的产品,包括了常见的扫码支付、小程序支付、APP支付等。

详细的介绍,在官网文档中:“微信支付商户平台”。

17.1-2 微信支付接入流程

图中展示的是接入微信支付的流程介绍。

 详细的介绍,在官网文档中:“微信支付商户平台”。

17.1-3 微信支付时序图

图中展示的是微信支付时序图。

其中重要的就是“调用微信下单接口”,这个接口连接微信后台,用以生成一个预支付订单。

“调起微信支付”,作用是在用户输入支付密码后将信息传递给微信后台完成扣款。

“推送支付结果”,微信后台完成扣款后,将支付成功结果返回给商家。

17.1-4 调用微信下单接口

图中展示的就是 “调用微信下单接口” 实例,需要哪些参数,返回值有哪些,非常详细。

详情官网地址:JSAPI下单 - JSAPI支付 | 微信支付商户文档中心

17.1-5 小程序调起支付

图中展示的是 “小程序调起支付” 实例,需要哪些参数,返回值有哪些,非常详细。

详情官网地址:小程序调起支付 - 小程序支付 | 微信支付商户文档中心

17.2 微信支付准备工作

17.2.1 如何保障支付过程安全性
17.2.1-1 获取微信支付平台证书、商户私钥文件

图中展示的微信支付平台证书、商户私钥文件,是用来加密、解密在支付过程中传输的数据的,这样保障了数据的安全性。

微信支付平台证书、商户私钥文件是从微信商户平台下载来的,开发者根据不同的项目,更换微信支付平台证书和商户私钥文件就好。

17.2.2 微信后台如何调用商户系统
17.2.2-1 内网穿透(获取临时域名)

图中所示,在开发阶段中商户系统是运行在开发者的电脑中的,然而开发者电脑是出于局域网中,微信后台只能访问公网IP,这时就需要赋予处于局域网中的电脑一个公网IP,即我们俗称的内网穿透。

cpolar官网地址:文档 - cpolar 极点云官网

17.3 代码开发

17.3.1 代码导入
17.3.1-1 微信支付相关配置
图中展示的是微信支付的相关配置属性,对应的属性配置类。
17.3.2 微信支付工具类
package com.sky.utils;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.sky.properties.WeChatProperties;
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import org.apachemons.lang.RandomStringUtils;
import org.apache.http.HttpHeaders;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.math.BigDecimal;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;

/**
 * 微信支付工具类
 */
@Component
public class WeChatPayUtil {

    //微信支付下单接口地址
    public static final String JSAPI = "https://api.mch.weixin.qq/v3/pay/transactions/jsapi";

    //申请退款接口地址
    public static final String REFUNDS = "https://api.mch.weixin.qq/v3/refund/domestic/refunds";

    @Autowired
    private WeChatProperties weChatProperties;

    /**
     * 获取调用微信接口的客户端工具对象
     *
     * @return
     */
    private CloseableHttpClient getClient() {
        PrivateKey merchantPrivateKey = null;
        try {
            //merchantPrivateKey商户API私钥,如何加载商户API私钥请看常见问题
            merchantPrivateKey = PemUtil.loadPrivateKey(new FileInputStream(new File(weChatProperties.getPrivateKeyFilePath())));
            //加载平台证书文件
            X509Certificate x509Certificate = PemUtil.loadCertificate(new FileInputStream(new File(weChatProperties.getWeChatPayCertFilePath())));
            //wechatPayCertificates微信支付平台证书列表。你也可以使用后面章节提到的“定时更新平台证书功能”,而不需要关心平台证书的来龙去脉
            List<X509Certificate> wechatPayCertificates = Arrays.asList(x509Certificate);

            WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                    .withMerchant(weChatProperties.getMchid(), weChatProperties.getMchSerialNo(), merchantPrivateKey)
                    .withWechatPay(wechatPayCertificates);

            // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签
            CloseableHttpClient httpClient = builder.build();
            return httpClient;
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 发送post方式请求
     *
     * @param url
     * @param body
     * @return
     */
    private String post(String url, String body) throws Exception {
        CloseableHttpClient httpClient = getClient();

        HttpPost httpPost = new HttpPost(url);
        httpPost.addHeader(HttpHeaders.ACCEPT, ContentType.APPLICATION_JSON.toString());
        httpPost.addHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString());
        httpPost.addHeader("Wechatpay-Serial", weChatProperties.getMchSerialNo());
        httpPost.setEntity(new StringEntity(body, "UTF-8"));

        CloseableHttpResponse response = httpClient.execute(httpPost);
        try {
            String bodyAsString = EntityUtils.toString(response.getEntity());
            return bodyAsString;
        } finally {
            httpClient.close();
            response.close();
        }
    }

    /**
     * 发送get方式请求
     *
     * @param url
     * @return
     */
    private String get(String url) throws Exception {
        CloseableHttpClient httpClient = getClient();

        HttpGet httpGet = new HttpGet(url);
        httpGet.addHeader(HttpHeaders.ACCEPT, ContentType.APPLICATION_JSON.toString());
        httpGet.addHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString());
        httpGet.addHeader("Wechatpay-Serial", weChatProperties.getMchSerialNo());

        CloseableHttpResponse response = httpClient.execute(httpGet);
        try {
            String bodyAsString = EntityUtils.toString(response.getEntity());
            return bodyAsString;
        } finally {
            httpClient.close();
            response.close();
        }
    }

    /**
     * jsapi下单
     *
     * @param orderNum    商户订单号
     * @param total       总金额
     * @param description 商品描述
     * @param openid      微信用户的openid
     * @return
     */
    private String jsapi(String orderNum, BigDecimal total, String description, String openid) throws Exception {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("appid", weChatProperties.getAppid());
        jsonObject.put("mchid", weChatProperties.getMchid());
        jsonObject.put("description", description);
        jsonObject.put("out_trade_no", orderNum);
        jsonObject.put("notify_url", weChatProperties.getNotifyUrl());

        JSONObject amount = new JSONObject();
        amount.put("total", total.multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).intValue());
        amount.put("currency", "CNY");

        jsonObject.put("amount", amount);

        JSONObject payer = new JSONObject();
        payer.put("openid", openid);

        jsonObject.put("payer", payer);

        String body = jsonObject.toJSONString();
        return post(JSAPI, body);
    }

    /**
     * 小程序支付
     *
     * @param orderNum    商户订单号
     * @param total       金额,单位 元
     * @param description 商品描述
     * @param openid      微信用户的openid
     * @return
     */
    public JSONObject pay(String orderNum, BigDecimal total, String description, String openid) throws Exception {
        //统一下单,生成预支付交易单
        String bodyAsString = jsapi(orderNum, total, description, openid);
        //解析返回结果
        JSONObject jsonObject = JSON.parseObject(bodyAsString);
        System.out.println(jsonObject);

        String prepayId = jsonObject.getString("prepay_id");
        if (prepayId != null) {
            String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
            String nonceStr = RandomStringUtils.randomNumeric(32);
            ArrayList<Object> list = new ArrayList<>();
            list.add(weChatProperties.getAppid());
            list.add(timeStamp);
            list.add(nonceStr);
            list.add("prepay_id=" + prepayId);
            //二次签名,调起支付需要重新签名
            StringBuilder stringBuilder = new StringBuilder();
            for (Object o : list) {
                stringBuilder.append(o).append("\n");
            }
            String signMessage = stringBuilder.toString();
            byte[] message = signMessage.getBytes();

            Signature signature = Signature.getInstance("SHA256withRSA");
            signature.initSign(PemUtil.loadPrivateKey(new FileInputStream(new File(weChatProperties.getPrivateKeyFilePath()))));
            signature.update(message);
            String packageSign = Base64.getEncoder().encodeToString(signature.sign());

            //构造数据给微信小程序,用于调起微信支付
            JSONObject jo = new JSONObject();
            jo.put("timeStamp", timeStamp);
            jo.put("nonceStr", nonceStr);
            jo.put("package", "prepay_id=" + prepayId);
            jo.put("signType", "RSA");
            jo.put("paySign", packageSign);

            return jo;
        }
        return jsonObject;
    }

    /**
     * 申请退款
     *
     * @param outTradeNo    商户订单号
     * @param outRefundNo   商户退款单号
     * @param refund        退款金额
     * @param total         原订单金额
     * @return
     */
    public String refund(String outTradeNo, String outRefundNo, BigDecimal refund, BigDecimal total) throws Exception {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("out_trade_no", outTradeNo);
        jsonObject.put("out_refund_no", outRefundNo);

        JSONObject amount = new JSONObject();
        amount.put("refund", refund.multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).intValue());
        amount.put("total", total.multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).intValue());
        amount.put("currency", "CNY");

        jsonObject.put("amount", amount);
        jsonObject.put("notify_url", weChatProperties.getRefundNotifyUrl());

        String body = jsonObject.toJSONString();

        //调用申请退款接口
        return post(REFUNDS, body);
    }
}
17.3.3 支付成功进行回调
package com.sky.controller.notify;

import com.alibaba.druid.support.json.JSONUtils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.sky.properties.WeChatProperties;
import com.sky.service.OrderService;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.entity.ContentType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;

/**
 * 支付回调相关接口
 */
@RestController
@RequestMapping("/notify")
@Slf4j
public class PayNotifyController {
    @Autowired
    private OrderService orderService;
    @Autowired
    private WeChatProperties weChatProperties;

    /**
     * 支付成功回调
     *
     * @param request
     */
    @RequestMapping("/paySuccess")
    public void paySuccessNotify(HttpServletRequest request, HttpServletResponse response) throws Exception {
        //读取数据
        String body = readData(request);
        log.info("支付成功回调:{}", body);

        //数据解密
        String plainText = decryptData(body);
        log.info("解密后的文本:{}", plainText);

        JSONObject jsonObject = JSON.parseObject(plainText);
        String outTradeNo = jsonObject.getString("out_trade_no");//商户平台订单号
        String transactionId = jsonObject.getString("transaction_id");//微信支付交易号

        log.info("商户平台订单号:{}", outTradeNo);
        log.info("微信支付交易号:{}", transactionId);

        //业务处理,修改订单状态、来单提醒
        orderService.paySuccess(outTradeNo);

        //给微信响应
        responseToWeixin(response);
    }

    /**
     * 读取数据
     *
     * @param request
     * @return
     * @throws Exception
     */
    private String readData(HttpServletRequest request) throws Exception {
        BufferedReader reader = request.getReader();
        StringBuilder result = new StringBuilder();
        String line = null;
        while ((line = reader.readLine()) != null) {
            if (result.length() > 0) {
                result.append("\n");
            }
            result.append(line);
        }
        return result.toString();
    }

    /**
     * 数据解密
     *
     * @param body
     * @return
     * @throws Exception
     */
    private String decryptData(String body) throws Exception {
        JSONObject resultObject = JSON.parseObject(body);
        JSONObject resource = resultObject.getJSONObject("resource");
        String ciphertext = resource.getString("ciphertext");
        String nonce = resource.getString("nonce");
        String associatedData = resource.getString("associated_data");

        AesUtil aesUtil = new AesUtil(weChatProperties.getApiV3Key().getBytes(StandardCharsets.UTF_8));
        //密文解密
        String plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),
                nonce.getBytes(StandardCharsets.UTF_8),
                ciphertext);

        return plainText;
    }

    /**
     * 给微信响应
     * @param response
     */
    private void responseToWeixin(HttpServletResponse response) throws Exception{
        response.setStatus(200);
        HashMap<Object, Object> map = new HashMap<>();
        map.put("code", "SUCCESS");
        map.put("message", "SUCCESS");
        response.setHeader("Content-type", ContentType.APPLICATION_JSON.toString());
        response.getOutputStream().write(JSONUtils.toJSONString(map).getBytes(StandardCharsets.UTF_8));
        response.flushBuffer();
    }
}

18、订单状态定时更新

18.1 需求分析和设计

18.1-1 需求分析和设计图

图中展示的就是外卖订单可能存在的两种情况,这就需要一个定时任务,来处理这种情况

18.2 代码开发

package com.sky.task;

import com.sky.entity.Orders;
import com.sky.mapper.OrderMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;

/**
 * 自定义任务类
 */
@Component
@Slf4j
public class MyTask {

    @Autowired
    private OrderMapper orderMapper;

    /**
     * 定时任务 每隔五秒触发一次
     */
    @Scheduled(cron = " 0/5 * * * * ? ")
    public void executeTask(){
        log.info("定时任务开始执行: {}", new Date());
    }

    /**
     * 处理超时订单的方法
     */
    @Scheduled(cron = "0 * * * * ?") //每分钟处理一次
    public void processTimeoutOrder(){
        log.info("定时处理超时订单: {}", LocalDateTime.now());
        LocalDateTime time = LocalDateTime.now().plusMinutes(-15);
        //查找超时订单
        List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.PENDING_PAYMENT, time);
        //对超时订单进行处理
        if (ordersList != null && ordersList.size() > 0){
            for (Orders order : ordersList) {
                order.setStatus(Orders.CANCELLED);
                order.setCancelReason("订单超时、自动取消");
                order.setCancelTime(LocalDateTime.now());
                orderMapper.update(order);
            }
        }
    }

    /**
     * 处理一直处于派送中的订单
     */
    @Scheduled(cron = "0 0 1 * * ?") //每天凌晨1点触发一次
    public void processDeliveryOrder(){
        log.info("定时处理处于派送中的订单: {}", LocalDateTime.now());
        LocalDateTime time = LocalDateTime.now().plusMinutes(-60);
        //查找超时订单
        List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.DELIVERY_IN_PROGRESS, time);
        if (ordersList != null && ordersList.size() > 0){
            for (Orders order : ordersList) {
                order.setStatus(Orders.COMPLETED);
                orderMapper.update(order);
            }
        }
    }
}

19、来单提醒

19.1 需求分析和设计

19.1-1 需求分析和设计图

分析:用户下单而且支付成功后,需要提醒商家有新的订单

  • 语音播报
  • 弹框提醒

设计:

  • 通过 WebSocket 实现管理端页面和服务器端保持长连接状态
  • 当客户支付后,调用 WebSocket 的相关API实现服务器端向客户端推送消息
  • 客户端浏览器解析服务器端推送的消息,判断是来电提醒还是客户催单,进行相应的消息提示和语音播报
  • 约定服务器端发送给客户端的消息为JSON格式
图中说明的就是关于来单提醒的需求分析。

19.2 代码开发

19.2-1 Controller层代码
图中展示的是Controller层代码,在图片中的功能是当用户支付成功后,通过使用回调函数通知商家用户已经支付。
19.2-2 Service层代码
图中展示的是Service层代码,在图片中的功能是区别具体的用户的消息;来新订单、还是客户催单。

20、客户催单

20.1 需求分析和设计

20.1-1 需求分析和设计图

用户在小程序中点击催单按钮后,需要第一时间通知商家。通知形式有如下两种。

  • 语音播报
  • 弹出提示框

设计:

  • 通过 WebSocket 实现管理端页面和服务器端保持长连接状态
  • 当客户支付后,调用 WebSocket 的相关API实现服务器端向客户端推送消息
  • 客户端浏览器解析服务器端推送的消息,判断是来电提醒还是客户催单,进行相应的消息提示和语音播报
  • 约定服务器端发送给客户端的消息为JSON格式

20.2 代码开发

20.2-1 Controller层代码
图中展示的代码是Controller层代码,实现了当用户端催单时,服务器调用相关接口通知商家
20.2-2 Service层代码
图中展示的代码是Service层代码,实现了当用户端催单时,首先判断当前订单是否真实有效,然后执行查询操作

21、营业额统计

21.1 需求分析和设计

图 21.1-1 需求分析和设计
图中展示的是营业额统计的需求分析图,主要实现根据指定的日期完成订单的营业额统计

21.2 代码开发

21.2-1 Controller层代码
图中展示的是营业额统计的Controller层代码,用于接受两个时间变量begin、end,从而统计这两个时间范围内的营业额
21.2-2 Service层代码
图中展示的是营业额统计的Service层代码,实现在指定日期内查询当天的营业额
21.2-3 Mapper层代码

图中展示的是营业额统计的Mapper层代码,实现查询数据库中当天的营业数据

22、用户统计

22.1 需求分析和设计

图 22.1-1 需求分析和设计 
图中展示的是用户统计需求分析和设计图,主要是根据选定的时间,统计每天的用户总量和新增用户。

22.2 代码开发

22.2-1 Controller层代码
图中展示的是用户统计的Controller层代码,用于接受两个时间变量begin、end,从而统计这两个时间范围内的用户数据
    /**
     * 统计指定时间内的用户数据
     * @param begin
     * @param end
     * @return
     */
    @Override
    public UserReportVO getUserStatistics(LocalDate begin, LocalDate end) {
        //当前集合用于存放从begin到end范围内的每天的日期
        List<LocalDate> dateList = new ArrayList<>();
        dateList.add(begin);
        while (!begin.equals(end)){
            begin = begin.plusDays(1);
            dateList.add(begin);
        }
        //调用StringUtils工具的连接方法,实现将dateList数组中的元素全部取出并用逗号作为分隔符
        String s = StringUtils.join(dateList, ",");
        //存放每天的新增用户量
        List<Integer> newUserList = new ArrayList<>();
        //存放每天的总的用户数量
        List<Integer> totalUserList = new ArrayList<>();

        for (LocalDate date : dateList) {
            LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);
            LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);
            Map map = new HashMap();
            map.put("end", endTime);
            //获取总用户数量
            Integer totalUser = userMapper.countByMap(map);
            //获取新增用户数量
            map.put("begin", beginTime);
            Integer newUser = userMapper.countByMap(map);
            totalUserList.add(totalUser);
            newUserList.add(newUser);
        }
        String s1 = StringUtils.join(newUserList, ",");
        String s2 = StringUtils.join(totalUserList, ",");
        //封装结果数据
        return UserReportVO.builder()
                .dateList(s)
                .newUserList(s1)
                .totalUserList(s2)
                .build();
    }
上图中展示的是Service层代码,封装了指定时间内,用户数量和新增用户数量。
22.2-3 Mapper层代码

图中展示的是用户统计的Mapper层代码,通过直接访问数据库获取在指定时间区间内新增用户数量和总用户数量

23、订单统计

23.1 需求分析和设计

图 23.1-1 订单统计-需求分析和设计
图中展示的是订单统计的业务规则要求,即统计在规定时间区间内的有效订单、总订单、订单完成率

23.2 代码开发

图 23.2-1 Controller 层代码
图中展示的是订单统计的Controller层代码,用于接受两个时间变量begin、end,从而统计这两个时间范围内的订单数据
package com.sky.service.impl;

import com.sky.entity.Orders;
import com.sky.entity.User;
import com.sky.mapper.OrderMapper;
import com.sky.mapper.UserMapper;
import com.sky.service.ReportService;
import com.sky.vo.OrderReportVO;
import com.sky.vo.TurnoverReportVO;
import com.sky.vo.UserReportVO;
import lombok.extern.slf4j.Slf4j;
import org.apachemons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
@Slf4j
public class ReportServiceImpl implements ReportService {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private UserMapper userMapper;

    /**
     * 统计指定时间内的营业额数据
     * @param begin
     * @param end
     * @return
     */
    @Override
    public TurnoverReportVO getTurnoverStatistics(LocalDate begin, LocalDate end) {
        //当前集合用于存放从begin到end范围内的每天的日期
        List<LocalDate> dateList = new ArrayList<>();
        dateList.add(begin);
        while (!begin.equals(end)){
            begin = begin.plusDays(1);
            dateList.add(begin);
        }
        //调用StringUtils工具的连接方法,实现将dateList数组中的元素全部取出并用逗号作为分隔符
        String s = StringUtils.join(dateList, ",");
        //查询指定日期的营业额数据,营业额是指:状态为“已完成”的订单金额合计
        List<Double> turnoverList = new ArrayList<>();
        for (LocalDate date : dateList) {
            LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);
            LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);
            Map map = new HashMap();
            map.put("begin", beginTime);
            map.put("end", endTime);
            map.put("status", Orders.COMPLETED);
            Double turnover = orderMapper.sumByMap(map);
            if (turnover == null){
                turnover = 0.0;
            }
            turnoverList.add(turnover);
        }
        return TurnoverReportVO
                .builder()
                .turnoverList(StringUtils.join(turnoverList, ","))
                .dateList(s)
                .build();
    }

    /**
     * 统计指定时间内的用户数据
     * @param begin
     * @param end
     * @return
     */
    @Override
    public UserReportVO getUserStatistics(LocalDate begin, LocalDate end) {
        //当前集合用于存放从begin到end范围内的每天的日期
        List<LocalDate> dateList = new ArrayList<>();
        dateList.add(begin);
        while (!begin.equals(end)){
            begin = begin.plusDays(1);
            dateList.add(begin);
        }
        //调用StringUtils工具的连接方法,实现将dateList数组中的元素全部取出并用逗号作为分隔符
        String s = StringUtils.join(dateList, ",");
        //存放每天的新增用户量
        List<Integer> newUserList = new ArrayList<>();
        //存放每天的总的用户数量
        List<Integer> totalUserList = new ArrayList<>();

        for (LocalDate date : dateList) {
            LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);
            LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);
            Map map = new HashMap();
            map.put("end", endTime);
            //获取总用户数量
            Integer totalUser = userMapper.countByMap(map);
            //获取新增用户数量
            map.put("begin", beginTime);
            Integer newUser = userMapper.countByMap(map);
            totalUserList.add(totalUser);
            newUserList.add(newUser);
        }
        String s1 = StringUtils.join(newUserList, ",");
        String s2 = StringUtils.join(totalUserList, ",");
        //封装结果数据
        return UserReportVO.builder()
                .dateList(s)
                .newUserList(s1)
                .totalUserList(s2)
                .build();
    }

    /**
     * 统计指定时间内的订单数据
     * @param begin
     * @param end
     * @return
     */
    @Override
    public OrderReportVO getOrderStatistics(LocalDate begin, LocalDate end) {
        //当前集合用于存放从begin到end范围内的每天的日期
        List<LocalDate> dateList = new ArrayList<>();
        //当前集合用于存放每天的订单总数
        List<Integer> orderCountList = new ArrayList<>();
        //当前集合用于存放每天的有效订单数
        List<Integer> validOrderCountList = new ArrayList<>();
        dateList.add(begin);
        while (!begin.equals(end)){
            begin = begin.plusDays(1);
            dateList.add(begin);
        }
        //查询指定时间内的有效订单数和订单总数
        for (LocalDate date : dateList) {
            //查询每天订单总数
            LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);
            LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);
            Integer orderCount = getOrderCount(beginTime, endTime, null);
            orderCountList.add(orderCount);
            //查询每天有效订单数
            Integer validOrderCount = getOrderCount(beginTime, endTime, Orders.COMPLETED);
            validOrderCountList.add(validOrderCount);
        }
        //计算时间区间内订单的总数量
        Integer totalOrderCount = orderCountList.stream().reduce(Integer::sum).get();
        //计算时间区间内有效定的总数量
        Integer validOrderCount = validOrderCountList.stream().reduce(Integer::sum).get();
        //计算订单完成率
        Double orderCompletionRate = 0.0;
        if (totalOrderCount != 0){
            orderCompletionRate = validOrderCount.doubleValue() / totalOrderCount;
        }
        return OrderReportVO.builder()
                .dateList(StringUtils.join(dateList, ","))
                .orderCountList(StringUtils.join(orderCountList, ","))
                .validOrderCountList(StringUtils.join(validOrderCountList, ","))
                .validOrderCount(validOrderCount)
                .totalOrderCount(totalOrderCount)
                .orderCompletionRate(orderCompletionRate)
                .build();
    }

    private Integer getOrderCount(LocalDateTime begin, LocalDateTime end, Integer status){
        Map map = new HashMap<>();
        map.put("begin", begin);
        map.put("end", end);
        map.put("status", status);
        Integer integer = orderMapper.countByMap(map);
        return integer;
    }
}

图中展示的是订单统计中的 Service 层代码,接收来自Controller层的开始时间、结束时间参数,实现找到总的订单数和完成订单数

图 23.2-3 Mapper层代码

图中展示的是订单统计中的 Mapper 层代码,接收来自 Service 层的 Map封装的开始时间、结束时间参数,实现找到总的订单数和完成订单数

24、销量排名

24.1 需求分析和设计

图 24.1-1 销量排名需求分析和设计
图中展示的是销量排名的业务规则要求,即统计在规定时间区间内的排名前十的商品和套餐数据

24.2 代码开发

图 24.2-1 Controller层代码
图中展示的是销量排名Top10的Controller层代码,用于接受两个时间变量begin、end,从而统计这两个时间范围内的销量排名Top10数据
图 24.2-2 Service层代码
图中展示的是销量排名Top10的Service层代码,接收来自Controller层的开始时间、结束时间参数,实现找到销量排名Top10
图 24.2-3 Mapper层代码

图中展示的是销量排名Top10的Mapper层代码,实现通过查询数据库中的数据统计销量排名Top10商品信息

25、工作台

25.1 需求分析和设计

图 25.1-1 工作台需求分析和设计

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

工作台展示的数据:

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

名词解释:

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

接口设计:

  • 今日数据接口
  • 订单管理接口
  • 菜品总览接口
  • 套餐总览接口

25.2 代码开发

package com.sky.controller.admin;

import com.sky.result.Result;
import com.sky.service.WorkspaceService;
import com.sky.vo.BusinessDataVO;
import com.sky.vo.DishOverViewVO;
import com.sky.vo.OrderOverViewVO;
import com.sky.vo.SetmealOverViewVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.time.LocalTime;

/**
 * 工作台
 */
@RestController
@RequestMapping("/admin/workspace")
@Slf4j
@Api(tags = "工作台相关接口")
public class WorkSpaceController {

    @Autowired
    private WorkspaceService workspaceService;

    /**
     * 工作台今日数据查询
     * @return
     */
    @GetMapping("/businessData")
    @ApiOperation("工作台今日数据查询")
    public Result<BusinessDataVO> businessData(){
        //获得当天的开始时间
        LocalDateTime begin = LocalDateTime.now().with(LocalTime.MIN);
        //获得当天的结束时间
        LocalDateTime end = LocalDateTime.now().with(LocalTime.MAX);

        BusinessDataVO businessDataVO = workspaceService.getBusinessData(begin, end);
        return Result.success(businessDataVO);
    }

    /**
     * 查询订单管理数据
     * @return
     */
    @GetMapping("/overviewOrders")
    @ApiOperation("查询订单管理数据")
    public Result<OrderOverViewVO> orderOverView(){
        return Result.success(workspaceService.getOrderOverView());
    }

    /**
     * 查询菜品总览
     * @return
     */
    @GetMapping("/overviewDishes")
    @ApiOperation("查询菜品总览")
    public Result<DishOverViewVO> dishOverView(){
        return Result.success(workspaceService.getDishOverView());
    }

    /**
     * 查询套餐总览
     * @return
     */
    @GetMapping("/overviewSetmeals")
    @ApiOperation("查询套餐总览")
    public Result<SetmealOverViewVO> setmealOverView(){
        return Result.success(workspaceService.getSetmealOverView());
    }
}
package com.sky.service.impl;

import com.sky.constant.StatusConstant;
import com.sky.entity.Orders;
import com.sky.mapper.DishMapper;
import com.sky.mapper.OrderMapper;
import com.sky.mapper.SetmealMapper;
import com.sky.mapper.UserMapper;
import com.sky.service.WorkspaceService;
import com.sky.vo.BusinessDataVO;
import com.sky.vo.DishOverViewVO;
import com.sky.vo.OrderOverViewVO;
import com.sky.vo.SetmealOverViewVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.HashMap;
import java.util.Map;

@Service
@Slf4j
public class WorkspaceServiceImpl implements WorkspaceService {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private DishMapper dishMapper;
    @Autowired
    private SetmealMapper setmealMapper;

    /**
     * 根据时间段统计营业数据
     * @param begin
     * @param end
     * @return
     */
    public BusinessDataVO getBusinessData(LocalDateTime begin, LocalDateTime end) {
        /**
         * 营业额:当日已完成订单的总金额
         * 有效订单:当日已完成订单的数量
         * 订单完成率:有效订单数 / 总订单数
         * 平均客单价:营业额 / 有效订单数
         * 新增用户:当日新增用户的数量
         */

        Map map = new HashMap();
        map.put("begin",begin);
        map.put("end",end);

        //查询总订单数
        Integer totalOrderCount = orderMapper.countByMap(map);

        map.put("status", Orders.COMPLETED);
        //营业额
        Double turnover = orderMapper.sumByMap(map);
        turnover = turnover == null? 0.0 : turnover;

        //有效订单数
        Integer validOrderCount = orderMapper.countByMap(map);

        Double unitPrice = 0.0;

        Double orderCompletionRate = 0.0;
        if(totalOrderCount != 0 && validOrderCount != 0){
            //订单完成率
            orderCompletionRate = validOrderCount.doubleValue() / totalOrderCount;
            //平均客单价
            unitPrice = turnover / validOrderCount;
        }

        //新增用户数
        Integer newUsers = userMapper.countByMap(map);

        return BusinessDataVO.builder()
                .turnover(turnover)
                .validOrderCount(validOrderCount)
                .orderCompletionRate(orderCompletionRate)
                .unitPrice(unitPrice)
                .newUsers(newUsers)
                .build();
    }


    /**
     * 查询订单管理数据
     *
     * @return
     */
    public OrderOverViewVO getOrderOverView() {
        Map map = new HashMap();
        map.put("begin", LocalDateTime.now().with(LocalTime.MIN));
        map.put("status", Orders.TO_BE_CONFIRMED);

        //待接单
        Integer waitingOrders = orderMapper.countByMap(map);

        //待派送
        map.put("status", Orders.CONFIRMED);
        Integer deliveredOrders = orderMapper.countByMap(map);

        //已完成
        map.put("status", Orders.COMPLETED);
        Integer completedOrders = orderMapper.countByMap(map);

        //已取消
        map.put("status", Orders.CANCELLED);
        Integer cancelledOrders = orderMapper.countByMap(map);

        //全部订单
        map.put("status", null);
        Integer allOrders = orderMapper.countByMap(map);

        return OrderOverViewVO.builder()
                .waitingOrders(waitingOrders)
                .deliveredOrders(deliveredOrders)
                pletedOrders(completedOrders)
                .cancelledOrders(cancelledOrders)
                .allOrders(allOrders)
                .build();
    }

    /**
     * 查询菜品总览
     *
     * @return
     */
    public DishOverViewVO getDishOverView() {
        Map map = new HashMap();
        map.put("status", StatusConstant.ENABLE);
        Integer sold = dishMapper.countByMap(map);

        map.put("status", StatusConstant.DISABLE);
        Integer discontinued = dishMapper.countByMap(map);

        return DishOverViewVO.builder()
                .sold(sold)
                .discontinued(discontinued)
                .build();
    }

    /**
     * 查询套餐总览
     *
     * @return
     */
    public SetmealOverViewVO getSetmealOverView() {
        Map map = new HashMap();
        map.put("status", StatusConstant.ENABLE);
        Integer sold = setmealMapper.countByMap(map);

        map.put("status", StatusConstant.DISABLE);
        Integer discontinued = setmealMapper.countByMap(map);

        return SetmealOverViewVO.builder()
                .sold(sold)
                .discontinued(discontinued)
                .build();
    }
}
图 25.2-3 工作台Mapper层代码
工作台Mapper层代码是与前面实现的功能Mapper层代码是重复的,这里不再展示了

26、总结

以上就是一个完整项目的开发流程和代码详解,完整的项目源码可以去我的百度网盘获取,地址是链接:https://pan.baidu/s/1t1NRbEaML2UiX5qS8JkKcw  提取码:6666  

本文标签: 苍穹外卖大学生项目