【苍穹外卖】项目技术万字总结,看看有没有你不会的技术吧!!!

  本文主要是对【苍穹外卖】中后端项目所用到的技术进行总结,博主能力有限,希望大家能够多多批评指正。

目录

一. 熟悉项目结构

1. 模块介绍

在这里插入图片描述

序号模块名称说明
1sky-take-outmaven父工程,统一管理依赖版本,聚合其他子块
2sky-common子模块,存放公共类,例如:工具类、常量、异常类等
3sky-pojo子模块,存放实体类、DTO、VO等
4sky-server子模块,后端服务,存放配置文件、Controller、Service、Mapper等

2. sky-common子模块介绍

在这里插入图片描述

序号软件包名称说明
1constant存放定义的常量
2context存放跟上下文操作有关的类
3enumeration枚举类
4exception自定义异常类
5json存放对象映射器
6properties存放配置属性类
7result返回的统一响应结果
8utils自定义工具类

3. sky-pojo子模块介绍

在这里插入图片描述

序号软件包名称说明
1dto数据传输对象,DTO一般是作为方法传入的参数在使用,不局限于前端给controller层传参,也可以是controller层给service层传参。
2entity实体类,实体类一般与数据库表对应(数据库字段一般是下划线命名,实体类属性一般是驼峰命名)
3vo视图对象,用于前端数据的展示,所以一般是controller层把VO传给前端,然后前端展示

4. sky-server子模块介绍

在这里插入图片描述

序号软件包名称说明
1annotation存放自定义注解类
2aspect自定义切面类
3config配置类
4controller控制层
5handler存放全局异常处理器
6interceptor存放拦截器
7mapper持久层
8service业务逻辑层
9task定时任务
10websocketWebSoket 服务

二. 主要技术应用

1. maven依赖管理

1.1 继承关系:通过 < parent > </ parent >标签实现

父工程sky-take-out的pom.xml文件:
  继承pring-boot-starter-parent工程

    <parent>
        <artifactId>spring-boot-starter-parent</artifactId>
        <groupId>org.springframework.boot</groupId>
        <version>2.7.3</version>
    </parent>

子模块pom.xml文件
  继承sky-take-out工程

    <parent>
        <artifactId>sky-take-out</artifactId>
        <groupId>com.sky</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
问题1:为什么父工程要继承spring-boot-starter-parent?
  • 依赖管理:spring-boot-starter-parent 提供了 Spring Boot 常用的依赖版本,使用这个父POM,可以省去你在项目中显式指定这些依赖版本的麻烦。
  • 插件管理:它包含了常用的 Maven 插件(比如 spring-boot-maven-plugin)的配置,帮助你打包和构建 Spring
    Boot 项目。
  • 默认配置:这个父 POM 提供了 Spring Boot 项目常用的配置,如编码格式、构建插件、Java 版本等
问题2:为什么要使用继承关系?
  • 集中管理:所有子项目都可以共享父 POM 中的配置,避免重复。
  • 版本控制:父项目可以统一管理依赖版本,确保所有子项目的一致性。
  • 简化配置:子项目可以不必每次都显式声明版本或插件,继承父项目的配置更加简洁。

1.2 版本锁定:通过< dependencyManagement >< /dependencyManagement >标签实现

   < dependencyManagement >标签允许你在父 POM 中定义依赖的版本,但不会直接将这些依赖添加到项目中。它只是声明了版本,以便子模块使用时能够自动继承。
父 pom.xml:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.7.3</version>
        </dependency>
        <!-- 其他依赖的版本 -->
    </dependencies>
</dependencyManagement>

子 pom.xml:

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

1.3 集中管理版本号:通过< properties >< /properties >标签实现

父pom.xml:

   在< properties >< /properties >标签中统一管理依赖版本,
在< dependencies >< /dependencies >标签中的依赖版本号用 $ {自定义名称}格式替换。

//集中管理版本号
<properties>
    <mybatis.spring>2.2.0</mybatis.spring>
    <lombok>1.18.20</lombok>
    <fastjson>1.2.76</fastjson>
    <commons.lang>2.6</commons.lang>
    <druid>1.2.1</druid>
    <pagehelper>1.3.0</pagehelper>
    <aliyun.sdk.oss>3.10.2</aliyun.sdk.oss>
    <knife4j>3.0.2</knife4j>
    <aspectj>1.9.4</aspectj>
    <jjwt>0.9.1</jjwt>
    <jaxb-api>2.3.1</jaxb-api>
    <poi>3.16</poi>
    <junit>4.12</junit>
</properties>

//版本号用${ }格式替换
<dependencyManagement>
        <dependencies>
            <!--web起步依赖-->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>${mybatis.spring}</version>
            </dependency>

            <!--lombok-->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>${lombok}</version>
            </dependency>

            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>${fastjson}</version>
            </dependency>

            <dependency>
                <groupId>commons-lang</groupId>
                <artifactId>commons-lang</artifactId>
                <version>${commons.lang}</version>
            </dependency>

            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid-spring-boot-starter</artifactId>
                <version>${druid}</version>
            </dependency>

            <!--PageHelper分页插件-->
            <dependency>
                <groupId>com.github.pagehelper</groupId>
                <artifactId>pagehelper-spring-boot-starter</artifactId>
                <version>${pagehelper}</version>
            </dependency>

            <!--Swagger-->
            <dependency>
                <groupId>com.github.xiaoymin</groupId>
                <artifactId>knife4j-spring-boot-starter</artifactId>
                <version>${knife4j}</version>
            </dependency>

            <!--AOP-->
            <dependency>
                <groupId>org.aspectj</groupId>
                <artifactId>aspectjrt</artifactId>
                <version>${aspectj}</version>
            </dependency>

            <dependency>
                <groupId>org.aspectj</groupId>
                <artifactId>aspectjweaver</artifactId>
                <version>${aspectj}</version>
            </dependency>

            <!--JWT令牌-->
            <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt</artifactId>
                <version>${jjwt}</version>
            </dependency>

            <!--阿里云OSS-->
            <dependency>
                <groupId>com.aliyun.oss</groupId>
                <artifactId>aliyun-sdk-oss</artifactId>
                <version>${aliyun.sdk.oss}</version>
            </dependency>

            <!--jdj1.7以上OSS额外需要的依赖-->
            <dependency>
                <groupId>javax.xml.bind</groupId>
                <artifactId>jaxb-api</artifactId>
                <version>${jaxb-api}</version>
            </dependency>

            <!-- poi -->
            <dependency>
                <groupId>org.apache.poi</groupId>
                <artifactId>poi</artifactId>
                <version>${poi}</version>
            </dependency>
            <dependency>
                <groupId>org.apache.poi</groupId>
                <artifactId>poi-ooxml</artifactId>
                <version>${poi}</version>
            </dependency>
            <!--微信支付-->
            <dependency>
                <groupId>com.github.wechatpay-apiv3</groupId>
                <artifactId>wechatpay-apache-httpclient</artifactId>
                <version>0.4.8</version>
            </dependency>

            <!-- JUnit 4 依赖 -->
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>${junit}</version> <!-- 版本号可以根据需要修改 -->
                <scope>test</scope> <!-- 只在测试阶段使用 -->
            </dependency>
        </dependencies>
    </dependencyManagement>
子pom.xml:

  子工程会直接继承父工程中的版本号,无需在子pom.xml中声明。

    <dependencies>
            <!-- 自定义公共组件 -->
            <dependency>
                <groupId>com.sky</groupId>
                <artifactId>sky-common</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>

            <dependency>
                <groupId>com.sky</groupId>
                <artifactId>sky-pojo</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>

            <!-- Spring Boot 核心启动器 -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter</artifactId>
            </dependency>

            <!-- Spring Boot 测试支持 -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope> <!-- 只在测试阶段使用 -->
            </dependency>

            <!-- Spring Boot Web 支持 -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
                <scope>compile</scope> <!-- 主要用于Web开发 -->
            </dependency>

            <!-- MySQL 数据库连接器 -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <scope>runtime</scope> <!-- 在运行时使用 -->
            </dependency>

            <!-- MyBatis 支持 Spring Boot -->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
            </dependency>

            <!-- Lombok 用于简化 Java 代码 -->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
            </dependency>

            <!-- Fastjson 用于 JSON 处理 -->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
            </dependency>

            <!-- Druid 数据库连接池 Spring Boot 启动器 -->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid-spring-boot-starter</artifactId>
            </dependency>

            <!-- MyBatis 分页插件支持 -->
            <dependency>
                <groupId>com.github.pagehelper</groupId>
                <artifactId>pagehelper-spring-boot-starter</artifactId>
            </dependency>

            <!-- AspectJ 运行时库 -->
            <dependency>
                <groupId>org.aspectj</groupId>
                <artifactId>aspectjrt</artifactId>
            </dependency>

            <!-- AspectJ 编译时织入支持 -->
            <dependency>
                <groupId>org.aspectj</groupId>
                <artifactId>aspectjweaver</artifactId>
            </dependency>

            <!-- Knife4j Spring Boot 启动器,用于API文档生成 -->
            <dependency>
                <groupId>com.github.xiaoymin</groupId>
                <artifactId>knife4j-spring-boot-starter</artifactId>
            </dependency>

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

            <!-- Spring Boot 缓存支持 -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-cache</artifactId>
            </dependency>

            <!-- WebSocket 支持 Spring Boot -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-websocket</artifactId>
            </dependency>

            <!-- JAXB API(用于 XML 处理) -->
            <dependency>
                <groupId>javax.xml.bind</groupId>
                <artifactId>jaxb-api</artifactId>
            </dependency>

            <!-- Apache POI 库,用于处理 Microsoft Office 文档 -->
            <dependency>
                <groupId>org.apache.poi</groupId>
                <artifactId>poi</artifactId>
            </dependency>
            <dependency>
                <groupId>org.apache.poi</groupId>
                <artifactId>poi-ooxml</artifactId>
            </dependency>

            <!-- JUnit 4 用于单元测试 -->
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <scope>test</scope> <!-- 只在测试阶段使用 -->
            </dependency>

            <!-- Spring Web,提供MVC框架支持 -->
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-web</artifactId>
            </dependency>

            <!-- Jackson 数据绑定库,用于Java对象和JSON之间的转换 -->
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-databind</artifactId>
            </dependency>
    </dependencies>

