准备工作

个人在学习该项目时使用的是SpringBoot3,所以有些操作与视频有所不同

  • 首先创建所需的数据库表

    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
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    348
    349
    350
    351
    352
    353
    354
    355
    356
    357
    358
    359
    360
    361
    362
    363
    364
    365
    366
    367
    368
    369
    370
    371
    372
    373
    374
    375
    /*
    Navicat MySQL Data Transfer

    Source Server : localhost
    Source Server Version : 50728
    Source Host : localhost:3306
    Source Database : reggie

    Target Server Type : MYSQL
    Target Server Version : 50728
    File Encoding : 65001

    Date: 2021-07-23 10:41:41
    */

    SET FOREIGN_KEY_CHECKS=0;

    -- ----------------------------
    -- Table structure for address_book
    -- ----------------------------
    DROP TABLE IF EXISTS `address_book`;
    CREATE TABLE `address_book` (
    `id` bigint(20) NOT NULL COMMENT '主键',
    `user_id` bigint(20) NOT NULL COMMENT '用户id',
    `consignee` varchar(50) COLLATE utf8_bin NOT NULL COMMENT '收货人',
    `sex` tinyint(4) NOT NULL COMMENT '性别 0 女 1 男',
    `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是',
    `create_time` datetime NOT NULL COMMENT '创建时间',
    `update_time` datetime NOT NULL COMMENT '更新时间',
    `create_user` bigint(20) NOT NULL COMMENT '创建人',
    `update_user` bigint(20) NOT NULL COMMENT '修改人',
    `is_deleted` int(11) NOT NULL DEFAULT '0' COMMENT '是否删除',
    PRIMARY KEY (`id`) USING BTREE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='地址管理';

    -- ----------------------------
    -- Records of address_book
    -- ----------------------------
    INSERT INTO `address_book` VALUES ('1417414526093082626', '1417012167126876162', '小明', '1', '13812345678', null, null, null, null, null, null, '昌平区金燕龙办公楼', '公司', '1', '2021-07-20 17:22:12', '2021-07-20 17:26:33', '1417012167126876162', '1417012167126876162', '0');
    INSERT INTO `address_book` VALUES ('1417414926166769666', '1417012167126876162', '小李', '1', '13512345678', null, null, null, null, null, null, '测试', '家', '0', '2021-07-20 17:23:47', '2021-07-20 17:23:47', '1417012167126876162', '1417012167126876162', '0');

    -- ----------------------------
    -- Table structure for category
    -- ----------------------------
    DROP TABLE IF EXISTS `category`;
    CREATE TABLE `category` (
    `id` bigint(20) NOT NULL COMMENT '主键',
    `type` int(11) DEFAULT NULL COMMENT '类型 1 菜品分类 2 套餐分类',
    `name` varchar(64) COLLATE utf8_bin NOT NULL COMMENT '分类名称',
    `sort` int(11) NOT NULL DEFAULT '0' COMMENT '顺序',
    `create_time` datetime NOT NULL COMMENT '创建时间',
    `update_time` datetime NOT NULL COMMENT '更新时间',
    `create_user` bigint(20) NOT NULL COMMENT '创建人',
    `update_user` bigint(20) NOT NULL COMMENT '修改人',
    PRIMARY KEY (`id`) USING BTREE,
    UNIQUE KEY `idx_category_name` (`name`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='菜品及套餐分类';

    -- ----------------------------
    -- Records of category
    -- ----------------------------
    INSERT INTO `category` VALUES ('1397844263642378242', '1', '湘菜', '1', '2021-05-27 09:16:58', '2021-07-15 20:25:23', '1', '1');
    INSERT INTO `category` VALUES ('1397844303408574465', '1', '川菜', '2', '2021-05-27 09:17:07', '2021-06-02 14:27:22', '1', '1');
    INSERT INTO `category` VALUES ('1397844391040167938', '1', '粤菜', '3', '2021-05-27 09:17:28', '2021-07-09 14:37:13', '1', '1');
    INSERT INTO `category` VALUES ('1413341197421846529', '1', '饮品', '11', '2021-07-09 11:36:15', '2021-07-09 14:39:15', '1', '1');
    INSERT INTO `category` VALUES ('1413342269393674242', '2', '商务套餐', '5', '2021-07-09 11:40:30', '2021-07-09 14:43:45', '1', '1');
    INSERT INTO `category` VALUES ('1413384954989060097', '1', '主食', '12', '2021-07-09 14:30:07', '2021-07-09 14:39:19', '1', '1');
    INSERT INTO `category` VALUES ('1413386191767674881', '2', '儿童套餐', '6', '2021-07-09 14:35:02', '2021-07-09 14:39:05', '1', '1');

    -- ----------------------------
    -- Table structure for dish
    -- ----------------------------
    DROP TABLE IF EXISTS `dish`;
    CREATE TABLE `dish` (
    `id` bigint(20) NOT NULL COMMENT '主键',
    `name` varchar(64) COLLATE utf8_bin NOT NULL COMMENT '菜品名称',
    `category_id` bigint(20) NOT NULL COMMENT '菜品分类id',
    `price` decimal(10,2) DEFAULT NULL COMMENT '菜品价格',
    `code` varchar(64) COLLATE utf8_bin NOT NULL COMMENT '商品码',
    `image` varchar(200) COLLATE utf8_bin NOT NULL COMMENT '图片',
    `description` varchar(400) COLLATE utf8_bin DEFAULT NULL COMMENT '描述信息',
    `status` int(11) NOT NULL DEFAULT '1' COMMENT '0 停售 1 起售',
    `sort` int(11) NOT NULL DEFAULT '0' COMMENT '顺序',
    `create_time` datetime NOT NULL COMMENT '创建时间',
    `update_time` datetime NOT NULL COMMENT '更新时间',
    `create_user` bigint(20) NOT NULL COMMENT '创建人',
    `update_user` bigint(20) NOT NULL COMMENT '修改人',
    `is_deleted` int(11) NOT NULL DEFAULT '0' COMMENT '是否删除',
    PRIMARY KEY (`id`) USING BTREE,
    UNIQUE KEY `idx_dish_name` (`name`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='菜品管理';

    -- ----------------------------
    -- Records of dish
    -- ----------------------------
    INSERT INTO `dish` VALUES ('1397849739276890114', '辣子鸡', '1397844263642378242', '7800.00', '222222222', 'f966a38e-0780-40be-bb52-5699d13cb3d9.jpg', '来自鲜嫩美味的小鸡,值得一尝', '1', '0', '2021-05-27 09:38:43', '2021-05-27 09:38:43', '1', '1', '0');
    INSERT INTO `dish` VALUES ('1397850140982161409', '毛氏红烧肉', '1397844263642378242', '6800.00', '123412341234', '0a3b3288-3446-4420-bbff-f263d0c02d8e.jpg', '毛氏红烧肉毛氏红烧肉,确定不来一份?', '1', '0', '2021-05-27 09:40:19', '2021-05-27 09:40:19', '1', '1', '0');
    INSERT INTO `dish` VALUES ('1397850392090947585', '组庵鱼翅', '1397844263642378242', '4800.00', '123412341234', '740c79ce-af29-41b8-b78d-5f49c96e38c4.jpg', '组庵鱼翅,看图足以表明好吃程度', '1', '0', '2021-05-27 09:41:19', '2021-05-27 09:41:19', '1', '1', '0');
    INSERT INTO `dish` VALUES ('1397850851245600769', '霸王别姬', '1397844263642378242', '12800.00', '123412341234', '057dd338-e487-4bbc-a74c-0384c44a9ca3.jpg', '还有什么比霸王别姬更美味的呢?', '1', '0', '2021-05-27 09:43:08', '2021-05-27 09:43:08', '1', '1', '0');
    INSERT INTO `dish` VALUES ('1397851099502260226', '全家福', '1397844263642378242', '11800.00', '23412341234', 'a53a4e6a-3b83-4044-87f9-9d49b30a8fdc.jpg', '别光吃肉啦,来份全家福吧,让你长寿又美味', '1', '0', '2021-05-27 09:44:08', '2021-05-27 09:44:08', '1', '1', '0');
    INSERT INTO `dish` VALUES ('1397851370462687234', '邵阳猪血丸子', '1397844263642378242', '13800.00', '1246812345678', '2a50628e-7758-4c51-9fbb-d37c61cdacad.jpg', '看,美味不?来嘛来嘛,这才是最爱吖', '1', '0', '2021-05-27 09:45:12', '2021-05-27 09:45:12', '1', '1', '0');
    INSERT INTO `dish` VALUES ('1397851668262465537', '口味蛇', '1397844263642378242', '16800.00', '1234567812345678', '0f4bd884-dc9c-4cf9-b59e-7d5958fec3dd.jpg', '爬行界的扛把子,东兴-口味蛇,让你欲罢不能', '1', '0', '2021-05-27 09:46:23', '2021-05-27 09:46:23', '1', '1', '0');
    INSERT INTO `dish` VALUES ('1397852391150759938', '辣子鸡丁', '1397844303408574465', '8800.00', '2346812468', 'ef2b73f2-75d1-4d3a-beea-22da0e1421bd.jpg', '辣子鸡丁,辣子鸡丁,永远的魂', '1', '0', '2021-05-27 09:49:16', '2021-05-27 09:49:16', '1', '1', '0');
    INSERT INTO `dish` VALUES ('1397853183287013378', '麻辣兔头', '1397844303408574465', '19800.00', '123456787654321', '2a2e9d66-b41d-4645-87bd-95f2cfeed218.jpg', '麻辣兔头的详细制作,麻辣鲜香,色泽红润,回味悠长', '1', '0', '2021-05-27 09:52:24', '2021-05-27 09:52:24', '1', '1', '0');
    INSERT INTO `dish` VALUES ('1397853709101740034', '蒜泥白肉', '1397844303408574465', '9800.00', '1234321234321', 'd2f61d70-ac85-4529-9b74-6d9a2255c6d7.jpg', '多么的有食欲啊', '1', '0', '2021-05-27 09:54:30', '2021-05-27 09:54:30', '1', '1', '0');
    INSERT INTO `dish` VALUES ('1397853890262118402', '鱼香肉丝', '1397844303408574465', '3800.00', '1234212321234', '8dcfda14-5712-4d28-82f7-ae905b3c2308.jpg', '鱼香肉丝简直就是我们童年回忆的一道经典菜,上学的时候点个鱼香肉丝盖饭坐在宿舍床上看着肥皂剧,绝了!现在完美复刻一下上学的时候感觉', '1', '0', '2021-05-27 09:55:13', '2021-05-27 09:55:13', '1', '1', '0');
    INSERT INTO `dish` VALUES ('1397854652581064706', '麻辣水煮鱼', '1397844303408574465', '14800.00', '2345312·345321', '1fdbfbf3-1d86-4b29-a3fc-46345852f2f8.jpg', '鱼片是买的切好的鱼片,放几个虾,增加味道', '1', '0', '2021-05-27 09:58:15', '2021-05-27 09:58:15', '1', '1', '0');
    INSERT INTO `dish` VALUES ('1397854865672679425', '鱼香炒鸡蛋', '1397844303408574465', '2000.00', '23456431·23456', '0f252364-a561-4e8d-8065-9a6797a6b1d3.jpg', '鱼香菜也是川味的特色。里面没有鱼却鱼香味', '1', '0', '2021-05-27 09:59:06', '2021-05-27 09:59:06', '1', '1', '0');
    INSERT INTO `dish` VALUES ('1397860242057375745', '脆皮烧鹅', '1397844391040167938', '12800.00', '123456786543213456', 'e476f679-5c15-436b-87fa-8c4e9644bf33.jpeg', '“广东烤鸭美而香,却胜烧鹅说古冈(今新会),燕瘦环肥各佳妙,君休偏重便宜坊”,可见烧鹅与烧鸭在粤菜之中已早负盛名。作为广州最普遍和最受欢迎的烧烤肉食,以它的“色泽金红,皮脆肉嫩,味香可口”的特色,在省城各大街小巷的烧卤店随处可见。', '1', '0', '2021-05-27 10:20:27', '2021-05-27 10:20:27', '1', '1', '0');
    INSERT INTO `dish` VALUES ('1397860578738352129', '白切鸡', '1397844391040167938', '6600.00', '12345678654', '9ec6fc2d-50d2-422e-b954-de87dcd04198.jpeg', '白切鸡是一道色香味俱全的特色传统名肴,又叫白斩鸡,是粤菜系鸡肴中的一种,始于清代的民间。白切鸡通常选用细骨农家鸡与沙姜、蒜茸等食材,慢火煮浸白切鸡皮爽肉滑,清淡鲜美。著名的泮溪酒家白切鸡,曾获商业部优质产品金鼎奖。湛江白切鸡更是驰名粤港澳。粤菜厨坛中,鸡的菜式有200余款之多,而最为人常食不厌的正是白切鸡,深受食家青睐。', '1', '0', '2021-05-27 10:21:48', '2021-05-27 10:21:48', '1', '1', '0');
    INSERT INTO `dish` VALUES ('1397860792492666881', '烤乳猪', '1397844391040167938', '38800.00', '213456432123456', '2e96a7e3-affb-438e-b7c3-e1430df425c9.jpeg', '广式烧乳猪主料是小乳猪,辅料是蒜,调料是五香粉、芝麻酱、八角粉等,本菜品主要通过将食材放入炭火中烧烤而成。烤乳猪是广州最著名的特色菜,并且是“满汉全席”中的主打菜肴之一。烤乳猪也是许多年来广东人祭祖的祭品之一,是家家都少不了的应节之物,用乳猪祭完先人后,亲戚们再聚餐食用。', '1', '0', '2021-05-27 10:22:39', '2021-05-27 10:22:39', '1', '1', '0');
    INSERT INTO `dish` VALUES ('1397860963880316929', '脆皮乳鸽', '1397844391040167938', '10800.00', '1234563212345', '3fabb83a-1c09-4fd9-892b-4ef7457daafa.jpeg', '“脆皮乳鸽”是广东菜中的一道传统名菜,属于粤菜系,具有皮脆肉嫩、色泽红亮、鲜香味美的特点,常吃可使身体强健,清肺顺气。随着菜品制作工艺的不断发展,逐渐形成了熟炸法、生炸法和烤制法三种制作方法。无论那种制作方法,都是在鸽子经过一系列的加工,挂脆皮水后再加工而成,正宗的“脆皮乳鸽皮脆肉嫩、色泽红亮、鲜香味美、香气馥郁。这三种方法的制作过程都不算复杂,但想达到理想的效果并不容易。', '1', '0', '2021-05-27 10:23:19', '2021-05-27 10:23:19', '1', '1', '0');
    INSERT INTO `dish` VALUES ('1397861683434139649', '清蒸河鲜海鲜', '1397844391040167938', '38800.00', '1234567876543213456', '1405081e-f545-42e1-86a2-f7559ae2e276.jpeg', '新鲜的海鲜,清蒸是最好的处理方式。鲜,体会为什么叫海鲜。清蒸是广州最经典的烹饪手法,过去岭南地区由于峻山大岭阻隔,交通不便,经济发展起步慢,自家打的鱼放在锅里煮了就吃,没有太多的讲究,但却发现这清淡的煮法能使鱼的鲜甜跃然舌尖。', '1', '0', '2021-05-27 10:26:11', '2021-05-27 10:26:11', '1', '1', '0');
    INSERT INTO `dish` VALUES ('1397862198033297410', '老火靓汤', '1397844391040167938', '49800.00', '123456786532455', '583df4b7-a159-4cfc-9543-4f666120b25f.jpeg', '老火靓汤又称广府汤,是广府人传承数千年的食补养生秘方,慢火煲煮的中华老火靓汤,火候足,时间长,既取药补之效,又取入口之甘甜。 广府老火汤种类繁多,可以用各种汤料和烹调方法,烹制出各种不同口味、不同功效的汤来。', '1', '0', '2021-05-27 10:28:14', '2021-05-27 10:28:14', '1', '1', '0');
    INSERT INTO `dish` VALUES ('1397862477831122945', '上汤焗龙虾', '1397844391040167938', '108800.00', '1234567865432', '5b8d2da3-3744-4bb3-acdc-329056b8259d.jpeg', '上汤焗龙虾是一道色香味俱全的传统名菜,属于粤菜系。此菜以龙虾为主料,配以高汤制成的一道海鲜美食。本品肉质洁白细嫩,味道鲜美,蛋白质含量高,脂肪含量低,营养丰富。是色香味俱全的传统名菜。', '1', '0', '2021-05-27 10:29:20', '2021-05-27 10:29:20', '1', '1', '0');
    INSERT INTO `dish` VALUES ('1413342036832100354', '北冰洋', '1413341197421846529', '500.00', '', 'c99e0aab-3cb7-4eaa-80fd-f47d4ffea694.png', '', '1', '0', '2021-07-09 11:39:35', '2021-07-09 15:12:18', '1', '1', '0');
    INSERT INTO `dish` VALUES ('1413384757047271425', '王老吉', '1413341197421846529', '500.00', '', '00874a5e-0df2-446b-8f69-a30eb7d88ee8.png', '', '1', '0', '2021-07-09 14:29:20', '2021-07-12 09:09:16', '1', '1', '0');
    INSERT INTO `dish` VALUES ('1413385247889891330', '米饭', '1413384954989060097', '200.00', '', 'ee04a05a-1230-46b6-8ad5-1a95b140fff3.png', '', '1', '0', '2021-07-09 14:31:17', '2021-07-11 16:35:26', '1', '1', '0');

    -- ----------------------------
    -- Table structure for dish_flavor
    -- ----------------------------
    DROP TABLE IF EXISTS `dish_flavor`;
    CREATE TABLE `dish_flavor` (
    `id` bigint(20) NOT NULL COMMENT '主键',
    `dish_id` bigint(20) NOT NULL COMMENT '菜品',
    `name` varchar(64) COLLATE utf8_bin NOT NULL COMMENT '口味名称',
    `value` varchar(500) COLLATE utf8_bin DEFAULT NULL COMMENT '口味数据list',
    `create_time` datetime NOT NULL COMMENT '创建时间',
    `update_time` datetime NOT NULL COMMENT '更新时间',
    `create_user` bigint(20) NOT NULL COMMENT '创建人',
    `update_user` bigint(20) NOT NULL COMMENT '修改人',
    `is_deleted` int(11) NOT NULL DEFAULT '0' COMMENT '是否删除',
    PRIMARY KEY (`id`) USING BTREE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='菜品口味关系表';

    -- ----------------------------
    -- Records of dish_flavor
    -- ----------------------------
    INSERT INTO `dish_flavor` VALUES ('1397849417888346113', '1397849417854791681', '辣度', '[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]', '2021-05-27 09:37:27', '2021-05-27 09:37:27', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397849739297861633', '1397849739276890114', '忌口', '[\"不要葱\",\"不要蒜\",\"不要香菜\",\"不要辣\"]', '2021-05-27 09:38:43', '2021-05-27 09:38:43', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397849739323027458', '1397849739276890114', '辣度', '[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]', '2021-05-27 09:38:43', '2021-05-27 09:38:43', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397849936421761025', '1397849936404983809', '忌口', '[\"不要葱\",\"不要蒜\",\"不要香菜\",\"不要辣\"]', '2021-05-27 09:39:30', '2021-05-27 09:39:30', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397849936438538241', '1397849936404983809', '辣度', '[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]', '2021-05-27 09:39:30', '2021-05-27 09:39:30', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397850141015715841', '1397850140982161409', '忌口', '[\"不要葱\",\"不要蒜\",\"不要香菜\",\"不要辣\"]', '2021-05-27 09:40:19', '2021-05-27 09:40:19', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397850141040881665', '1397850140982161409', '辣度', '[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]', '2021-05-27 09:40:19', '2021-05-27 09:40:19', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397850392120307713', '1397850392090947585', '辣度', '[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]', '2021-05-27 09:41:19', '2021-05-27 09:41:19', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397850392137084929', '1397850392090947585', '辣度', '[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]', '2021-05-27 09:41:19', '2021-05-27 09:41:19', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397850630734262274', '1397850630700707841', '忌口', '[\"不要葱\",\"不要蒜\",\"不要香菜\",\"不要辣\"]', '2021-05-27 09:42:16', '2021-05-27 09:42:16', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397850630755233794', '1397850630700707841', '辣度', '[\"微辣\",\"中辣\",\"重辣\"]', '2021-05-27 09:42:16', '2021-05-27 09:42:16', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397850851274960898', '1397850851245600769', '忌口', '[\"不要蒜\",\"不要香菜\",\"不要辣\"]', '2021-05-27 09:43:08', '2021-05-27 09:43:08', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397850851283349505', '1397850851245600769', '辣度', '[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]', '2021-05-27 09:43:08', '2021-05-27 09:43:08', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397851099523231745', '1397851099502260226', '忌口', '[\"不要葱\",\"不要蒜\",\"不要香菜\",\"不要辣\"]', '2021-05-27 09:44:08', '2021-05-27 09:44:08', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397851099527426050', '1397851099502260226', '辣度', '[\"不辣\",\"微辣\",\"中辣\"]', '2021-05-27 09:44:08', '2021-05-27 09:44:08', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397851370483658754', '1397851370462687234', '温度', '[\"热饮\",\"常温\",\"去冰\",\"少冰\",\"多冰\"]', '2021-05-27 09:45:12', '2021-05-27 09:45:12', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397851370483658755', '1397851370462687234', '忌口', '[\"不要葱\",\"不要蒜\",\"不要香菜\",\"不要辣\"]', '2021-05-27 09:45:12', '2021-05-27 09:45:12', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397851370483658756', '1397851370462687234', '辣度', '[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]', '2021-05-27 09:45:12', '2021-05-27 09:45:12', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397851668283437058', '1397851668262465537', '温度', '[\"热饮\",\"常温\",\"去冰\",\"少冰\",\"多冰\"]', '2021-05-27 09:46:23', '2021-05-27 09:46:23', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397852391180120065', '1397852391150759938', '忌口', '[\"不要葱\",\"不要香菜\",\"不要辣\"]', '2021-05-27 09:49:16', '2021-05-27 09:49:16', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397852391196897281', '1397852391150759938', '辣度', '[\"不辣\",\"微辣\",\"重辣\"]', '2021-05-27 09:49:16', '2021-05-27 09:49:16', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397853183307984898', '1397853183287013378', '辣度', '[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]', '2021-05-27 09:52:24', '2021-05-27 09:52:24', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397853423486414850', '1397853423461249026', '辣度', '[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]', '2021-05-27 09:53:22', '2021-05-27 09:53:22', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397853709126905857', '1397853709101740034', '忌口', '[\"不要葱\",\"不要蒜\",\"不要香菜\",\"不要辣\"]', '2021-05-27 09:54:30', '2021-05-27 09:54:30', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397853890283089922', '1397853890262118402', '辣度', '[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]', '2021-05-27 09:55:13', '2021-05-27 09:55:13', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397854133632413697', '1397854133603053569', '温度', '[\"热饮\",\"常温\",\"去冰\",\"少冰\",\"多冰\"]', '2021-05-27 09:56:11', '2021-05-27 09:56:11', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397854652623007745', '1397854652581064706', '忌口', '[\"不要葱\",\"不要蒜\",\"不要香菜\",\"不要辣\"]', '2021-05-27 09:58:15', '2021-05-27 09:58:15', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397854652635590658', '1397854652581064706', '辣度', '[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]', '2021-05-27 09:58:15', '2021-05-27 09:58:15', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397854865735593986', '1397854865672679425', '辣度', '[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]', '2021-05-27 09:59:06', '2021-05-27 09:59:06', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397855742303186946', '1397855742273826817', '辣度', '[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]', '2021-05-27 10:02:35', '2021-05-27 10:02:35', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397855906497605633', '1397855906468245506', '忌口', '[\"不要葱\",\"不要蒜\",\"不要香菜\",\"不要辣\"]', '2021-05-27 10:03:14', '2021-05-27 10:03:14', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397856190573621250', '1397856190540066818', '辣度', '[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]', '2021-05-27 10:04:21', '2021-05-27 10:04:21', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397859056709316609', '1397859056684150785', '辣度', '[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]', '2021-05-27 10:15:45', '2021-05-27 10:15:45', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397859277837217794', '1397859277812051969', '辣度', '[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]', '2021-05-27 10:16:37', '2021-05-27 10:16:37', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397859487502086146', '1397859487476920321', '辣度', '[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]', '2021-05-27 10:17:27', '2021-05-27 10:17:27', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397859757061615618', '1397859757036449794', '甜味', '[\"无糖\",\"少糖\",\"半躺\",\"多糖\",\"全糖\"]', '2021-05-27 10:18:32', '2021-05-27 10:18:32', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397860242086735874', '1397860242057375745', '辣度', '[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]', '2021-05-27 10:20:27', '2021-05-27 10:20:27', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397860963918065665', '1397860963880316929', '辣度', '[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]', '2021-05-27 10:23:19', '2021-05-27 10:23:19', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397861135754506242', '1397861135733534722', '甜味', '[\"无糖\",\"少糖\",\"半躺\",\"多糖\",\"全糖\"]', '2021-05-27 10:24:00', '2021-05-27 10:24:00', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397861370035744769', '1397861370010578945', '辣度', '[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]', '2021-05-27 10:24:56', '2021-05-27 10:24:56', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397861683459305474', '1397861683434139649', '忌口', '[\"不要葱\",\"不要蒜\",\"不要香菜\",\"不要辣\"]', '2021-05-27 10:26:11', '2021-05-27 10:26:11', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397861898467717121', '1397861898438356993', '忌口', '[\"不要葱\",\"不要蒜\",\"不要香菜\",\"不要辣\"]', '2021-05-27 10:27:02', '2021-05-27 10:27:02', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397862198054268929', '1397862198033297410', '忌口', '[\"不要葱\",\"不要蒜\",\"不要香菜\",\"不要辣\"]', '2021-05-27 10:28:14', '2021-05-27 10:28:14', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1397862477835317250', '1397862477831122945', '辣度', '[\"不辣\",\"微辣\",\"中辣\"]', '2021-05-27 10:29:20', '2021-05-27 10:29:20', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1398089545865015297', '1398089545676271617', '温度', '[\"热饮\",\"常温\",\"去冰\",\"少冰\",\"多冰\"]', '2021-05-28 01:31:38', '2021-05-28 01:31:38', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1398089782323097601', '1398089782285348866', '辣度', '[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]', '2021-05-28 01:32:34', '2021-05-28 01:32:34', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1398090003262255106', '1398090003228700673', '忌口', '[\"不要葱\",\"不要蒜\",\"不要香菜\",\"不要辣\"]', '2021-05-28 01:33:27', '2021-05-28 01:33:27', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1398090264554811394', '1398090264517062657', '忌口', '[\"不要葱\",\"不要蒜\",\"不要香菜\",\"不要辣\"]', '2021-05-28 01:34:29', '2021-05-28 01:34:29', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1398090455399837698', '1398090455324340225', '辣度', '[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]', '2021-05-28 01:35:14', '2021-05-28 01:35:14', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1398090685449023490', '1398090685419663362', '温度', '[\"热饮\",\"常温\",\"去冰\",\"少冰\",\"多冰\"]', '2021-05-28 01:36:09', '2021-05-28 01:36:09', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1398090825358422017', '1398090825329061889', '忌口', '[\"不要葱\",\"不要蒜\",\"不要香菜\",\"不要辣\"]', '2021-05-28 01:36:43', '2021-05-28 01:36:43', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1398091007051476993', '1398091007017922561', '辣度', '[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]', '2021-05-28 01:37:26', '2021-05-28 01:37:26', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1398091296164851713', '1398091296131297281', '辣度', '[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]', '2021-05-28 01:38:35', '2021-05-28 01:38:35', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1398091546531246081', '1398091546480914433', '忌口', '[\"不要葱\",\"不要蒜\",\"不要香菜\",\"不要辣\"]', '2021-05-28 01:39:35', '2021-05-28 01:39:35', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1398091729809747969', '1398091729788776450', '辣度', '[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]', '2021-05-28 01:40:18', '2021-05-28 01:40:18', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1398091889499484161', '1398091889449152513', '辣度', '[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]', '2021-05-28 01:40:56', '2021-05-28 01:40:56', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1398092095179763713', '1398092095142014978', '辣度', '[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]', '2021-05-28 01:41:45', '2021-05-28 01:41:45', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1398092283877306370', '1398092283847946241', '辣度', '[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]', '2021-05-28 01:42:30', '2021-05-28 01:42:30', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1398094018939236354', '1398094018893099009', '辣度', '[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]', '2021-05-28 01:49:24', '2021-05-28 01:49:24', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1398094391494094850', '1398094391456346113', '辣度', '[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]', '2021-05-28 01:50:53', '2021-05-28 01:50:53', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1399574026165727233', '1399305325713600514', '辣度', '[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]', '2021-06-01 03:50:25', '2021-06-01 03:50:25', '1399309715396669441', '1399309715396669441', '0');
    INSERT INTO `dish_flavor` VALUES ('1413389540592263169', '1413384757047271425', '温度', '[\"常温\",\"冷藏\"]', '2021-07-12 09:09:16', '2021-07-12 09:09:16', '1', '1', '0');
    INSERT INTO `dish_flavor` VALUES ('1413389684020682754', '1413342036832100354', '温度', '[\"常温\",\"冷藏\"]', '2021-07-09 15:12:18', '2021-07-09 15:12:18', '1', '1', '0');

    -- ----------------------------
    -- Table structure for employee
    -- ----------------------------
    DROP TABLE IF EXISTS `employee`;
    CREATE TABLE `employee` (
    `id` bigint(20) NOT NULL 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(11) NOT NULL DEFAULT '1' COMMENT '状态 0:禁用,1:正常',
    `create_time` datetime NOT NULL COMMENT '创建时间',
    `update_time` datetime NOT NULL COMMENT '更新时间',
    `create_user` bigint(20) NOT NULL COMMENT '创建人',
    `update_user` bigint(20) NOT NULL COMMENT '修改人',
    PRIMARY KEY (`id`) USING BTREE,
    UNIQUE KEY `idx_username` (`username`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='员工信息';

    -- ----------------------------
    -- Records of employee
    -- ----------------------------
    INSERT INTO `employee` VALUES ('1', '管理员', 'admin', 'e10adc3949ba59abbe56e057f20f883e', '13812312312', '1', '110101199001010047', '1', '2021-05-06 17:20:07', '2021-05-10 02:24:09', '1', '1');

    -- ----------------------------
    -- Table structure for orders
    -- ----------------------------
    DROP TABLE IF EXISTS `orders`;
    CREATE TABLE `orders` (
    `id` bigint(20) NOT NULL COMMENT '主键',
    `number` varchar(50) COLLATE utf8_bin DEFAULT NULL COMMENT '订单号',
    `status` int(11) NOT NULL DEFAULT '1' COMMENT '订单状态 1待付款,2待派送,3已派送,4已完成,5已取消',
    `user_id` bigint(20) NOT NULL COMMENT '下单用户',
    `address_book_id` bigint(20) NOT NULL COMMENT '地址id',
    `order_time` datetime NOT NULL COMMENT '下单时间',
    `checkout_time` datetime NOT NULL COMMENT '结账时间',
    `pay_method` int(11) NOT NULL DEFAULT '1' COMMENT '支付方式 1微信,2支付宝',
    `amount` decimal(10,2) NOT NULL COMMENT '实收金额',
    `remark` varchar(100) COLLATE utf8_bin DEFAULT NULL COMMENT '备注',
    `phone` varchar(255) COLLATE utf8_bin DEFAULT NULL,
    `address` varchar(255) COLLATE utf8_bin DEFAULT NULL,
    `user_name` varchar(255) COLLATE utf8_bin DEFAULT NULL,
    `consignee` varchar(255) COLLATE utf8_bin DEFAULT NULL,
    PRIMARY KEY (`id`) USING BTREE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='订单表';

    -- ----------------------------
    -- Records of orders
    -- ----------------------------

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

    -- ----------------------------
    -- Records of order_detail
    -- ----------------------------

    -- ----------------------------
    -- Table structure for setmeal
    -- ----------------------------
    DROP TABLE IF EXISTS `setmeal`;
    CREATE TABLE `setmeal` (
    `id` bigint(20) NOT NULL COMMENT '主键',
    `category_id` bigint(20) NOT NULL COMMENT '菜品分类id',
    `name` varchar(64) COLLATE utf8_bin NOT NULL COMMENT '套餐名称',
    `price` decimal(10,2) NOT NULL COMMENT '套餐价格',
    `status` int(11) DEFAULT NULL COMMENT '状态 0:停用 1:启用',
    `code` varchar(32) COLLATE utf8_bin DEFAULT NULL COMMENT '编码',
    `description` varchar(512) COLLATE utf8_bin DEFAULT NULL COMMENT '描述信息',
    `image` varchar(255) COLLATE utf8_bin DEFAULT NULL COMMENT '图片',
    `create_time` datetime NOT NULL COMMENT '创建时间',
    `update_time` datetime NOT NULL COMMENT '更新时间',
    `create_user` bigint(20) NOT NULL COMMENT '创建人',
    `update_user` bigint(20) NOT NULL COMMENT '修改人',
    `is_deleted` int(11) NOT NULL DEFAULT '0' COMMENT '是否删除',
    PRIMARY KEY (`id`) USING BTREE,
    UNIQUE KEY `idx_setmeal_name` (`name`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='套餐';

    -- ----------------------------
    -- Records of setmeal
    -- ----------------------------
    INSERT INTO `setmeal` VALUES ('1415580119015145474', '1413386191767674881', '儿童套餐A计划', '4000.00', '1', '', '', '61d20592-b37f-4d72-a864-07ad5bb8f3bb.jpg', '2021-07-15 15:52:55', '2021-07-15 15:52:55', '1415576781934608386', '1415576781934608386', '0');

    -- ----------------------------
    -- Table structure for setmeal_dish
    -- ----------------------------
    DROP TABLE IF EXISTS `setmeal_dish`;
    CREATE TABLE `setmeal_dish` (
    `id` bigint(20) NOT NULL COMMENT '主键',
    `setmeal_id` varchar(32) COLLATE utf8_bin NOT NULL COMMENT '套餐id ',
    `dish_id` varchar(32) COLLATE utf8_bin NOT NULL COMMENT '菜品id',
    `name` varchar(32) COLLATE utf8_bin DEFAULT NULL COMMENT '菜品名称 (冗余字段)',
    `price` decimal(10,2) DEFAULT NULL COMMENT '菜品原价(冗余字段)',
    `copies` int(11) NOT NULL COMMENT '份数',
    `sort` int(11) NOT NULL DEFAULT '0' COMMENT '排序',
    `create_time` datetime NOT NULL COMMENT '创建时间',
    `update_time` datetime NOT NULL COMMENT '更新时间',
    `create_user` bigint(20) NOT NULL COMMENT '创建人',
    `update_user` bigint(20) NOT NULL COMMENT '修改人',
    `is_deleted` int(11) NOT NULL DEFAULT '0' COMMENT '是否删除',
    PRIMARY KEY (`id`) USING BTREE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='套餐菜品关系';

    -- ----------------------------
    -- Records of setmeal_dish
    -- ----------------------------
    INSERT INTO `setmeal_dish` VALUES ('1415580119052894209', '1415580119015145474', '1397862198033297410', '老火靓汤', '49800.00', '1', '0', '2021-07-15 15:52:55', '2021-07-15 15:52:55', '1415576781934608386', '1415576781934608386', '0');
    INSERT INTO `setmeal_dish` VALUES ('1415580119061282817', '1415580119015145474', '1413342036832100354', '北冰洋', '500.00', '1', '0', '2021-07-15 15:52:55', '2021-07-15 15:52:55', '1415576781934608386', '1415576781934608386', '0');
    INSERT INTO `setmeal_dish` VALUES ('1415580119069671426', '1415580119015145474', '1413385247889891330', '米饭', '200.00', '1', '0', '2021-07-15 15:52:55', '2021-07-15 15:52:55', '1415576781934608386', '1415576781934608386', '0');

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

    -- ----------------------------
    -- Records of shopping_cart
    -- ----------------------------

    -- ----------------------------
    -- Table structure for user
    -- ----------------------------
    DROP TABLE IF EXISTS `user`;
    CREATE TABLE `user` (
    `id` bigint(20) NOT NULL COMMENT '主键',
    `name` varchar(50) COLLATE utf8_bin DEFAULT NULL COMMENT '姓名',
    `phone` varchar(100) COLLATE utf8_bin NOT 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 '头像',
    `status` int(11) DEFAULT '0' COMMENT '状态 0:禁用,1:正常',
    PRIMARY KEY (`id`) USING BTREE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='用户信息';
  • 创建一个SpringBoot项目,勾选上SpringWeb,Lombok,MySQL,然后在pom.xml中导入druidMyBatisPlus的依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
    <version>3.5.11</version>
    </dependency>
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.6</version>
    </dependency>

导入黑马提供的前端资源,放在resources/static目录下
如果直接放在resources目录下,会无法查找到,此时需要配置一下资源映射

注: 要么放到static目录下要么配置资源映射,执行其一即可,不可以同时配置(从后面的结果来看,此处推荐配置资源映射)

1
2
3
4
5
6
7
8
9
10
11
@Configuration
@Slf4j
public class WebMvcConfig extends WebMvcConfigurationSupport {
// 注:如果放到static目录下则不需要该步骤
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
log.info("开始进行静态资源映射...");
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
}
}

然后配置一下端口号和数据库连接四要素就能访问静态页面了,这里别忘了更改数据库的名称和密码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
server:
port: 8080
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/reggie?serverTimezone=Asia
username: root
password: password
type: com.alibaba.druid.pool.DruidDataSource


mybatis-plus:
configuration:
#在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: ASSIGN_ID


打开浏览器,访问 http://localhost:8080/backend/page/login/login.html 就可以看到登录页面了,不过此时还无法登录

后台系统登录功能

数据库的数据和简单的SQL语句都不用我们管,数据已经提供好了,简单的SQL语句直接使用MyBatisPlus,所以需要自己编写的SQL语句非常少。

在准备工作已经将所有前端页面放在了resources/static目录下,可以直接在resource/static/backend/page/login.html看到登录页面的代码

创建实体类

登录功能比较简单,只需要输入账号密码后点击登录就应当能登陆成功


此时使用驼峰命名法的属性名,可以通过在配置文件中设置的map-underscore-to-camel-case属性,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射(现在MybatisPlus默认开启)
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
@Data
public class Employee implements Serializable {

private static final long serialVersionUID = 1L;

private Long id;

private String username;

private String name;

private String password;

private String phone;

private String sex;

private String idNumber;

private Integer status;

private LocalDateTime createTime;

private LocalDateTime updateTime;

//这两个注解是MyBatisPlus提供的,用于实现实体类字段的自动填充(Auto-Fill)功能。
@TableField(fill = FieldFill.INSERT)
private Long createUser;

@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;
}

创建Mapper、Service、Controller

直接继承BaseMapper就行了,别忘了@Mapper注解

1
2
3
4
@Mapper
public interface EmployeeMapper extends BaseMapper<Employee> {

}

1
2
public interface EmployeeService extends IService<Employee> {
}

继承ServiceImpl,实现EmployeeService接口,别忘了@Service注解

1
2
3
@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements EmployeeService {
}

1
2
3
4
5
6
7
8
@Slf4j
@RestController
@RequestMapping("/employee")
public class EmployeeController {

@Autowired
private EmployeeService employeeService;
}

编写Controller

登录处理逻辑:

  1. 接受页面提交的密码并对其进行加密处理
  2. 根据页面提交的用户名查询数据库
  3. 如果没有查询到就返回登录失败
  4. 查询到,则对比密码是否一致,不一致返回失败
  5. 查看员工状态,如果状态为已禁用则返回已禁用的结果
  6. 登录成功,将员工Id存入Session并返回登陆成功

给EmployeeController层添加login方法

  • @RequestBody 用于接收前端传递给后端的json字符串(请求体中的数据)
  • HttpServletRequest 作用:如果登录成功,将员工对应的id存到session一份,这样浏览器可以获得登录员工的数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//通过表单提交,所以发送post请求
@PostMapping("/login")
public Result<Employee> login(HttpServletRequest request, @RequestBody Employee employee) {
String password = employee.getPassword();
password = DigestUtils.md5DigestAsHex(password.getBytes());
//这部分就是MP
LambdaQueryWrapper<Employee> lqw = new LambdaQueryWrapper<>();
lqw.eq(Employee::getUsername, employee.getUsername());
Employee emp = employeeService.getOne(lqw);
if (emp == null) {
return Result.error("登陆失败");
}
if (!emp.getPassword().equals(password)) {
return Result.error("登录失败");
}
if (emp.getStatus() == 0) {
return Result.error("该用户已被禁用");
}
//存个Session,只存个id就行了
request.getSession().setAttribute("employee",emp.getId());
return Result.success(emp);
}

统一结果封装

为了方便前端接收数据,编写一个Result结果类,统一返回数据的格式

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
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result<T> {
private Integer code; // 编码:1成功。0和其他数字失败
private String errMsg; // 错误信息
private T data; // 数据
private Map map = new HashMap(); // 动态数据

public static <T> Result<T> success(T data) {
Result<T> r = new Result<>();
r.code = 1; //成功状态码
r.data = data;
return r;
}

public static <T> Result<T> error(String errMsg) {
Result<T> r = new Result<>();
r.errMsg = errMsg; //设置错误信息
r.code = 0; //默认失败状态码,后期我们可以根据自己的需求来设置其他状态码
return r;
}

public Result<T> add(String msg, String value) {
this.map.put(msg, value);
return this;
}
}

登出功能

登出处理逻辑:

  1. 清理Session中保存的当前登录员工的id
  2. 返回结果

在Controller层添加Logout方法

1
2
3
4
5
6
7
8
9
10
/**
* 登出功能
* @param request
* @return
*/
@PostMapping("/logout")
public Result<String> logout(HttpServletRequest request) {
request.getSession().removeAttribute("employee");
return Result.success("退出成功");
}

登出的功能在index页面,右上角有一个按钮,点击就能登出

1
2
3
4
5
6
<div class="right-menu">
<!--这里动态的显示登录的用户名-->
<div class="avatar-wrapper">{{ userInfo.name }}</div>
<!--这里就是登出的按钮-->
<img src="images/icons/btn_close@2x.png" class="outLogin" alt="退出" @click="logout" />
</div>

对应的函数如下,这里的logoutApi用来发送post请求

1
2
3
4
5
6
7
8
logout() {
logoutApi().then((res)=>{
if(res.code === 1){
localStorage.removeItem('userInfo')
window.location.href = '/backend/page/login/login.html'
}
})
}
1
2
3
4
5
6
function logoutApi(){
return $axios({
'url': '/employee/logout',
'method': 'post',
})
}

登录测试

数据库中目前只有一条用户信息,username为admin,password为123456(已经经过MD5加密了)
现在访问 http://localhost/backend/page/login/login.html
当输入正确的用户名和密码时,可以跳转至http://localhost/backend/index.html页面
当输入错误的用户名或密码,会显示错误信息


对应的HTML代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
methods: {
async handleLogin() {
this.$refs.loginForm.validate(async (valid) => {
if (valid) {
this.loading = true
let res = await loginApi(this.loginForm)
if (String(res.code) === '1') {
localStorage.setItem('userInfo',JSON.stringify(res.data))
window.location.href= '/backend/index.html'
} else {
this.$message.error(res.msg)
this.loading = false
}
}
})
}
}

对应的JS代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function loginApi(data) {
return $axios({
'url': '/employee/login',
'method': 'post',
data
})
}

function logoutApi(){
return $axios({
'url': '/employee/logout',
'method': 'post',
})
}

完善登录功能

使用过滤器来拦截请求

实现步骤:

  1. 创建自定义过滤器LoginCheckFilter
  2. 在启动类上添加@ServletComponentScan注解,添加该注解才会对WebServlet注解的类进行扫描
  3. 完善过滤器的处理逻辑
  • 创建过滤器,设置所有请求全部拦截
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Slf4j
    @WebFilter(filterName = "loginCheckFilter", urlPatterns = "/*")
    public class LoginCheckFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    HttpServletRequest request = (HttpServletRequest) servletRequest;
    HttpServletResponse response = (HttpServletResponse) servletResponse;
    //将拦截到的URI输出到日志,这是新的输出方式,{}是占位符,将自动填充request.getRequestURI()的内容
    log.info("拦截到的URI:{}", request.getRequestURI());
    filterChain.doFilter(request, response);
    }
    }

并在启动类上加入注解@ServletComponentScan

1
2
3
4
5
6
7
@SpringBootApplication
@ServletComponentScan
public class ReggieApplication {
public static void main(String[] args) {
SpringApplication.run(ReggieApplication.class, args);
}
}

启动服务器,访问index页面,查看日志,现在可以拦截到URI了

2022-09-29 18:05:53.190 …… : 拦截到的URI:/backend/index.html
2022-09-29 18:06:01.174 …… : 拦截到的URI:/employee/page

编写Filter处理逻辑

过滤器具体处理逻辑:

  1. 获取本次请求的URI
  2. 判断该请求是否需要处理
  3. 不需要处理则直接放行
  4. 判断登录状态,如果已登录则放行
  5. 未登录返回未登录结果
  1. 获取本次请求的URI

    1
    2
    3
    4
    5
    6
    7
    8
    9
    //获取本次请求的URI
    String uri = request.getRequestURI();
    //定义不需要被拦截的请求
    String[] urls = new String[]{
    "/employee/login.html",
    "/employee/logout.html",
    "/backend/**",
    "/front/**"
    };
  2. 判断本次请求是否需要处理
    使用Spring 的PathMatcher路径匹配器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();

    private boolean check(String[] urls, String uri) {
    for (String url : urls) {
    boolean match = PATH_MATCHER.match(url, uri);
    if (match)
    return true;
    }
    return false;
    }
  1. 如果不需要处理,则直接放行
    1
    2
    3
    4
    if (check) {
    filterChain.doFilter(request, response);
    return;
    }
  1. 判断登录状态,如果已登录,则直接放行

    1
    2
    3
    4
    5
    //我们当初存的session是employee,所以这里就拿它判断
    if (request.getSession().getAttribute("employee") != null) {
    filterChain.doFilter(request,response);
    return;
    }
  2. 如果未登录则返回未登录结果

1
2
//未登录状态为什么要返回一个error呢?而且msg为NOTLOGIN
response.getWriter().write(JSON.toJSONString(Result.error("NOTLOGIN")));

从JS代码可以看出,当符合未登录状态的条件时,会自动重定向到登录页面

1
2
3
4
5
6
7
8
9
10
// 响应拦截器
service.interceptors.response.use(res => {
if (res.data.code === 0 && res.data.msg === 'NOTLOGIN') {// 返回登录页面
console.log('---/backend/page/login/login.html---')
localStorage.removeItem('userInfo')
window.top.location.href = '/backend/page/login/login.html'
} else {
return res.data
}
}

这里需要导入fastjson的坐标

1
2
3
4
5
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.59</version>
</dependency>

  1. 完整代码

    完整步骤就是上面的五步再使用日志来输出一些东西,方便我们来调试代码

    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
    @WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
    @Slf4j
    public class LoginCheckFilter implements Filter {

    //路径匹配
    public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

    //强转
    HttpServletRequest request = (HttpServletRequest) servletRequest;
    HttpServletResponse response = (HttpServletResponse) servletResponse;

    //1.获取本次请求的URI
    String requestURI = request.getRequestURI();
    log.info("拦截到请求:{}",requestURI);

    //定义不需要处理的请求
    String[] urls = new String[]{
    "/employee/login",
    "/employee/logout",
    "/backend/**",
    "/front/**"
    };

    //2.判断本次请求是否需要处理
    boolean check = check(urls, requestURI);

    //3.如果不需要处理,则直接放行
    if (check) {
    log.info("本次请求:{},不需要处理",requestURI);
    filterChain.doFilter(request,response);
    return;
    }

    //4.判断登录状态,如果已登录,则直接放行
    if (request.getSession().getAttribute("employee") != null) {
    log.info("用户已登录,id为{}",request.getSession().getAttribute("employee"));
    filterChain.doFilter(request,response);
    return;
    }

    //5.如果未登录则返回未登录结果,通过输出流方式向客户端页面响应数据
    log.info("用户未登录");
    log.info("用户id{}",request.getSession().getAttribute("employee"));
    response.getWriter().write(JSON.toJSONString(Result.error("NOTLOGIN")));

    }

    public boolean check(String[] urls, String requestURI){
    for (String url : urls) {
    boolean match = PATH_MATCHER.match(url, requestURI);
    if (match) {
    //匹配
    return true;
    }
    }
    //不匹配
    return false;
    }
    }

测试登录

当我们直接访问 http://localhost/backend/index.html 时,可以发现已经自动跳转到登录页面并输出以下日志:

添加员工

流程分析

执行流程:

  1. 页面发送ajax请求,将新增员工页面中输入的数据以json的形式提交到服务端
  2. 服务端Controller接收页面提交的数据并调用Service将数据进行保存
  3. Service调用Mapper操作数据库,保存数据

简单看一下前端的代码,当点击添加员工按钮时,就会调用addEmployee方法,跳转到发送post请求,此时的界面信息,数据模型绑定的是ruleForm,通过双向绑定来接受用户的信息,输入信息后保存并添加按钮绑定了submitForm函数,调用相关方法并返回操作信息

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
67
68
69
70
71
72
73
<el-form
ref="ruleForm"
:model="ruleForm"
:rules="rules"
:inline="false"
label-width="180px"
class="demo-ruleForm"
>
<el-form-item label="账号:" prop="username">
<el-input v-model="ruleForm.username" placeholder="请输入账号" maxlength="20"/>
</el-form-item>
<el-form-item
label="员工姓名:"
prop="name"
>
<el-input
v-model="ruleForm.name"
placeholder="请输入员工姓名"
maxlength="20"
/>
</el-form-item>

<el-form-item
label="手机号:"
prop="phone"
>
<el-input
v-model="ruleForm.phone"
placeholder="请输入手机号"
maxlength="20"
/>
</el-form-item>
<el-form-item
label="性别:"
prop="sex"
>
<el-radio-group v-model="ruleForm.sex">
<el-radio label="男"></el-radio>
<el-radio label="女"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
label="身份证号:"
prop="idNumber"
>
<el-input
v-model="ruleForm.idNumber"
placeholder="请输入身份证号"
maxlength="20"
/>
</el-form-item>
<div class="subBox address">
<el-form-item>
<el-button @click="goBack()">
取消
</el-button>
<el-button
type="primary"
@click="submitForm('ruleForm', false)"
>
保存
</el-button>
<el-button
v-if="actionType == 'add'"
type="primary"
class="continue"
@click="submitForm('ruleForm', true)"
>
保存并继续添加
</el-button>
</el-form-item>
</div>
</el-form>
1
2
3
4
5
6
7
ruleForm : {
'name': '',
'phone': '',
'sex': '男',
'idNumber': '',
username: ''
}
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
submitForm (formName, st) {
this.$refs[formName].validate((valid) => {
if (valid) {
if (this.actionType === 'add') {
const params = {
...this.ruleForm,
sex: this.ruleForm.sex === '女' ? '0' : '1'
}
addEmployee(params).then(res => {
if (res.code === 1) {
this.$message.success('员工添加成功!')
if (!st) {
this.goBack()
} else {
this.ruleForm = {
username: '',
'name': '',
'phone': '',
// 'password': '',
// 'rePassword': '',/
'sex': '男',
'idNumber': ''
}
}
} else {
this.$message.error(res.msg || '操作失败')
}
}).catch(err => {
this.$message.error('请求出错了:' + err)
})
} else {
const params = {
...this.ruleForm,
sex: this.ruleForm.sex === '女' ? '0' : '1'
}
editEmployee(params).then(res => {
if (res.code === 1) {
this.$message.success('员工信息修改成功!')
this.goBack()
} else {
this.$message.error(res.msg || '操作失败')
}
}).catch(err => {
this.$message.error('请求出错了:' + err)
})
}
} else {
console.log('error submit!!')
return false
}
})
}
1
2
3
4
5
6
7
8
// 新增---添加员工
function addEmployee (params) {
return $axios({
url: '/employee',
method: 'post',
data: { ...params }
})
}


接下来来完善后端方法,先通过日志来测试能否接收到提交的员工信息

1
2
3
4
5
@PostMapping
public Result<String> save(@RequestBody Employee employee){
log.info("新增的员工信息:{}",employee.toString());
return null;
}

  • 启动服务器后,按照要求填写员工信息
  • 可以看到能接受到员工的相关信息,但有一些信息是null

    新增的员工信息:新增的员工信息:Employee(id=null, username=111, name=111, password=null, phone=15845678912, sex=1, idNumber=371312222222222222, status=null, createTime=null, updateTime=null, createUser=null, updateUser=null)

分析员工信息:

  • id:可以通过雪花算法或自动递增来自动生成
  • password:可以设置为默认值123456,但是需要进行MD5加密后存储(数据库中应当设置为加密后的密码,因为在登录时对比的就是加密后的密码)
  • status:设定员工的状态,1表示启用,0表示禁用,设置默认值为1
  • createTime:创建时间,这个可以指定当前系统时间
  • updateTime:作用同上
  • createUser:保存创建员工信息的人的名称,以免随意创建员工账号
  • updateUser:作用同上

具体实现

综上所述,我们设置创建时间和更新时间,创建人ID和修改人ID,其他采用默认值即可,但是密码需要将默认值加密后再存储到数据库中,所以也需要手动编写代码添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@PostMapping
public Result<String> save(HttpServletRequest request, @RequestBody Employee employee) {
log.info("新增的员工信息:{}", employee.toString());
//设置默认密码为123456,并采用MD5加密
employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));
//设置createTime和updateTime
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());
//根据session来获取创建人的id
Long empId = (Long) request.getSession().getAttribute("employee");
//并设置
employee.setCreateUser(empId);
employee.setUpdateUser(empId);
//存入数据库
employeeService.save(employee);
return Result.success("添加员工成功");
}

现在添加员工的功能大致完成,但在网页中依旧会返回错误,因为分页查询的操作还没有添加,此时可以在数据库看到员工信息已经被添加上去了。

完善全局异常处理器并测试

需求:因为username是唯一的,所以在添加员工时,如果输入的username已经存在,则会报错,所以需要对这种异常情况进行处理

解决方案:

  1. 在Controller层中添加try、catch进行异常捕获
  2. 使用异常处理器进行全局异常捕获

    推荐使用异常处理器来进行异常捕获,因为随着程序开发,功能会越来越多,可能出现异常的情况也越多,如果使用try、catch来捕获异常,可能会频繁书写重复的语句。

com.blog.common包下创建一个全局异常处理类GlobalExceptionHandler,并添加ExceptionHandler方法用来捕获异常,并返回结果,在方法上添加@ExceptionHandler来表示该方法专门处理该种异常

1
2
3
4
5
6
7
8
9
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public Result<String> exceptionHandler(SQLIntegrityConstraintViolationException exception) {
log.error(exception.getMessage());
return Result.error("未知错误");
}
}

先用日志输出一下看看能不能正常运行,这也是代码开发的一个好习惯
再次添加重复用户名的员工信息,发现这次会报错就会出现未知错误的弹窗了
控制台日志输出的错误信息为Duplicate entry 'Kyle' for key 'employee.idx_username'

  • 只提示未知错误过于笼统,应当提示用户准确的错误信息才是好的程序,所以需要对错误信息进行判断,根据返回的错误信息,再对错误信息中动态的部分进行切面,利用字符串拼接来告诉用户错误的原因
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public Result<String> exceptionHandler(SQLIntegrityConstraintViolationException exception) {
log.error(exception.getMessage());
//如果包含Duplicate entry,则说明有条目重复
if (exception.getMessage().contains("Duplicate entry")) {
//对字符串切片
String[] split = exception.getMessage().split(" ");
//字符串格式是固定的,所以这个位置必然是username
String username = split[2];
//拼串作为错误信息返回
return Result.error("用户名" + username + "已存在");
}
return Result.error("未知错误");
}
}

接下来重启服务器,测试添加功能,输入已经存在的username,此时的报错信息就更加准确了

员工信息分页查询

分页查询执行过程

  1. 页面发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务
  2. 服务端Controller接收页面提交的数据并调用Service查询数据
  3. Service调用Mapper操作数据库,查询分页数据
  4. Controller将查询到的分页数据响应给页面
  5. 页面接收到分页数据并通过ElementUI的Table组件展示到页面上

配置MyBatisPlus分页插件

温馨提示:

  • 这里如果使用的Mybatis-Plus版本是3.5.9+,那么想使用分页功能需要额外添加一个依赖管理和一个依赖,官方说明
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <dependencyManagement>
    <dependencies>
    <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-bom</artifactId>
    <version>3.5.11</version>
    <type>pom</type>
    <scope>import</scope>
    </dependency>
    </dependencies>
    </dependencyManagement>

    <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-jsqlparser</artifactId>
    </dependency>

新建com.blog.config包,并在其中新建MybatisPlusConfig

1
2
3
4
5
6
7
8
9
10
@Configuration
public class MybatisPlusConfig {

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}

前端代码分析

为什么每次登录后都会先报一个错误?因为当我们访问页面时,页面会自动执行一个分页查询操作,

使用GET发送的请求,请求参数在URL中

分析前端代码:

这段代码位于resource/backend/page/member/list.html
这段生命周期函数会在页面初始化时调用来构造数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async init () {
const params = {
page: this.page,
pageSize: this.pageSize,
name: this.input ? this.input : undefined
}
await getMemberList(params).then(res => {
if (String(res.code) === '1') {
this.tableData = res.data.records || []
this.counts = res.data.total
}
}).catch(err => {
this.$message.error('请求出错了:' + err)
})
}S

这里的rows对应tableData,totalCount对应counts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//分页查询的JavaBean
public class PageBean<T> {
// 总记录数
private int totalCount;
// 当前页数据
private List<T> rows;


public int getTotalCount() {
return totalCount;
}

public void setTotalCount(int totalCount) {
this.totalCount = totalCount;
}

public List<T> getRows() {
return rows;
}

public void setRows(List<T> rows) {
this.rows = rows;
}
}

发送的是GET请求,请求路径为/employee/page,请求参数为前面初始化的params对象

1
2
3
4
5
6
7
function getMemberList (params) {
return $axios({
url: '/employee/page',
method: 'get',
params
})
}

这是前端提供的拦截器,因为前面的params是json格式的,通过该拦截器将请求参数使用拼串的方式拼接到URL上

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
// request拦截器
service.interceptors.request.use(config => {
// 是否需要设置 token
// const isToken = (config.headers || {}).isToken === false
// if (getToken() && !isToken) {
// config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
// }
// get请求映射params参数
if (config.method === 'get' && config.params) {
let url = config.url + '?';
for (const propName of Object.keys(config.params)) {
const value = config.params[propName];
var part = encodeURIComponent(propName) + "=";
if (value !== null && typeof(value) !== "undefined") {
if (typeof value === 'object') {
for (const key of Object.keys(value)) {
let params = propName + '[' + key + ']';
var subPart = encodeURIComponent(params) + "=";
url += subPart + encodeURIComponent(value[key]) + "&";
}
} else {
url += part + encodeURIComponent(value) + "&";
}
}
}
url = url.slice(0, -1);
config.params = {};
config.url = url;
}
return config
}, error => {
console.log(error)
Promise.reject(error)
})

编写具体的业务逻辑

先通过日志来检查是否能正常接收到数据,因为这里的需求是服务端Controller接收页面提交的数据并调用Service查询数据,所以返回值中应当包含当前页数,总页数等数据,因此返回值的泛型不是Employee而是Page

1
2
3
4
5
@GetMapping("/page")
public Result<Page> page(int page, int pageSize, String name) {
log.info("page={},pageSize={},name={}", page, pageSize, name);
return null;
}

此时在搜索框输入123,发现日志输出如下,符合预期

: page=1,pageSize=10,name=123

继续完善业务逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@GetMapping("/page")
public Result<Page> page(int page, int pageSize, String name) {
log.info("page={},pageSize={},name={}", page, pageSize, name);
//构造分页构造器
Page<Employee> pageConstructor = new Page<>(page, pageSize);
//构造条件构造器,动态封装查询条件
LambdaQueryWrapper<Employee> wrapper = new LambdaQueryWrapper<>();
//添加过滤条件(当我们没有输入name时,就相当于查询所有了)
wrapper.like(!(name == null || "".equals(name)), Employee::getName, name);
//并对查询的结果进行降序排序,根据更新时间
wrapper.orderByDesc(Employee::getUpdateTime);
//执行查询,查询之后不需要返回,他会直接将查询结果封装到pageConstructor对象中
employeeService.page(pageConstructor, wrapper);
return Result.success(pageConstructor);
}

此时页面的数据就可以正常展示了,还支持根据名称模糊查询

补充说明

  • 为什么后端传给页面的status数据为Integer类型,到页面展示效果的时候显示的是已禁用或者正常?

    • 看一下源码就知道了

      三目运算符+插值表达式

      1
      2
      3
      4
      5
      <el-table-column label="账号状态">
      <template slot-scope="scope">
      {{ String(scope.row.status) === '0' ? '已禁用' : '正常' }}
      </template>
      </el-table-column>

启用/禁用员工账号

需求分析

  1. 在员工管理列表页面,可以对某个员工账号进行启用或者禁用操作。账号状态为禁用的员工不能登录系统,启用后的员工可以正常登录。
  2. 需要注意,只有管理员(admin用户)可以对其他普通用户进行启用、禁用操作,所以普通用户登录系统后启用、禁用按钮不显示。
  3. 管理员admin登录系统可以对所有员工账号进行启用、禁用操作。
  4. 如果某个员工账号状态为正常,则按钮显示为“禁用”,如果员工账号状态为已禁用,则按钮显示为“启用”

动态按钮显示分析

怎么才能做到:只有当登录的是管理员账号时,才能看到启用/禁用按钮呢?

  • 当我们加载完页面的时候,获取一下当前登录账号的用户名,也就是username
1
2
3
4
created() {
this.init()
this.user = JSON.parse(localStorage.getItem('userInfo')).username
}
  • 随后判断一下这个用户名是不是admin,如果是的话就显示启用/禁用,否则不显示,通过v-if来判断

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <el-button
    type="text"
    size="small"
    class="delBut non"
    @click="statusHandle(scope.row)"
    v-if="user === 'admin'"
    >
    {{ scope.row.status == '1' ? '禁用' : '启用' }}
    </el-button>

Ajax请求发送过程

发送过程:

  1. 点击禁用/启用后,页面发送ajax请求,将参数(id、status)提交到服务端
  2. 服务端Controller接收页面提交的数据并调用Service更新数据
  3. Service调用Mapper操作数据库
  • 前端代码:

    在按钮上绑定了statusHandle(scope.row)方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <el-button
    type="text"
    size="small"
    class="delBut non"
    @click="statusHandle(scope.row)"
    v-if="user === 'admin'"
    >
    {{ scope.row.status == '1' ? '禁用' : '启用' }}
    </el-button>

    该方法先获得当前行的id与status,接着弹出提示框确认是否要修改状态,确认后调用enableOrDisableEmployee将其状态取反

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
     //状态修改
    statusHandle (row) {
    this.id = row.id
    this.status = row.status
    this.$confirm('确认调整该账号的状态?', '提示', {
    'confirmButtonText': '确定',
    'cancelButtonText': '取消',
    'type': 'warning'
    }).then(() => {
    enableOrDisableEmployee({ 'id': this.id, 'status': !this.status ? 1 : 0 }).then(res => {
    console.log('enableOrDisableEmployee',res)
    if (String(res.code) === '1') {
    this.$message.success('账号状态更改成功!')
    this.handleQuery()
    }
    }).catch(err => {
    this.$message.error('请求出错了:' + err)
    })
    })
    }

    1
    2
    3
    4
    5
    6
    7
    8
    // 修改---启用禁用接口
    function enableOrDisableEmployee (params) {
    return $axios({
    url: '/employee',
    method: 'put',
    data: { ...params }
    })
    }
  • 后端代码分析
    启用、禁用员工账号,本质上是更新操作,也就是对status状态字段进行修改
    养成先用日志测试的习惯
    1
    2
    3
    4
    5
    @PutMapping
    public Result<String> update(@RequestBody Employee employee) {
    log.info(employee.toString());
    return null;
    }
  • 通过日志可以看到能够正常接收到employee对象数据,接着完善update方法,在更新用户信息时自动更新修改时间和修改用户
1
2
3
4
5
6
7
8
9
10
@PutMapping
public Result<String> update(@RequestBody Employee employee, HttpServletRequest request) {
log.info(employee.toString());
Long id = (Long) request.getSession().getAttribute("employee");
employee.setUpdateUser(id);
employee.setUpdateTime(LocalDateTime.now());
employeeService.updateById(employee);
log.info("员工信息修改成功");
return Result.success("员工信息修改成功");
}
  • 通过日志可以看到更新语句输出了,但没有输出更新语句,数据库中的status字段也没有被修改

配置消息转换器

更新失败的原因是JS对Long型数据进行处理时丢失了精度(前端接受数据类型,超过16位会精度损失),导致提交的id和数据库中的id不一致,导致更新失效,可以通过配置消息转换器将Long型数据转为String型数据,再进行数据库更新

具体实现步骤:

  1. 提供对象转换器JacksonObjectMapper,基于Jackson进行对象到Json数据的转换(资料中有,直接复制)
  2. 在WebMvcConfig配置类中扩展SpringMvc的消息转换器,在此消息转换器中使用提供的对象转换器进行Java对象到Json数据的转换

直接复制下面的对象转换器放到common包下

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

/**
* 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
* 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
* 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
*/
public class JacksonObjectMapper extends ObjectMapper {

public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";

public JacksonObjectMapper() {
super();
//收到未知属性时不报异常
this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);

//反序列化时,属性不存在的兼容处理
this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);


SimpleModule simpleModule = new SimpleModule()
.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))

.addSerializer(BigInteger.class, ToStringSerializer.instance)
.addSerializer(Long.class, ToStringSerializer.instance)
.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));

//注册功能模块 例如,可以添加自定义序列化器和反序列化器
this.registerModule(simpleModule);
}
}

扩展Mvc框架的消息转换器

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@Slf4j
public class WebMvcConfig extends WebMvcConfigurationSupport {
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
//设置对象转化器,底层使用jackson将java对象转为json
messageConverter.setObjectMapper(new JacksonObjectMapper());
//将上面的消息转换器对象追加到mvc框架的转换器集合当中(index设置为0,表示设置在第一个位置,避免被其它转换器接收,从而达不到想要的功能)
converters.add(0, messageConverter);
}
}

这里如果前面在一开始没有配置资源映射器,则需要添加,并将前端和后端的静态文件直接放在resource目录下,否则会报以下错误:

再次测试

再次点击禁用按钮,此时数据库中的status字段数据发生了改变,且页面上也显示已禁用,再次点击启用,也能正常操作

编辑员工信息

流程分析

在开发代码之前先梳理一下整个操作流程与对应程序的执行顺序:

  1. 点击编辑按钮时,页面将跳转到add.html,并在url中携带参数员工id
  2. add.html页面中获取url中的参数员工id
  3. 发送ajax请求,请求服务端,同时提交员工id参数
  4. 服务端接受请求,并根据员工id查询员工信息,并将员工信息以json形式响应给页面
  5. 页面接收服务端响应的json数据,并通过Vue的双向绑定进行员工信息回显
  6. 点击保存按钮,发送ajax请求,将页面中的员工信息以json形式提交给服务端
  7. 服务端接受员工信息,并进行处理,完成后给页面响应
  8. 页面接收到服务端响应信息后进行相应处理

具体实现

add.html是添加员工和修改员工共用的页面

  1. 点击编辑按钮时,页面将跳转到add.html,并在url中携带参数员工id,编辑按钮绑定的点击事件为addMemberHandle(scope.row.id)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <el-button
    type="text"
    size="small"
    class="blueBug"
    @click="addMemberHandle(scope.row.id)"
    :class="{notAdmin:user !== 'admin'}"
    >
    编辑
    </el-button>
  2. add.html页面中获取url中的参数员工id

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    addMemberHandle (st) {
    if (st === 'add'){
    window.parent.menuHandle({
    id: '2',
    url: '/backend/page/member/add.html',
    name: '添加员工'
    },true)
    } else {
    window.parent.menuHandle({
    id: '2',
    url: '/backend/page/member/add.html?id='+st,
    name: '修改员工'
    },true)
    }
    }
  3. 发送ajax请求,请求服务端,同时提交员工id参数

    当add.html加载完毕之后,调用钩子函数,当参数存在时,说明是编辑员工,否则是添加员工

1
2
3
4
5
6
7
created() {
this.id = requestUrlParam('id')
this.actionType = this.id ? 'edit' : 'add'
if (this.id) {
this.init()
}
}
1
2
3
4
5
6
7
8
9
10
11
12
function requestUrlParam(argname){
var url = location.href
var arrStr = url.substring(url.indexOf("?")+1).split("&")
for(var i =0;i<arrStr.length;i++)
{
var loc = arrStr[i].indexOf(argname+"=")
if(loc!=-1){
return arrStr[i].replace(argname+"=","").replace("?","")
}
}
return ""
}
  1. 服务端接受请求,并根据员工id查询员工信息,并将员工信息以json形式响应给页面
1
2
3
4
5
6
7
8
9
@GetMapping("/{id}")
public Result<Employee> getById(@PathVariable Long id) {
log.info("根据id查询员工信息..");
Employee employee = employeeService.getById(id);
if (employee != null) {
return Result.success(employee);
}
return Result.error("未查询到该员工信息");
}
  1. add.html的钩子函数中调用了init函数,接收到服务端响应的Json数据后,判断状态码,如果操作成功则将获取到的数据赋给表单,并通过双向绑定来实现数据回显
1
2
3
4
5
6
7
8
9
10
11
12
13
async init () {
queryEmployeeById(this.id).then(res => {
console.log(res)
if (String(res.code) === '1') {
console.log(res.data)
this.ruleForm = res.data
this.ruleForm.sex = res.data.sex === '0' ? '女' : '男'
// this.ruleForm.password = ''
} else {
this.$message.error(res.msg || '操作失败')
}
})
}
  1. 点击保存按钮,发送ajax请求,将页面中的员工信息以json形式提交给服务端
1
2
3
4
5
6
<el-button
type="primary"
@click="submitForm('ruleForm', false)"
>
保存
</el-button>

可以看出添加和修改用的是一个表单来提交数据

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
submitForm (formName, st) {
this.$refs[formName].validate((valid) => {
if (valid) {
if (this.actionType === 'add') {
const params = {
...this.ruleForm,
sex: this.ruleForm.sex === '女' ? '0' : '1'
}
addEmployee(params).then(res => {
if (res.code === 1) {
this.$message.success('员工添加成功!')
if (!st) {
this.goBack()
} else {
this.ruleForm = {
username: '',
'name': '',
'phone': '',
// 'password': '',
// 'rePassword': '',/
'sex': '男',
'idNumber': ''
}
}
} else {
this.$message.error(res.msg || '操作失败')
}
}).catch(err => {
this.$message.error('请求出错了:' + err)
})
} else {
const params = {
...this.ruleForm,
sex: this.ruleForm.sex === '女' ? '0' : '1'
}
editEmployee(params).then(res => {
if (res.code === 1) {
this.$message.success('员工信息修改成功!')
this.goBack()
} else {
this.$message.error(res.msg || '操作失败')
}
}).catch(err => {
this.$message.error('请求出错了:' + err)
})
}
} else {
console.log('error submit!!')
return false
}
})
}

1
2
3
4
5
6
7
8
// 修改---添加员工
function editEmployee (params) {
return $axios({
url: '/employee',
method: 'put',
data: { ...params }
})
}
  1. 服务端接受信息后,再次调用前面写的update方法,对员工信息进行修改
1
2
3
4
5
6
7
8
9
@PutMapping
public Result<String> update(@RequestBody Employee employee, HttpServletRequest request) {
log.info(employee.toString());
Long id = (Long) request.getSession().getAttribute("employee");
employee.setUpdateUser(id);
employee.setUpdateTime(LocalDateTime.now());
employeeService.updateById(employee);
return Result.success("员工信息修改成功");
}
  1. 员工信息修改成功之后,调用goBack函数,跳转至员工管理页面
    1
    2
    3
    4
    5
    6
    7
    goBack(){
    window.parent.menuHandle({
    id: '2',
    url: '/backend/page/member/list.html',
    name: '员工管理'
    },false)
    }

公共字段自动填充

问题引出

  • 前面完成了对员工数据的添加与修改,在添加/修改员工数据的时候,需要指定创建人、创建时间、修改人、修改时间等字段,而这些字段又属于公共字段,不仅员工表有这些字段,在菜品表、分类表等其他表中,也拥有这些字段。
  • 有没有办法让这些字段在一个地方统一管理,以此来简化开发呢?
    • 答案是使用MybatisPlus提供的公共字段自动填充功能

代码实现

  • 实现步骤

    1. 在实体类的属性上方加入@TableFiled注解,指定自动填充的策略

      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
      @Data
      public class Employee implements Serializable {

      private static final long serialVersionUID = 1L;

      private Long id;

      private String username;

      private String name;

      private String password;

      private String phone;

      private String sex;

      private String idNumber;//身份证号码

      private Integer status;

      @TableField(fill = FieldFill.INSERT)
      private LocalDateTime createTime;

      @TableField(fill = FieldFill.INSERT_UPDATE)
      private LocalDateTime updateTime;

      @TableField(fill = FieldFill.INSERT)
      private Long createUser;

      @TableField(fill = FieldFill.INSERT_UPDATE)
      private Long updateUser;
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      public enum FieldFill {
      DEFAULT,
      INSERT,
      UPDATE,
      INSERT_UPDATE;

      private FieldFill() {
      }
      }
    2. 按照框架要求编写元数据对象处理器,在此类中统一对公共字段赋值,此类需要实现MetaObjectHandler接口,实现接口之后,重写两个方法,一个是插入时填充,一个是修改时填充
      关于字段填充方式,使用metaObject的setValue来实现
      关于id的获取,我们之前是存到session里的,但在MyMetaObjectHandler类中不能获得HttpSession对象,所以我们需要用其他方式来获取登录用户Id

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      @Component
      @Slf4j
      public class MyMetaObjectHandler implements MetaObjectHandler {

      @Override
      public void insertFill(MetaObject metaObject) {
      log.info("公共字段自动填充(insert)...");
      log.info(metaObject.toString());
      metaObject.setValue("createTime", LocalDateTime.now());
      metaObject.setValue("updateTime", LocalDateTime.now());
      }

      @Override
      public void updateFill(MetaObject metaObject) {
      log.info("公共字段自动填充(update)...");
      log.info(metaObject.toString());
      metaObject.setValue("updateTime", LocalDateTime.now());
      }
      }

功能完善

  • 现在已经能够填充时间和创建人两个公共字段了,但是还不能添加用户的id,因为之前是通过httpSession对象来获取的,但是现在不能使用session对象了,此时我们需要使用ThreadLocal来解决
  • 在学习ThreadLocal之前,需要先确认一个事情,就是客户端发送的每次http请求,对应的在服务端都会分配一个新的线程来处理,在处理过程中涉及到下面类中的方法都属于相同的一个线程:
    1. LocalCheekFilter中的doFilter方法
    2. EmployeeController中的update方法
    3. MyMetaObjectHandler中的updateFill方法

可以通过在这三个方法中添加日志输出测试,此处省略

什么是ThreadLocal?

  • ThreadLocal并不是一个Thread,而是Thread的局部变量
  • 当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本
  • 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本
  • ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。

ThreadLocal常用方法:

  • public void set(T value) 设置当前线程的线程局部变量的值
  • public T get() 返回当前线程所对应的线程局部变量的值

如何用ThreadLocal来解决上述的问题呢?

  • 我们可以在LoginCheckFilterdoFilter方法中获取当前登录用户id,并调用ThreadLocalset方法来设置当前线程的线程局部变量的值(用户id),然后在MyMetaObjectHandlerupdateFill方法中调用ThreadLocalget方法来获得当前线程所对应的线程局部变量的值(用户id)。

具体实现

  • 在com.blog.common包下新建BaseContext类
  • 作用:基于ThreadLocal的封装工具类,用于保护和获取当前用户id

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class BaseContext {
    private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    public static void setCurrentId(Long id) {
    threadLocal.set(id);
    }

    public static Long getCurrentId() {
    return threadLocal.get();
    }
    }
  • 随后在LoginCheckFilter类中添加代码,获取到id后保存到ThreadLocal中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    //4.判断登录状态,如果已登录,则直接放行
    if (request.getSession().getAttribute("employee") != null) {
    log.info("用户已登录,id为{}", request.getSession().getAttribute("employee"));
    //在这里获取一下线程id
    long id = Thread.currentThread().getId();
    log.info("doFilter的线程id为:{}", id);
    //根据session来获取之前我们存的id值
    Long empId = (Long) request.getSession().getAttribute("employee");
    //使用BaseContext封装id
    BaseContext.setCurrentId(empId);
    filterChain.doFilter(request, response);
    return;
    }
  • 在MyMetaObjectHandler类中,调整设置id的代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    @Component
    @Slf4j
    public class MyMetaObjectHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
    log.info("公共字段填充(create)...");
    metaObject.setValue("createTime", LocalDateTime.now());
    metaObject.setValue("updateTime", LocalDateTime.now());
    //设置创建人id
    metaObject.setValue("createUser", BaseContext.getCurrentId());
    metaObject.setValue("updateUser", BaseContext.getCurrentId());
    }

    @Override
    public void updateFill(MetaObject metaObject) {
    log.info("公共字段填充(insert)...");
    metaObject.setValue("updateTime", LocalDateTime.now());
    //设置更新人id
    metaObject.setValue("updateUser", BaseContext.getCurrentId());
    }
    }

新增菜品分类

需求分析

  • 后台系统中可以管理分类信息,分类包括两种类型,分别是菜品分类套餐分类
  • 当我们在后台系统中添加菜品时,需要选择一个菜品分类
  • 当我们在后台系统中添加一个套餐时,需要选择一个套餐分类
  • 在移动端也会按照菜品分类和套餐分类来展示对应的菜品和套餐

可以在后台系统的分类管理页面分别添加菜品分类和套餐分类,如下

数据模型

简单了解category表中的数据

id是主键,name分类名称Unique,type为1表示菜品分类,type为2表示套餐分类

准备工作

开发业务之前,先将需要用到的类和接口的基本结构先创建好

  1. 根据数据库信息来创建实体类Category

    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
    @Data
    public class Category implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;


    //类型 1 菜品分类 2 套餐分类
    private Integer type;


    //分类名称
    private String name;


    //顺序
    private Integer sort;


    //创建时间
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;


    //更新时间
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;


    //创建人
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;


    //修改人
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;

    }
  • 创建接口和实现类
    1
    2
    3
    @Mapper
    public interface CategoryMapper extends BaseMapper<Category> {
    }
    1
    2
    public class CategoryMapperImpl {
    }
    1
    2
    public interface CategoryService extends IService<Category> {
    }
    1
    2
    3
    @Service
    public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {
    }
  • 创建控制层
    1
    2
    3
    4
    5
    6
    7
    8
    @Slf4j
    @RestController
    @RequestMapping("/category")
    public class CategoryController {
    @Autowired
    private CategoryService categoryService;

    }

流程分析

分析整个流程

  1. 页面发送ajax请求,将新增分类窗口输入的数据以json形式提交给服务端
  2. 服务端Controller接收页面提交的数据并调用Service将数据存储到数据库
  3. Service调用Mapper操作数据库,保存数据

我们先尝试监测一下前端给我们提供的是什么请求,以及会提交什么数据,打开开发者工具,监测NetWork,点击新增菜品分类表单的确定按钮

  • 请求方式

    请求网址: http://localhost/category
    请求方法: POST

  • json数据

    {name: “川菜”, type: “1”, sort: “10”}

点击新增套餐分类表单的确定按钮

  • 请求方式

    请求网址: http://localhost/category
    请求方法: POST

  • json数据

    {name: “好吃的套餐”, type: “2”, sort: “10”}

新增菜品分类和新增套餐分类请求的服务端地址和提交的json数据结构相同,所以服务端只需要提供一个方法就可以统一处理新增菜品和新增套餐

代码实现

接受数据后调用MybatisPlus提供的save方法保存数据即可,并返回成功添加的提示信息

1
2
3
4
5
6
@PostMapping
public Result<String> save(@RequestBody Category category) {
log.info("category:{}", category);
categoryService.save(category);
return Result.success("新增分类成功");
}

  • 但通过查看前端代码,发现显示的信息在前端写死了,只要最后的状态码是成功状态码,则均显示分类添加成功!

    1
    2
    if (res.code === 1) {
    this.$message.success('分类添加成功!')
  • 如果想要添加菜品和添加套餐显示不同的响应结果,可以按照如下方式修改代码


1
2
if (res.code === 1) {
this.$message.success(res.data)

1
return Result.success(category.getType() == 1 ? "添加菜品分类成功!" : "添加套餐分类成功!");

建表时设置的name字段为unique唯一,如果尝试添加重复的name,则会报错,但在前面的全局异常处理器中已经处理了,所以这里我们不需要处理

分类信息分页查询

与员工信息分页查询类似,只是查询的表不同

流程分析

分析流程:

  1. 页面发送Ajax请求,将分页查询的参数(page、pageSize)提交到服务端
  2. 服务端Controller接受到页面提交的数据之后,调用Service进行查询
  3. Service调用Mapper操作数据库,查询分页数据
  4. Controller将查询到的分页数据响应给页面
  5. 页面接收分页数据,并通过ElementUI的Table组件展示到页面上

前端代码分析

跟前面基本一致,简单回顾

页面加载完毕之后调用created钩子函数
钩子函数内又调用的是init进行初始化

1
2
3
4

created() {
this.init()
}

1
2
3
4
5
6
7
8
9
10
11
12
13
async init () {
await getCategoryPage({'page': this.page, 'pageSize': this.pageSize}).then(res => {
if (String(res.code) === '1') {
//将服务端查询到的数据赋给tableData,然后就能看到了
this.tableData = res.data.records
this.counts = Number(res.data.total)
} else {
this.$message.error(res.msg || '操作失败')
}
}).catch(err => {
this.$message.error('请求出错了:' + err)
})
}
1
2
3
4
5
6
7
8
// 查询列表接口
const getCategoryPage = (params) => {
return $axios({
url: '/category/page',
method: 'get',
params
})
}

代码实现

因为前面写员工分页查询,相关配置已经完成,所以直接在CategoryController类中编写page方法即可

1
2
3
4
5
6
7
8
9
10
11
12
@GetMapping("/page")
public Result<Page> page(int page, int pageSize) {
//分页构造器
Page<Category> pageInfo = new Page<>(page, pageSize);
//条件查询器
LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
//添加排序条件
queryWrapper.orderByDesc(Category::getSort);
//分页查询
categoryService.page(pageInfo, queryWrapper);
return Result.success(pageInfo);
}

查看效果

删除分类

需求分析

  • 在分类管理列表页面,可以对某个分类进行删除操作
  • 需要注意的是:当某一分类还有关联的菜品或者套餐时,该分类将不允许被删除

流程分析

  1. 页面发送ajax请求,将参数(id)提交给服务端
  2. 服务端Controller接收页面提交的数据,并调用Service删除数据
  3. Service调用Mapper操作数据库

后端代码实现

在CategoryController类上添加delete方法

1
2
3
4
5
6
@DeleteMapping
private Result<String> delete(Long id) {
log.info("将被删除的id:{}", id);
categoryService.removeById(id);
return Result.success("分类信息删除成功");
}

前端代码分析

点击删除按钮后会直接调用deleteHandler()方法

1
2
3
4
5
6
7
8
<el-button
type="text"
size="small"
class="delBut non"
@click="deleteHandle(scope.row.id)"
>
删除
</el-button>

返回提示信息,确认删除后调用deleteCategory()方法去发送删除命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
deleteHandle(id) {
this.$confirm('此操作将永久删除该文件, 是否继续?', '提示', {
'confirmButtonText': '确定',
'cancelButtonText': '取消',
'type': 'warning'
}).then(() => {
deleCategory(id).then(res => {
if (res.code === 1) {
this.$message.success('删除成功!')
this.handleQuery()
} else {
this.$message.error(res.msg || '操作失败')
}
}).catch(err => {
this.$message.error('请求出错了:' + err)
})
})
}

这里需要注意没有使用Restful风格,因为请求中为?id=xxx,还需要注意修改黑马提供的前端资料,路径为backend/api/category.js,将此处的ids改为id(两处都要修改),然后清除缓存

1
2
3
4
5
6
7
8
// 删除当前列的接口
const deleCategory = (id) => {
return $axios({
url: '/category',
method: 'delete',
params: {id}
})
}

此时再重启服务器再次测试即可

功能完善

完成了单纯的删除操作,现在来完善功能,当菜品分类或套餐分类关联了其他菜品或套餐时,该分类将不允许被删除

思路:删除时拿着当前分类的id值,去对应的菜品/套餐表中进行查询,如果能查询到数据,则说明该分类关联了菜品

代码完善:

  • 首先根据数据表创建菜品和套餐对应的模型类
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
@Data
public class Dish implements Serializable {
private static final long serialVersionUID = 1L;

private Long id;

//菜品名称
private String name;

//菜品分类id
private Long categoryId;

//菜品价格
private BigDecimal price;

//商品码
private String code;

//图片
private String image;

//描述信息
private String description;

//0 停售 1 起售
private Integer status;

//顺序
private Integer sort;

@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;

@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;

@TableField(fill = FieldFill.INSERT)
private Long createUser;

@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;

}
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
/**
* 套餐
*/
@Data
public class Setmeal implements Serializable {

private static final long serialVersionUID = 1L;

private Long id;

//分类id
private Long categoryId;

//套餐名称
private String name;

//套餐价格
private BigDecimal price;

//状态 0:停用 1:启用
private Integer status;

//编码
private String code;

//描述信息
private String description;

//图片
private String image;

@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;

@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;

@TableField(fill = FieldFill.INSERT)
private Long createUser;

@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;

}
  • 编写对应的Mapper接口
1
2
3
@Mapper
public interface DishMapper extends BaseMapper<Dish> {
}
1
2
3
@Mapper
public interface SetmealMapper extends BaseMapper<Setmeal> {
}
  • 编写对应的Service接口及Impl实现类
1
2
public interface DishService extends IService<Dish> {
}
1
2
3
@Service
public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements DishService {
}
1
2
public interface SetmealService extends IService<Setmeal> {
}
1
2
3
@Service
public class SetmealServiceImpl extends ServiceImpl<SetmealMapper, Setmeal> implements SetmealService {
}
  • 在common包下新增CustomException类,用于封装我们的自定义异常
1
2
3
4
5
public class CustomException extends RuntimeException{
public CustomException(String msg){
super(msg);
}
}
  • 在全局异常处理器类中,添加上CustomerException异常的处理
1
2
3
4
5
@ExceptionHandler(CustomException.class)
public Result<String> exceptionHandler(CustomException exception) {
log.error(exception.getMessage());
return Result.error(exception.getMessage());
}
  • 在CategoryService接口中自己写一个remove方法
1
2
3
public interface CategoryService extends IService<Category> {
void remove(Long id);
}
  • 在CategoryServiceImpl中来写具体业务逻辑
    我们需要在删除数据之前,根据id值,去Dish表和Setmeal表中查询是否管理数据
    如果未查询到数据则代码不关联数据,则可以删除,查询到数据则代表不能删除并抛出异常

    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
    @Service
    @Slf4j
    public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {
    @Autowired
    DishService dishService;

    @Autowired
    SetmealService setmealService;

    /**
    * 根据id删除分类,删除之前需要进行判断
    * @param id
    */
    @Override
    public void remove(Long id) {
    LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
    //添加dish查询条件,根据分类id进行查询
    dishLambdaQueryWrapper.eq(Dish::getCategoryId, id);
    //方便Debug用的
    int count1 = dishService.count(dishLambdaQueryWrapper);
    log.info("dish查询条件,查询到的条目数为:{}",count1);
    //查看当前分类是否关联了菜品,如果已经关联,则抛出异常
    if (count1 > 0){
    //已关联菜品,抛出一个业务异常
    throw new CustomException("当前分类下关联了菜品,不能删除");
    }

    LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
    //添加dish查询条件,根据分类id进行查询
    setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId,id);
    int count2 = setmealService.count(setmealLambdaQueryWrapper);
    //方便Debug用的
    log.info("setmeal查询条件,查询到的条目数为:{}",count2);
    //查看当前分类是否关联了套餐,如果已经关联,则抛出异常
    if (count2 > 0){
    //已关联套餐,抛出一个业务异常
    throw new CustomException("当前分类下关联了套餐,不能删除");
    }
    //正常删除
    super.removeById(id);
    }
    }
  • 最后在controller的删除方法中调用自己写的remove方法

    1
    2
    3
    4
    5
    6
    @DeleteMapping
    public Result<String> delete(Long id){
    log.info("将要删除的分类id:{}",id);
    categoryService.remove(id);
    return Result.success("分类信息删除成功");
    }

最终效果:

修改分类

需求分析

与修改员工信息类似,点击修改按钮后,在修改窗口回显信息,最后点击确认完成修改操作

代码开发

数据的回显效果由前端来实现,与员工信息修改大体相同,简略说一些即可

  • 修改按钮绑定了editHandle函数,一旦点击就会调用该函数,并以该行的数据作为参数
  • editHandle函数负责初始化表格中数据的值,也就是数据回显
  • 表单通过v-model实现双向绑定
    此处修改功能也和添加功能共用一个方法
    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
    //数据提交
    submitForm(st) {
    const classData = this.classData
    const valid = (classData.name === 0 ||classData.name) && (classData.sort === 0 || classData.sort)
    if (this.action === 'add') {
    if (valid) {
    const reg = /^\d+$/
    if (reg.test(classData.sort)) {
    addCategory({'name': classData.name,'type':this.type, sort: classData.sort}).then(res => {
    console.log(res)
    if (res.code === 1) {
    this.$message.success('分类添加成功!')
    if (!st) {
    this.classData.dialogVisible = false
    } else {
    this.classData.name = ''
    this.classData.sort = ''
    }
    this.handleQuery()
    } else {
    this.$message.error(res.msg || '操作失败')
    }
    }).catch(err => {
    this.$message.error('请求出错了:' + err)
    })
    } else {
    this.$message.error('排序只能输入数字类型')
    }

    } else {
    this.$message.error('请输入分类名称或排序')
    }
    } else if (valid) {
    const reg = /^\d+$/
    if (reg.test(this.classData.sort)) {
    editCategory({'id':this.classData.id,'name': this.classData.name, sort: this.classData.sort}).then(res => {
    if (res.code === 1) {
    this.$message.success('分类修改成功!')
    this.classData.dialogVisible = false
    this.handleQuery()
    } else {
    this.$message.error(res.msg || '操作失败')
    }
    }).catch(err => {
    this.$message.error('请求出错了:' + err)
    })
    } else {
    this.$message.error('排序只能输入数字类型')
    }
    } else {
    this.$message.error('请输入分类名称或排序')
    }
    }

    添加操作是post请求,修改是发送PUT请求

    1
    2
    3
    4
    5
    6
    7
    8
    // 修改接口
    const editCategory = (params) => {
    return $axios({
    url: '/category',
    method: 'put',
    data: { ...params }
    })
    }

  • 后端代码开发
    1
    2
    3
    4
    5
    6
    @PutMapping
    public Result<String> update(@RequestBody Category category) {
    log.info("修改分类信息为:{}", category);
    categoryService.updateById(category);
    return Result.success("修改分类信息成功");
    }

文件上传与下载

文件上传简介

文件上传,也叫upload,是指将本地图片、视频、音频等文件上传到服务器中,可以供其他用户浏览或下载的过程

  • 文件上传时,对页面的form表单有如下要求:

    1. method="post",采用post方式提交数据
    2. enctype="multipart/form-data",采用multipart格式上传文件
    3. type="file",使用input的file控件上传
  • 举例

    1
    2
    头像:
    <input type="file"><br>

    头像:

  • 目前一些前端组件库也提供了相应的上传组件,但是底层原理还是基于form表单的文件上传,这里直接使用提供好的组件就行了
    把这段代码放在backend/demo目录下,命名为upload.html

    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
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>文件上传</title>
    <!-- 引入样式 -->
    <link rel="stylesheet" href="../../plugins/element-ui/index.css" />
    <link rel="stylesheet" href="../../styles/common.css" />
    <link rel="stylesheet" href="../../styles/page.css" />
    </head>
    <body>
    <div class="addBrand-container" id="food-add-app">
    <div class="container">
    <el-upload class="avatar-uploader"
    action="/common/upload"
    :show-file-list="false"
    :on-success="handleAvatarSuccess"
    :before-upload="beforeUpload"
    ref="upload">
    <img v-if="imageUrl" :src="imageUrl" class="avatar"></img>
    <i v-else class="el-icon-plus avatar-uploader-icon"></i>
    </el-upload>
    </div>
    </div>
    <!-- 开发环境版本,包含了有帮助的命令行警告 -->
    <script src="../../plugins/vue/vue.js"></script>
    <!-- 引入组件库 -->
    <script src="../../plugins/element-ui/index.js"></script>
    <!-- 引入axios -->
    <script src="../../plugins/axios/axios.min.js"></script>
    <script src="../../js/index.js"></script>
    <script>
    new Vue({
    el: '#food-add-app',
    data() {
    return {
    imageUrl: ''
    }
    },
    methods: {
    handleAvatarSuccess (response, file, fileList) {
    this.imageUrl = `/common/download?name=${response.data}`
    },
    beforeUpload (file) {
    if(file){
    const suffix = file.name.split('.')[1]
    const size = file.size / 1024 / 1024 < 2
    if(['png','jpeg','jpg'].indexOf(suffix) < 0){
    this.$message.error('上传图片只支持 png、jpeg、jpg 格式!')
    this.$refs.upload.clearFiles()
    return false
    }
    if(!size){
    this.$message.error('上传文件大小不能超过 2MB!')
    return false
    }
    return file
    }
    }
    }
    })
    </script>
    </body>
    </html>
  • 服务端接收客户端页面上传的文件,通常都会使用Apache的两个组件:

    • commons-fileupload
    • commons-io
  • Spring框架在spring-web包中对文件上传进行了封装,大大简化了服务端代码,只需要在Controller的方法中声明一个MultipartFile类型的参数即可接收上传的文件,例如

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @RestController
    @RequestMapping("/common")
    @Slf4j
    public class CommonController {
    @PostMapping("/upload")
    public Result<String> upload(MultipartFile file) {
    log.info("获取文件:{}", file.toString());
    return null;
    }
    }
  • 启动服务器,登陆之后访问http://localhost/backend/page/demo/upload.html,传入一张图片,便能在日志上看到相关信息(需要先有过登录行为)

文件下载简介

  • 文件下载,也称download,是指将文件从服务器传输到本地计算机的过程
  • 通过浏览器进行文件下载,通常有两种表现形式
    1. 以附件形式下载,弹出保存对话框,将文件保存到指定磁盘目录
    2. 直接在浏览器中打开
  • 通过浏览器进行文件下载,本质上就是服务端将文件以流的形式写回浏览器的过程

文件上传代码实现

  • 在编写代码之前,先来设置一下拦截路径,回到拦截器类中

    1
    2
    3
    4
    5
    6
    7
    8
    //定义不需要处理的请求
    String[] urls = new String[]{
    "/employee/login",
    "/employee/logout",
    "/backend/**",
    "/front/**",
    "/common/**"
    };
  • 随后使用transferTo方法将上传的临时文件转存到指定位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RestController
@RequestMapping("/common")
@Slf4j
public class CommonController {
@PostMapping("/upload")
//file是个临时文件,我们在断点调试的时候可以看到,但是执行完整个方法之后就消失了
public Result<String> upload(MultipartFile file) {
log.info("获取文件:{}", file.toString());
//方法会抛异常,我们这里用try/catch处理一下
try {
//我们将其转存为E盘下的test.jpg
file.transferTo(new File("D:\\Test\\test.jpg"));
} catch (IOException e) {
throw new RuntimeException(e);
}
return null;
}
}
  • 此时可以通过在http://localhost:8080/backend/page/demo/upload.html该网址内继续上传图片来测试功能是否能正常执行

  • 文件转存的位置可以通过修改配置文件来动态改变:在application.yml文件中加入以下内容

    1
    2
    reggie:
    path: D:\\Test\\
  • 使用@Value("${reggie.path}")读取到配置文件中的动态转存位置

  • 使用uuid方式重新生成文件名,避免文件名重复造成文件覆盖

  • 通过获取原文件名来截取文件后缀,并拼接成新的文件名

注意事项:我们需要先判断一下文件目录是否存在,如果不存在则先创建

  • 最后的返回值是将我们生成的新文件名返回给前端

    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
    @RestController
    @RequestMapping("/common")
    @Slf4j
    public class CommonController {
    @Value("${reggie.path}")
    private String basePath;

    @PostMapping("/upload")
    //file是个临时文件,我们在断点调试的时候可以看到,但是执行完整个方法之后就消失了
    public Result<String> upload(MultipartFile file) {
    log.info("获取文件:{}", file.toString());
    //判断一下当前目录是否存在,不存在则创建
    File dir = new File(basePath);
    if (!dir.exists()) {
    dir.mkdirs();
    }


    //获取一下传入的原文件名
    String originalFilename = file.getOriginalFilename();
    //我们只需要获取一下格式后缀,取子串,起始点为最后一个.
    String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
    //为了防止出现重复的文件名,我们需要使用UUID
    String fileName = UUID.randomUUID() + suffix;
    log.info("文件保存路径:{}", basePath + fileName);
    try {
    //我们将其转存到我们的指定目录下
    file.transferTo(new File(basePath + fileName));
    } catch (IOException e) {
    throw new RuntimeException(e);
    }
    //将文件名返回给前端,便于后期的开发
    return Result.success(fileName);
    }

    }
  • 重启服务器,随便上传一张图片,然后去对应的目录下看看是否有上传的图片

  • 如果一切顺利的话,目录不存在则会自动创建,而且上传的图片也在文件夹下

文件下载代码实现

前端处理

  • 前端页面的ElementUI的upload组件会在上传完图片后,触发img组件发送请求,服务端以流的方式(输出流)将文件写回给浏览器,在浏览器中展示图片

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <el-upload class="avatar-uploader"
    action="/common/upload"
    :show-file-list="false"
    :on-success="handleAvatarSuccess"
    :before-upload="beforeUpload"
    ref="upload">
    <img v-if="imageUrl" :src="imageUrl" class="avatar"></img>
    <i v-else class="el-icon-plus avatar-uploader-icon"></i>
    </el-upload>
  • 定义前端发送回显图片请求的地址
    通过这个url我们可以看出,请求路径为/common/download,且发送的是GET请求

    1
    2
    3
    handleAvatarSuccess (response, file, fileList) {
    this.imageUrl = `/common/download?name=${response.data}`
    }

后端处理

  • CommonController类中添加download方法
    1. 通过输入流读取文件内容
    2. 通过输出流将文件写回浏览器,在浏览器展示图片
    3. 关闭输入输出流,释放资源
1
2
3
4
5
6
7
8
9
10
11
@GetMapping("/download")
public void download(String name, HttpServletResponse response) {
FileInputStream fis = new FileInputStream(basePath + name);
ServletOutputStream os = response.getOutputStream();
int len;
byte[] buffer = new byte[1024];
while ((len = fis.read(buffer)) != -1)
os.write(buffer, 0, len);
fis.close();
os.close();
}
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
@GetMapping("/download")
public void download(String name, HttpServletResponse response) {
FileInputStream fis = null;
ServletOutputStream os = null;
try {
fis = new FileInputStream(basePath + name);
os = response.getOutputStream();
response.setContentType("image/jpeg");
int len;
byte[] buffer = new byte[1024];
while ((len = fis.read(buffer)) != -1){
os.write(buffer, 0, len);
}

} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (fis != null) {

try {
fis.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
if (os != null) {
try {
os.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
  • 然后启动服务器,上传一张图片,就会发现图片直接展示在页面上了

新增菜品

需求分析

  1. 后台系统中可以管理菜品信息,通过新增功能来添加一个新的菜品
  2. 在添加菜品时需要选择当前菜品所属的菜品分类,并且上传菜品图片
  3. 在移动端会按照菜品分类来展示对应的菜品信息(前端的活儿,跟后端没啥太大关系)

数据模型

dish表,最后一条字段is_deleted是逻辑删除

代码开发

准备工作

  • 前面跟Dish有关的已经创建好了,接下来创建DishFlavor对应的实体类,Mapper接口,Service接口及其对应的实现类
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
/**
* 菜品口味
*/
@Data
public class DishFlavor implements Serializable {

private static final long serialVersionUID = 1L;

private Long id;


//菜品id
private Long dishId;


//口味名称
private String name;


//口味数据list
private String value;


@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;


@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;


@TableField(fill = FieldFill.INSERT)
private Long createUser;


@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;


//是否删除
private Integer isDeleted;

}
1
2
3
@Mapper
public interface DishFlavorMapper extends BaseMapper<DishFlavor> {
}
1
2
public interface DishFlavorService extends IService<DishFlavor> {
}
1
2
3
@Service
public class DishFlavorServiceImpl extends ServiceImpl<DishFlavorMapper, DishFlavor> implements DishFlavorService {
}

编写Controller层代码

1
2
3
4
5
6
7
8
9
10
11
@RestController
@RequestMapping("/dish")
@Slf4j
public class DishController {

@Autowired
private DishService dishService;
@Autowired
private DishFlavorService dishFlavorService;

}

查询分类数据

流程分析:

  1. 页面(backend/page/food/add.html)发送ajax请求,请求服务端获取菜品分类数据并展示到下拉框中
  2. 页面发送请求进行图片上传,请求服务端将图片保存到服务器
  3. 页面发送请求进行图片下载,并回显上传的图片
  4. 点击保存按钮,发送ajax请求,将菜品相关数据以json形式提交到服务端

接下来逐步完成这四个请求即可

  • 调用getDishList方法来初始化表格,并通过传入的id来判断是添加菜品还是编辑菜品
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    created() {
    this.getDishList()
    // 口味临时数据
    this.getFlavorListHand()
    this.id = requestUrlParam('id')
    this.actionType = this.id ? 'edit' : 'add'
    if (this.id) {
    this.init()
    }
    }

根据响应的Code值来判断操作是否成功

1
2
3
4
5
6
7
8
9
getDishList () {
getCategoryList({ 'type': 1 }).then(res => {
if (res.code === 1) {
this.dishList = res.data
} else {
this.$message.error(res.msg || '操作失败')
}
})
}

发送get请求来获取菜品分类列表

1
2
3
4
5
6
7
8
// 获取菜品分类列表
const getCategoryList = (params) => {
return $axios({
url: '/category/list',
method: 'get',
params
})
}

使用v-for遍历菜品信息

1
2
3
4
5
6
<el-select
v-model="ruleForm.categoryId"
placeholder="请选择菜品分类"
>
<el-option v-for="(item,index) in dishList" :key="index" :label="item.name" :value="item.id" />
</el-select>

  • CategoryController类中,添加list方法,查询菜品信息即可

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @GetMapping("/list")
    public Result<List<Category>> list(Category category) {
    //条件构造器
    LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
    //添加条件,这里只需要判断是否为菜品(type为1是菜品,type为2是套餐)
    queryWrapper.eq(category.getType() != null,Category::getType,category.getType());
    //添加排序条件
    queryWrapper.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime);
    //查询数据
    List<Category> list = categoryService.list(queryWrapper);
    //返回数据
    return Result.success(list);
    }

接收与回显图片

该功能前面已经在CommonController中添加了对应的uploaddownload方法,因此可以直接使用

提交数据到服务端

  • 先随便提交点数据测试一下
  • 以下是浏览器发送的Json数据
  • 此处可以发现价格变成了输入价格的100倍,这是前端在submitForm方法中对数据进行的处理,一般金额都以最小单位进行存储,数据库存储的单位为分
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
submitForm(formName, st) {
this.$refs[formName].validate((valid) => {
if (valid) {
let params = {...this.ruleForm}
// params.flavors = this.dishFlavors
params.status = this.ruleForm ? 1 : 0
params.price *= 100
params.categoryId = this.ruleForm.categoryId
params.flavors = this.dishFlavors.map(obj => ({ ...obj, value: JSON.stringify(obj.value) }))
delete params.dishFlavors
if(!this.imageUrl){
this.$message.error('请上传菜品图片')
return
}
if (this.actionType == 'add') {
delete params.id
addDish(params).then(res => {
if (res.code === 1) {
this.$message.success('菜品添加成功!')
if (!st) {
this.goBack()
} else {
this.dishFlavors = []
// this.dishFlavorsData = []
this.imageUrl = ''
this.ruleForm = {
'name': '',
'id': '',
'price': '',
'code': '',
'image': '',
'description': '',
'dishFlavors': [],
'status': true,
categoryId: ''
}
}
} else {
this.$message.error(res.msg || '操作失败')
}
}).catch(err => {
this.$message.error('请求出错了:' + err)
})
} else {
delete params.updateTime
editDish(params).then(res => {
if (res.code === 1) {
this.$message.success('菜品修改成功!')
this.goBack()
} else {
this.$message.error(res.msg || '操作失败')
}
}).catch(err => {
this.$message.error('请求出错了:' + err)
})
}
} else {
return false
}
})
}
  • 因为Dish实体类不满足接收flavor参数,即需要导入DishDto,用于封装页面提交的数据

  • DTO,全称为

    1
    Data Transfer Object

    ,即数据传输对象,一般用于展示层与服务层之间的数据传输。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Data
    public class DishDto extends Dish {

    private List<DishFlavor> flavors = new ArrayList<>();

    //后面这两条属性暂时没用,这里只需要用第一条属性
    private String categoryName;

    private Integer copies;
    }
  • 1
    DishController

    类中添加

    1
    save

    方法,重启服务器,断点调试一下看看是否封装好了数据

    1
    2
    3
    4
    5
    @PostMapping
    public Result<String> save(@RequestBody DishDto dishDto) {
    log.info("接收到的数据为:{}",dishDto);
    return null;
    }

img
从图中我们可以看出,DishFlavor中的dishId为null
但是我们需要对DishFlavor中的dishId进行赋值
所以我们要取出dishDto的dishId,然后对每一组flavor的dishId赋值

  • 这里进行一下小结,我们需要做的有以下几点

    • 将菜品数据保存到dish

    • 将菜品口味数据保存到dish_flavor

      • 但是dish_flavor表中需要一个dishId字段值,这个字段值需要我们从dishDto中获取
      • 获取方式为:取出dishDtodishId,对每一组flavordishId赋值
  • 梳理完毕之后,那么我们就在DishFlavorService中编写一个saveWithFlavor方法

    1
    2
    3
    public interface DishService extends IService<Dish> {
    void saveWithFlavor(DishDto dishDto);
    }
  • 同时在DishFlavorServiceImpl中重写方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @Service
    public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements DishService {
    @Autowired
    private DishFlavorService dishFlavorService;

    @Override
    public void saveWithFlavor(DishDto dishDto) {
    //将菜品数据保存到dish表
    this.save(dishDto);
    //获取dishId
    Long dishId = dishDto.getId();
    //将获取到的dishId赋值给dishFlavor的dishId属性
    List<DishFlavor> flavors = dishDto.getFlavors();
    for (DishFlavor dishFlavor : flavors) {
    dishFlavor.setDishId(dishId);
    }
    //同时将菜品口味数据保存到dish_flavor表
    dishFlavorService.saveBatch(flavors);
    }
    }

    最后别忘了改Controller层的方法

    1
    2
    3
    4
    5
    6
    @PostMapping
    public Result<String> save(@RequestBody DishDto dishDto) {
    log.info("接收到的数据为:{}",dishDto);
    dishService.saveWithFlavor(dishDto);
    return Result.success("菜品添加成功");
    }

菜品信息分页查询

需求分析

  • 与之前一样,如果将所有数据直接在土匪页面展示出来会显得非常乱,不易于查看,所以需要以分页的方式展示列表数据,其中图片列和菜品分类列比较特殊
    • 图片列:会用到文件的下载功能
    • 菜品分类列:由于菜品表只保存了category_id,所以需要查询category_id对应的菜品分类名称,从而回显数据

梳理流程:

  1. 页面(backend/page/food/list.html)发送ajax请求,将分页查询参数(pagepageSizename),提交到服务端,获取分页数据
  2. 页面发送请求,请求服务端进行图片下载,用于页面图片展示

代码开发

开发分页功能只需要编写代码实现前端发送的两次请求即可

  • DishController下添加page方法,来进行分页查询
1
2
3
4
5
6
7
8
9
10
11
12
13
@GetMapping("/page")
public Result<Page> page(int page, int pageSize, String name) {
//构造分页构造器对象
Page<Dish> pageInfo = new Page<>(page, pageSize);
//条件构造器
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
//添加条件
queryWrapper.like(name != null, Dish::getName, name);
queryWrapper.orderByDesc(Dish::getUpdateTime);
//执行分页查询
dishService.page(pageInfo, queryWrapper);
return Result.success(pageInfo);
}
  • 重启服务器可以发现,此时数据能够分页展示,但是图片和菜品分类的数据都没有展示
    • 图片只需要将黑马提供的图片资源放到存放图片的目录下即可
  • 为什么没有菜品分类数据?

    • 我们传递的是一个Dish对象,dish对象只有菜品分类id,没有菜品分类名称属性,前端是根据分类名称来填写,所以暂时没有分类数据
    • 可以根据这个菜品分类id,去菜品分类表中查询对应的菜品分类名称
  • 此时可以使用DishDto类中的另外一个属性,返回一个DishDto对象就有菜品分类名称数据了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Data
    public class DishDto extends Dish {
    //菜品口味
    private List<DishFlavor> flavors = new ArrayList<>();
    //菜品分类名称
    private String categoryName;

    private Integer copies;
    }
  • DishDto直接继承了Dish,所以可以看作是Dish类额外添加了一个categoryName属性。

    实现思路:将查询的dish数据传递给dishDto,再根据dish中的分类id去菜品分类表查询对应的category_name,将其赋值给dishDto的categoryName属性。

  • 修改Controller层中的page方法
    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
    @GetMapping("/page")
    public Result<Page> page(int page, int pageSize, String name) {
    //构造分页构造器对象
    Page<Dish> pageInfo = new Page<>(page, pageSize);
    //这个就是我们到时候返回的结果
    Page<DishDto> dishDtoPage = new Page<>(page, pageSize);
    //条件构造器
    LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
    //添加条件
    queryWrapper.like(name != null, Dish::getName, name);
    queryWrapper.orderByDesc(Dish::getUpdateTime);
    //执行分页查询
    dishService.page(pageInfo, queryWrapper);

    //对象拷贝,这里只需要拷贝一下查询到的条目数
    BeanUtils.copyProperties(pageInfo, dishDtoPage, "records");

    //获取原records数据
    List<Dish> records = pageInfo.getRecords();

    //遍历每一条records数据
    List<DishDto> list = records.stream().map((item) -> {
    DishDto dishDto = new DishDto();
    //将数据赋给dishDto对象
    BeanUtils.copyProperties(item, dishDto);
    //然后获取一下dish对象的category_id属性
    Long categoryId = item.getCategoryId(); //分类id
    //根据这个属性,获取到Category对象(这里需要用@Autowired注入一个CategoryService对象)
    Category category = categoryService.getById(categoryId);
    //随后获取Category对象的name属性,也就是菜品分类名称
    String categoryName = category.getName();
    //最后将菜品分类名称赋给dishDto对象就好了
    dishDto.setCategoryName(categoryName);
    //结果返回一个dishDto对象
    return dishDto;
    //并将dishDto对象封装成一个集合,作为我们的最终结果
    }).collect(Collectors.toList());

    dishDtoPage.setRecords(list);
    return Result.success(dishDtoPage);
    }

修改菜品

梳理交互过程

梳理流程:

  1. 页面发送ajax请求,请求服务器获取分类数据,用于菜品分类下拉框的数据回显(之前我们已经实现过了)
  2. 页面发送ajax请求,请求服务端,根据id查询当前菜品信息,用于菜品信息回显
  3. 页面发送请求,请求服务端进行图片下载,用于页面图片回显(之前我们已经实现过了)
  4. 点击保存按钮,页面发送ajax请求,将修改后的菜品相关数据以json形式提交到服务端

开发修改菜品功能,其实就是在服务端写代码去处理以上四次请求

查询菜品信息

  • 先获取修改行的数据,根据id来查询到对应菜品信息进行回显

  • 含有菜品口味属性,所以还是要用到DishDto

  • 先在service层编写一个getByIdWithFlavor方法,根据dish_iddish_flavor表中查询,将查询到的菜品口味数据赋给我们的DishDto对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @Override
    public DishDto getByIdWithFlavor(Long id) {
    //先根据id查询到对应的dish对象
    Dish dish = this.getById(id);
    //创建一个dishDao对象
    DishDto dishDto = new DishDto();
    //拷贝对象
    BeanUtils.copyProperties(dish, dishDto);
    //条件构造器,对DishFlavor表查询
    LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
    //根据dish_id来查询对应的菜品口味数据
    queryWrapper.eq(DishFlavor::getDishId, id);
    //获取查询的结果
    List<DishFlavor> flavors = dishFlavorService.list(queryWrapper);
    //并将其赋给dishDto
    dishDto.setFlavors(flavors);
    //作为结果返回给前端
    return dishDto;
    }
  • DishController中添加get方法,实现添加在DishServicelmpl

    中的逻辑代码,返回查询到的数据信息

    1
    2
    3
    4
    5
    6
    @GetMapping("/{id}")
    public Result<DishDto> getByIdWithFlavor(@PathVariable Long id) {
    DishDto dishDto = dishService.getByIdWithFlavor(id);
    log.info("查询到的数据为:{}", dishDto);
    return Result.success(dishDto);
    }
  • 此时重启服务器,就看到数据成功回显在表格中了

    修改菜品信息

由于Dish表中没有Flavor这个属性,所以修改的时候依旧要通过DishDto

修改菜品和添加菜品共用一个界面,根据参数来区分两种操作

1
2
3
4
5
6
7
8
<el-button
type="text"
size="small"
class="blueBug"
@click="addFoodtype(scope.row.id)"
>
修改
</el-button>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 添加
addFoodtype (st) {
if (st === 'add'){
window.parent.menuHandle({
id: '4',
url: '/backend/page/food/add.html',
name: '添加菜品'
},true)
} else {
window.parent.menuHandle({
id: '4',
url: '/backend/page/food/add.html?id='+st,
name: '修改菜品'
},true)
}
1
2
3
4
5
6
7
8
// 修改接口
const editDish = (params) => {
return $axios({
url: '/dish',
method: 'put',
data: { ...params }
})
}
  • 编写后端逻辑

和之前类似,重点是编写updateWithFlavor方法

1
2
3
4
5
6
@PutMapping
public Result<String> update(@RequestBody DishDto dishDto) {
log.info("接收到的数据为:{}", dishDto);
dishService.updateWithFlavor(dishDto);
return Result.success("修改菜品成功");
}

通过id来修改基本信息,通过dish_id来删除口味信息,然后重新传入新的口味信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
public void updateWithFlavor(DishDto dishDto) {
//更新当前菜品数据(dish表)
this.updateById(dishDto);
//下面是更新当前菜品的口味数据
//条件构造器
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
//条件是当前菜品id
queryWrapper.eq(DishFlavor::getDishId, dishDto.getId());
//将其删除掉
dishFlavorService.remove(queryWrapper);
//获取传入的新的口味数据
List<DishFlavor> flavors = dishDto.getFlavors();
//这些口味数据还是没有dish_id,所以需要赋予其dishId
flavors = flavors.stream().map((item) -> {
item.setDishId(dishDto.getId());
return item;
}).collect(Collectors.toList());
//再重新加入到表中
dishFlavorService.saveBatch(flavors);
}

注意要在DishServiceImpl上添加@Transactional注解,同时也要在主启动类上加上@EnableTransactionManagement注解

新增套餐

需求分析

  • 套餐就是多个菜品的集合
  • 后台系统中可以管理套餐信息,通过新增套餐来添加一个新的套餐
  • 在添加套餐时需要选择当前套餐所属的套餐分类和包含的菜品,并且需要上传套餐对应的图片

梳理新增套餐时前端页面与服务端的交互过程:

  1. 页面发送ajax请求,请求服务端,获取套餐分类数据并展示到下拉框中(这个之前做过)
  2. 页面发送ajax请求,请求服务端,获取菜品分类数据并展示到添加菜品窗口中
  3. 页面发送ajax请求,请求服务端,根据菜品分类查询对应的菜品数据并展示到添加菜品窗口中
  4. 页面发送请求进行图片上传,请求服务端将图片保存到服务器(已完成)
  5. 页面发送请求进行图片下载,将上传的图片进行回显(已完成)
  6. 点击保存按钮,发送ajax请求,将套餐相关数据以json形式提交到服务端

开发新增套餐功能,其实就是在服务端编写代码去处理前端页面发送的这6次请求

数据模型

  • 新增套餐,其实就是将新增页面录入的套餐信息插入到setmeal表中,而且还要向setmeal_dish表中插入套餐和菜品关联数据
  • 所以在新增套餐时,需要对两张表进行操作

    1. setmeal表 —> 套餐表
    2. setmeal_dish表 —> 套餐菜品关系表

准备工作

在开发业务功能前,先将需要用到的类和接口基本结构创建好:

  1. 实体类SetmealDish

    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
    /**
    * 套餐菜品关系
    */
    @Data
    public class SetmealDish implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;


    //套餐id
    private Long setmealId;


    //菜品id
    private Long dishId;


    //菜品名称 (冗余字段)
    private String name;

    //菜品原价
    private BigDecimal price;

    //份数
    private Integer copies;


    //排序
    private Integer sort;


    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;


    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;


    @TableField(fill = FieldFill.INSERT)
    private Long createUser;


    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;


    //是否删除
    private Integer isDeleted;
    }
  2. DTO SetmealDto

    普通的SetmealDish类肯定是不够我们用的,这里还需要加上套餐内的具体菜品和套餐分类名称

    1
    2
    3
    4
    5
    6
    7
    @Data
    public class SetmealDto extends Setmeal {

    private List<SetmealDish> setmealDishes;

    private String categoryName;
    }
  3. Mapper接口SetmealDishMapper

    1
    2
    3
    @Mapper
    public interface SetmealDishMapper extends BaseMapper<SetmealDish> {
    }
  4. 业务层接口SetmealDishService

    1
    2
    public interface SetmealDishService extends IService<SetmealDish> {
    }
  5. 业务层实现类SetmealDishservicelmpl

    1
    2
    3
    @Service
    public class SetmealDishServiceImpl extends ServiceImpl<SetmealDishMapper, SetmealDish> implements SetmealDishService {
    }
  6. 控制层SetmealController

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @RestController
    @RequestMapping("/setmeal")
    @Slf4j
    public class SetmealController {
    @Autowired
    private SetmealService setmealService;
    @Autowired
    private SetmealDishService setmealDishService;
    }

    代码开发

新增套餐页面中套餐分类的下拉框能够直接显示套餐分类数据,该功能在之前已经实现

  • 点击添加菜品后可以看到发送了一个dish/list?categoryId=xxx的get请求,请求参数中包含categoryId,根据这个参数查询对应的菜品数据

  • 先去DishController中编写对应的get方法来正确显示菜品数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @GetMapping("/list")
    public Result<List<Dish>> get(Dish dish) {
    //条件查询器
    LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
    //根据传进来的categoryId查询
    queryWrapper.eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId());
    //只查询状态为1的菜品(启售菜品)
    queryWrapper.eq(Dish::getStatus, 1);
    //简单排下序,其实也没啥太大作用
    queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
    //获取查询到的结果作为返回值
    List<Dish> list = dishService.list(queryWrapper);
    return Result.success(list);
    }
  • 前端代码分析

1
<el-button type="primary" @click="submitForm('ruleForm', false)"> 保存 </el-button>

表单和前面一样,是新增和修改共用一个表单。

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
67
68
69
70
71
72
73
74
75
submitForm(formName, st) {
this.$refs[formName].validate((valid) => {
if (valid) {
let prams = { ...this.ruleForm }
prams.price *= 100
prams.setmealDishes = this.dishTable.map((obj) => ({
copies: obj.copies,
dishId: obj.dishId,
name: obj.name,
price: obj.price,
}))
prams.status = this.ruleForm ? 1 : 0
prams.categoryId = this.ruleForm.idType
if(prams.setmealDishes.length < 1){
this.$message.error('请选择菜品!')
return
}
if(!this.imageUrl){
this.$message.error('请上传套餐图片')
return
}
// delete prams.dishList
if (this.actionType == 'add') {
delete prams.id
addSetmeal(prams)
.then((res) => {
if (res.code === 1) {
this.$message.success('套餐添加成功!')
if (!st) {
this.goBack()
} else {
this.$refs.ruleForm.resetFields()
this.dishList = []
this.dishTable = []
this.ruleForm = {
name: '',
categoryId: '',
price: '',
code: '',
image: '',
description: '',
dishList: [],
status: true,
id: '',
idType: '',
}
this.imageUrl = ''
}
} else {
this.$message.error(res.msg || '操作失败')
}
})
.catch((err) => {
this.$message.error('请求出错了:' + err)
})
} else {
delete prams.updateTime
editSetmeal(prams)
.then((res) => {
if (res.code === 1) {
this.$message.success('套餐修改成功!')
this.goBack()
} else {
this.$message.error(res.msg || '操作失败')
}
})
.catch((err) => {
this.$message.error('请求出错了:' + err)
})
}
} else {
return false
}
})
}

1
2
3
4
5
6
7
8
// 新增数据接口
const addSetmeal = (params) => {
return $axios({
url: '/setmeal',
method: 'post',
data: { ...params }
})
}
  • 先来测试一下,查看提交的数据

    1
    2
    3
    4
    5
    @PostMapping
    public Result<String> save(@RequestBody SetmealDto setmealDto) {
    log.info("套餐信息:{}", setmealDto);
    return Result.success("套餐添加成功");
    }

套餐信息:SetmealDto(setmealDishes=[SetmealDish(id=null, setmealId=null, dishId=1397851370462687234, name=邵阳猪血丸子, price=13800, copies=1, sort=null, createTime=null, updateTime=null, createUser=null, updateUser=null, isDeleted=null)], categoryName=null)

此处setmealId为null,再具体的代码中,要从setmealDao中获取并赋值

  • 具体业务逻辑如下
1
2
3
4
5
6
@PostMapping
public Result<String> save(@RequestBody SetmealDto setmealDto) {
log.info("套餐信息:{}", setmealDto);
setmealService.saveWithDish(setmealDto);
return Result.success("套餐添加成功");
}
1
2
3
public interface SetmealService extends IService<Setmeal> {
void saveWithDish(SetmealDto setmealDto);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service
public class SetmealServiceImpl extends ServiceImpl<SetmealMapper, Setmeal> implements SetmealService {
@Autowired
protected SetmealDishService setmealDishService;

@Override
public void saveWithDish(SetmealDto setmealDto) {
this.save(setmealDto);
List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
setmealDishes = setmealDishes.stream().map((item) -> {
item.setSetmealId(setmealDto.getId());
return item;
}).collect(Collectors.toList());
setmealDishService.saveBatch(setmealDishes);
}
}
  • 至此,新增套餐的功能就实现了,重启服务器测试一下,查看数据库,套餐信息和套餐菜品信息都能成功插入

套餐信息分页查询

梳理交互过程

  1. 页面发送ajax请求,将分页查询参数(page,pageSize,name)提交到服务端,获取分页数据
  2. 页面发送请求,请求服务端进行图片下载,用于页面图片展示(已完成)

前端分析

点击套餐管理,在搜索框输入1,获取请求url与请求方式

代码开发

  • 在SetmealController类中,添加list方法,这里和前面的查询方法几乎一致,对比学习
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@GetMapping("/page")
public Result<Page> page(int page, int pageSize, String name) {
Page<Setmeal> pageInfo = new Page<>(page, pageSize);
Page<SetmealDto> dtoPage = new Page<>(page, pageSize);
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.like(name != null, Setmeal::getName, name);
queryWrapper.orderByDesc(Setmeal::getUpdateTime);
setmealService.page(pageInfo, queryWrapper);
BeanUtils.copyProperties(pageInfo, dtoPage, "records");
List<Setmeal> records = pageInfo.getRecords();
List<SetmealDto> list = records.stream().map((item) -> {
SetmealDto setmealDto = new SetmealDto();
BeanUtils.copyProperties(item, setmealDto);
Long categoryId = item.getCategoryId();
Category category = categoryService.getById(categoryId);
if (category != null) {
setmealDto.setCategoryName(category.getName());
}
return setmealDto;
}).collect(Collectors.toList());
dtoPage.setRecords(list);
return Result.success(dtoPage);
}
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
@GetMapping("/page")
public Result<Page> page(int page, int pageSize, String name) {
//构造分页构造器对象
Page<Dish> pageInfo = new Page<>(page, pageSize);
//这个就是我们到时候返回的结果
Page<DishDto> dishDtoPage = new Page<>(page, pageSize);
//条件构造器
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
//添加条件
queryWrapper.like(name != null, Dish::getName, name);
queryWrapper.orderByDesc(Dish::getUpdateTime);
//执行分页查询
dishService.page(pageInfo, queryWrapper);

//对象拷贝,这里只需要拷贝一下查询到的条目数
BeanUtils.copyProperties(pageInfo, dishDtoPage, "records");

//获取原records数据
List<Dish> records = pageInfo.getRecords();

//遍历每一条records数据
List<DishDto> list = records.stream().map((item) -> {
DishDto dishDto = new DishDto();
//将数据赋给dishDto对象
BeanUtils.copyProperties(item, dishDto);
//然后获取一下dish对象的category_id属性
Long categoryId = item.getCategoryId(); //分类id
//根据这个属性,获取到Category对象(这里需要用@Autowired注入一个CategoryService对象)
Category category = categoryService.getById(categoryId);
//随后获取Category对象的name属性,也就是菜品分类名称
String categoryName = category.getName();
//最后将菜品分类名称赋给dishDto对象就好了
dishDto.setCategoryName(categoryName);
//结果返回一个dishDto对象
return dishDto;
//并将dishDto对象封装成一个集合,作为我们的最终结果
}).collect(Collectors.toList());

dishDtoPage.setRecords(list);
return Result.success(dishDtoPage);
}

删除套餐

需求分析

  • 套餐管理列表页面点击删除按钮,可以删除对应的套餐信息
  • 可以通过复选框选择多个套餐,选择批量删除一次性删除多个套餐

注意:在售中的套餐不能删除,需要先停售,然后才能删除

梳理交互过程

  1. 删除单个套餐时,页面发送ajax请求,根据套餐id删除对应套餐
  2. 删除多个套餐时,页面发送ajax请求,根据提交的多个套餐id删除对应套餐开发删除套餐功能

删除单个套餐和批量删除这两种请求的地址和请求方式都是相同的,不同的则是传递的id个数,所以在服务端可以提供一个方法来统一处理。

代码开发

  • SetmealController中添加delete方法

    1
    2
    3
    4
    5
    6
    @DeleteMapping
    public Result<String> deleteByIds(@RequestParam List<Long> ids) {
    log.info("要删除的套餐id为:{}",ids);
    setmealService.removeWithDish(ids);
    return Result.success("删除成功");
    }
  • SetmealService中创建removeWithDish方法

    1
    void removeWithDish(List<Long> ids);
  • SetmealServiceImpl中重写方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    @Override
    public void removeWithDish(List<Long> ids) {
    //先判断一下能不能删,如果status为1,则套餐在售,不能删
    //select * from setmeal where id in (ids) and status = 1
    LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
    setmealLambdaQueryWrapper.in(Setmeal::getId, ids);
    setmealLambdaQueryWrapper.eq(Setmeal::getStatus, 1);
    int count = this.count(setmealLambdaQueryWrapper);
    //下面两行是我debug输出的日志,没啥用
    List<Setmeal> list = this.list(setmealLambdaQueryWrapper);
    log.info("查询到的数据为:{}",list);
    if (count > 0) {
    throw new CustomException("套餐正在售卖中,请先停售再进行删除");
    }
    //如果没有在售套餐,则直接删除
    this.removeByIds(ids);
    //继续删除
    LambdaQueryWrapper<SetmealDish> setmealDishLambdaQueryWrapper = new LambdaQueryWrapper<>();
    setmealDishLambdaQueryWrapper.in(SetmealDish::getSetmealId, ids);
    setmealDishService.remove(setmealDishLambdaQueryWrapper);
    }
  • 重启服务器,并测试

    注意:

    1. 因为此处还没有设置停售功能,所以删除套餐前需要去数据库将status字段设置为0,即可成功删除
    2. 之前在启动类上已经添加了@EnableTransactionManagement注解,所以在SetmealServiceImpl类上添加@Transactional注解就可以实现事务了

邮件发送(替换手机验证)

该处黑马使用的是阿里云短信验证码,但是使用该功能需要一定的费用,且需要申请签名等流程,非常繁琐,所以这里使用别的大佬给的QQ邮箱验证码的方式来完成


  • 手机号(邮箱)是区分不同用户的标识,在用户登录的时候判断所输入的手机号(邮箱)是否存储在表中
  • 如果不在表中,说明该用户为一个新的用户,将该用户自动保在user表中

准备工作

先将要用到的类和接口的基本结构都创建好

  • 实体类User
    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
    /**
    * 用户信息
    */
    @Data
    public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;

    //姓名
    private String name;

    //手机号
    private String phone;

    //性别 0 女 1 男
    private String sex;

    //身份证号
    private String idNumber;

    //头像
    private String avatar;

    //状态 0:禁用,1:正常
    private Integer status;
    }
1
2
3
@Mapper
public interface UserMapper extends BaseMapper {
}
1
2
public interface UserService extends IService<User> {
}
1
2
3
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper,User> implements UserService {
}
  • 控制层UserController

    1
    2
    3
    4
    5
    6
    7
    8
    @RestController
    @Slf4j
    @RequestMapping("/user")
    public class UserController {
    @Autowired
    private UserService userService;

    }
  • 工具类(我们自己造自己的邮箱工具类)

    • 首先导入坐标
      1
      2
      3
      4
      <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-mail</artifactId>
      </dependency>
    • 添加配置文件
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      spring:
      mail:
      password: tzunlrdblmhvijce
      username: 1330132229@qq.com
      host: smtp.qq.com
      port: 465 # 必须是 465 或 587
      properties:
      mail:
      smtp:
      auth: true
      ssl:
      enable: true
  • 然后编写一个工具类,用于发送邮件验证码和随机生成验证码

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
@Component
public class MailUtils {

private static JavaMailSender sender;
@Autowired
public void setSender(JavaMailSender sender) {
MailUtils.sender = sender;
}

public static void sendTestMail(String email, String code) throws MessagingException {
SimpleMailMessage mail = new SimpleMailMessage();
mail.setSubject("瑞吉外卖验证码");
mail.setTo(email);
mail.setFrom("1330132229@qq.com");
mail.setText("您的验证码为:" + code);
sender.send(mail);
System.out.println("邮件发送完毕");
}

public static String achieveCode() { //由于数字 1 、 0 和字母 O 、l 有时分不清楚,所以,没有数字 1 、 0
String[] beforeShuffle = new String[]{"2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F",
"G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "a",
"b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v",
"w", "x", "y", "z"};
List<String> list = Arrays.asList(beforeShuffle);//将数组转换为集合
Collections.shuffle(list); //打乱集合顺序
StringBuilder sb = new StringBuilder();
for (String s : list) {
sb.append(s); //将集合转化为字符串
}
return sb.substring(3, 8);
}
}

修改拦截器

  • 对用户登录操作放行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //定义不需要处理的请求
    String[] urls = new String[]{
    "/employee/login",
    "/employee/logout",
    "/backend/**",
    "/front/**",
    "/common/**",
    //对用户登陆操作放行
    "/user/login",
    "/user/sendMsg"
    };
  • 判断用户是否登录(移动端)

    1
    2
    3
    4
    5
    6
    7
    8
    //判断用户是否登录
    if(request.getSession().getAttribute("user") != null){
    log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("user"));
    Long userId = (Long)request.getSession().getAttribute("user");
    BaseContext.setCurrentId(userId);
    filterChain.doFilter(request,response);
    return;
    }

发送验证码

此处需要重新导入前端资料,将day06中的front资料重新导入一遍
以下是通过网站对比文件区别的结果,之前的login.html文件是直接将生成的验证码赋值给code并显示,现在是调用后端结果来实现功能

将login.html中判断手机号的正则表达式换成判断邮箱的正则表达式:const regex = /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/;

  • 重新导入完资源之后,清除浏览器缓存(或使用无痕浏览),并重启服务器,访问登录页面,获取验证码,这下应该是能收到请求的
  • 从上图中可以看到,发送验证码的请求方式是POST,路径为/user/sendMsg

    • 请求方式:POST
    • 请求路径:/user/sendMsg
  • 那么我们在UserController控制层中,添加sendMsg方法,用来接收指令发送邮件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@PostMapping("/sendMsg")
public Result<String> sendMsg(@RequestBody User user, HttpSession session) throws MessagingException {
// 黑马提供的前端字段是 phone,懒得改了所以这里依旧是getPhone
String mail = user.getPhone();
if (!mail.isEmpty()) {
//随机生成一个验证码
String code = MailUtils.achieveCode();
log.info(code);
//这里的mail其实就是邮箱,code是我们生成的验证码
MailUtils.sendTestMail(mail, code);
//验证码存session,方便后面拿出来比对
session.setAttribute(mail, code);
log.info("验证码:{}", code);
return Result.success("验证码发送成功");
}
return Result.error("验证码发送失败");
}
  • 此时邮箱中就已经能接收到验证码了
  • 输入验证码,点击登录

    • 请求路径为:/user/login,数据以json格式返回给服务端
  • 在UserController控制层中,添加login方法,并使用日志输出一下,看看能否接受到数据

    1
    2
    3
    4
    5
    @PostMapping("/login")
    public Result<String> login(@RequestBody Map map,HttpSession session){
    log.info(map.toString());
    return null;
    }

    com.blog.controller.UserController : {phone=1586385296@qq.com, code=bxQCK}

  • 看样子是可以获取到数据的,那么我们继续完善login方法

    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
    @PostMapping("/login")
    public Result<User> login(@RequestBody Map map, HttpSession session) {
    log.info(map.toString());
    //获取邮箱
    String phone = map.get("phone").toString();
    //获取验证码
    String code = map.get("code").toString();
    //从session中获取验证码
    String codeInSession = session.getAttribute(phone).toString();
    //比较这用户输入的验证码和session中存的验证码是否一致
    if (code != null && code.equals(codeInSession)) {
    //如果输入正确,判断一下当前用户是否存在
    LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
    //判断依据是从数据库中查询是否有其邮箱
    queryWrapper.eq(User::getPhone, phone);
    User user = userService.getOne(queryWrapper);
    //如果不存在,则创建一个,存入数据库
    if (user == null) {
    user = new User();
    user.setPhone(phone);
    userService.save(user);
    user.setName("用户" + codeInSession);
    }
    //存个session,表示登录状态
    session.setAttribute("user",user.getId());
    //并将其作为结果返回
    return Result.success(user);
    }
    return Result.error("登录失败");
    }
  • 此时输入验证码就能完成登录了。

    发送验证码大概会有10秒钟的延迟,一定要等待验证码发送成功再登录,否则此时session为空会报错

地址簿

需求分析

  • 地址簿,指的是移动端消费者用户的地址信息(外卖快递的收货地址)
  • 用户登录成功后可以维护自己的地址信息(自己修改删除新增等)
  • 同一个用户可以有多个地址信息,但是只能有一个默认地址。

数据模型

注意这里黑马提供的phone类型为varchar(11),显然邮箱长度更长,因此需要自己将该字段改大一点。

准备工作

  1. 创建对应的实体类AddressBook

    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
    67
    68
    /**
    * 地址簿
    */
    @Data
    public class AddressBook implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;

    //用户id
    private Long userId;

    //收货人
    private String consignee;

    //手机号
    private String phone;

    //性别 0 女 1 男
    private String sex;

    //省级区划编号
    private String provinceCode;

    //省级名称
    private String provinceName;

    //市级区划编号
    private String cityCode;

    //市级名称
    private String cityName;

    //区级区划编号
    private String districtCode;

    //区级名称
    private String districtName;

    //详细地址
    private String detail;

    //标签
    private String label;

    //是否默认 0否 1是
    private Integer isDefault;

    //创建时间
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    //更新时间
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;

    //创建人
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;

    //修改人
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;

    //是否删除
    private Integer isDeleted;
    }
  2. Mapper接口AddressBookMapper

    1
    2
    3
    @Mapper
    public interface AddressBookMapper extends BaseMapper<AddressBook> {
    }
  3. 业务层接口AddressBookService

    1
    2
    public interface AddressBookService extends IService<AddressBook> {
    }
  4. 业务层实现类AddressBookServicelmpl

    1
    2
    3
    @Service
    public class AddressBookServiceImpl extends ServiceImpl<AddressBookMapper, AddressBook> implements AddressBookService {
    }
  5. 控制层AddressBookController

    1
    2
    3
    4
    5
    6
    7
    8
    @RestController
    @Slf4j
    @RequestMapping("/addressBook")
    public class AddressBookController {

    @Autowired
    private AddressBookService addressBookService;
    }

完善地址管理页面

  • 点击头像下的地址管理,查看请求方式与地址
  • AddressBookController中编写对应的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @GetMapping("/list")
    public Result<List<AddressBook>> list(AddressBook addressBook) {
    addressBook.setUserId(BaseContext.getCurrentId());
    log.info("addressBook={}", addressBook);

    //条件构造器
    LambdaQueryWrapper<AddressBook> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(addressBook.getUserId() != null, AddressBook::getUserId, addressBook.getUserId());
    queryWrapper.orderByDesc(AddressBook::getUpdateTime);

    List<AddressBook> addressBooks = addressBookService.list(queryWrapper);
    return Result.success(addressBooks);
    }
  • 不过写完了暂时还是不能看到效果的,数据库中并没有添加对应账号的数据,所以我们继续来做新增收货地址功能

新增收货地址

  • 修改前端代码
    这段代码是新增地址的前端代码,将其中的手机号全部替换成邮箱,判断手机号的正则也换成判断邮箱的正则,也可以直接复制这段代码更换掉front/page/address-edit.html文件中的代码

    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
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0,user-scalable=no,minimal-ui">
    <title>菩提阁</title>
    <link rel="icon" href="./../images/favico.ico">
    <!--不同屏幕尺寸根字体设置-->
    <script src="./../js/base.js"></script>
    <!--element-ui的样式-->
    <link rel="stylesheet" href="../../backend/plugins/element-ui/index.css" />
    <!--引入vant样式-->
    <link rel="stylesheet" href="../styles/vant.min.css"/>
    <!-- 引入样式 -->
    <link rel="stylesheet" href="../styles/index.css" />
    <!--本页面内容的样式-->
    <link rel="stylesheet" href="./../styles/address-edit.css" />
    </head>
    <body>
    <div id="address_edit" class="app">
    <div class="divHead">
    <div class="divTitle">
    <i class="el-icon-arrow-left" @click="goBack"></i>{{title}}
    </div>
    </div>
    <div class="divContent">
    <div class="divItem">
    <span>联系人:</span>
    <el-input placeholder=" 请填写收货人的姓名" v-model="form.consignee" maxlength='10' class="inputUser"/></el-input>
    <span class="spanChecked" @click="form.sex = '1'">
    <i :class="{iActive:form.sex === '1'}"></i>
    先生
    </span>
    <span class="spanChecked" @click="form.sex = '0'">
    <i :class="{iActive:form.sex === '0'}"></i>
    女士
    </span>
    </div>
    <div class="divItem">
    <span>邮箱:</span>
    <el-input placeholder=" 请填写收货人邮箱" v-model="form.phone" maxlength='20' style="width: calc(100% - 80rem);"/></el-input>
    </div>
    <div class="divItem">
    <span>收货地址:</span>
    <el-input placeholder=" 请输入收货地址" v-model="form.detail" maxlength='140'/></el-input>
    </div>
    <div class="divItem ">
    <span>标签:</span>
    <span v-for="(item,index) in labelList" :key="index" @click="form.label = item;activeIndex = index" :class="{spanItem:true,spanActiveSchool:activeIndex === index}">{{item}}</span>
    </div>
    <div class="divSave" @click="saveAddress">保存地址</div>
    <div class="divDelete" @click="deleteAddress" v-if="id">删除地址</div>
    </div>
    </div>
    <!-- 开发环境版本,包含了有帮助的命令行警告 -->
    <script src="../../backend/plugins/vue/vue.js"></script>
    <!-- 引入组件库 -->
    <script src="../../backend/plugins/element-ui/index.js"></script>
    <!-- 引入vant样式 -->
    <script src="./../js/vant.min.js"></script>
    <script src="./../js/common.js"></script>
    <script src="./../api/address.js"></script>
    <!-- 引入axios -->
    <script src="../../backend/plugins/axios/axios.min.js"></script>
    <script src="./../js/request.js"></script>
    <script>
    new Vue({
    el:"#address_edit",
    data(){
    return {
    title:'新增收货地址',
    form:{
    consignee:'',//联系人
    phone:undefined,//手机号
    sex:'1',//0表示女 1 表示男
    detail:'',//收货地址
    label:'公司',//标签
    },
    labelList:[
    '无','公司','家','学校'
    ],
    id:undefined,
    activeIndex :0
    }
    },
    computed:{},
    created(){
    this.initData()
    },
    mounted(){
    },
    methods:{
    goBack(){
    history.go(-1)
    },
    async initData(){
    const params = parseUrl(window.location.search)
    this.id = params.id
    if(params.id){
    this.title = '编辑收货地址'
    const res = await addressFindOneApi(params.id)
    if(res.code === 1){
    this.form = res.data
    }else{
    this.$notify({ type:'warning', message:res.msg});
    }
    }
    },
    async saveAddress(){
    const form = this.form
    if(!form.consignee){
    this.$notify({ type:'warning', message:'请输入联系人'});
    return
    }
    if(!form.phone){
    this.$notify({ type:'warning', message:'请输入邮箱'});
    return
    }
    if(!form.detail){
    this.$notify({ type:'warning', message:'请输入收货地址'});
    return
    }
    const reg = /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/
    if(!reg.test(form.phone)){
    this.$notify({ type:'warning', message:'邮箱不合法'});
    return
    }
    let res= {}
    if(this.id){
    res = await updateAddressApi(this.form)
    }else{
    res = await addAddressApi(this.form)
    }

    if(res.code === 1){
    window.requestAnimationFrame(()=>{
    window.location.replace('/front/page/address.html')
    })
    }else{
    this.$notify({ type:'warning', message:res.msg});
    }
    },
    deleteAddress(){
    this.$dialog.confirm({
    title: '确认删除',
    message: '确认要删除当前地址吗?',
    })
    .then( async () => {
    const res = await deleteAddressApi({ids:this.id })
    if(res.code === 1){
    window.requestAnimationFrame(()=>{
    window.location.replace('/front/page/address.html')
    })
    }else{
    this.$notify({ type:'warning', message:res.msg});
    }
    })
    .catch(() => {
    });
    },
    }
    })
    </script>
    </body>
    </html>
  • 填写表单,点击保存,发送请求

请求网址: http://localhost/addressBook
请求方法: POST

  • 请求路径是/addressBook,请求方式为POST,那么我们在AddressBookController中编写对应的方法

    1
    2
    3
    4
    5
    6
    7
    @PostMapping
    public Result<AddressBook> addAddress(@RequestBody AddressBook addressBook) {
    addressBook.setUserId(BaseContext.getCurrentId());
    log.info("addressBook:{}", addressBook);
    addressBookService.save(addressBook);
    return Result.success(addressBook);
    }
  • 添加完地址后的效果

设置默认地址

  • 先来想想怎么设置默认地址
    • 默认地址,按理说数据库中,有且仅有一条数据为默认地址,也就是is_default字段为1
  • 如何保证整个表中的is_default字段只有一条为1

    • 每次设置默认地址的时候,将当前用户所有地址的is_default字段设为0,随后将当前地址的is_default字段设为1
  • 当我们点击上图的设为默认按钮的时候,会发送请求

    请求网址: http://localhost/addressBook/default
    请求方法: PUT

  • 请求路径为/addressBook/default,请求方式为PUT,现在在AddressBookController中编写对应的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @PutMapping("/default")
    public Result<AddressBook> setDefaultAddress(@RequestBody AddressBook addressBook) {
    //获取当前用户id
    addressBook.setUserId(BaseContext.getCurrentId());
    //条件构造器
    LambdaUpdateWrapper<AddressBook> queryWrapper = new LambdaUpdateWrapper<>();
    //条件:当前用户的地址
    queryWrapper.eq(addressBook.getUserId() != null, AddressBook::getUserId, addressBook.getUserId());
    //将当前用户地址的is_default字段全部设为0
    queryWrapper.set(AddressBook::getIsDefault, 0);
    //执行更新操作
    addressBookService.update(queryWrapper);
    //随后再将当前地址的is_default字段设为1
    addressBook.setIsDefault(1);
    //再次执行更新操作
    addressBookService.updateById(addressBook);
    return Result.success(addressBook);
    }

这里的条件构造器是LambdaUpdateWrapper,而不是我们前面经常用的LambdaQueryWrapper

菜品展示

需求分析

  • 用户登陆成功之后,跳转到菜品页面,根据菜品分类来展示菜品和套餐
  • 如果菜品设置了口味信息,则需要展示选择规格按钮,否则只展示+按钮(这部分前端来实现)

梳理交互过程

  1. 页面(front/index.html)发送ajax请求,获取分类数据(菜品分类和套餐分类)
  2. 页面发送ajax请求,根据具体的菜品/套餐分类,展示对应分类中的具体菜品

前端分析

  • 启动服务器,登录账号,看到登录到首页会发送两个请求

    • 分类

      请求网址: http://localhost/category/list
      请求方法: GET

    • 购物车

      请求网址: http://localhost/shoppingCart/list
      请求方法: GET

  • 其中分类请求之前就写过了,且返回的状态码是200,但是页面并没有显示出来,看看前端代码来寻找原因:

Promise.all在处理多个异步请求时,需要等待绑定的每个ajax请求返回数据以后才能正常显示
虽然categoryListApi可以正常返回数据,但是cartListApi还不能,因为现在才正要开始处理,因此导致菜品数据没有正常显示

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
//初始化数据
initData(){
Promise.all([categoryListApi(),cartListApi({})]).then(res=>{
//获取分类数据
if(res[0].code === 1){
this.categoryList = res[0].data
if(Array.isArray(res[0].data) && res[0].data.length > 0){
this.categoryId = res[0].data[0].id
if(res[0].data[0].type === 1){
this.getDishList()
}else{
this.getSetmealData()
}
}
}else{
this.$notify({ type:'warning', message:res[0].msg});
}
//获取菜品数据
if(res[1].code === 1){
this.cartData = res[1].data
}else{
this.$notify({ type:'warning', message:res[1].msg});
}
})
}

该请求路径之前写过,能够正常返回

1
2
3
4
5
6
7
//获取所有的菜品分类
function categoryListApi() {
return $axios({
'url': '/category/list',
'method': 'get',
})
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@GetMapping("/list")
public Result<List<Dish>> get(Dish dish) {
//条件查询器
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
//根据传进来的categoryId查询
queryWrapper.eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId());
//只查询状态为1的菜品(在售菜品)
queryWrapper.eq(Dish::getStatus, 1);
//简单排下序
queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
//获取查询到的结果作为返回值
List<Dish> list = dishService.list(queryWrapper);
return Result.success(list);
}

这里是因为购物车数据还没有写,因此应当先将url设置为一个空的json文件,先让页面正常显示

1
2
3
4
5
6
7
8
9
//获取购物车内商品的集合
function cartListApi(data) {
return $axios({
'url': '/shoppingCart/list',
//'url': '/front/cartData.json',
'method': 'get',
params: {...data}
})
}

1
{"code":1,"msg":null,"data":[],"map":{}}
  • 此时再次重启服务器,打开无痕模式,看看首页是否能显示分类数据
  • 现在还存在一个问题,我们的菜品是有口味数据的,那么这里的按钮不该是一个+,而应该是选择规格

    1
    2
    <div class="divTypes" v-if="detailsDialog.item.flavors && detailsDialog.item.flavors.length > 0 && !detailsDialog.item.number " 
    @click ="chooseFlavorClick(detailsDialog.item)">选择规格</div>

    通过前端代码可以分析得出,根据服务器返回的结果是否有flavors字段来决定,但此时在后端返回的是List<Dish>,其中并没有flavors属性,所以应该修改为返回DishDto

    选择规格

  • 修改原本的list方法

    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
    @GetMapping("/list")
    public Result<List<DishDto>> get(Dish dish) {
    //条件查询器
    LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
    //根据传进来的categoryId查询
    queryWrapper.eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId());
    //只查询状态为1的菜品(在售菜品)
    queryWrapper.eq(Dish::getStatus, 1);
    //简单排下序,其实也没啥太大作用
    queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
    //获取查询到的结果作为返回值
    List<Dish> list = dishService.list(queryWrapper);
    log.info("查询到的菜品信息list:{}",list);
    //item就是list中的每一条数据,相当于遍历了
    List<DishDto> dishDtoList = list.stream().map((item) -> {
    //创建一个dishDto对象
    DishDto dishDto = new DishDto();
    //将item的属性全都copy到dishDto里
    BeanUtils.copyProperties(item, dishDto);
    //由于dish表中没有categoryName属性,只存了categoryId
    Long categoryId = item.getCategoryId();
    //所以我们要根据categoryId查询对应的category
    Category category = categoryService.getById(categoryId);
    if (category != null) {
    //然后取出categoryName,赋值给dishDto
    dishDto.setCategoryName(category.getName());
    }
    //然后获取一下菜品id,根据菜品id去dishFlavor表中查询对应的口味,并赋值给dishDto
    Long itemId = item.getId();
    //条件构造器
    LambdaQueryWrapper<DishFlavor> lambdaQueryWrapper = new LambdaQueryWrapper<>();
    //条件就是菜品id
    lambdaQueryWrapper.eq(itemId != null, DishFlavor::getDishId, itemId);
    //根据菜品id,查询到菜品口味
    List<DishFlavor> flavors = dishFlavorService.list(lambdaQueryWrapper);
    //赋给dishDto的对应属性
    dishDto.setFlavors(flavors);
    //并将dishDto作为结果返回
    return dishDto;
    //将所有返回结果收集起来,封装成List
    }).collect(Collectors.toList());
    return Result.success(dishDtoList);
    }
  • 至此,菜品展示功能就做好了

套餐展示

  • 菜品展示和套餐展示类似,但用的不是同一个controller,因此还需要再设置一遍

    请求网址: http://localhost/setmeal/list?categoryId=1413342269393674242&status=1
    请求方法: GET

  • 那么我们现在就在SetmealController中编写对应的方法
    套餐没有口味数据,因此更加简单

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @GetMapping("/list")
    public Result<List<Setmeal>> list(Setmeal setmeal) {
    //条件构造器
    LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
    //添加条件
    queryWrapper.eq(setmeal.getCategoryId() != null, Setmeal::getCategoryId, setmeal.getCategoryId());
    queryWrapper.eq(setmeal.getStatus() != null, Setmeal::getStatus, 1);
    //排序
    queryWrapper.orderByDesc(Setmeal::getUpdateTime);
    List<Setmeal> setmealList = setmealService.list(queryWrapper);
    return Result.success(setmealList);
    }

购物车

需求分析

  • 移动端用户可以将菜品/套餐添加到购物车
  • 对于菜品来说,如果设置了口味信息,则需要选择规格后才能加入购物车(前端实现)
  • 对于套餐来说,可以直接点击当前套餐加入购物车
  • 在购物车中可以修改菜品/套餐的数量,也可以清空购物车

数据模型

梳理交互过程

  1. 点击加入购物车按钮,页面发送ajax请求,请求服务端,将菜品/套餐添加到购物车
  2. 点击购物车图标,页面发送ajax请求,请求服务端,查询购物车中的菜品和套餐
  3. 点击清空购物车按钮,页面发送ajax请求,请求服务端来执行清空购物车操作

准备工作

在开发业务功能之前,先将需要用到的类和接口的基本结构都创建好

  1. 实体类ShoppingCart

    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
    /**
    * 购物车
    */
    @Data
    public class ShoppingCart implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;

    //名称
    private String name;

    //用户id
    private Long userId;

    //菜品id
    private Long dishId;

    //套餐id
    private Long setmealId;

    //口味
    private String dishFlavor;

    //数量
    private Integer number;

    //金额
    private BigDecimal amount;

    //图片
    private String image;

    private LocalDateTime createTime;
    }
  2. Mapper接口ShoppingCartMapper

    1
    2
    3
    @Mapper
    public interface ShoppingCartMapper extends BaseMapper<ShoppingCart> {
    }
  3. 业务层接口ShoppingCartService

    1
    2
    public interface ShoppingCartService extends IService<ShoppingCart> {
    }
  4. 业务层实现类ShoppingCartServiceImpl

    1
    2
    3
    @Service
    public class ShoppingCartServiceImpl extends ServiceImpl<ShoppingCartMapper, ShoppingCart> implements ShoppingCartService {
    }
  5. 控制层ShoppingCartController

    1
    2
    3
    4
    5
    6
    7
    @RestController
    @Slf4j
    @RequestMapping("/shoppingCart")
    public class ShoppingCartController {
    @Autowired
    private ShoppingCartService shoppingCartService;
    }

代码开发

加入购物车

  • 那么我们在ShoppingCartController添加对应的方法
    养成随时测试的习惯

    1
    2
    3
    4
    5
    @PostMapping("/add")
    public Result<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart){
    log.info("购物车添加信息:{}",shoppingCart);
    return null;
    }
  • 完善业务逻辑

    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
    @PostMapping("/add")
    public Result<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart) {
    log.info("shoppingCart={}", shoppingCart);
    //获取当前用户id
    Long currentId = BaseContext.getCurrentId();
    //设置当前用户id
    shoppingCart.setUserId(currentId);
    //获取当前菜品id
    Long dishId = shoppingCart.getDishId();
    //条件构造器
    LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
    //判断添加的是菜品还是套餐
    if (dishId != null) {
    queryWrapper.eq(ShoppingCart::getDishId, dishId);
    } else {
    queryWrapper.eq(ShoppingCart::getSetmealId, shoppingCart.getSetmealId());
    }
    //查询当前菜品或者套餐是否在购物车中
    ShoppingCart cartServiceOne = shoppingCartService.getOne(queryWrapper);
    if (cartServiceOne != null) {
    //如果已存在就在当前的数量上加1
    Integer number = cartServiceOne.getNumber();
    cartServiceOne.setNumber(number + 1);
    shoppingCartService.updateById(cartServiceOne);
    } else {
    //如果不存在,则还需设置一下创建时间
    shoppingCart.setCreateTime(LocalDateTime.now());
    //如果不存在,则添加到购物车,数量默认为1
    shoppingCartService.save(shoppingCart);
    //这里是为了统一结果,最后都返回cartServiceOne会比较方便
    cartServiceOne = shoppingCart;
    }
    return Result.success(cartServiceOne);
    }
  • 此时可以测试能否成功添加购物车,不过现在还不会显示,因为之前设置前端页面为死数据,现在可以去数据库查看购物车的数据是否添加上

查看购物车

  • 之前为了页面展示不报错,直接将购物车的地址换成了一个死数据,现在购物车的功能以及完成,因此应当换回真数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    //获取购物车内商品的集合
    function cartListApi(data) {
    return $axios({
    // 'url': '/shoppingCart/list',
    'url': '/front/cartData.json',
    'method': 'get',
    params: {...data}
    })
    }

请求路径:http://localhost/shoppingCart/list
请求方式:GET

  • ShoppingCartController中添加对应的方法

    1
    2
    3
    4
    5
    6
    7
    8
    @GetMapping("/list")
    public Result<List<ShoppingCart>> list() {
    LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
    Long userId = BaseContext.getCurrentId();
    queryWrapper.eq(ShoppingCart::getUserId, userId);
    List<ShoppingCart> shoppingCarts = shoppingCartService.list(queryWrapper);
    return Result.success(shoppingCarts);
    }
  • 此时就可以添加菜品/套餐到购物车中了,不过现在还不能完成购物车的其他功能,比如减少购物车中的数量,删除购物车中的商品等

清空购物车

  • 点击清空按钮,发现此时又发送了请求

    请求网址: http://localhost/shoppingCart/clean
    请求方法: DELETE

  • 清空购物车比较简单,只需要获取用户id,然后去shopping__cart表中删除对应id的数据即可
    ShoppingCartController中编写对应的方法

1
2
3
4
5
6
7
8
9
10
11
@DeleteMapping("/clean")
public Result<String> clean() {
//条件构造器
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
//获取当前用户id
Long userId = BaseContext.getCurrentId();
queryWrapper.eq(userId != null, ShoppingCart::getUserId, userId);
//删除当前用户id的所有购物车数据
shoppingCartService.remove(queryWrapper);
return Result.success("成功清空购物车");
}

用户下单

需求分析

  • 移动端用户将菜品或者套餐加入购物车后,可以点击购物车中的去结算按钮,页面跳转到订单确认页面,点击去支付按钮,完成下单操作

数据模型

户下单业务对应的数据表为orders表和order_detail

梳理交互过程

  1. 在购物车中点击去结算按钮,页面跳转到订单确认页面
  2. 在订单确认页面中,发送ajax请求,请求服务端,获取当前登录用户的默认地址
  3. 在订单确认页面,发送ajax请求,请求服务端,获取当前登录用户的购物车数据
  4. 在订单确认页面点击去支付按钮,发送ajax请求,请求服务端,完成下单操作

准备工作

  1. 实体类OrdersOrderDetail
    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
    /**
    * 订单
    */
    @Data
    public class Orders implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;

    //订单号
    private String number;

    //订单状态 1待付款,2待派送,3已派送,4已完成,5已取消
    private Integer status;


    //下单用户id
    private Long userId;

    //地址id
    private Long addressBookId;


    //下单时间
    private LocalDateTime orderTime;


    //结账时间
    private LocalDateTime checkoutTime;


    //支付方式 1微信,2支付宝
    private Integer payMethod;


    //实收金额
    private BigDecimal amount;

    //备注
    private String remark;

    //用户名
    private String userName;

    //手机号
    private String phone;

    //地址
    private String address;

    //收货人
    private String consignee;
    }
    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
    /**
    * 订单明细
    */
    @Data
    public class OrderDetail implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;

    //名称
    private String name;

    //订单id
    private Long orderId;


    //菜品id
    private Long dishId;


    //套餐id
    private Long setmealId;


    //口味
    private String dishFlavor;


    //数量
    private Integer number;

    //金额
    private BigDecimal amount;

    //图片
    private String image;
    }
  2. Mapper接口OrdersMapperOrderDetailMapper
    1
    2
    3
    @Mapper
    public interface OrderMapper extends BaseMapper<Orders> {
    }
    1
    2
    3
    @Mapper
    public interface OrderDetailMapper extends BaseMapper<OrderDetail> {
    }
  3. Service接口OrdersServiceOrderDetailService
    1
    2
    public interface OrderService extends IService<Orders> {
    }
    1
    2
    public interface OrderDetailService extends IService<OrderDetail> {
    }
  4. Service实现类OrdersServiceImplOrderDetailServiceImpl
    1
    2
    3
    @Service
    public class OrderServiceImpl extends ServiceImpl<OrderMapper, Orders> implements OrderService {
    }
    1
    2
    3
    @Service
    public class OrderDetailServiceImpl extends ServiceImpl<OrderDetailMapper, OrderDetail> implements OrderDetailService {
    }
  5. Controller接口OrdersControllerOrderDetailController
    1
    2
    3
    4
    5
    6
    7
    @RestController
    @Slf4j
    @RequestMapping("/order")
    public class OrderController {
    @Autowired
    private OrderService orderService;
    }
    1
    2
    3
    4
    5
    6
    7
    @RestController
    @Slf4j
    @RequestMapping("/orderDetail")
    public class OrderDetailController {
    @Autowired
    private OrderDetailService orderDetailService;
    }

前端分析

  • 点击去结算按钮,然后查看发送的请求url和方式

    请求网址: http://localhost/addressBook/default
    请求方法: GET

  • 页面跳转到确认订单页面,发送ajax请求,用于获取用户的默认地址,但是请求失败,服务端没有对应的映射

  • 根据请求路径/addressBook/default,请求方式GET,进入到AddressBookController编写响应方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @GetMapping("/default")
    public Result<AddressBook> defaultAddress() {
    //获取当前用户id
    Long userId = BaseContext.getCurrentId();
    //条件构造器
    LambdaQueryWrapper<AddressBook> queryWrapper = new LambdaQueryWrapper<>();
    //当前用户
    queryWrapper.eq(userId != null, AddressBook::getUserId, userId);
    //默认地址
    queryWrapper.eq(AddressBook::getIsDefault, 1);
    AddressBook addressBook = addressBookService.getOne(queryWrapper);
    return Result.success(addressBook);
    }
  • 重启服务器,再次点击按钮就能看到地址了

结算

  • 继续点击去支付,查看发送的请求url与请求方式

    请求网址: http://localhost/order/submit
    请求方法: POST

  • 提交给服务端的数据格式为JSON

  • 请求路径/order/submit,请求方式POST,现在去OrderController中开发对应的功能
    具体的submit方法放在OrderService写,OrderController调用写好的submit方法即可
1
2
3
public interface OrderService extends IService<Orders> {
void submit(Orders orders);
}
1
2
3
4
5
6
7
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Orders> implements OrderService {
@Override
public void submit(Orders orders) {

}
}

养成测试接收数据的习惯

1
2
3
4
5
6
@PostMapping("/submit")
public Result<String> submit(@RequestBody Orders orders) {
log.info("orders:{}", orders);
orderService.submit(orders);
return Result.success("用户下单成功");
}

1
2
3
public interface OrderService extends IService<Orders> {
void submit(Orders orders);
}

此时可以测试一下是否能接收到数据

orders:Orders(id=null, number=null, status=null, userId=null, addressBookId=1986397979540123650, orderTime=null, checkoutTime=null, payMethod=1, amount=null, remark=, userName=null, phone=null, address=null, consignee=null)

  • 编写具体的submit方法的逻辑代码,需要先分析下单功能需要获取哪些数据

    • 获取当前用户id
    • 根据用户id查询其购物车数据
    • 根据查询到的购物车数据,对订单表插入数据(1条)
    • 根据查询到的购物车数据,对订单明细表插入数据(多条)

    • 下单后清空购物车数据

具体实现:

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Orders> implements OrderService {

@Autowired
private ShoppingCartService shoppingCartService;

@Autowired
private UserService userService;

@Autowired
private AddressBookService addressBookService;

@Autowired
private OrderDetailService orderDetailService;

@Override
public void submit(Orders orders) {
//获取当前用户id
Long userId = BaseContext.getCurrentId();
//条件构造器
LambdaQueryWrapper<ShoppingCart> shoppingCartLambdaQueryWrapper = new LambdaQueryWrapper<>();
//根据当前用户id查询其购物车数据
shoppingCartLambdaQueryWrapper.eq(userId != null, ShoppingCart::getUserId, userId);
List<ShoppingCart> shoppingCarts = shoppingCartService.list(shoppingCartLambdaQueryWrapper);
//判断一下购物车是否为空
if (shoppingCarts == null) {
throw new CustomException("购物车数据为空,不能下单");
}
//判断一下地址是否有误
Long addressBookId = orders.getAddressBookId();
AddressBook addressBook = addressBookService.getById(addressBookId);
if (addressBookId == null) {
throw new CustomException("地址信息有误,不能下单");
}
//获取用户信息,为了后面赋值
User user = userService.getById(userId);
long orderId = IdWorker.getId();
AtomicInteger amount = new AtomicInteger(0);
//向订单细节表设置属性
List<OrderDetail> orderDetailList= shoppingCarts.stream().map((item) -> {
OrderDetail orderDetail = new OrderDetail();
orderDetail.setOrderId(orderId);
orderDetail.setName(item.getName());
orderDetail.setImage(item.getImage());
orderDetail.setDishId(item.getDishId());
orderDetail.setSetmealId(item.getSetmealId());
orderDetail.setDishFlavor(item.getDishFlavor());
orderDetail.setNumber(item.getNumber());
orderDetail.setAmount(item.getAmount());
amount.addAndGet(item.getAmount().multiply(new BigDecimal(item.getNumber())).intValue());

return orderDetail;
}).collect(Collectors.toList());

//向订单表设置属性
orders.setId(orderId);
orders.setNumber(String.valueOf(orderId));
orders.setStatus(2);
orders.setUserId(userId);
orders.setAddressBookId(addressBookId);
orders.setOrderTime(LocalDateTime.now());
orders.setCheckoutTime(LocalDateTime.now());
orders.setAmount(new BigDecimal(amount.get()));
orders.setPhone(addressBook.getPhone());
orders.setUserName(user.getName());
orders.setConsignee(addressBook.getConsignee());
orders.setAddress(
(addressBook.getProvinceName() == null ? "":addressBook.getProvinceName())+
(addressBook.getCityName() == null ? "":addressBook.getCityName())+
(addressBook.getDistrictName() == null ? "":addressBook.getDistrictName())+
(addressBook.getDetail() == null ? "":addressBook.getDetail())
);

//根据查询到的购物车数据,对订单表插入数据(1条)
super.save(orders);
//根据查询到的购物车数据,对订单明细表插入数据(多条)
orderDetailService.saveBatch(orderDetailList);
//清空购物车数据
shoppingCartService.remove(shoppingCartLambdaQueryWrapper);
}
}

  • 代码量很多,但是大部分都是赋值操作,由于购物车数据与订单数据和订单详情的重复字段不是很多,所以这里就没采用BeanUtils.copyProperties()来复制属性,而是自己一个一个set的

  • 重启服务器,测试结算按钮,下单后就可以在数据库的orders表中找到相关数据了

移动端补充功能

历史订单功能

  • 每次访问个人中心/历史订单时,都会发送请求

    请求网址: http://localhost/order/userPage?page=1&pageSize=1
    请求方法: GET

  • 根据请求网址,看样子是个分页请求,我们之前把订单数据存进了order表中,那么该功能,大概率就是从表中查出数据然后返回给前端

  • 直接在OrderController中编写对应的方法

    • 在此之前,先创建一个OrderDto,传输固定的属性

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      @Data
      public class OrdersDto extends Orders {

      private String userName;

      private String phone;

      private String address;

      private String consignee;

      private List<OrderDetail> orderDetails;

      }
    • 分页代码跟之前的也没啥区别,反复练习更有印象

      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
      @GetMapping("/userPage")
      public Result<Page> page(int page, int pageSize) {
      //获取当前id
      Long userId = BaseContext.getCurrentId();
      Page<Orders> pageInfo = new Page<>(page, pageSize);
      Page<OrdersDto> ordersDtoPage = new Page<>(page, pageSize);
      //条件构造器
      LambdaQueryWrapper<Orders> queryWrapper = new LambdaQueryWrapper<>();
      //查询当前用户id订单数据
      queryWrapper.eq(userId != null, Orders::getUserId, userId);
      //按时间降序排序
      queryWrapper.orderByDesc(Orders::getOrderTime);
      orderService.page(pageInfo, queryWrapper);
      List<OrdersDto> list = pageInfo.getRecords().stream().map((item) -> {
      OrdersDto ordersDto = new OrdersDto();
      //获取orderId,然后根据这个id,去orderDetail表中查数据
      Long orderId = item.getId();
      LambdaQueryWrapper<OrderDetail> wrapper = new LambdaQueryWrapper<>();
      wrapper.eq(OrderDetail::getOrderId, orderId);
      List<OrderDetail> details = orderDetailService.list(wrapper);
      BeanUtils.copyProperties(item, ordersDto);
      //之后set一下属性
      ordersDto.setOrderDetails(details);
      return ordersDto;
      }).collect(Collectors.toList());
      BeanUtils.copyProperties(pageInfo, ordersDtoPage, "records");
      ordersDtoPage.setRecords(list);
      //日志输出看一下
      log.info("list:{}", list);
      return Result.success(ordersDtoPage);
      }

登出功能

  • 这个应该算简单的了吧,清除该用户的session记录即可,点击退出登录,请求如下

    请求网址: http://localhost/user/loginout
    请求方法: POST

  • 请求路径/user/loginout,请求方式POST
    所以我们应该去UserController中编写对应的方法

    1
    2
    3
    4
    5
    @PostMapping("/loginout")
    public Result<String> logout(HttpServletRequest request) {
    request.getSession().removeAttribute("user");
    return Result.success("退出成功");
    }

前提login方法的是写的setAttribute("user", user.getId());,字段名要对上

修改/删除地址

  • 数据回显

    • 点击地址选项卡的铅笔图案,会发送修改请求并跳转到修改地址页面

      请求网址: http://localhost:8080/addressBook/1986395330853879810
      请求方法: GET

    • 可以看到请求地址非常像Restful风格,因此推测请求路径大概率为/addressBook/{id}

    • AddressBookController中编写对应的方法

      1
      2
      3
      4
      5
      6
      7
      8
      @GetMapping("/{id}")
      public Result<AddressBook> getById(@PathVariable Long id) {
      AddressBook addressBook = addressBookService.getById(id);
      if (addressBook == null){
      throw new CustomException("地址信息不存在");
      }
      return Result.success(addressBook);
      }
  • 此时再点击修改按钮,就可以看到该地址的数据回显到修改页面了
  • 修改地址

    • 点击上图中的保存地址按钮,查看发送的请求

      请求网址: http://localhost/addressBook
      请求方法: PUT

    • 请求方式PUT,直接在AddressBookController中编写对应的方法

      1
      2
      3
      4
      5
      6
      7
      8
      @PutMapping
      public Result<String> updateAdd(@RequestBody AddressBook addressBook) {
      if (addressBook == null) {
      throw new CustomException("地址信息不存在,请刷新重试");
      }
      addressBookService.updateById(addressBook);
      return Result.success("地址修改成功");
      }
  • 删除地址

    • 点击上图中的删除地址按钮,查看发送的请求

      请求网址: http://localhost/addressBook?ids=1579828298672885762
      请求方法: DELETE

    • AddressBookController中编写对应的方法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      @DeleteMapping()
      public Result<String> deleteAdd(@RequestParam("ids") Long id) {
      if (id == null) {
      throw new CustomException("地址信息不存在,请刷新重试");
      }
      AddressBook addressBook = addressBookService.getById(id);
      if (addressBook == null) {
      throw new CustomException("地址信息不存在,请刷新重试");
      }
      addressBookService.removeById(id);
      return Result.success("地址删除成功");
      }

此时对于地址的操作基本已经完成,可以自己去测试一下,看看效果

再来一单

  • 这个功能其实比较隐晦,因为当订单状态为已完成时才会出现这个按钮(修改orders表中的status字段为4)

前端代码

  • 点击再来一单,查看发送的请求

    请求网址: http://localhost/order/again
    请求方法: POST

  • 数据只携带了一个json格式的id数据,根据常识,这个id只能是orders表中的订单id,即order_id

    1
    {id: "1986672088484409346"}
  • 现在传回的数据只有一个order_id,因此要根据它去查询对应的下单信息

  • 分析再来一单具体实现思路(参考一下当初我们怎么添加购物车的)

    • 参考点外卖的经验,再来一单应当是将该订单的数据添加到购物车,并跳转到之前的下单页面
    • 之前是我们手动选择数据(菜品/套餐)添加到购物车,现在相当于手里有发票,根据发票上的数据,再买一遍
  • 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
    @PostMapping("/again")
    public Result<String> again(@RequestBody Map<String,String> map){
    //获取order_id
    Long orderId = Long.valueOf(map.get("id"));
    //条件构造器
    LambdaQueryWrapper<OrderDetail> queryWrapper = new LambdaQueryWrapper<>();
    //查询订单的口味细节数据
    queryWrapper.eq(OrderDetail::getOrderId,orderId);
    List<OrderDetail> details = orderDetailService.list(queryWrapper);
    //获取用户id,待会需要set操作
    Long userId = BaseContext.getCurrentId();
    List<ShoppingCart> shoppingCarts = details.stream().map((item) ->{
    ShoppingCart shoppingCart = new ShoppingCart();
    //Copy对应属性值
    BeanUtils.copyProperties(item,shoppingCart);
    //设置一下userId
    shoppingCart.setUserId(userId);
    //设置一下创建时间为当前时间
    shoppingCart.setCreateTime(LocalDateTime.now());
    return shoppingCart;
    }).collect(Collectors.toList());
    //加入购物车
    shoppingCartService.saveBatch(shoppingCarts);
    return Result.success("喜欢吃就再来一单吖~");
    }

    这里直接用BeanUtils.copyProperties进行复制,地址是选择当前默认地址(如果改了默认地址,那么不是之前的地址,很合理)

减号按钮

  • 之前下单的时候,只有加号按钮能用,减号按钮还没配置,我们点击减号,看看发送了什么请求

    请求网址: http://localhost/shoppingCart/sub
    请求方法: POST

  • 返回的json数据如下,只有dishIdsetmealId

    1
    2
    3
    4
    {   
    dishId: null,
    setmealId: "1986672088484409346"
    }
  • 思路分析: 根据返回的id,来对不同的菜品/套餐的number属性修改(对应的数量-1),如果number等于0,则删除

  • ShoppingCartController中开发对应的方法

    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
    @PostMapping("/sub")
    public Result<ShoppingCart> sub(@RequestBody ShoppingCart shoppingCart) {
    Long dishId = shoppingCart.getDishId();
    Long setmealId = shoppingCart.getSetmealId();
    //条件构造器
    LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
    //只查询当前用户ID的购物车
    queryWrapper.eq(ShoppingCart::getUserId, BaseContext.getCurrentId());
    //代表数量减少的是菜品数量
    if (dishId != null) {
    //通过dishId查出购物车菜品数据
    queryWrapper.eq(ShoppingCart::getDishId, dishId);
    ShoppingCart dishCart = shoppingCartService.getOne(queryWrapper);
    //将查出来的数据的数量-1
    dishCart.setNumber(dishCart.getNumber() - 1);
    Integer currentNum = dishCart.getNumber();
    //然后判断
    if (currentNum > 0) {
    //大于0则更新
    shoppingCartService.updateById(dishCart);
    } else if (currentNum == 0) {
    //小于0则删除
    shoppingCartService.removeById(dishCart.getId());
    }
    return Result.success(dishCart);
    }

    if (setmealId != null) {
    //通过setmealId查询购物车套餐数据
    queryWrapper.eq(ShoppingCart::getSetmealId, setmealId);
    ShoppingCart setmealCart = shoppingCartService.getOne(queryWrapper);
    //将查出来的数据的数量-1
    setmealCart.setNumber(setmealCart.getNumber() - 1);
    Integer currentNum = setmealCart.getNumber();
    //然后判断
    if (currentNum > 0) {
    //大于0则更新
    shoppingCartService.updateById(setmealCart);
    } else if (currentNum == 0) {
    //等于0则删除
    shoppingCartService.removeById(setmealCart.getId());
    }
    return Result.success(setmealCart);
    }
    return Result.error("系统繁忙,请稍后再试");
    }

后台系统补充功能

菜品启售/停售

  • 点击停售按钮,查看发送的请求

    请求网址: http://localhost:8080/dish/status/0?ids=1985250880349995010
    请求方法: POST

  • 当前商品为启售状态,其status为1,但点击停售按钮时,发送的status为0,前端是直接对这个status取反了,我们直接用发送的这个status来更新我们的商品状态就好了,不用在后端再次进行判断

  • DishController中编写对应的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @PostMapping("/status/{status}")
    public Result<String> status(@PathVariable Integer status, Long ids) {
    log.info("status:{},ids:{}", status, ids);
    Dish dish = dishService.getById(ids);
    if (dish != null) {
    //直接用它传进来的这个status改就行
    dish.setStatus(status);
    dishService.updateById(dish);
    return Result.success("售卖状态修改成功");
    }
    return Result.error("系统繁忙,请稍后再试");
    }

菜品批量启售/停售

  • 这个其实就是传进来了一个ids的数组,我们在上面的方法上稍作修改就好了,直接用LambdaUpdateWrapper更方便

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @PostMapping("/status/{status}")
    public Result<String> status(@PathVariable Integer status, @RequestParam List<Long> ids) {
    log.info("status:{},ids:{}", status, ids);
    LambdaUpdateWrapper<Dish> updateWrapper = new LambdaUpdateWrapper<>();
    updateWrapper.in(ids != null, Dish::getId, ids);
    updateWrapper.set(Dish::getStatus, status);
    dishService.update(updateWrapper);
    return Result.success("批量操作成功");
    }

菜品批量删除

  • 删除跟批量删除应该也是同一个操作,点击删除按钮,查看请求

    请求网址: http://localhost:8080/dish?ids=1985250880349995010,1413384757047271425
    请求方法: DELETE

  • 按理说,这里应该是逻辑删除,表中有一个字段为is_delete,但是要按逻辑删除的话,还得改前面的listpage代码,因为查询的时候,没涉及到逻辑删除,模型类中也没有isDelete属性

  • 所以这里为了省事选择直接删除,但如果是逻辑删除,执行的是update,将逻辑删除字段设为1表示逻辑删除,查询的时候只查询逻辑删除字段为0的数据,表示未删除的数据

  • 需要注意的是,如果选中的删除列表中,存在启售状态商品,则不允许删除

  • DishController中编写对应的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @DeleteMapping
    public Result<String> delete(@RequestParam List<Long> ids) {
    log.info("删除的ids:{}", ids);
    LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.in(Dish::getId, ids);
    queryWrapper.eq(Dish::getStatus, 1);
    long count = dishService.count(queryWrapper);
    if (count > 0) {
    throw new CustomException("删除列表中存在启售状态商品,无法删除");
    }
    dishService.removeByIds(ids);
    return Result.success("删除成功");
    }

至此关于菜品的操作功能,就相对完善了

套餐批量启售/停售

  • 点击批量停售按钮,查看发送的请求

    请求网址: http://localhost:8080/setmeal/status/0?ids=1986422148185178113,1415580119015145474
    请求方法: POST

  • 跟之前的菜品批量启售/停售没有太大区别

    1
    2
    3
    4
    5
    6
    7
    8
    @PostMapping("/status/{status}")
    public Result<String> status(@PathVariable String status, @RequestParam List<Long> ids) {
    LambdaUpdateWrapper<Setmeal> updateWrapper = new LambdaUpdateWrapper<>();
    updateWrapper.in(Setmeal::getId, ids);
    updateWrapper.set(Setmeal::getStatus, status);
    setmealService.update(updateWrapper);
    return Result.success("批量操作成功");
    }

套餐修改

  • 数据回显

    • 点击修改按钮,查看发送的请求

      请求网址: http://localhost:8080/setmeal/1986422148185178113
      请求方法: GET

    • 这个请求大概率是用于处理数据回显的,请求路径/setmeal/{setmealId},请求方式GET

    • 普通的Setmeal实体类肯定是不够用的,因此要使用SetmealDto

    • SetmealController中编写对应的方法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      @GetMapping("/{id}")
      public Result<SetmealDto> getById(@PathVariable Long id) {
      Setmeal setmeal = setmealService.getById(id);
      SetmealDto setmealDto = new SetmealDto();
      //拷贝数据
      BeanUtils.copyProperties(setmeal, setmealDto);
      //条件构造器
      LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>();
      //根据setmealId查询具体的setmealDish
      queryWrapper.eq(SetmealDish::getSetmealId, id);
      List<SetmealDish> setmealDishes = setmealDishService.list(queryWrapper);
      //然后再设置属性
      setmealDto.setSetmealDishes(setmealDishes);
      //作为结果返回
      return Result.success(setmealDto);
      }
  • 套餐修改

    • 点击保存按钮,查看发送的请求

      请求网址: http://localhost/setmeal
      请求方法: PUT

    • 携带的数据如下

    • 请求路径/setmeal,请求方式PUT

    • SetmealController中编写对应的方法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      @PutMapping
      public Result<Setmeal> updateWithDish(@RequestBody SetmealDto setmealDto) {
      List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
      Long setmealId = setmealDto.getId();
      //先根据id把setmealDish表中对应套餐的数据删了
      LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>();
      queryWrapper.eq(SetmealDish::getSetmealId,setmealId);
      setmealDishService.remove(queryWrapper);
      //然后在重新添加
      setmealDishes = setmealDishes.stream().map((item) ->{
      //这属性没有,需要我们手动设置一下
      item.setSetmealId(setmealId);
      return item;
      }).collect(Collectors.toList());
      //更新套餐数据
      setmealService.updateById(setmealDto);
      //更新套餐对应菜品数据
      setmealDishService.saveBatch(setmealDishes);
      return Result.success(setmealDto);
      }

订单明细

主要是删除了按当前userId查询,新增了按订单号和事件段查询

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
@GetMapping("/userPage")
public Result<Page> userPage(int page, int pageSize) {
//获取当前id
Long userId = BaseContext.getCurrentId();
Page<Orders> pageInfo = new Page<>(page, pageSize);
Page<OrdersDto> ordersDtoPage = new Page<>(page, pageSize);
//条件构造器
LambdaQueryWrapper<Orders> queryWrapper = new LambdaQueryWrapper<>();
//查询当前用户id订单数据
queryWrapper.eq(userId != null, Orders::getUserId, userId);
//按时间降序排序
queryWrapper.orderByDesc(Orders::getOrderTime);
orderService.page(pageInfo, queryWrapper);
List<OrdersDto> list = pageInfo.getRecords().stream().map((item) -> {
OrdersDto ordersDto = new OrdersDto();
//获取orderId,然后根据这个id,去orderDetail表中查数据
Long orderId = item.getId();
LambdaQueryWrapper<OrderDetail> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(OrderDetail::getOrderId, orderId);
List<OrderDetail> details = orderDetailService.list(wrapper);
BeanUtils.copyProperties(item, ordersDto);
//之后set一下属性
ordersDto.setOrderDetails(details);
return ordersDto;
}).collect(Collectors.toList());
BeanUtils.copyProperties(pageInfo, ordersDtoPage, "records");
ordersDtoPage.setRecords(list);
//日志输出看一下
log.info("list:{}", list);
return Result.success(ordersDtoPage);
}

历史订单是只查询指定用户的数据,那我们后台这里,查询所有的用户数据就行,也就不用指定userId
但是需要判断输入的订单号和时间段,这个要写动态SQL,不过我们可以用MP来帮我们完成

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
@GetMapping("/page")
public Result<Page> page(int page, int pageSize, Long number, String beginTime, String endTime) {
//获取当前id
Page<Orders> pageInfo = new Page<>(page, pageSize);
Page<OrdersDto> ordersDtoPage = new Page<>(page, pageSize);
//条件构造器
LambdaQueryWrapper<Orders> queryWrapper = new LambdaQueryWrapper<>();
//按时间降序排序
queryWrapper.orderByDesc(Orders::getOrderTime);
//订单号
queryWrapper.eq(number != null, Orders::getId, number);
//时间段,大于开始,小于结束
queryWrapper.gt(!StringUtils.isEmpty(beginTime), Orders::getOrderTime, beginTime)
.lt(!StringUtils.isEmpty(endTime), Orders::getOrderTime, endTime);
orderService.page(pageInfo, queryWrapper);
List<OrdersDto> list = pageInfo.getRecords().stream().map((item) -> {
OrdersDto ordersDto = new OrdersDto();
//获取orderId,然后根据这个id,去orderDetail表中查数据
Long orderId = item.getId();
LambdaQueryWrapper<OrderDetail> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(OrderDetail::getOrderId, orderId);
List<OrderDetail> details = orderDetailService.list(wrapper);
BeanUtils.copyProperties(item, ordersDto);
//之后set一下属性
ordersDto.setOrderDetails(details);
return ordersDto;
}).collect(Collectors.toList());
BeanUtils.copyProperties(pageInfo, ordersDtoPage, "records");
ordersDtoPage.setRecords(list);
//日志输出看一下
log.info("list:{}", list);
return Result.success(ordersDtoPage);
}

  • 最终效果如下,输入时间段/订单号也能正常查询

  • 关于用户名字段为null,去修改前端代码/backend/order/list.html,找到用户,将userName改成consignee就好了,如果还不显示,清除浏览器缓存再刷新重试

    1
    2
    <!--<el-table-column prop="userName" label="用户"></el-table-column>-->
    <el-table-column prop="consignee" label="用户"></el-table-column>

修改订单状态

  • 点击上图中的派送按钮,查看发送的请求

    请求网址: http://localhost/order
    请求方法: PUT

  • 携带的json数据

    1
    2
    3
    4
    {   
    status: 3,
    id: "1986674402569883649"
    }
  • 携带的status为3,那该按钮的作用应该是将订单状态设置为传入的status

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    switch(row.status){
    case 1:
    str = '待付款'
    break;
    case 2:
    str = '正在派送'
    break;
    case 3:
    str = '已派送'
    break;
    case 4:
    str = '已完成'
    break;
    case 5:
    str = '已取消'
    break;
    }
  • OrderController中编写对应的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @PutMapping
    public Result<String> changeStatus(@RequestBody Map<String, String> map) {
    int status = Integer.parseInt(map.get("status"));
    Long orderId = Long.valueOf(map.get("id"));
    log.info("修改订单状态:status={status},id={id}", status, orderId);
    LambdaUpdateWrapper<Orders> updateWrapper = new LambdaUpdateWrapper<>();
    updateWrapper.eq(Orders::getId, orderId);
    updateWrapper.set(Orders::getStatus, status);
    orderService.update(updateWrapper);
    return Result.success("订单状态修改成功");
    }

那么至此,应该是把页面里的所有功能都实现了