瑞吉外卖开发总结(全功能实现)

3 篇文章 0 订阅
2 篇文章 0 订阅

技术栈

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3RChlZZs-1684482588578)(C:\Users\许正\AppData\Roaming\Typora\typora-user-images\image-20220424174038689.png)]

项目部署

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vcVZDGXZ-1684482588579)(C:\Users\许正\AppData\Roaming\Typora\typora-user-images\image-20220510151900496.png)]

简历上可写的点

  1. 集中处理系统异常,自定义统一的错误码, 并封装了全局异常处理器,屏蔽了项目冗余的报错细节、便于接口调用方理解和统一处理。

  2. 基于静态ThreadLocal封装了线程隔离的全局上下文对象,便于在请求内部存取用户信息,减少用户远程查询次数。

  3. 为兼容请求参数date类型的序列化,定义Jackson对象映射器处理日期;并扩展SpringMVC的消息转换器,实现自动序列化。

  4. 自定义MyBatis PlusMetaObjectHandle,配合全局上下文实现写数据前的创建时间、用户id字段的自动填充。

  5. 遵循Restful设计规范编写接口,降低前后端接口沟通和理解成本。

  6. 为解决原胜Jdk例化器导致的缓存key乱码问题,自定义RedisTemplate Bean的Redis Key列化器为StringRedisSerializer.

  7. 使用Knife4j + Swagger自动生成后端接口文档,并通过编写ApiOperation等注解补充接口注释,避免了人工编写维护文档的麻烦。

  8. 为省复编写用户校验的麻烦,基于WebFilter实现全局登录校验;通过AntPathMatcher匹配动态请求路径,实现灵活的可选鉴权。

  9. 为保证数据的完整性和一致性,使佣@Transactional实现数据库事务,并配置rollbackFor = Exception.class支持受检异常的事务回滚。

  10. 为提高XX信息页加载速度,基于Spring Cache注解+ Redis 实现对XX信息的自动缓存,大幅降低数据库压力的同时将接口响应耗时由0.8s减少至50ms (数值自己再测一下)

  11. 为降低开发成本,使佣MyBatis Plus框架自动生成业务的增删改查重复代码,并使用LambdaQueryWrapper实现更灵活地自定义查询。

  12. 为降低用户注册成本、保证用户真实性,二次封装XX云SDK接入短信服务,并通过Redis来集中缓存验证码,防止单手机号的重复发送。

  13. 为提高数据库整体读写性能,配置MySQL主从同步,并使用sharding-jdbc实现业务无侵入的读写分离。

  14. 封装全局Axios请求实例,添加全局请求拦截和全局异常响应处理器,减少重复的状态码判断、提升项目可维护性。

关于项目

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.xz</groupId>
    <artifactId>angong_takeout</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <scope>compile</scope>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.2</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.20</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.76</version>
        </dependency>

        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.23</version>
        </dependency>
        <dependency>
            <groupId>org.jetbrains</groupId>
            <artifactId>annotations</artifactId>
            <version>RELEASE</version>
            <scope>compile</scope>
        </dependency>

        <!--阿里云短信服务-->
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-core</artifactId>
            <version>4.5.16</version>
        </dependency>
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-dysmsapi</artifactId>
            <version>2.1.0</version>
        </dependency>

        <!--Redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!--SpringCache-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.4.5</version>
            </plugin>
        </plugins>
    </build>

</project>

application.yml

server:
  port: 8080
spring:
  application:
    name: angong_takeout
  datasource:
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/angong?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
      username: root
      password: root
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
angong-pic:
  path: D:\img\

数据库设计

字段及含义

  • address_book(地址簿,指的是移动端消费者用户的地址信息,用户登录成功后可以维护自己的地址信息。同一个用户可以有多个地址信息,但是只能有一个默认地址。)
  • category(分类,name(Unique,唯一索引),type字段:1为菜品分类,2为套餐分类)
  • dish(菜品)
  • dish_flavor(菜品口味)
  • employee(员工表,username(Unique,唯一索引),status(状态默认为1【正常】,0为【禁用】)。登陆时查询,password进行MD5加密。)
  • order_detail(订单明细)
  • orders(订单)
  • setmeal(套餐)
  • setmeal_dish(套餐菜品关系)
  • shopping_cart(购物车)
  • user(用户信息)

表结构

/*
 Navicat Premium Data Transfer

 Source Server         : root
 Source Server Type    : MySQL
 Source Server Version : 50719
 Source Host           : localhost:3306
 Source Schema         : angong

 Target Server Type    : MySQL
 Target Server Version : 50719
 File Encoding         : 65001

 Date: 14/04/2023 14:31:56
*/