1.3 聚合关系:通过 < modules > 标签实现。

父pom.xml:

    <modules>
        <module>sky-common</module>
        <module>sky-pojo</module>
        <module>sky-server</module>
    </modules>
聚合与继承的区别:

  在 Maven 中,聚合(Aggregation)和继承(Inheritance)是两个不同的概念:

  • 聚合(Aggregation): 父项目不关心子模块的代码,只是用来组织多个子模块,集中管理构建和依赖。
    聚合关系通过 < modules > 标签定义。
  • 继承(Inheritance): 子项目会继承父项目的配置、依赖、插件等,子项目可以覆盖或扩展父项目的配置。
    继承关系通过 < parent > 标签定义。

  简而言之,聚合侧重于“组织和管理”多个模块,而继承侧重于“配置共享和代码继承”。

  当父项目和子项目配置完成后,你可以通过执行父项目的 mvn install 或 mvn clean install 命令来构建所有的子模块。Maven 会按顺序构建各个子模块,同时确保依赖关系被正确处理。

2. 使用Git版本控制

2.1 先创建Git仓库

VCS - Create Git Repository创建远程仓库,选中根目录即可,若右上角出现标志说明成功
在这里插入图片描述
在这里插入图片描述

2.2 进入提交界面,编写提交信息,点击提交

在这里插入图片描述
在这里插入图片描述

2.3 进入推送界面, 点击推送

在这里插入图片描述

在这里插入图片描述

2.4 显示提交成功之后,可到个人仓库中去查看

在这里插入图片描述
在这里插入图片描述

3. Spring Boot三层架构模式

3.1 介绍

  Spring Boot 的三层架构模式是指将应用程序按照 控制层(Controller)业务逻辑层(Service)数据访问层(持久层)(Repository) 进行分层设计,这样可以使得系统更具模块化、易于维护和扩展。Spring Boot 作为一个快速开发框架,提供了强大的支持来实现这种分层结构。

3.2 常用Spring注解使用说明

控制层@RestController
类注解
注解描述
@RestController组合注解,包含 @Controller@ResponseBody。适用于开发 RESTful APIs。控制器类的所有方法默认返回 JSON 或其他格式的数据。
@Controller定义 Spring MVC 控制器类,用于处理 HTTP 请求。
@ResponseBody将方法返回的对象直接序列化为 HTTP 响应体(通常是 JSON 或 XML)。
/**
 * 菜品管理
 */
@Slf4j
@RestController
@RequestMapping("/admin/dish")
@Api(tags = "菜品管理相关接口")
public class DishController {
    @Autowired
    private DishService dishService;
    ...
    ...
}
处理特定 HTTP 请求方法的注解
注解描述
@RequestMapping可以处理多种 HTTP 请求(GET、POST、PUT、DELETE 等)。需要明确指定请求方法。
@GetMapping处理 HTTP GET 请求,通常用于获取资源。
@PostMapping处理 HTTP POST 请求,通常用于创建资源。
@PutMapping处理 HTTP PUT 请求,通常用于更新资源。
@DeleteMapping处理 HTTP DELETE 请求,通常用于删除资源。
/**
     * 新增菜品
     * @param dishDTO
     * @return
     */
    @PostMapping
    @ApiOperation("新增菜品")
    public Result save(@RequestBody DishDTO dishDTO) {

        log.info("新增菜品:{}", dishDTO);

        dishService.saveWithFlavor(dishDTO);

        //清理缓存数据
        String key = "dish_" + dishDTO.getId();
        cleanCache(key);

        return Result.success();
    }
方法参数注解
注解描述
@RequestParam用于处理查询参数(例如:/example?id=1),可以将多个参数绑定到一个集合中(如 List)。
@RequestBody用于接收 JSON 格式的数据,将其自动转换为 Java 对象。
@PathVariable用于接收 URL 中的路径变量(例如:/dept/{id})。
@DateTimeFormat用于处理日期格式的参数,常用于指定日期格式转换。
@RequestParam
    /**
     * 菜品批量删除
     * @param ids
     * @return
     */
    @DeleteMapping
    @ApiOperation("菜品批量删除功能")
    public Result deleteBatch(@RequestParam List<Long> ids) {
        log.info("菜品批量删除:{}",ids);
        dishService.deleteBatch(ids);
        //清理缓存
        cleanCache("dish_*");
        return Result.success();
    }
@RequestBody
    /**
     * 修改菜品信息
     * @param dishDTO
     * @return
     */
    @PutMapping
    @ApiOperation("修改菜品信息")
    public Result update(@RequestBody DishDTO dishDTO) {
        log.info("修改菜品信息:{}",dishDTO);
        dishService.updateWithFlavor(dishDTO);
        //清理缓存
        cleanCache("dish_*");
        return Result.success();
    }
@PathVariable
   /**
     * 通过id查找菜品
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    @ApiOperation("通过id查找菜品")
    public Result getById(@PathVariable Long id) {
        log.info("通过id查找菜品:{}", id);
        DishVO dishVO = dishService.getByIdWithFlavor(id);
        return Result.success(dishVO);
    }
@DateTimeFormat
    /**
     * 指定时间段内营业额统计
     * @param begin
     * @param end
     * @return
     */
    @GetMapping("/turnoverStatistics")
    @ApiOperation("指定时间段内营业额统计")
    public Result<TurnoverReportVO> turnoverStatistics(@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
                                                       @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end){
        log.info("指定时间段内营业额统计:{},{}", begin, end);
        TurnoverReportVO turnoverReportVO = reportService.getTurnoverStatistics(begin,end);
        return Result.success(turnoverReportVO);
    }
业务逻辑层@Service
@Service
public class DishServiceImpl implements DishService {

    @Autowired
    private DishMapper dishMapper;
    ...
    ...
}
数据访问层(持久层)@Mapper
@Mapper
public interface DishMapper {
	...
	...
}

4. 登录校验:JWT令牌技术+拦截器

  JWT(JSON Web Token)与拦截器结合使用,通常用于在 Web 应用中实现用户认证与授权。JWT 作为一种认证方式,可以携带用户身份信息(如用户 ID、角色等),而拦截器则能帮助我们在请求到达控制器之前进行 JWT 验证、解析和处理。

JWT令牌技术

(1)导入maven坐标
(2)在yml文件中配置JWT参数

application.yml

sky:
  # 管理端jwt令牌
  jwt:
    # 设置jwt签名加密时使用的秘钥
    admin-secret-key: itcast
    # 设置jwt过期时间
    admin-ttl: 7200000
    # 设置前端传递过来的令牌名称
    admin-token-name: token
    # 用户端jwt令牌
    user-secret-key: itheima
    user-ttl: 7200000
    user-token-name: authentication
(3)编写JWT属性类,用于传入配置的JWT参数
public class JwtProperties {

    /**
     * 管理端员工生成jwt令牌相关配置
     */
    private String adminSecretKey;
    private long adminTtl;
    private String adminTokenName;

    /**
     * 用户端微信用户生成jwt令牌相关配置
     */
    private String userSecretKey;
    private long userTtl;
    private String userTokenName;

}
(4)封装一个JwtUtil类
public class JwtUtil {
    /**
     * 生成jwt
     * 使用Hs256算法, 私匙使用固定秘钥
     *
     * @param secretKey jwt秘钥
     * @param ttlMillis jwt过期时间(毫秒)
     * @param claims    设置的信息
     * @return
     */
    public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {
        // 指定签名的时候使用的签名算法,也就是header那部分
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

        // 生成JWT的时间
        // TODO 后期需要将令牌过期时间改回来
        long expMillis = System.currentTimeMillis() + ttlMillis*100;
        Date exp = new Date(expMillis);

        // 设置jwt的body
        JwtBuilder builder = Jwts.builder()
                // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                .setClaims(claims)
                // 设置签名使用的签名算法和签名使用的秘钥
                .signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
                // 设置过期时间
                .setExpiration(exp);

        return builder.compact();
    }

    /**
     * Token解密
     *
     * @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个
     * @param token     加密后的token
     * @return
     */
    public static Claims parseJWT(String secretKey, String token) {
        // 得到DefaultJwtParser
        Claims claims = Jwts.parser()
                // 设置签名的秘钥
                .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
                // 设置需要解析的jwt
                .parseClaimsJws(token).getBody();
        return claims;
    }

}

拦截器

(5)编写jwt令牌校验的拦截器:管理端+用户端

管理端jwt令牌校验的拦截器

/**
 * 管理端jwt令牌校验的拦截器
 */
@Component
@Slf4j
public class JwtTokenAdminInterceptor implements HandlerInterceptor {

    @Autowired
    private JwtProperties jwtProperties;

    /**
     * 校验jwt
     *
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //判断当前拦截到的是Controller的方法还是其他资源
        if (!(handler instanceof HandlerMethod)) {
            //当前拦截到的不是动态方法,直接放行
            return true;
        }

        //1、从请求头中获取令牌
        String token = request.getHeader(jwtProperties.getAdminTokenName());

        //2、校验令牌
        try {
            log.info("管理端jwt校验:{}", token);
            Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
            Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
            log.info("当前员工id:", empId);

            //将解析出来的员工id存到ThreadLocal中
            BaseContext.setCurrentId(empId);

            //3、通过,放行
            return true;
        } catch (Exception ex) {
            //4、不通过,响应401状态码
            response.setStatus(401);
            return false;
        }
    }
}

用户端jwt令牌校验的拦截器

/**
 * 用户端jwt令牌校验的拦截器
 */
@Component
@Slf4j
public class JwtTokenUserInterceptor implements HandlerInterceptor {
    @Autowired
    private JwtProperties jwtProperties;
    //校验jwt
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //判断当前拦截到的是Controller的方法还是其他资源
        if (!(handler instanceof HandlerMethod)) {
            //当前拦截到的不是动态方法,直接放行
            return true;
        }
        //1、从请求头中获取令牌
        String token = request.getHeader(jwtProperties.getUserTokenName());
        //2、校验令牌
        try {
            log.info("用户端jwt校验:{}", token);
            Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token);
            Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString());
            BaseContext.setCurrentId(userId);
            log.info("当前员工id:", userId);
            //3、通过,放行
            return true;
        } catch (Exception ex) {
            //4、不通过,响应401状态码
            response.setStatus(401);
            return false;
        }
    }
}
(6)配置到WebMvcConfiguration 类中
/**
 * 配置类,注册web层相关组件
 */
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {

    @Autowired
    private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;
    @Autowired
    private JwtTokenUserInterceptor jwtTokenUserInterceptor;
    
    /**
     * 注册自定义拦截器
     *
     * @param registry
     */
    protected void addInterceptors(InterceptorRegistry registry) {
        log.info("开始注册自定义拦截器...");
        registry.addInterceptor(jwtTokenAdminInterceptor)
                .addPathPatterns("/admin/**")
                .excludePathPatterns("/admin/employee/login");

        registry.addInterceptor(jwtTokenUserInterceptor)
                .addPathPatterns("/user/**")
                .excludePathPatterns("/user/user/login")
                .excludePathPatterns("/user/shop/status");
    }
}

5. 阿里云OSS对象存储服务

  本项目文件上传功能使用的是阿里云OSS对象存储服务。
  阿里云 OSS(Object Storage Service) 是阿里云提供的一项对象存储服务,用户可以通过它进行海量数据的存储和管理。OSS 提供可靠、安全、低成本的存储解决方案,支持存储各种类型的文件,如图片、视频、文档、备份数据等。

实现步骤:

(1)开通阿里云对象存储服务oss,创建bucket,获得密钥
①进入阿里云官网,注册或登录后点击控制台

在这里插入图片描述

②开通对象存储服务

在这里插入图片描述

③创建bucket仓库,设置读写权限为公共读

在这里插入图片描述
在这里插入图片描述

④获取id和密钥,注意密钥只能获取一次,获取之后无法再次

在这里插入图片描述

(2)导入maven坐标
<properties>
	<aliyun.sdk.oss>3.10.2</aliyun.sdk.oss>
	<jaxb-api>2.3.1</jaxb-api>
</properties>

<dependencies>
	<!--阿里云OSS-->
	<dependency>
	    <groupId>com.aliyun.oss</groupId>
	    <artifactId>aliyun-sdk-oss</artifactId>
	    <version>${aliyun.sdk.oss}</version>
	</dependency>
	
	<!--jdj1.7以上OSS额外需要的依赖-->
	<dependency>
	    <groupId>javax.xml.bind</groupId>
	    <artifactId>jaxb-api</artifactId>
	    <version>${jaxb-api}</version>
	</dependency>
<dependencies>
(3)在yml文件中添加阿里云OSS服务的配置信息

application.yml主配置文件中的配置信息:通过引用的方式

spring:
  profiles:
用开发环境的配置文件
    active: dev
 
sky:
  alioss:
    endpoint: ${sky.alioss.endpoint}
    bucket-name: ${sky.alioss.bucket-name}
    access-key-id: ${sky.alioss.access-key-id}
    access-key-secret: ${sky.alioss.access-key-secret}

真正是信息配置在:开发环境application-dev.yml中

sky:
 
  alioss:
    endpoint: oss-cn-beijing.aliyuncs.com//自己选择地区的endpoint
    bucket-name:  ****//自己的bucket仓库名称
    access-key-id: ***************//自己的id
    access-key-secret: **************//自己的密钥
(4)声明一个配置属性的文件用于传入连接的参数

@ConfigurationProperties(prefix = “sky.alioss”):负责属性的批量注入

package com.sky.properties;
 
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
 
@Component
@ConfigurationProperties(prefix = "sky.alioss")
@Data
public class AliOssProperties {
 
    private String endpoint;
    private String accessKeyId;
    private String accessKeySecret;
    private String bucketName;
 
}
(5)封装一个aliyunOssUtil工具类,用于上传文件并拼接返回上传的文件的路径
@Data
@AllArgsConstructor
@Slf4j
public class AliOssUtil {
 
    private String endpoint;
    private String accessKeyId;
    private String accessKeySecret;
    private String bucketName;
 
    /**
     * 文件上传
     *
     * @param bytes
     * @param objectName
     * @return
     */
    public String upload(byte[] bytes, String objectName) {
 
        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
 
        try {
            // 创建PutObject请求就是把图片上传到阿里云OSS上。
            ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes));
        } catch (OSSException oe) {
            System.out.println("Caught an OSSException, which means your request made it to OSS, "
                    + "but was rejected with an error response for some reason.");
            System.out.println("Error Message:" + oe.getErrorMessage());
            System.out.println("Error Code:" + oe.getErrorCode());
            System.out.println("Request ID:" + oe.getRequestId());
            System.out.println("Host ID:" + oe.getHostId());
        } catch (ClientException ce) {
            System.out.println("Caught an ClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with OSS, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message:" + ce.getMessage());
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }
 
        //文件访问路径规则 https://BucketName.Endpoint/ObjectName
        StringBuilder stringBuilder = new StringBuilder("https://");
        stringBuilder
                .append(bucketName)
                .append(".")
                .append(endpoint)
                .append("/")
                .append(objectName);
 
        log.info("文件上传到:{}", stringBuilder.toString());
 
        return stringBuilder.toString();
    }
}
(6)编写OssConfiguration 配置类,用于创建AliOssUtil对象
/**
 * 配置类,用于创建AliOssUtil对象
 */
@Configuration
@Slf4j
public class OssConfiguration {

    @Bean
    @ConditionalOnMissingBean//当没有这个对象的时候再去创建
    public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties){
        log.info("开始创建阿里云文件上传工具类对象:{}", aliOssProperties);

        return new AliOssUtil(aliOssProperties.getEndpoint(), aliOssProperties.getAccessKeyId(),
                aliOssProperties.getAccessKeySecret(), aliOssProperties.getBucketName());
    }
}

(7)在Controller层编写接受文件上传请求功能代码

  UUID类负责生成一个新的文件名称;防止文件名称重复

@RestController
@RequestMapping("/admin/common")
@Api (tags = "通用接口")
@Slf4j
public class commonController {
    @Autowired
    private AliOssUtil aliOssUtil;
    @PostMapping("/upload")
    @ApiOperation("文件上传")
    public Result<String> upload(MultipartFile file){
 
        try {
            //接下来就是拼接文件名
            String originalFilename = file.getOriginalFilename();
            String substring = originalFilename.substring(originalFilename.lastIndexOf("."));
            String objectString= UUID.randomUUID()+substring;
            //第一参数为:文件的输入流;第二个参数为上传文件的名称;
            String upload = aliOssUtil.upload(file.getBytes(), objectString);
            return Result.success(upload);
        } catch (IOException e) {
            System.out.println("文件上传异常:"+e);
        }
 
        return Result.error(MessageConstant.UPLOAD_FAILED);
    }
 
}