SET NAMES utf8mb4;
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) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '收货人',
  `sex` tinyint(4) NOT NULL COMMENT '性别 0 女 1 男',
  `phone` varchar(11) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '手机号',
  `province_code` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '省级区划编号',
  `province_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '省级名称',
  `city_code` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '市级区划编号',
  `city_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '市级名称',
  `district_code` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '区级区划编号',
  `district_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '区级名称',
  `detail` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '详细地址',
  `label` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL 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 CHARACTER SET = utf8 COLLATE = utf8_bin COMMENT = '地址管理' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for category
-- ----------------------------
DROP TABLE IF EXISTS `category`;
CREATE TABLE `category`  (
  `id` bigint(20) NOT NULL COMMENT '主键',
  `type` int(11) NULL DEFAULT NULL COMMENT '类型   1 菜品分类 2 套餐分类',
  `name` varchar(64) CHARACTER SET utf8 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 INDEX `idx_category_name`(`name`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin COMMENT = '菜品及套餐分类' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for dish
-- ----------------------------
DROP TABLE IF EXISTS `dish`;
CREATE TABLE `dish`  (
  `id` bigint(20) NOT NULL COMMENT '主键',
  `name` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '菜品名称',
  `category_id` bigint(20) NOT NULL COMMENT '菜品分类id',
  `price` decimal(10, 2) NULL DEFAULT NULL COMMENT '菜品价格',
  `code` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '商品码',
  `image` varchar(200) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '图片',
  `description` varchar(400) CHARACTER SET utf8 COLLATE utf8_bin NULL 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 INDEX `idx_dish_name`(`name`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin COMMENT = '菜品管理' ROW_FORMAT = Dynamic;

-- ----------------------------
-- 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) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '口味名称',
  `value` varchar(500) CHARACTER SET utf8 COLLATE utf8_bin NULL 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 CHARACTER SET = utf8 COLLATE = utf8_bin COMMENT = '菜品口味关系表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for employee
-- ----------------------------
DROP TABLE IF EXISTS `employee`;
CREATE TABLE `employee`  (
  `id` bigint(20) NOT NULL COMMENT '主键',
  `name` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '姓名',
  `username` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '用户名',
  `password` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '密码',
  `phone` varchar(11) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '手机号',
  `sex` varchar(2) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '性别',
  `id_number` varchar(18) CHARACTER SET utf8 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 INDEX `idx_username`(`username`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin COMMENT = '员工信息' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for order_detail
-- ----------------------------
DROP TABLE IF EXISTS `order_detail`;
CREATE TABLE `order_detail`  (
  `id` bigint(20) NOT NULL COMMENT '主键',
  `name` varchar(50) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '名字',
  `image` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '图片',
  `order_id` bigint(20) NOT NULL COMMENT '订单id',
  `dish_id` bigint(20) NULL DEFAULT NULL COMMENT '菜品id',
  `setmeal_id` bigint(20) NULL DEFAULT NULL COMMENT '套餐id',
  `dish_flavor` varchar(50) CHARACTER SET utf8 COLLATE utf8_bin NULL 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 CHARACTER SET = utf8 COLLATE = utf8_bin COMMENT = '订单明细表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for orders
-- ----------------------------
DROP TABLE IF EXISTS `orders`;
CREATE TABLE `orders`  (
  `id` bigint(20) NOT NULL COMMENT '主键',
  `number` varchar(50) CHARACTER SET utf8 COLLATE utf8_bin NULL 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) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '备注',
  `phone` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
  `address` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
  `user_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
  `consignee` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin COMMENT = '订单表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- 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) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '套餐名称',
  `price` decimal(10, 2) NOT NULL COMMENT '套餐价格',
  `status` int(11) NULL DEFAULT NULL COMMENT '状态 0:停用 1:启用',
  `code` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '编码',
  `description` varchar(512) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '描述信息',
  `image` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL 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 INDEX `idx_setmeal_name`(`name`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin COMMENT = '套餐' ROW_FORMAT = Dynamic;

-- ----------------------------
-- 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) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '套餐id ',
  `dish_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '菜品id',
  `name` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '菜品名称 (冗余字段)',
  `price` decimal(10, 2) NULL 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 CHARACTER SET = utf8 COLLATE = utf8_bin COMMENT = '套餐菜品关系' ROW_FORMAT = Dynamic;

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

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `id` bigint(20) NOT NULL COMMENT '主键',
  `name` varchar(50) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '姓名',
  `phone` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '手机号',
  `sex` varchar(2) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '性别',
  `id_number` varchar(18) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '身份证号',
  `avatar` varchar(500) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '头像',
  `status` int(11) NULL DEFAULT 0 COMMENT '状态 0:禁用,1:正常',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin COMMENT = '用户信息' ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

项目文件结构/命名

  • common(通用包)
  • config(配置)
  • filter(拦截类)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V2D1fiDU-1684482588580)(C:\Users\许正\AppData\Roaming\Typora\typora-user-images\image-20230414153026454.png)]

返回结果类(R.java)

package com.xz.angong.common;

import lombok.Data;
import java.util.HashMap;
import java.util.Map;

@Data
public class R<T> {

    private Integer code; //编码:1成功,0和其它数字为失败

    private String msg; //错误信息

    private T data; //数据

    private Map map = new HashMap(); //动态数据

    public static <T> R<T> success(T object) {
        R<T> r = new R<T>();
        r.data = object;
        r.code = 1;
        return r;
    }

    public static <T> R<T> error(String msg) {
        R r = new R();
        r.msg = msg;
        r.code = 0;
        return r;
    }

    public R<T> add(String key, Object value) {
        this.map.put(key, value);
        return this;
    }

}

private T data; //数据

  • 这里使用泛型,增加数据可用性,可以传各种对象进去

业务功能开发

登陆&退出

后台登陆功能

  • 登录功能还进行了超时管理(超过10s登陆超时)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7QsXCxxl-1684482588580)(C:\Users\许正\AppData\Roaming\Typora\typora-user-images\image-20230417164229471.png)]

后台退出功能

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lqZc67vz-1684482588581)(C:\Users\许正\AppData\Roaming\Typora\typora-user-images\image-20230417170355982.png)]

员工管理业务开发

  • 针对员工实体(employee表)进行操作

  • 添加员工(前端进行电话和身份证校验,新增员工添加初始密码:123456,后面可以自行登录修改)

  • 分页查询(用MP实现,添加配置,写处理逻辑)

    /**
         * 分页展示
         *
         * @param page
         * @param pageSize
         * @param name
         * @return
         */
        @GetMapping("/page")
        public R<Page> page(int page, int pageSize, String name) {
    //        log.info("page: {}, pageSize: {}, name: {}", page, pageSize, name);
    
            //分页构造器
            Page<Employee> pageInfo = new Page(page, pageSize);
    
            //条件构造器
            LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.like(StringUtils.isNotEmpty(name), Employee::getName, name);
            queryWrapper.orderByDesc(Employee::getUpdateTime);
    
            //执行查询
            employeeService.page(pageInfo, queryWrapper);
    
            return R.success(pageInfo);
        }
    
  • 编辑(对已有员工进行修改操作)/ 启用(禁用)

    • 与后端进行两次交互
      • 根据员工id查询员工信息并回显到前端页面
      • 点击【保存】按钮将页面中员工信息提交给后端,并进行数据库修改

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QjMZ6f6K-1684482588581)(C:\Users\许正\AppData\Roaming\Typora\typora-user-images\image-20230418161134852.png)]

分类管理业务开发

  • 新增分类
  • 分类信息分页查询
  • 删除分类
  • 修改分类

菜品管理业务开发==[包含文件上传下载]==

  • 文件上传(upload)下载(download)

    • 上传(upload)

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dYOoDKzH-1684482588582)(C:\Users\许正\AppData\Roaming\Typora\typora-user-images\image-20230418231736740.png)]

      @Value("${angong-pic.path}")
      private String basePath;
      
      /**
       * 文件上传
       *
       * @param file
       * @return
       */
      @PostMapping("/upload")
      public R<String> upload(MultipartFile file/* 这里file不能随便起,必须与前端form表单的name保持一致 */) {
          //file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会删除
          log.info(String.valueOf(file));
      
          //原始文件名
          String originalFilename = file.getOriginalFilename();
          String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
      
          //使用UUID重新生成文件名,防止文件名称重复造成文件覆盖
          String filename = UUID.randomUUID().toString() + suffix;
      
          //创建一个目录对象
          File dir = new File(basePath);
      
          //判断是否存在
          if (!dir.exists()) {
              dir.mkdirs();
          }
      
          try {
              //将临时文件转存到指定位置
              file.transferTo(new File(basePath + filename));
          } catch (IOException e) {
              e.printStackTrace();
          }
      
          return R.success(filename);
      }
      
    • 下载(download)

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WAbOBPgo-1684482588582)(C:\Users\许正\AppData\Roaming\Typora\typora-user-images\image-20230418232035027.png)]

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EUvuZDGt-1684482588583)(C:\Users\许正\AppData\Roaming\Typora\typora-user-images\image-20230418234307734.png)]

      @Value("${angong-pic.path}")
          private String basePath;
      
      /**
       * 文件下载
       *
       * @param name
       * @param response
       */
      @GetMapping("/download")
      public void download(String name, HttpServletResponse response) {
      
          FileInputStream fileInputStream = null;
          ServletOutputStream outputStream = null;
      
      
          try {
              //通过输入流读取文件内容
              fileInputStream = new FileInputStream(new File(basePath + name));
              //通过输出流将文件写回浏览器,在浏览器展示图片
              outputStream = response.getOutputStream();
      
              response.setContentType("image/jpeg");
      
              int len = 0;
              byte[] bytes = new byte[1024];
              while ((len = fileInputStream.read(bytes)) != -1) {
                  outputStream.write(bytes, 0, len);
                  outputStream.flush();
              }
      
          } catch (Exception e) {
              e.printStackTrace();
          } finally {
              try {
                  fileInputStream.close();
                  outputStream.close();
              } catch (IOException e) {
                  e.printStackTrace();
              }
          }
      }
      
  • 新增菜品(利用Dto实现,DishDto:包含菜品以及口味字段)

  • 菜品信息分页查询(因页面需要除dish以外的其他字段,例如categoryName等,利用Dto实现)

  • 修改菜品(多表操作,更新dish表,修改dish_flavor表【先删除后添加】)

套餐管理业务开发

  • 新增套餐(setmeal和setmeal_dish)
  • 套餐信息分页查询
  • 删除套餐(对于状态为售卖中的套餐不能删除,需要先停售,然后才能删除。支持批量删除)

手机验证码登录

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cBStzdtQ-1684482588584)(C:\Users\许正\AppData\Roaming\Typora\typora-user-images\image-20230420223915587.png)]

package com.xz.angong.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xz.angong.common.R;
import com.xz.angong.entity.User;
import com.xz.angong.service.UserService;
import com.xz.angong.utils.ValidateCodeUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
//import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpSession;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * @author 许正
 * @version 1.0
 */
@RestController
@Slf4j
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;

//    @Autowired
//    private RedisTemplate redisTemplate;

    /**
     * 发送手机短信验证码
     *
     * @param user
     * @return
     */
    @PostMapping("/sendMsg")
    public R<String> sendMsg(@RequestBody User user, HttpSession session) {
        //获取手机号
        String phone = user.getPhone();
        if (StringUtils.isNotEmpty(phone)) {
            //生成随机的4位验证码
            String code = ValidateCodeUtils.generateValidateCode(4).toString();
            log.info("手机验证码 code: {}", code);

            //调用API发送短信
            //需填入 signName, templateCode 即可使用
            //SMSUtils.sendMessage("安工外卖--许正", "", phone, code);

            //需要将生成的验证码保存到Session
            session.setAttribute(phone, code);

//            //将生成的验证码缓存到Redis中, 并且设置有效期为5分钟
//            redisTemplate.opsForValue().set(phone, code, 5, TimeUnit.MINUTES);

            return R.success("手机验证码短信发送成功");
        }

        return R.error("短信发送失败");
    }

    /**
     * 移动端登录
     *
     * @param map
     * @param session
     * @return
     */
    @PostMapping("/login")
    public R<User> login(@RequestBody Map map, HttpSession session) {
        log.info(map.toString());

        //获取手机号
        String phone = map.get("phone").toString();
        //获取验证码
        String code = map.get("code").toString();

        //从Session中获取保存的验证码
        Object codeInSession = session.getAttribute(phone);

//        //使用缓存
//        Object codeInSession = redisTemplate.opsForValue().get(phone);

        //进行验证码的比对(页面提交的验证码和Session中保存的验证码比对)
        if (codeInSession != null && codeInSession.equals(code)) {
            //如果能够比对成功,说明登录成功
            LambdaQueryWrapper<User> userLambdaQueryWrapper = new LambdaQueryWrapper<>();
            userLambdaQueryWrapper.eq(User::getPhone, phone);
            User user = userService.getOne(userLambdaQueryWrapper);

            if (user == null) {
                //判断当前手机号对应的用户是否为新用户,如果是新用户就自动完成注册
                user = new User();
                user.setPhone(phone);
                user.setStatus(1);
                userService.save(user);
            }
            session.setAttribute("user", user.getId());

//            //如果用户登陆成功, 删除Redis中缓存的验证码
//            redisTemplate.delete(phone);

            return R.success(user);
        }
        return R.error("登陆失败");
    }
}

地址簿增删改查

package com.xz.angong.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.xz.angong.common.BaseContext;
import com.xz.angong.common.R;
import com.xz.angong.entity.AddressBook;
import com.xz.angong.service.AddressBookService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * 地址簿管理
 */
@Slf4j
@RestController
@RequestMapping("/addressBook")
public class AddressBookController {

    @Autowired
    private AddressBookService addressBookService;

    /**
     * 新增
     *
     * @param addressBook
     * @return
     */
    @PostMapping
    public R<AddressBook> save(@RequestBody AddressBook addressBook) {
        addressBook.setUserId(BaseContext.getCurrentId());
        log.info("addressBook: {}", addressBook);
        addressBookService.save(addressBook);
        return R.success(addressBook);
    }

    /**
     * 设置默认地址
     *
     * @param addressBook
     * @return
     */
    @PutMapping("default")
    public R<AddressBook> setDefault(@RequestBody AddressBook addressBook) {
        log.info("addressBook:{}", addressBook);
        LambdaUpdateWrapper<AddressBook> addressBookLambdaUpdateWrapper = new LambdaUpdateWrapper<>();
        addressBookLambdaUpdateWrapper.eq(AddressBook::getUserId, BaseContext.getCurrentId());
        addressBookLambdaUpdateWrapper.set(AddressBook::getIsDefault, 0);
        //SQL:update address_book set is_default = 0 where user_id = ?
        addressBookService.update(addressBookLambdaUpdateWrapper);

        addressBook.setIsDefault(1);
        //SQL:update address_book set is_default = 1 where id = ?
        addressBookService.updateById(addressBook);
        return R.success(addressBook);
    }

    /**
     * 根据id查询地址
     *
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    public R get(@PathVariable Long id) {
        AddressBook addressBook = addressBookService.getById(id);
        if (addressBook != null) {
            return R.success(addressBook);
        } else {
            return R.error("没有找到该对象");
        }
    }

    /**
     * 查询默认地址
     *
     * @return
     */
    @GetMapping("default")
    public R<AddressBook> getDefault() {
        LambdaQueryWrapper<AddressBook> addressBookLambdaQueryWrapper = new LambdaQueryWrapper<>();
        addressBookLambdaQueryWrapper.eq(AddressBook::getUserId, BaseContext.getCurrentId());
        addressBookLambdaQueryWrapper.eq(AddressBook::getIsDefault, 1);

        //SQL:select * from address_book where user_id = ? and is_default = 1
        AddressBook addressBook = addressBookService.getOne(addressBookLambdaQueryWrapper);

        if (null == addressBook) {
            return R.error("没有找到该对象");
        } else {
            return R.success(addressBook);
        }
    }

    /**
     * 查询指定用户的全部地址
     *
     * @param addressBook
     * @return
     */
    @GetMapping("/list")
    public R<List<AddressBook>> list(AddressBook addressBook) {
        addressBook.setUserId(BaseContext.getCurrentId());
        log.info("addressBook:{}", addressBook);

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

        //SQL:select * from address_book where user_id = ? order by update_time desc
        List<AddressBook> addressBookList = addressBookService.list(addressBookLambdaQueryWrapper);
        return R.success(addressBookList);
    }
}

菜品展示

 /**
     * 因前端需要调用list接口,但同时需要返回相应的口味信息,对list接口进行改造,返回一个DishDto对象
     *
     * @param dish
     * @return
     */
    @GetMapping("/list")
    public R<List<DishDto>> list(Dish dish) {
        List<DishDto> dtoList = null;

//        //动态构造key
//        String key = "dish_" + dish.getCategoryId() + "_" + dish.getStatus();//例:dish_1397844391040167938_1
//
//        //先从Redis中获取缓存数据
//        dtoList = (List<DishDto>) redisTemplate.opsForValue().get(key);
//
//        if (dtoList != null) {
//            //如果存在, 直接返回, 无需查询数据库
//            return R.success(dtoList);
//        }

        //如果不存在, 需要查询数据库, 将查询到的菜品数据缓存到Redis中
        //构造查询条件
        LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
        dishLambdaQueryWrapper.eq(Dish::getStatus, 1).eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId());
        //添加排序条件
        dishLambdaQueryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);

        List<Dish> dishList = dishService.list(dishLambdaQueryWrapper);

        dtoList = dishList.stream().map((item) -> {
            DishDto dishDto = new DishDto();
            BeanUtils.copyProperties(item, dishDto);

            Category category = categoryService.getById(item.getCategoryId());

            if (category != null) {
                dishDto.setCategoryName(category.getName());
            }

            //select * from dish_flavor where dishId = ?
            LambdaQueryWrapper<DishFlavor> dishFlavorLambdaQueryWrapper = new LambdaQueryWrapper<>();
            dishFlavorLambdaQueryWrapper.eq(DishFlavor::getDishId, item.getId());
            List<DishFlavor> flavors = dishFlavorService.list(dishFlavorLambdaQueryWrapper);

            dishDto.setFlavors(flavors);
            return dishDto;
        }).collect(Collectors.toList());

购物车

package com.xz.angong.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xz.angong.common.BaseContext;
import com.xz.angong.common.R;
import com.xz.angong.entity.ShoppingCart;
import com.xz.angong.service.ShoppingCartService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

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

/**
 * @author 许正
 * @version 1.0
 */
@RestController
@Slf4j
@RequestMapping("/shoppingCart")
public class ShoppingCartController {
    @Autowired
    private ShoppingCartService shoppingCartService;

    /**
     * 往购物车添加菜品
     *
     * @param shoppingCart
     * @return
     */
    @PostMapping("/add")
    public R<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart) {
        log.info("购物车数据: {}", shoppingCart.toString());

        //设置用户id,指定当前是哪个用户的购物车数据
        shoppingCart.setUserId(BaseContext.getCurrentId());

        //查询当前菜品或者套餐是否在购物车中
        Long dishId = shoppingCart.getDishId();

        //select * from shopping_cart where userId = ? and dish_id/setmeal_id = ?
        LambdaQueryWrapper<ShoppingCart> shoppingCartLambdaQueryWrapper = new LambdaQueryWrapper<>();
        shoppingCartLambdaQueryWrapper.eq(ShoppingCart::getUserId, BaseContext.getCurrentId());

        if (dishId != null) {//添加的是菜品
            shoppingCartLambdaQueryWrapper.eq(ShoppingCart::getDishId, shoppingCart.getDishId());
        } else {//添加的是套餐
            shoppingCartLambdaQueryWrapper.eq(ShoppingCart::getSetmealId, shoppingCart.getSetmealId());
        }

        ShoppingCart shoppingCartServiceOne = shoppingCartService.getOne(shoppingCartLambdaQueryWrapper);

        if (shoppingCartServiceOne != null) {
            //如果已经存在, 就在原来数量基础上加1
            shoppingCartServiceOne.setNumber(shoppingCartServiceOne.getNumber() + 1);
            shoppingCartService.updateById(shoppingCartServiceOne);
        } else {
            //如果不存在, 则添加到购物车, 数量默认就是1
            shoppingCart.setNumber(1);
            shoppingCart.setCreateTime(LocalDateTime.now());
            shoppingCartService.save(shoppingCart);
            shoppingCartServiceOne = shoppingCart;
        }

        return R.success(shoppingCartServiceOne);
    }

    /**
     * 查看购物车
     *
     * @return
     */
    @GetMapping("/list")
    public R<List<ShoppingCart>> list() {
        log.info("查看购物车...");

        LambdaQueryWrapper<ShoppingCart> shoppingCartLambdaQueryWrapper = new LambdaQueryWrapper<>();
        shoppingCartLambdaQueryWrapper.eq(ShoppingCart::getUserId, BaseContext.getCurrentId()).orderByAsc(ShoppingCart::getCreateTime);

        List<ShoppingCart> cartList = shoppingCartService.list(shoppingCartLambdaQueryWrapper);

        return R.success(cartList);
    }

    /**
     * 减少已选菜品数量
     *
     * @param shoppingCart
     * @return
     */
    @PostMapping("/sub")
    public R<ShoppingCart> sub(@RequestBody ShoppingCart shoppingCart) {
        log.info("购物车数据: {}", shoppingCart.toString());

        //设置用户id,指定当前是哪个用户的购物车数据
        shoppingCart.setUserId(BaseContext.getCurrentId());

        //查询当前菜品或者套餐是否在购物车中
        Long dishId = shoppingCart.getDishId();

        //select * from shopping_cart where userId = ? and dish_id/setmeal_id = ?
        LambdaQueryWrapper<ShoppingCart> shoppingCartLambdaQueryWrapper = new LambdaQueryWrapper<>();
        shoppingCartLambdaQueryWrapper.eq(ShoppingCart::getUserId, BaseContext.getCurrentId());

        if (dishId != null) {//菜品
            shoppingCartLambdaQueryWrapper.eq(ShoppingCart::getDishId, shoppingCart.getDishId());
        } else {//套餐
            shoppingCartLambdaQueryWrapper.eq(ShoppingCart::getSetmealId, shoppingCart.getSetmealId());
        }

        ShoppingCart shoppingCartServiceOne = shoppingCartService.getOne(shoppingCartLambdaQueryWrapper);

        if (shoppingCartServiceOne.getNumber() > 1) {
            //如果已经存在, 就在原来数量基础上-1
            shoppingCartServiceOne.setNumber(shoppingCartServiceOne.getNumber() - 1);
            shoppingCartService.updateById(shoppingCartServiceOne);
        } else {
            shoppingCartService.remove(shoppingCartLambdaQueryWrapper);
        }

        return R.success(shoppingCartServiceOne);
    }

    /**
     * 清空购物车
     *
     * @return
     */
    @DeleteMapping("/clean")
    public R<String> clean() {

        LambdaQueryWrapper<ShoppingCart> shoppingCartLambdaQueryWrapper = new LambdaQueryWrapper<>();
        shoppingCartLambdaQueryWrapper.eq(ShoppingCart::getUserId, BaseContext.getCurrentId());

        shoppingCartService.remove(shoppingCartLambdaQueryWrapper);

        return R.success("清空购物车成功");
    }
}

用户下单

注意:金额进行原子操作,保证多线程的情况下计算也是准确的

//金额进行原子操作,保证多线程的情况下计算也是准确的
AtomicInteger amount = new AtomicInteger();
//1.计算总金额  2.封装订单明细表数据
List<OrderDetail> orderDetails = shoppingCartList.stream().map((item) -> {
    OrderDetail orderDetail = new OrderDetail();
    orderDetail.setOrderId(orderId);
    orderDetail.setNumber(item.getNumber());
    orderDetail.setDishFlavor(item.getDishFlavor());
    orderDetail.setDishId(item.getDishId());
    orderDetail.setSetmealId(item.getSetmealId());
    orderDetail.setName(item.getName());
    orderDetail.setImage(item.getImage());
    orderDetail.setAmount(item.getAmount());
    amount.addAndGet(item.getAmount().multiply(new BigDecimal(item.getNumber())).intValue());
    return orderDetail;
}).collect(Collectors.toList());

orders.setAmount(new BigDecimal(amount.get()));//总金额

OrdersServiceImpl.java

package com.xz.angong.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xz.angong.common.BaseContext;
import com.xz.angong.common.CustomException;
import com.xz.angong.entity.*;
import com.xz.angong.mapper.OrdersMapper;
import com.xz.angong.service.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

/**
 * @author 许正
 * @version 1.0
 */
@Service
public class OrdersServiceImpl extends ServiceImpl<OrdersMapper, Orders> implements OrdersService {

    @Autowired
    private ShoppingCartService shoppingCartService;

    @Autowired
    private UserService userService;

    @Autowired
    private AddressBookService addressBookService;

    @Autowired
    private OrderDetailService orderDetailService;

    /**
     * 用户下单
     *
     * @param orders
     */
    @Override
    @Transactional
    public void submit(Orders orders) {
        //获得当前用户id
        Long userId = BaseContext.getCurrentId();
        //查询当前用户的购物车数据
        LambdaQueryWrapper<ShoppingCart> shoppingCartLambdaQueryWrapper = new LambdaQueryWrapper<>();
        shoppingCartLambdaQueryWrapper.eq(ShoppingCart::getUserId, userId);
        List<ShoppingCart> shoppingCartList = shoppingCartService.list(shoppingCartLambdaQueryWrapper);
        if (shoppingCartList == null || shoppingCartList.size() == 0) {
            throw new CustomException("购物车为空,不能下单!");
        }

        //查询用户数据
        User user = userService.getById(userId);

        //查询地址数据
        AddressBook addressBook = addressBookService.getById(orders.getAddressBookId());
        if (addressBook == null) {
            throw new CustomException("地址信息有误,不能下单!");
        }

        //向订单表插入数据, 一条数据
        long orderId = IdWorker.getId();//订单号

        //金额进行原子操作,保证多线程的情况下计算也是准确的
        AtomicInteger amount = new AtomicInteger();
        //1.计算总金额  2.封装订单明细表数据
        List<OrderDetail> orderDetails = shoppingCartList.stream().map((item) -> {
            OrderDetail orderDetail = new OrderDetail();
            orderDetail.setOrderId(orderId);
            orderDetail.setNumber(item.getNumber());//数量
            orderDetail.setDishFlavor(item.getDishFlavor());
            orderDetail.setDishId(item.getDishId());
            orderDetail.setSetmealId(item.getSetmealId());
            orderDetail.setName(item.getName());
            orderDetail.setImage(item.getImage());
            orderDetail.setAmount(item.getAmount());//单份金额
            //累加:单价*数量(转为BigDecimal)
            amount.addAndGet(item.getAmount().multiply(new BigDecimal(item.getNumber())).intValue());
            return orderDetail;
        }).collect(Collectors.toList());

        orders.setAmount(new BigDecimal(amount.get()));//总金额

        orders.setId(orderId);
        orders.setOrderTime(LocalDateTime.now());//下单时间
        orders.setCheckoutTime(LocalDateTime.now());//支付时间,因支付系统未开发,这里设置为系统当前时间
        orders.setStatus(2);//2表示待派送
        orders.setUserId(userId);
        orders.setNumber(String.valueOf(orderId));
        orders.setUserName(user.getName());
        orders.setConsignee(addressBook.getConsignee());
        orders.setPhone(addressBook.getPhone());
        orders.setAddress((addressBook.getProvinceName() == null ? "" : addressBook.getProvinceName())
                + (addressBook.getCityName() == null ? "" : addressBook.getCityName())
                + (addressBook.getDistrictName() == null ? "" : addressBook.getDistrictName())
                + (addressBook.getDetail() == null ? "" : addressBook.getDetail()));
        this.save(orders);

        //向订单明细表插入数据, 多条数据
        orderDetailService.saveBatch(orderDetails);

        //清空购物车数据
        shoppingCartService.remove(shoppingCartLambdaQueryWrapper);
    }
}

项目优化

缓存优化

用户数量多,系统访问量大
频繁访问数据库,系统性能下降,用户体验差

优化原因:系统上线后(部署到云服务器),访问客户端获取数据特别慢,在解决的过程中,想到了用缓存解决。

缓存短信验证码

实现思路

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mepMvRGs-1684482588585)(C:\Users\许正\AppData\Roaming\Typora\typora-user-images\image-20230425200541045.png)]

UserController.java
package com.xz.angong.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xz.angong.common.R;
import com.xz.angong.entity.User;
import com.xz.angong.service.UserService;
import com.xz.angong.utils.ValidateCodeUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpSession;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * @author 许正
 * @version 1.0
 */
@RestController
@Slf4j
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 发送手机短信验证码
     *
     * @param user
     * @return
     */
    @PostMapping("/sendMsg")
    public R<String> sendMsg(@RequestBody User user, HttpSession session) {
        //获取手机号
        String phone = user.getPhone();
        if (StringUtils.isNotEmpty(phone)) {
            //生成随机的4位验证码
            String code = ValidateCodeUtils.generateValidateCode(4).toString();
            log.info("手机验证码 code: {}", code);

            //调用API发送短信
            //需填入 signName, templateCode 即可使用
            //SMSUtils.sendMessage("安工外卖--by许正", "", phone, code);

//            //需要将生成的验证码保存到Session
//            session.setAttribute(phone, code);

            //将生成的验证码缓存到Redis中, 并且设置有效期为5分钟
            redisTemplate.opsForValue().set(phone, code, 5, TimeUnit.MINUTES);

            return R.success("手机验证码短信发送成功");
        }

        return R.error("短信发送失败");
    }

    /**
     * 移动端登录
     *
     * @param map
     * @param session
     * @return
     */
    @PostMapping("/login")
    public R<User> login(@RequestBody Map map, HttpSession session) {
        log.info(map.toString());

        //获取手机号
        String phone = map.get("phone").toString();
        //获取验证码
        String code = map.get("code").toString();

//        //从Session中获取保存的验证码
//        Object codeInSession = session.getAttribute(phone);

        //从Redis中获取缓存的验证码
        Object codeInSession = redisTemplate.opsForValue().get(phone);

        //进行验证码的比对(页面提交的验证码和Session中保存的验证码比对)
        if (codeInSession != null && codeInSession.equals(code)) {
            //如果能够比对成功,说明登录成功
            LambdaQueryWrapper<User> userLambdaQueryWrapper = new LambdaQueryWrapper<>();
            userLambdaQueryWrapper.eq(User::getPhone, phone);
            User user = userService.getOne(userLambdaQueryWrapper);

            if (user == null) {
                //判断当前手机号对应的用户是否为新用户,如果是新用户就自动完成注册
                user = new User();
                user.setPhone(phone);
                user.setStatus(1);
                userService.save(user);
            }
            session.setAttribute("user", user.getId());

            //如果用户登陆成功, 删除Redis中缓存的验证码
            redisTemplate.delete(phone);

            return R.success(user);
        }
        return R.error("登录失败");
    }
}

缓存菜品数据(防止每次点击菜品都会查数据库)

实现思路

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1BYrrkqq-1684482588586)(C:\Users\许正\AppData\Roaming\Typora\typora-user-images\image-20230425214156507.png)]

调用delete方法同样需要清理缓存

DishController.java
package com.xz.angong.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.xz.angong.common.R;
import com.xz.angong.dto.DishDto;
import com.xz.angong.entity.Category;
import com.xz.angong.entity.Dish;
import com.xz.angong.entity.DishFlavor;
import com.xz.angong.service.CategoryService;
import com.xz.angong.service.DishFlavorService;
import com.xz.angong.service.DishService;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.jdbc.ScriptRunner;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;

import java.io.*;
import java.sql.Connection;
import java.sql.DriverManager;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @author 许正
 * @version 1.0
 */
@RestController
@RequestMapping("/dish")
@Slf4j
public class DishController {

    @Autowired
    private DishService dishService;

    @Autowired
    private DishFlavorService dishFlavorService;

    @Autowired
    private CategoryService categoryService;

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 新增菜品
     *
     * @param dishDto
     * @return
     */
    @PostMapping
    public R<String> save(@RequestBody DishDto dishDto) {
        log.info(dishDto.toString());

        dishService.saveWithFlavor(dishDto);

//        //清理所有菜品的缓存数据
//        Set keys = redisTemplate.keys("dish_*");
//        redisTemplate.delete(keys);

        //清理某个分类下面的菜品缓存数据
        String key = "dish_" + dishDto.getCategoryId() + "_" + dishDto.getStatus();
        redisTemplate.delete(key);

        return R.success("添加成功");
    }

    /**
     * 菜品信息分页查询
     *
     * @param page
     * @param pageSize
     * @param name
     * @return
     */
    @GetMapping("/page")
    public R<Page> page(int page, int pageSize, String name) {
        //得到dish的基本属性
        Page<Dish> dishPage = new Page<>(page, pageSize);
        LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.like(name != null, Dish::getName, name)
                .orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
        //执行分页查询
        dishService.page(dishPage, queryWrapper);

        //赋给dishDto
        Page<DishDto> dishDtoPage = new Page<>();
        //除了“records”之外的属性进行复制
        BeanUtils.copyProperties(dishPage, dishDtoPage, "records");

        List<Dish> dishPageRecords = dishPage.getRecords();
        List<DishDto> dishDtoList = dishPageRecords.stream().map(dish -> {
            DishDto dishDto = new DishDto();
            BeanUtils.copyProperties(dish, dishDto);

            Category category = categoryService.getById(dish.getCategoryId());

            if (category != null) {
                dishDto.setCategoryName(category.getName());
            }

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

        dishDtoPage.setRecords(dishDtoList);

        return R.success(dishDtoPage);
    }

    /**
     * 修改菜品及口味信息
     *
     * @param dishDto
     * @return
     */
    @PutMapping
    public R<String> update(@RequestBody DishDto dishDto) {

        dishService.updateWithFlavor(dishDto);

//        //清理所有菜品的缓存数据
//        Set keys = redisTemplate.keys("dish_*");
//        redisTemplate.delete(keys);

        //清理某个分类下面的菜品缓存数据
        String key = "dish_" + dishDto.getCategoryId() + "_" + dishDto.getStatus();
        redisTemplate.delete(key);

        return R.success("修改菜品成功");
    }

    /**
     * 根据id查询菜品信息和对应的口味信息
     *
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    public R<DishDto> get(@PathVariable Long id) {
        DishDto dishDto = dishService.getByIdWithFlavor(id);
        return R.success(dishDto);
    }

    /**
     * 修改菜品售卖状态
     *
     * @param status
     * @param ids
     * @return
     */
    @PostMapping("/status/{status}")
    public R<String> statusWithIds(@PathVariable("status") Integer status, @RequestParam("ids") List<Long> ids) {
        log.info("售卖状态:{},ids: {}", status, ids);
        Dish dish = new Dish();
        for (Long dishId : ids) {
            dish.setId(dishId);
            dish.setStatus(status);
            dishService.updateById(dish);
        }
        return R.success("修改售卖状态成功");
    }

//    /**
//     * 根据条件查询对应的菜品数据
//     *
//     * @param dish
//     * @return
//     */
//    @GetMapping("/list")
//    public R<List<Dish>> list(Dish dish) {
//        LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
//        dishLambdaQueryWrapper.eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId())
//                .eq(Dish::getStatus, 1)//状态为1(起售)
//                .orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
//
//        List<Dish> dishList = dishService.list(dishLambdaQueryWrapper);
//
//        return R.success(dishList);
//    }

    /**
     * 因前端需要调用list接口,但同时需要返回相应的口味信息,对list接口进行改造,返回一个DishDto对象
     *
     * @param dish
     * @return
     */
    @GetMapping("/list")
    public R<List<DishDto>> list(Dish dish) {
        List<DishDto> dishDtoList = null;

        //动态构造key
        String key = "dish_" + dish.getCategoryId() + "_" + dish.getStatus();//例:dish_1397844391040167938_1

        //先从Redis中获取缓存数据
        dishDtoList = (List<DishDto>) redisTemplate.opsForValue().get(key);

        if (dishDtoList != null) {
            //如果存在, 直接返回, 无需查询数据库
            return R.success(dishDtoList);
        }

        //如果不存在, 需要查询数据库, 将查询到的菜品数据缓存到Redis中
        //构造查询条件
        LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
        dishLambdaQueryWrapper.eq(Dish::getStatus, 1).eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId());
        //添加排序条件
        dishLambdaQueryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);

        List<Dish> dishList = dishService.list(dishLambdaQueryWrapper);

        dishDtoList = dishList.stream().map((item) -> {
            DishDto dishDto = new DishDto();
            BeanUtils.copyProperties(item, dishDto);

            Category category = categoryService.getById(item.getCategoryId());

            if (category != null) {
                dishDto.setCategoryName(category.getName());
            }

            //select * from dish_flavor where dishId = ?
            LambdaQueryWrapper<DishFlavor> dishFlavorLambdaQueryWrapper = new LambdaQueryWrapper<>();
            dishFlavorLambdaQueryWrapper.eq(DishFlavor::getDishId, item.getId());
            List<DishFlavor> flavors = dishFlavorService.list(dishFlavorLambdaQueryWrapper);

            dishDto.setFlavors(flavors);
            return dishDto;
        }).collect(Collectors.toList());

        //将查询到的菜品数据缓存到Redis中
        redisTemplate.opsForValue().set(key, dishDtoList, 1, TimeUnit.HOURS);

        return R.success(dishDtoList);
    }

    /**
     * 删除菜品
     *
     * @param ids
     * @return
     */
    @DeleteMapping
    public R<String> delete(@RequestParam List<Long> ids) {
        log.info("ids: {}", ids);

        dishService.removeWithFlavor(ids);

        //清理所有菜品的缓存数据
        Set keys = redisTemplate.keys("dish_*");
        redisTemplate.delete(keys);

        return R.success("菜品删除成功");
    }

    /**
     * 登录时可以初始化数据库
     *
     * @return
     */
    @RequestMapping("/restart")
    public R<String> restart() {
        log.info("........");

        try {
            //执行日志文件配置
            BufferedWriter log = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new File(System.getProperty("user.dir") + "/src/main/resources/sql/log.txt")), "UTF-8"));
            //执行sql语句报错时文件配置
            BufferedWriter error = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new File(System.getProperty("user.dir") + "/src/main/resources/sql/error.txt")), "UTF-8"));
            Class.forName("com.mysql.jdbc.Driver").newInstance();
            //获取数据源
            Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/angong?useUnicode=true&characterEncoding=utf-8&reConnect=true;", "root", "root");
            //创建脚本执行对象
            ScriptRunner r = new ScriptRunner(conn);
            //设置日志输出流,将执行日志保存至流中,每次将会覆写
            r.setErrorLogWriter(new PrintWriter(error));
            //设置错误信息输出,将错误日志保存至流中,每次将会覆写,如果sql脚本无错误运行成功,则该文件内容为空
            r.setLogWriter(new PrintWriter(log));
            //执行sql脚本,默认位置为classpath,也就是与src文件夹同级
            r.runScript(new BufferedReader(new InputStreamReader(new FileInputStream(new File(System.getProperty("user.dir") + "/src/main/resources/angong.sql")), "UTF-8")));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return R.success("初始化成功!");
    }
}

缓存套餐数据

实现思路

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hLyudypp-1684482588586)(C:\Users\许正\AppData\Roaming\Typora\typora-user-images\image-20230425223420475.png)]

项目亮点

登录拦截

  • 只有登录成功后才可以访问系统中的页面,如果没有登陆则跳转到登录页面
  • 实现方案:过滤器或拦截器,本项目使用过滤器
  • 实现步骤:
    1. 创建自定义过滤器(LoginCheckFilter)
    2. 在启动类上加注解(@ServletComponentScan)
    3. 完善过滤器处理逻辑[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AnU9gkhE-1684482588586)(C:\Users\许正\AppData\Roaming\Typora\typora-user-images\image-20230418094340888.png)]
package com.xz.angong.filter;

import com.alibaba.fastjson.JSON;
import com.xz.angong.common.BaseContext;
import com.xz.angong.common.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.util.AntPathMatcher;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author 许正
 * @version 1.0
 */
@Slf4j
@WebFilter(filterName = "loginCheckFilter", urlPatterns = "/*")
public class LoginCheckFilter implements Filter {

    //路径匹配器,支持通配符
    public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();

    /**
     * 过滤请求
     *
     * @param servletRequest
     * @param servletResponse
     * @param filterChain
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

//        log.info("拦截到请求:{}", request.getRequestURI());

        String requestURI = request.getRequestURI();

        String[] urls = new String[]{
                "/employee/login",
                "/employee/logout",
                "/backend/**",
                "/front/**",
                "/common/**"
        };

        boolean check = check(urls, requestURI);

        if (check) {
//            log.info("本次请求不需要处理: " + requestURI);
            filterChain.doFilter(request, response);
            return;
        }

        //判断登陆状态
        if (request.getSession().getAttribute("employee") != null) {
//            log.info("用户 " + request.getSession().getAttribute("employee") + " 已登录");

            //利用线程记录当前用户id
            Long empId = (Long) request.getSession().getAttribute("employee");
            BaseContext.setCurrentId(empId);

            filterChain.doFilter(request, response);
            return;
        }

//        log.info("用户未登录!");

        //通过输出流向客户端页面响应数据("NOTLOGIN")
        response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
    }

    /**
     * 检查是否可以放行的方法
     *
     * @param urls
     * @param requestURI
     * @return
     */
    public boolean check(String[] urls, String requestURI) {
        for (String url : urls) {
            boolean match = PATH_MATCHER.match(url, requestURI);
            if (match) {
                return true;
            }
        }
        return false;
    }
}

全局异常捕获

  • 全局异常处理
package com.xz.angong.common;

/**
 * @author 许正
 * @version 1.0
 */

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import java.sql.SQLIntegrityConstraintViolationException;

/**
 * 全局异常处理
 */
@ControllerAdvice(annotations = {RestController.class, ControllerAdvice.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 新增员工异常处理
     *
     * @param ex
     * @return
     */
    @ExceptionHandler
    public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex) {
        log.error(ex.getMessage());

        if (ex.getMessage().contains("Duplicate entry")) {
            String[] split = ex.getMessage().split(" ");
            return R.error("用户名" + split[2] + "已存在");
        }

        return R.error("未知错误");
    }

    /**
     * 自定义业务异常处理
     *
     * @param ex
     * @return
     */
    @ExceptionHandler
    public R<String> exceptionHandler(CustomException ex) {
        log.error(ex.getMessage());

        return R.error(ex.getMessage());
    }
}

全局分页插件配置

package com.xz.angong.config;

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author 许正
 * @version 1.0
 */
@Configuration
public class MybatisPlusConfig {

    /**
     * 配置分页插件
     *
     * @return
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return mybatisPlusInterceptor;
    }
}

权限配置

  • 只有管理员(admin用户)可以对其他普通用户进行启用、禁用操作
  • 所有普通用户登录系统后启用、禁用按钮不显示,只有编辑权限

数据库公共字段自动填充

数据库公共字段:

  • create_time
  • update_time
  • create_user
  • update_user

实现方案

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-be4rdiqq-1684482588587)(C:\Users\许正\AppData\Roaming\Typora\typora-user-images\image-20230418170629230.png)]

Employee.java

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

MyMetaObjectHandler.java

package com.xz.angong.common;

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

/**
 * @author 许正
 * @version 1.0
 */

/**
 * 自定义元数据对象处理器
 */
@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {

    /**
     * 插入操作自动填充
     *
     * @param metaObject
     */
    @Override
    public void insertFill(MetaObject metaObject) {
        metaObject.setValue("createTime", LocalDateTime.now());
        metaObject.setValue("updateTime", LocalDateTime.now());
        metaObject.setValue("createUser", BaseContext.getCurrentId());
        metaObject.setValue("updateUser", BaseContext.getCurrentId());
    }

    /**
     * 更新操作自动填充
     *
     * @param metaObject
     */
    @Override
    public void updateFill(MetaObject metaObject) {
        metaObject.setValue("updateTime", LocalDateTime.now());
        metaObject.setValue("updateUser", BaseContext.getCurrentId());
    }
}

补充(从线程获取数据)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XJAOjPOf-1684482588587)(C:\Users\许正\AppData\Roaming\Typora\typora-user-images\image-20230418210251326.png)]

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

实现步骤

  1. 编写BaseContext工具类,基于ThreadLocal封装的工具类

    package com.xz.angong.common;
    
    /**
     * @author 许正
     * @version 1.0
     */
    
    /**
     * 基于ThreadLocal封装工具类,用于保存和获取当前登录用户id
     */
    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();
        }
    }
    
  2. LoginCheckFilterdoFilter方法中调用BaseContext来设置当前登录用户的id

    LoginCheckFilter.java–>doFilter() 利用线程记录当前用户id

     //判断登陆状态
            if (request.getSession().getAttribute("employee") != null) {
    //            log.info("用户 " + request.getSession().getAttribute("employee") + " 已登录");
    
                //利用线程记录当前用户id
                Long empId = (Long) request.getSession().getAttribute("employee");
                BaseContext.setCurrentId(empId);
    
                filterChain.doFilter(request, response);
                return;
            }
    
  3. MyMetaObjectHandler的方法中调用BaseContext获取登录用户的id

    MyMetaObjectHandler.java 从线程中获取之前存放的当前用户id

    package com.xz.angong.common;
    
    import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.ibatis.reflection.MetaObject;
    import org.springframework.stereotype.Component;
    
    import java.time.LocalDateTime;
    
    /**
     * @author 许正
     * @version 1.0
     */
    
    /**
     * 自定义元数据对象处理器
     */
    @Component
    @Slf4j
    public class MyMetaObjectHandler implements MetaObjectHandler {
    
        /**
         * 插入操作自动填充
         *
         * @param metaObject
         */
        @Override
        public void insertFill(MetaObject metaObject) {
            metaObject.setValue("createTime", LocalDateTime.now());
            metaObject.setValue("updateTime", LocalDateTime.now());
    
    //        long id = Thread.currentThread().getId();
    //        log.info("MyMetaObjectHandler-->insertFill()线程id: " + id);
    
            metaObject.setValue("createUser", BaseContext.getCurrentId());
            metaObject.setValue("updateUser", BaseContext.getCurrentId());
        }
    
        /**
         * 更新操作自动填充
         *
         * @param metaObject
         */
        @Override
        public void updateFill(MetaObject metaObject) {
            metaObject.setValue("updateTime", LocalDateTime.now());
    
    //        long id = Thread.currentThread().getId();
    //        log.info("MyMetaObjectHandler-->updateFill()线程id: " + id);
    
            metaObject.setValue("updateUser", BaseContext.getCurrentId());
        }
    }
    

DTO(数据传输对象)

DTO,全称为Data Transfer Object,即数据传输对象

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

本项目中使用到了DTO,如下所示:

在这里插入图片描述

DishDto.java

package com.xz.angong.dto;

import com.xz.angong.entity.Dish;
import com.xz.angong.entity.DishFlavor;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;

@Data
public class DishDto extends Dish {//继承Dish字段,并新增口味集合等,用于接收前端参数

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

    private String categoryName;

    private Integer copies;
}

OrdersDto.java

package com.xz.angong.dto;

import com.xz.angong.entity.OrderDetail;
import com.xz.angong.entity.Orders;
import lombok.Data;
import java.util.List;

@Data
public class OrdersDto extends Orders {

    private String userName;

    private String phone;

    private String address;

    private String consignee;

    private List<OrderDetail> orderDetails;
   
}

SetmealDto.java

package com.xz.angong.dto;

import com.xz.angong.entity.Setmeal;
import com.xz.angong.entity.SetmealDish;
import lombok.Data;
import java.util.List;

@Data
public class SetmealDto extends Setmeal {

    private List<SetmealDish> setmealDishes;

    private String categoryName;
}

注意

Json格式数据后端需加注解

  • json形式的数据需要在参数前加注解:@RequestBody

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-robmrGv8-1684482588588)(C:\Users\许正\AppData\Roaming\Typora\typora-user-images\image-20230417164034370.png)]

/**
 * 登录功能
 *
 * @param request
 * @param employee
 * @return
 */
@PostMapping("/login")
public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee) {
    String password = employee.getPassword();
    password = DigestUtils.md5DigestAsHex(password.getBytes());

    LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(Employee::getUsername, employee.getUsername());
    Employee emp = employeeService.getOne(queryWrapper);

    if (emp == null) {
        return R.error("用户不存在");
    }

    if (!emp.getPassword().equals(password)) {
        return R.error("密码错误");
    }

    if (emp.getStatus() != 1) {
        return R.error("用户已被禁用");
    }

    request.getSession().setAttribute("employee", emp.getId());
    return R.success(emp);
}
  • json形式的参数需要在参数前加注解:@RequestPram

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rg8IsOYh-1684482588589)(C:\Users\许正\AppData\Roaming\Typora\typora-user-images\image-20230420215253948.png)]

/**
 * 删除套餐
 *
 * @param ids
 * @return
 */
@DeleteMapping
@CacheEvict(value = "setmealCache", allEntries = true)
public R<String> delete(@RequestParam List<Long> ids) {
    log.info("ids: {}", ids);

    setmealService.removeWithDish(ids);

    return R.success("套餐数据删除成功");
}

路径变量

  • 前端请求带有路径变量的,参数前需要加注解:@PathVariable
/**
 * 根据id查找员工信息
 *
 * @param id
 * @return
 */
@GetMapping("/{id}")
public R<Employee> getById(@PathVariable Long id) {
    Employee employee = employeeService.getById(id);
    if (employee == null) {
        return R.error("未找到该员工");
    }
    return R.success(employee);
}

事务

操作多张表需要加注解:@Transactional

并在启动类加注解:@EnableTransactionManagement

遇到的困难

启用/禁用员工账号

测试过程中没有出错,但是功能并没有实现,数据库中的数据也并未修改。

SQL执行的结果是更新的数据行数为0,仔细观察id的值,和数据库中对应记录的id值并不相同。

问题所在

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lv9ZNLgD-1684482588589)(C:\Users\许正\AppData\Roaming\Typora\typora-user-images\image-20230418151923804.png)]

解决方案

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E5SQysrJ-1684482588590)(C:\Users\许正\AppData\Roaming\Typora\typora-user-images\image-20230418152118658.png)]

具体实现步骤

  1. 提供对象转换器JacksonObjectMapper,基于 Jackson 进行java对象到 json 数据的转换

    package com.xz.angong.common;
    
    import com.fasterxml.jackson.databind.DeserializationFeature;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.fasterxml.jackson.databind.module.SimpleModule;
    import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
    import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
    import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
    import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
    import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
    import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
    import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
    import java.math.BigInteger;
    import java.time.LocalDate;
    import java.time.LocalDateTime;
    import java.time.LocalTime;
    import java.time.format.DateTimeFormatter;
    import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
    
    /**
     * 对象映射器:基于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);
        }
    }
    
  2. WebMvcConfig配置类中扩展Spring mvc的消息转换器(WebMvcConfig.java),在此消息转换器中使用提供的对象转换器进行 Java对象到 json 数据的转换

    /**
     * 扩展mvc框架的消息转换器
     *
     * @param converters
     */
    @Override
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        //创建消息转换器对象
        MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
        //设置对象转换器,底层使用Jackson将Java对象转为Json
        messageConverter.setObjectMapper(new JacksonObjectMapper());
        //将上面的消息转换器对象追加到mvc框架的转换器集合中
        converters.add(0, messageConverter);
    }
    

项目小结

业务开发顺序

  1. 根据产品原型明确业务需求
  2. 重点分析数据的流转过程(请求,响应模式)和数据格式
  3. 先完成框架再写业务逻辑,最后代码实现
  4. 通过debug断点调试跟踪程序执行过程
  • 10
    点赞
  • 77
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员正正

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值