6. Swagger

6.1 介绍:

  Swagger 是一个广泛使用的 API 文档生成工具现已更名为 OpenAPI,但很多开发者仍然习惯使用 Swagger 这一名称。它用于自动生成 RESTful API 文档、可交互式 API 测试以及接口定义。Swagger 使得 API 开发、测试、文档生成更加高效,提供了一套完整的解决方案,帮助开发人员和前端开发人员更好地理解和使用 API。

6.2 使用步骤

(1) 导入maven坐标
<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-spring-boot-starter</artifactId>
    <version>3.0.2</version>
</dependency>
(2) 在com.sky.config的WebMvcConfiguration类编写配置代码
/**
 * 配置类,注册web层相关组件
 */
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
    /**
     * 通过knife4j生成管理端接口文档
     * @return
     */
	@Bean
    public Docket docket1(){
        log.info("准备生成接口文档...");
        ApiInfo apiInfo = new ApiInfoBuilder()
                .title("苍穹外卖项目接口文档")
                .version("2.0")
                .description("苍穹外卖项目接口文档")
                .build();

        Docket docket = new Docket(DocumentationType.SWAGGER_2)
                .groupName("管理端接口")
                .apiInfo(apiInfo)
                .select()
                //指定生成接口需要扫描的包
                .apis(RequestHandlerSelectors.basePackage("com.sky.controller.admin"))
                .paths(PathSelectors.any())
                .build();

        return docket;
    }
    
     /**
     * 通过knife4j生成用户端接口文档
     * @return
     */
	@Bean
    public Docket docket2(){
        log.info("准备生成接口文档...");
        ApiInfo apiInfo = new ApiInfoBuilder()
                .title("苍穹外卖项目接口文档")
                .version("2.0")
                .description("苍穹外卖项目接口文档")
                .build();

        Docket docket = new Docket(DocumentationType.SWAGGER_2)
                .groupName("用户端接口")
                .apiInfo(apiInfo)
                .select()
                //指定生成接口需要扫描的包
                .apis(RequestHandlerSelectors.basePackage("com.sky.controller.user"))
                .paths(PathSelectors.any())
                .build();

        return docket;
    }
}

  它会扫描controller里面的所有方法,然后通过反射去解析,最终生成接口文档。

(3) 然后设置静态资源映射:
 /**
 * 配置类,注册web层相关组件
 */
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
    /**
     * 设置静态资源映射,主要是访问接口文档(html、js、css)
     * @param registry
     */
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        log.info("开始设置静态资源映射...");
        registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
    }
}
(4) 添加注解:主要用于Controller层
① 在类上添加@Api(tags = " ")注解,tags为标签名称

作用:
  给控制器类添加标签的注解,它帮助在 Swagger UI 中对 API 进行分类展示。
在这里插入图片描述

② 在方法上添加@ApiOperation(" ")注解

作用:
  主要用来描述一个 API 方法的功能,使得接口文档更加清晰。
在这里插入图片描述

7. 用户密码md5加密

  点开数据库,查看我们存储的密码。
在这里插入图片描述

  我们发现,员工密码在数据库中是明文存储的,这是十分不安全的,所以我们需要对密码进行加密,本项目采用的是md5码加密方式

实现方式:
  调用org.springframework.util包下的DigestUtils.md5DigestAsHex( )方法,并调用password.getBytes()将密码转换成字节数组当作参数传进去。

import org.junit.jupiter.api.Test;
import org.springframework.util.DigestUtils;
	...
	...
	@Test
    public void test01() {
        String s = "123456";
        String md5 = DigestUtils.md5DigestAsHex(s.getBytes());
        System.out.println("123456 的md5码格式:" + md5);
    }

在这里插入图片描述

  业务层员工登录功能完整代码:

    public Employee login(EmployeeLoginDTO employeeLoginDTO) {
        String username = employeeLoginDTO.getUsername();
        String password = employeeLoginDTO.getPassword();
        //1、根据用户名查询数据库中的数据
        Employee employee = employeeMapper.getByUsername(username);
        //2、处理各种异常情况(用户名不存在、密码不对、账号被锁定)
        if (employee == null) {
            //账号不存在
            throw new AccountNotFoundException(MessageConstant.ACCOUNT_NOT_FOUND);
        }
        //密码比对
        password = DigestUtils.md5DigestAsHex(password.getBytes());
        if (!password.equals(employee.getPassword())) {
            //密码错误
            throw new PasswordErrorException(MessageConstant.PASSWORD_ERROR);
        }
        if (employee.getStatus() == StatusConstant.DISABLE) {
            //账号被锁定
            throw new AccountLockedException(MessageConstant.ACCOUNT_LOCKED);
        }
        //3、返回实体对象
        return employee;
    }

  这样我们的密码就从明文转换成md5码格式了。
在这里插入图片描述

8. 给属性赋值的两种新方法

需求:创建一个Employee对象,并给其属性赋值。

传统方法:set()

  调用set()方法给属性赋值。

    @Test
    public void test02() {
        Employee employee = new Employee();
        
        employee.setId(1L);
        employee.setUsername("xiaomin");
        employee.setPassword("123456");
        employee.setName("小明");
        employee.setSex("男");
        employee.setName("小明");
        employee.setName("小明");
        employee.setPhone("10086");
        
        System.out.println(employee);
    }

在这里插入图片描述

新方法1:对象属性拷贝BeanUtils.copyProperties()

 &Emsp;调用org.springframework.beans包下的BeanUtils.copyProperties();即可实现将employee1 的属性赋值给employee2。

    @Test
    public void test02() {
        Employee employee1 = new Employee();

        employee1.setId(1L);
        employee1.setUsername("xiaomin");
        employee1.setPassword("123456");
        employee1.setName("小明");
        employee1.setSex("男");
        employee1.setName("小明");
        employee1.setName("小明");
        employee1.setPhone("10086");
        
        System.out.println(employee1);

        Employee employee2 = new Employee();
        BeanUtils.copyProperties(employee1, employee2);
        System.out.println(employee2);
    }

在这里插入图片描述

新方法2:构建器模式给属性赋值@Builder

 &emsp;在类上添加lombok包中的@Builder注解,即可开启构建者模式给属性赋值。

构建器模式是一种设计模式。

在这里插入图片描述

 &emsp;java代码实现

@Test
    public void test04(){
        Employee employee = Employee
                .builder()
                .id(1L)
                .username("xiaomin")
                .password("123456")
                .name("小明")
                .sex("男")
                .phone("10086")
                .build();
        System.out.println(employee);
    }

在这里插入图片描述

总结:

  • 传统 set() 方法 适合简单的对象,特别是当对象的属性不多时,直观且易于理解。
  • 对象属性拷贝 适合需要批量复制属性的场景,尤其是在不同对象间属性名称相同的时候。
  • @Builder 构建器模式 适合构建复杂对象或不可变对象,确保对象创建时所有必要的字段都已赋值,并且代码更具可读性和可维护性。

9. StringUtils

软件包:package org.apache.commons.lang;

  StringUtils.join(list, “-”);
  用于将 list 中的元素连接成一个字符串,并且在每个元素之间插入指定的分隔符(这里是 “-”)。

@Test
    public void test05(){
        List<String> list = new ArrayList<>();
        list.add("2024");
        list.add("12");
        list.add("23");
        String join = StringUtils.join(list, "-");
        System.out.println(join);
    }

在这里插入图片描述

10. 通过ThreadLocal获取用户当前线程id

  ThreadLocal,它是Thread的局部变量,为每个线程提供单独一份的存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,在线程外则不能访问。
  在本项目中,通过ThreadLocal来获取用户线程id,并将该值作为用户账号的创建人id和修改人id。

BaseContext 类

package com.sky.context;

public class BaseContext {

    public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

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

    public static Long getCurrentId() {
        return threadLocal.get();
    }

    public static void removeCurrentId() {
        threadLocal.remove();
    }

}

EmployeeServiceImpl类:

    /**
     * 新增员工
     * @param employeeDTO
     */
    @Override
    public void save(EmployeeDTO employeeDTO) {
        Employee employee = new Employee();
        //对象属性拷贝
        BeanUtils.copyProperties(employeeDTO,employee);
        //设置账号默认状态
        employee.setStatus(StatusConstant.ENABLE);
        //设置默认密码
        employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));
        //设置当前创建时间和修改时间
        employee.setCreateTime(LocalDateTime.now());
        employee.setUpdateTime(LocalDateTime.now());
        //设置创建人id和修改人id
        employee.setCreateUser(BaseContext.getCurrentId());
        employee.setUpdateUser(BaseContext.getCurrentId());
		//插入到数据库中
        employeeMapper.insert(employee);
    }

11. PageHelper分页插件

11.1 介绍

  PageHelper 是一个基于 MyBatis 的分页插件,能够通过拦截器在 MyBatis 执行 SQL 查询时自动为其添加分页参数,简化了分页查询的代码。pagehelper-spring-boot-starter 是 Spring Boot 项目的集成版本,它通过自动配置,简化了在 Spring Boot 环境中配置 PageHelper 的工作。

11.2 使用步骤

(1) 导入maven坐标
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.3.0</version>
</dependency>
(2) 编写分页查询代码

以员工分页查询为例:

EmployeeController:
    /**
     * 员工分页查询
     * @param employeePageQueryDTO
     * @return
     */
    @GetMapping("/page")
    @ApiOperation("员工分页查询")
    public Result<PageResult> pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
        log.info("员工分页查询:{}", employeePageQueryDTO);

        PageResult pageResult = employeeService.pageQuery(employeePageQueryDTO);

        return Result.success(pageResult);
    }
EmployeeService:
    /**
     * 员工分页查询
     * @param employeePageQueryDTO
     * @return
     */
    PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO);
EmployeeServiceImpl:
    /**
     * 员工分页查询
     * @param employeePageQueryDTO
     * @return
     */
    @Override
    public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
        //PageHelper
        PageHelper.startPage(employeePageQueryDTO.getPage(),employeePageQueryDTO.getPageSize());

        Page<Employee> employees = (Page<Employee>)employeeMapper.pageQuery(employeePageQueryDTO);

        return new PageResult(employees.getTotal(),employees.getResult());

    }
EmployeeMapper:
/**
 * 员工分页查询
 * @param employeePageQueryDTO
 * @return
 */
Page<Employee> pageQuery(EmployeePageQueryDTO employeePageQueryDTO);
EmployeeMapper.xml:
    <select id="pageQuery" resultType="com.sky.entity.Employee">
        select * from sky_take_out.employee
        <where>
            <if test="name != null and name != ''">
                and name like concat ('%',#{name},'%')
            </if>
        </where>
        order by create_time desc
    </select>

  在EmployeeMapper.xml中编写SQL语句,limit不用我们手写,pagehelper会自动帮我们追加拼接

12. 全局异常处理器

12.1 介绍

  在 Java 的 Spring 框架中,全局异常处理器(Global Exception Handler) 是一种统一处理应用中所有异常的机制,能够有效地集中处理各类异常,避免在每个控制器方法中重复编写异常处理代码。Spring 提供了几种方法来实现全局异常处理,其中最常见的是使用 @ControllerAdvice 和 @ExceptionHandler 注解。

12.2 Spring中的全局异常处理器

@RestControllerAdvice

  在 Spring 中,通常使用 @ControllerAdvice 或 @RestControllerAdvice 注解来定义全局异常处理器。其主要作用是捕获控制器(Controller)中抛出的异常,并返回统一格式的响应。

序号注解描述适用场景
1@ControllerAdvice用于捕获控制器中的异常,适用于传统的返回视图(如 JSP)的 Web 应用。传统的 Web 应用(如使用 JSP 或 Thymeleaf 渲染视图)
2@RestControllerAdvice基于 @ControllerAdvice,但默认所有方法的返回值会被转为 JSON 或其他格式,适用于 RESTful 风格的 API 开发。RESTful 风格的 API(返回 JSON 或其他格式)
@ExceptionHandler

  你可以通过 @ExceptionHandler 注解捕获特定类型的异常,并进行相应处理。例如,捕获数据库连接异常、空指针异常等。

@ExceptionHandler(" ")参数为:捕获的异常.class

12.3 项目中应用

/**
 * 全局异常处理器,处理项目中抛出的业务异常
 */
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 捕获业务异常
     * @param ex
     * @return
     */
    @ExceptionHandler(BaseException.class)
    public Result exceptionHandler(BaseException ex){
        log.error("异常信息:{}", ex.getMessage());
        return Result.error(ex.getMessage());
    }

    /**
     * 处理SQL字段添加重复异常
     * @param ex
     * @return
     */
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
        //Duplicate entry 'zhangsan' for key 'employee.idx_username'
        String message = ex.getMessage();
        if(message.contains("Duplicate entry")){
            String[] split = message.split(" ");
            String username = split[2];
            String msg = username + MessageConstant.ALREADY_EXISTS;
            return Result.error(msg);
        }else{
            return Result.error(MessageConstant.UNKNOWN_ERROR);
        }
    }
}

13. 日期时间格式化

  新增员工后,查看前端显示,发现日期时间显示有问题(数字挤在一起),原因是我们没有对日期时间进行格式化。
在这里插入图片描述
  我们有两种方式对日期时间进行格式化,分别是@JsonFormat注解方式和拓展(自定义)Spring MVC 消息转换器。

@JsonFormat注解方式

  在Employee实体类中的LocalDateTime属性上加上@JsonFormat注解,格式化时间。

public class Employee implements Serializable {
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;
}

  但是这种方式在面对非常多的实体类时,就显得有些力不从心了,我们需要一个一个给每个带有日期时间属性的类的添加@JsonFormat注解,会非常的繁琐,拓展(自定义)Spring MVC 消息转换器就很好的解决了这个问题

拓展(自定义)Spring MVC 消息转换器

(1)编写配置类

  在sky-server下的com/sky/config/WebMvcConfiguration下创建:

//托转Spring MVC框架的消息转换器
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        //先创建一个消息转换器对象
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        //为消息转换器设置一个对象转换器,对象转换器可以将Java对象序列化为json数据
        converter.setObjectMapper(new JacksonObjectMapper());
        //消息转换器还没交给框架,需要把消息转换器加到容器里
        converters.add(0,converter); //容器自带消息转换器,默认新加的排在末尾,0表示是首位,自己加的消息转换器排在首位
    }
(2)修改日期时间显示样式

  在sky-commom下com.sky.json的JacksonObjectMapper中可以修改日期时间显示样式

/**
 * 对象映射器:基于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_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm";
    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(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);
    }
}

14. AOP技术应用:公共字段自动填充

问题:

  在多个业务表中都有公共字段,如create_time、create_user(insert时用到);update_time,update_user(insert和update时用到)这些。
在这里插入图片描述

  插入数据的时候需要为这些字段赋值,会有大量重复的冗余set方法代码,后期如果表结构发生变化,代码需要跟着修改,此时非常不方便维护(如果后期进行修改要一个个进行修改)。
  解决方法:通过AOP技术,将公共字段的赋值逻辑抽取出来,减少冗余代码,提高代码的可维护性。

AOP原理可以查看【AOP】什么是AOP?AOP有哪些应用?

实现步骤:

(1)导入AOP maven坐标
<properties>
	<aspectj>1.9.4</aspectj>
</properties>

<!--AOP-->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>${aspectj}</version>
</dependency>

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>${aspectj}</version>
</dependency>
(2)自定义一个AutoFill的Annotation注解。

  在com.sky下创建annotation包,创建AutoFill注解。

//自定义注解,用于标识某个方法需要进行功能字段自动填充处理
@Target(ElementType.METHOD)//@Target注解指定了AutoFill注解可以应用的目标。ElementType.METHOD表示该注解只能应用于方法。
@Retention(RetentionPolicy.RUNTIME)//@Retention注解指定了注解的生命周期,
                                    //RetentionPolicy.RUNTIME表示该注解在运行时有效,即可以通过反射机制访问到该注解。
public @interface AutoFill {
    //数据库操作类型:UPDATE INSERT
    /**
     * AutoFill注解本身包含一个属性value(),类型为OperationType。
     * 这个属性的作用是标识当前方法是处理数据库操作中的“插入(INSERT)”还是“更新(UPDATE)”操作。
     * @return
     */
    OperationType value();
}
(3)自定义切面,实现公共字段自动填充处理逻辑
//自定义切面,实现公共字段自动填充处理逻辑
/**
 * @Pointcut定义了拦截的范围,即com.sky.mapper包下带有@AutoFill注解的方法。
 * @Before通知会在方法执行前触发,提供对方法参数的访问权限,可以在这里执行公共字段的自动填充。
 */
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
    //切入点
    @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
    public void autoFillPointCut(){}
    //前置通知,在通知中进行公共字段的赋值
    @Before("autoFillPointCut()")
    public void autoFill(JoinPoint joinPoint){
        log.info("开始进行公共字段自动填充...");

        MethodSignature signature = (MethodSignature) joinPoint.getSignature();//通过连接点对象来获取签名,向下转型为MethodSignature
        AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);//获得方法上的注解对象
        OperationType operationType = autoFill.value();//获得数据库操作类型(Insert or Update)

        Object[] args = joinPoint.getArgs(); //获得了方法所有的参数
        if(args == null || args.length==0 ){ //没有参数
            return;
        }
        Object entity = args[0];//现在约定实体放在第1个位置,传入实体可能不同所以用Object

        LocalDateTime now = LocalDateTime.now();
        Long currentId = BaseContext.getCurrentId();
        if(operationType == OperationType.INSERT){
            //为4个公共字段赋值
            try {
                Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class); //把方法名全部换成常量类,防止写错
                Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
                Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
                //4.根据当前不同的操作类型,为对应的属性通过反射来赋值
                setCreateTime.invoke(entity,now);
                setCreateUser.invoke(entity,currentId);
                setUpdateTime.invoke(entity,now);
                setUpdateUser.invoke(entity,currentId);
            }catch (Exception e){
                e.printStackTrace();
            }
        }else if(operationType == OperationType.UPDATE){
            try {
                //为2个公共字段赋值
                Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
                //4.根据当前不同的操作类型,为对应的属性通过反射来赋值
                setUpdateTime.invoke(entity, now);
                setUpdateUser.invoke(entity, currentId);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}
(4)添加@AutoFill注解

  在mapper层Insert和Update方法上加上@AutoFill注解,注解内容用OperationType.INSERT或OperationType.Update。

@Mapper
public interface CategoryMapper {
    /**
     * 插入数据
     * @param category
     */
    @Insert("insert into sky_take_out.category(type, name, sort, status, create_time, update_time, create_user, update_user)" +
            " VALUES" +
            " (#{type}, #{name}, #{sort}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})")
    @AutoFill(value = OperationType.INSERT)
    void insert(Category category);

    /**
     * 根据id修改分类
     * @param category
     */
    @AutoFill(value = OperationType.UPDATE)
    void update(Category category);
}
public interface EmployeeMapper {
    /**
     * 新增员工
     * @param employee
     */
    @Insert("insert into sky_take_out.employee(name, username, password, phone, sex, id_number, status, create_time, update_time, create_user, update_user) " +
            "values " +
            "(#{name},#{username},#{password},#{phone},#{sex},#{idNumber},#{status},#{createTime},#{updateTime},#{createUser},#{updateUser})")
    @AutoFill(OperationType.INSERT)
    void insert(Employee employee);

    /**
     * 修改员工信息
     * @param employee
     */
    @AutoFill(value = OperationType.UPDATE)
    void update(Employee employee);
}
注意:要把service层的那些手动赋值删除掉或者注释掉。
@Service
@Slf4j
public class CategoryServiceImpl implements CategoryService {

    @Autowired
    private CategoryMapper categoryMapper;
    @Autowired
    private DishMapper dishMapper;
    @Autowired
    private SetmealMapper setmealMapper;

    /**
     * 新增分类
     * @param categoryDTO
     */
    public void save(CategoryDTO categoryDTO) {
        Category category = new Category();
        //属性拷贝
        BeanUtils.copyProperties(categoryDTO, category);

        //分类状态默认为禁用状态0
        category.setStatus(StatusConstant.DISABLE);

        //设置创建时间、修改时间、创建人、修改人
//        category.setCreateTime(LocalDateTime.now());
//        category.setUpdateTime(LocalDateTime.now());
//        category.setCreateUser(BaseContext.getCurrentId());
//        category.setUpdateUser(BaseContext.getCurrentId());

        categoryMapper.insert(category);
    }
}

15. 微信小程序开发

15.1 介绍

  微信小程序(WeChat Mini Program) 是腾讯微信平台推出的一种不需要下载安装即可使用的应用,它实现了“触手可及”的体验。用户只需扫描二维码、搜索或从聊天记录中点击,就可以使用小程序。小程序具有高效、轻量、跨平台等优势,成为了开发者在移动互联网领域的一个重要工具。

15.1 微信小程序开发流程

前期准备

(1)注册小程序账号

  访问微信公众平台: https://mp.weixin.qq.com
  注册一个小程序账号,完成相关信息的填写和企业认证(如需要)。

(2)下载开发工具

  微信官方提供了微信开发者工具,用于编写、调试和预览小程序。

(3)创建小程序项目

  在微信开发者工具中登录你的微信小程序账号。
  直接导入微信小程序项目,我们注意负责小程序端的后端接口功能编写

IDEA配置

(1)yml配置微信开发者工具相关参数

application.yml

  wechat:
    appid: ${sky.wechat.appid}
    secret: ${sky.wechat.secret}

application-dev.yml

  wechat:
    appid: wxd00c****0b6c1b
    secret: 6bbf66057*******8107facd2073
(2)编写属性类,用于传入配置参数
package com.sky.properties;

import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "sky.wechat")
@Data
public class WeChatProperties {

    private String appid; //小程序的appid
    private String secret; //小程序的密钥
}

16. Redis相关。

16.1 Spring Data Redis

16.1.1 介绍

  Spring Data Redis 是一个 Spring 项目,用于在 Spring 应用程序中集成 Redis。它提供了对 Redis 操作的高层次抽象,并使得 Redis 的操作变得更为简便和灵活。Spring Data Redis 支持多种 Redis 数据结构,如字符串(String)、哈希(Hash)、列表(List)、集合(Set)、有序集合(Sorted Set)等,能够与 Spring 缓存、Spring Session、Spring Data 等模块紧密集成。

16.1.2 RedisTemplate

  Spring Data Redis 提供了一个封装类 RedisTemplate,将 Redis 的操作按数据类型进行了归类封装,具体分类如下:

操作接口说明
ValueOperations处理 string 类型的数据操作
SetOperations处理 set 类型的数据操作
ZSetOperations处理 zset 类型的数据操作
HashOperations处理 hash 类型的数据操作
ListOperations处理 list 类型的数据操作
16.1.2 使用步骤
(1)导入maven坐标
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
(2)配置Redis数据源
在application.yml中配置引用
spring:
  redis:
    host: ${sky.redis.host}
    port: ${sky.redis.port}
    database: ${sky.redis.database} 
在application-dev.yml中配置具体值
sky:
  redis:
    host: localhost
    port: 6379
    database: 0
(3)编写配置类
@Configuration
@Slf4j
public class RedisConfiguration {
    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
        log.info("开始创建redis模板对象...");
        RedisTemplate redisTemplate = new RedisTemplate();
        //设置redis的连接工厂对象
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        //设置redis key的序列化器
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        return redisTemplate;
    }
}
(4)创建RedisTemplate对象操作Redis
@SpringBootTest
public class SpringDataRedisTest {
    @Autowired
    private RedisTemplate redisTemplate;
    @Test
    public void testRedisTemplate(){
       System.out.println(redisTemplate);
        ValueOperations valueOperations = redisTemplate.opsForValue();
        HashOperations hashOperations = redisTemplate.opsForHash();
        ListOperations listOperations = redisTemplate.opsForList();
        ZSetOperations zSetOperations = redisTemplate.opsForZSet();
    }
}
注意:测试类上要添加@SpringBootTest注解

16.2 Spring Cache

16.2.1 介绍

  Spring Cache 是 Spring Framework 提供的缓存抽象,它简化了缓存的管理。通过在方法上使用缓存注解,你可以将缓存逻辑与业务逻辑分离,提升性能。Spring Cache 提供了多个缓存提供者支持,如 Ehcache、Redis、Guava 等。

16.2.2 常用注解
注解描述
@EnableCaching在启动类上加入 @EnableCaching,开启缓存注解功能。
@Cacheable在方法执行前查询缓存中是否有数据,如果有数据,直接返回缓存数据;如果没有,调用方法并将返回值放到缓存中
@CachePut将方法的返回值放到缓存中
@CacheEvict将一条或多条数据从缓存中删除
16.2.3 使用步骤
(1)导入maven坐标
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
(2)在启动类上添加加@EnableCaching注解
@SpringBootApplication
@EnableTransactionManagement //开启注解方式的事务管理
@EnableCaching //开启注解缓存
@EnableScheduling //开启任务调度
@Slf4j
public class SkyApplication {
    public static void main(String[] args) {
        SpringApplication.run(SkyApplication.class, args);
        log.info("server started");
    }
}
(3)应用:
① 查询操作:

  @Cacheable注解:在方法执行前查询缓存中是否有数据,如果有数据,直接返回缓存数据;如果没有,调用方法并将返回值放到缓存中
  多用于查询操作

@RestController("userSetmealController")
@RequestMapping("/user/setmeal")
@Api(tags = "C端-套餐浏览接口")
public class SetmealController {
    @Autowired
    private SetmealService setmealService;

    /**
     * 条件查询
     *
     * @param categoryId
     * @return
     */
    @GetMapping("/list")
    @ApiOperation("根据分类id查询套餐")
    @Cacheable(cacheNames = "setmealCache", key = "#categoryId")
    public Result<List<Setmeal>> list(Long categoryId) {
        Setmeal setmeal = new Setmeal();
        setmeal.setCategoryId(categoryId);
        setmeal.setStatus(StatusConstant.ENABLE);

        List<Setmeal> list = setmealService.list(setmeal);
        return Result.success(list);
    }
② 增删改操作:

  @CacheEvict注解:将一条或多条数据从缓存中删除
  所有查询操作无需添加注解,只有增删改操作涉及会对数据库数据产生变化的才需要清理缓存

@RestController
@RequestMapping("/admin/setmeal")
@Slf4j
@Api("套餐管理相关接口")
public class SetmealController {

    @Autowired
    private SetmealService setmealService;

    /**
     * 新增套餐
     * @param setmealDTO
     * @return
     */
    @PostMapping
    @ApiOperation("新增套餐")
    @CacheEvict(cacheNames = "setmealCache", key = "#setmealDTO.categoryId")
    public Result save(@RequestBody SetmealDTO setmealDTO) {

        log.info("新增套餐:{}",setmealDTO);

        setmealService.saveWithDish(setmealDTO);

        return Result.success();
    }

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

        setmealService.deteleBatch(ids);

        return Result.success();
    }

    /**
     * 修改套餐
     * @param setmealDTO
     * @return
     */
    @PutMapping
    @ApiOperation("修改套餐")
    @CacheEvict(cacheNames = "setmealCache", allEntries = true)
    public Result update(@RequestBody SetmealDTO setmealDTO) {
        setmealService.update(setmealDTO);
        return Result.success();
    }

    /**
     * 套餐启售、停售
     * @param status
     * @param id
     * @return
     */
    @PostMapping("/status/{status}")
    @ApiOperation("套餐起售停售")
    @CacheEvict(cacheNames = "setmealCache", allEntries = true)
    public Result startOrStop(@PathVariable Integer status, Long id) {
        setmealService.startOrStop(status, id);
        return Result.success();
    }
}

17. Spring Task

17.1 Spring Task 介绍

  Spring Task 是Spring框架提供的任务调度工具,可以按照约定的时间自动执行某个代码逻辑。
  定位: 定时任务框架
  作用: 定时自动执行某段java代码
  应用场景: (1)信用卡每月还款提醒
        (2)银行贷款每月还款提醒
        (3)火车票售票系统处理未支付订单
        (4)入职纪念日为用户发送通知
  强调: 只要是需要定时处理的场景都可以使用Spring Task

17.2 cron表达式

介绍:

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

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

  每个域的含义分别为:秒、分钟、小时、日、月、周、年(可选)

举例:

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

0091210?2022

  说明: 一般 的值不同时设置,其中一个设置,另一个用?表示。
  比如: 描述2月份的最后一天,最后一天具体是几号呢?可能是28号,也有可能是29号,所以就不能写具体数字。

  为了描述这些信息,提供一些特殊的字符。这些具体的细节,我们就不用自己去手写,因为这个cron表达式,它其实有在线生成器。
  cron表达式在线生成器:https://cron.qqe2.com/

请添加图片描述
  我们可以直接在这个网站上面,只要根据自己的要求去生成corn表达式即可。所以一般就不用自己去编写这个表达式。

通配符:

* 表示所有值;

? 表示未说明的值,即不关心它为何值;

- 表示一个指定的范围;

, 表示附加一个可能值;

/ 符号前表示开始时间,符号后表示每次递增的值;

cron表达式案例:

*/5 * * * * ? 每隔5秒执行一次

0 */1 * * * ? 每隔1分钟执行一次

0 0 5-15 * * ? 每天5-15点整点触发

0 0/3 * * * ? 每三分钟触发一次

0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发

0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发

0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发

0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时

0 0 10,14,16 * * ? 每天上午10点,下午2点,4点

17.3 入门案例

  编写一个简单的定时任务,每5秒输出一次当前时间。

Spring Task使用步骤:

  • ① 在pom.xml文件中导入maven坐标
  • ② 启动类添加注解 @EnableScheduling 开启任务调度
  • ③ 自定义定时任务类
(1)导入maven坐标
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tasks</artifactId>
    <version>5.3.25</version> <!-- 请根据需要替换为最新版本 -->
</dependency>

  如果使用 Spring Boot,可以直接引入 spring-boot-starter,它包含了定时任务的相关功能:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>
(2)启动类添加注解 @EnableScheduling 开启任务调度

(3)自定义定时任务类
package com.sky.task;

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component//将类标记为 Bean 的基本注解,表示这个类会被 Spring 容器管理。
@Slf4j
public class MyTask {
    /**
     * 定时任务 每隔5秒触发一次
     */
    @Scheduled(cron = "0/5 * * * * ?")
    public void performTask(){
        log.info("定时任务开始执行:{}", new Date());
    }

}
注意:

  类上要添加 @Component ,将类标记为 Bean 的基本注解,表示这个类会被 Spring 容器管理。
  除了 @Component,还有 @Repository、@Service、@Controller 等衍生注解,用于在不同的层(如 DAO、服务层、控制器层)标识 Bean。

  在方法上添加 @Scheduled 注解,表示该方法会被定时执行。注解内可以指定cron表达式 ,表示该方法定时多久被执行一次

运行结果

在这里插入图片描述
  从运行结果可以看出,程序每隔5秒输出一次当前时间,符合预期。

17.4 本项目中应用

@Component
@Slf4j
public class OrderTask {
    @Autowired
    OrderMapper orderMapper;

    /**
     * 定时处理超时订单
     */
    @Scheduled(cron = "0 * * * * ? ") //每分钟触发一次
    public void processingTimeoutOrder(){
        log.info("定时处理超时订单:{}" , LocalDateTime.now());
        //select * from orders status = ? and order_time < (当前时间 - 15分钟)
        LocalDateTime time = LocalDateTime.now().plusMinutes(15);//当前时间 - 15分钟
        List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.PENDING_PAYMENT, time);
        if(ordersList !=null && ordersList.size()>0){
            for(Orders order : ordersList){
                order.setCancelTime(LocalDateTime.now());
                order.setCancelReason("订单超时,自动取消");
                order.setStatus(Orders.CANCELLED);
                orderMapper.update(order);
            }
        }
    }
    /**
     * 定时处理未支付订单
     */
    @Scheduled(cron = "0 0 1 * * ?")//每天凌晨1点触发一次
    public void processingDeliveryOrder(){
        log.info("定时处理未支付订单:{}" , LocalDateTime.now());
        LocalDateTime time = LocalDateTime.now().plusMinutes(-60);
        List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.DELIVERY_IN_PROGRESS, time);
        if(ordersList != null && ordersList.size()>0) {
            for (Orders orders : ordersList) {
                orders.setStatus(Orders.COMPLETED);
                orderMapper.update(orders);
            }
        }
    }
}

18. WebSocket

18.1 介绍

  WebSocket 是一种网络通信协议,旨在通过一个持久的连接实现双向、实时通信。它允许客户端和服务器之间通过一个持久的连接进行数据交换,而不需要频繁地建立和关闭连接,从而显著提高了通信效率。
  WebSocket 主要应用于需要低延迟和高频率数据交换的场景,如在线游戏、实时聊天、股票市场、即时通知等。

18.2 Http协议和WebSocket协议对比

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

18.3 项目中应用

(1)导入maven坐标
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
(2)导入WebSocket服务端组件WebSocketServer,用于和客户端通信
package com.sky.websocket;

import org.springframework.stereotype.Component;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

/**
 * WebSocket服务
 */
@Component
@ServerEndpoint("/ws/{sid}")
public class WebSocketServer {

    //存放会话对象
    private static Map<String, Session> sessionMap = new HashMap();

    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("sid") String sid) {
        System.out.println("客户端:" + sid + "建立连接");
        sessionMap.put(sid, session);
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息
     */
    @OnMessage
    public void onMessage(String message, @PathParam("sid") String sid) {
        System.out.println("收到来自客户端:" + sid + "的信息:" + message);
    }

    /**
     * 连接关闭调用的方法
     *
     * @param sid
     */
    @OnClose
    public void onClose(@PathParam("sid") String sid) {
        System.out.println("连接断开:" + sid);
        sessionMap.remove(sid);
    }

    /**
     * 群发
     *
     * @param message
     */
    public void sendToAllClient(String message) {
        Collection<Session> sessions = sessionMap.values();
        for (Session session : sessions) {
            try {
                //服务器向客户端发送消息
                session.getBasicRemote().sendText(message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

}
(3)导入配置类WebSocketConfiguration,注册WebSocket的服务端组件
package com.sky.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * WebSocket配置类,用于注册WebSocket的Bean
 */
@Configuration
public class WebSocketConfiguration {

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}
(4)来单提醒

  在sky-server的service下的OrderServiceImpl中先自动注入WebSocketServer:

@Autowired
private WebSocketServer webSocketServer;

  在serviceOrderServiceImpl的payment方法中写入如下代码:

//通过websocket向客户端浏览器推送消息 type orderId content
Map map = new HashMap();
map.put("type",1);
map.put("orderId",this.orders.getId());
map.put("content","订单号:"+this.orders.getNumber());
String json = JSON.toJSONString(map);
webSocketServer.sendToAllClient(json);
(5) 客户催单

controller.user下的OrderController

@GetMapping("/reminder/{id}")
@ApiOperation("客户催单")
public Result reminder(@PathVariable("id") Long id){
    orderService.reminder(id);
    return Result.success();
}

service下的OrderService

//客户催单
void reminder(Long id);

OrderServiceImpl

//客户催单
public void reminder(Long id){
    // 根据id查询订单
    Orders ordersDB = orderMapper.getById(id);
    // 校验订单是否存在
    if (ordersDB == null) {
        throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
    }
    Map map = new HashMap();
    map.put("type",2);
    map.put("orderId",id);
    map.put("content","订单号:"+ordersDB.getNumber());
    webSocketServer.sendToAllClient(JSON.toJSONString(map));
}

19. Apache ECharts

19.1 介绍

  Apache ECharts(简称 ECharts) 是一个基于 JavaScript 的开源可视化图表库,由 Apache Software Foundation 维护。ECharts 支持多种图表类型、交互方式,并且具有良好的性能表现,特别适合用来展示大规模的数据集。ECharts 可以在浏览器中生成各种交互式图表,广泛应用于数据可视化领域,如商业分析、数据展示、报表等。
  ECharts 的特点是功能强大、定制性强,并且支持多种图表类型与丰富的交互方式,同时具有很好的响应式设计,能够适配不同的屏幕尺寸。
  我们作为后端开发人员需要做的是需供符合格式要求的动态数据,然后响应给前端,用于展示图表。

19.2 实现步骤

在sky-server.controller.admin下新建ReportController类
@RestController
@RequestMapping("/admin/report")
@Api(tags="数据统计相关接口")
@Slf4j
public class ReportController {
    @Autowired
    private ReportService reportService;
    //营业额统计
    @GetMapping("/turnoverStatistics")
    @ApiOperation("营业额统计")
    public Result<TurnoverReportVO> turnoverStatistics(
            @DateTimeFormat(pattern="yyyy-MM-dd") LocalDate begin,
            @DateTimeFormat(pattern="yyyy-MM-dd") LocalDate end){
        log.info("营业额数据统计:{},{}",begin,end);
        return Result.success(reportService.getTurnoverStatistics(begin,end));
 
    }
}
在sky-server.service下新建ReportService接口
public interface ReportService {
    //统计指定时间区间内的营业额数据
    TurnoverReportVO getTurnoverStatistics(LocalDate begin,LocalDate end);
}
在sky-server.service.Impl下新建ReportServiceImpl类
@Service
@Slf4j
public class ReportServiceImpl implements ReportService {
    @Autowired
    private OrderMapper orderMapper;
    //统计指定时间区间内的营业额数据
    public TurnoverReportVO getTurnoverStatistics(LocalDate begin,LocalDate end){
        //当前集合用于存放从begin到end范围内的每天的日期
        List<LocalDate> dateList = new ArrayList<>();
        dateList.add(begin);
        while(!begin.equals(end)) {
            //日期计算,计算指定日期的后一天对应的日期
            begin = begin.plusDays(1);
            dateList.add(begin);
        }
        //存放每天的营业额
        List<Double> turnoverList = new ArrayList<>();
        for(LocalDate date : dateList){
            //查询date日期对应的营业额数据,营业额是指:状态为“已完成”的订单金额合计。
            //LocalDate只有年月日
            LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN); //LocalTime.MIN相当于获得0点0分
            LocalDateTime endTime = LocalDateTime.of(date,LocalTime.MAX);//无限接近于下一个日期的0点0分0秒
            //select sum(amount) from orders where order_time > ? and order_time < ? and status = 5
            //status==5代表订单已完成
            Map map = new HashMap();
            map.put("begin",beginTime);
            map.put("end",endTime);
            map.put("status", Orders.COMPLETED);
            Double turnover = orderMapper.sumByMap(map); //算出当天的营业额
            //考虑当天营业额为0的情况,会返回空
            turnover = turnover == null ? 0.0:turnover;
            turnoverList.add(turnover);
        }
        //封装返回结果
        return TurnoverReportVO
                .builder()
                .dateList(StringUtils.join(dateList,","))
                .turnoverList(StringUtils.join(turnoverList,","))
                .build();
    }
}
sky-server.mapper层下的OrderMapper类
//根据动态条件统计营业额数据
Double sumByMap(Map map);
sky-server.resources.mapper下的ReportMapper.xml
<select id="sumByMap" resultType="java.lang.Double">
        select sum(amount) from orders
        <where>
            <if test="begin != null">and order_time &gt; #{begin}</if>
            <if test="end != null">and order_Time &lt; #{end}</if>
            <if test="status != null"> and status = #{status} </if>
        </where>
</select>

用户统计、订单统计类似,不在赘述。

20. Apache POI

20.1 介绍

  Apache POI 是一个开源 Java 库,用于读写 Microsoft Office 格式的文档,如 Word(.doc、.docx)、Excel(.xls、.xlsx)、PowerPoint(.ppt、.pptx)等。POI 的全称是 Poor Obfuscation Implementation,是由 Apache 软件基金会开发和维护的一个项目。通过 Apache POI,开发者可以通过 Java 程序操作和生成 Office 文档,而无需依赖 Microsoft Office 软件。

20.2 使用步骤

(1)导入maven坐标
<!-- poi -->
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>${poi}</version>
</dependency>
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>${poi}</version>
</dependency>
(2)将报表模板.xlsx导入到项目的resources/template目录下

在这里插入图片描述

(3)编写数据导出代码
sky-server.controller.admin下的ReportController类
//导出运营数据报表
@GetMapping("/export")
@ApiOperation("导出运营数据报表")
public void export(HttpServletResponse response){
    reportService.exportBusinessData(response);
}
sky-server.service下的ReportService接口
void exportBusinessData(HttpServletResponse response);
sky-server的service层的Impl下的ReportServiceImpl类
@Autowired
private WorkspaceService workspaceService;
//统计指定时间区间内的销量排名前10
public SalesTop10ReportVO getSalesTop10(LocalDate begin, LocalDate end) {
    LocalDateTime beginTime = LocalDateTime.of(begin, LocalTime.MIN);
    LocalDateTime endTime = LocalDateTime.of(end,LocalTime.MAX);
    List<GoodsSalesDTO> salesTop10 = orderMapper.getSalesTop10(beginTime, endTime);
    List<String> names = salesTop10.stream().map(GoodsSalesDTO::getName).collect(Collectors.toList());
    String nameList = StringUtils.join(names, ",");
    List<Integer> numbers = salesTop10.stream().map(GoodsSalesDTO::getNumber).collect(Collectors.toList());
    String numberList = StringUtils.join(numbers, ",");
    return SalesTop10ReportVO.builder().nameList(nameList).numberList(numberList).build();
}
@Autowired
private WorkspaceService workspaceService;
//导出运营数据报表
public void exportBusinessData(HttpServletResponse response){
    //1.查询数据库,获取营业数据--查询最近30天的运营数据
    LocalDate dateBegin = LocalDate.now().minusDays(30); //减30天的时间
    LocalDate dateEnd = LocalDate.now().minusDays(1);
    BusinessDataVO businessDatavo = workspaceService.getBusinessData(LocalDateTime.of(dateBegin, LocalTime.MIN), LocalDateTime.of(dateEnd, LocalTime.MAX));
    //2.通过POI将数据写入到Excel文件中
    InputStream in = this.getClass().getClassLoader().getResourceAsStream("template/运营数据报表模板.xlsx");//在类路径下读取资源返回输入流对象
 
    try {
        //基于模板文件创建一个新的Excel文件
        XSSFWorkbook excel = new XSSFWorkbook(in);
        //获取表格文件的Sheet文件
        XSSFSheet sheet = excel.getSheet("Sheet1");
        //填充数据--时间
        sheet.getRow(1).getCell(1).setCellValue("时间:"+dateBegin+"至"+dateEnd);
        //获得第4行
        XSSFRow row = sheet.getRow(3);
        row.getCell(2).setCellValue(businessDatavo.getTurnover()); //第3个单元格
        row.getCell(4).setCellValue(businessDatavo.getOrderCompletionRate());
        row.getCell(6).setCellValue(businessDatavo.getNewUsers());
        //获得第5行
        row = sheet.getRow(4);
        row.getCell(2).setCellValue(businessDatavo.getValidOrderCount());
        row.getCell(4).setCellValue(businessDatavo.getUnitPrice());
        //填充明细数据
        for(int i=0;i<30;i++){
            LocalDate date = dateBegin.plusDays(i);
            //查询某一天的营业数据
            workspaceService.getBusinessData(LocalDateTime.of(date,LocalTime.MIN),LocalDateTime.of(date,LocalTime.MAX));
            //获得某一行
            row = sheet.getRow(7+i);
            row.getCell(1).setCellValue(date.toString());
            row.getCell(2).setCellValue(businessDatavo.getTurnover());
            row.getCell(3).setCellValue(businessDatavo.getValidOrderCount());
            row.getCell(4).setCellValue(businessDatavo.getOrderCompletionRate());
            row.getCell(5).setCellValue(businessDatavo.getUnitPrice());
            row.getCell(6).setCellValue(businessDatavo.getNewUsers());
        }
        //3.通过输出流将Excel文件下载到客户端浏览器
        ServletOutputStream out = response.getOutputStream();
        excel.write(out);
        //关闭资源
        out.close();
        excel.close();
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
 
}
运行结果:

点击数据导出
在这里插入图片描述
生成了一个excel表格
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值