蒼穹外賣筆記(更新中)

后端从0搭建项目

1.首先创建一个spring boot项目,先不用勾选任何依赖或插件。

在这里插入图片描述

2.删除sky项目下的多余目录

在这里插入图片描述

3.在sky项目下新建三个子模块,只留下sky-server的一个Spring Boot 应用程序的入口类 SkyServiceApplication

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
sky下的pom文件,pom文件的作用是导入第三方库的依赖坐标,也就是可以使用别人写好的一些东西。

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.tang</groupId>
    <artifactId>sky</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>pom</packaging>
<!--    这段代码的主要作用是让当前项目继承Spring Boot的父项目,从而简化项目的配置和依赖管理。-->
    <parent>
        <artifactId>spring-boot-starter-parent</artifactId>
        <groupId>org.springframework.boot</groupId>
        <version>2.7.3</version>
    </parent>
<!--    父模塊下的三個子模塊-->
    <modules>
        <module>sky-common</module>
        <module>sky-pojo</module>
        <module>sky-server</module>
    </modules>
<!--    这个标签用于定义项目中使用的各种属性和版本号。这些属性可以在整个 pom.xml 文件中引用,从而简化版本管理。-->
    <properties>
        <mybatis.version>2.3.0</mybatis.version>
        <lombok.version>1.18.26</lombok.version>
        <fastjson.version>1.2.76</fastjson.version>
        <commons-lang.version>2.6</commons-lang.version>
        <druid.version>1.2.1</druid.version>
        <pagehelper.version>1.4.2</pagehelper.version>
        <knife4j.version>3.0.2</knife4j.version>
        <aspectj.version>1.9.7</aspectj.version>
        <jjwt.version>0.9.1</jjwt.version>
        <aliyun.oss.version>3.15.1</aliyun.oss.version>
        <jaxb.version>2.3.1</jaxb.version>
        <poi.version>3.16</poi.version>
        <wechatpay.version>0.4.8</wechatpay.version>
    </properties>

    <!--
    这段代码定义了一系列依赖项,涵盖了从数据库操作(MyBatis)、代码简化(Lombok)
    、JSON处理(Fastjson)、字符串处理(Commons Lang) 数据库连接池(Druid)、
    分页插件(PageHelper)、API文档生成(Knife4j)、AOP支持(AspectJ)、JWT生成与验证(JJWT)、
    云存储(Aliyun OSS)、XML绑定(JAXB)、文档处理(Apache POI)到支付服务(微信支付)等多个方面。
    这些依赖项共同构成了一个功能丰富的Spring Boot项目的基础架构,支持从数据访问到业务逻辑再到外部服务的完整开发流程。-->

    <dependencyManagement>
        <dependencies>
            <!--            这个依赖项的主要作用是为 Spring Boot 项目提供 MyBatis 的集成支持,-->
            <!--            简化了 MyBatisSpring Boot 项目中的配置和使用-->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>${mybatis.version}</version>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>${lombok.version}</version>
            </dependency>
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>${fastjson.version}</version>
            </dependency>
            <dependency>
                <groupId>commons-lang</groupId>
                <artifactId>commons-lang</artifactId>
                <version>${commons-lang.version}</version>
            </dependency>
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid-spring-boot-starter</artifactId>
                <version>${druid.version}</version>
            </dependency>
            <dependency>
                <groupId>com.github.pagehelper</groupId>
                <artifactId>pagehelper-spring-boot-starter</artifactId>
                <version>${pagehelper.version}</version>
            </dependency>
            <dependency>
                <groupId>com.github.xiaoymin</groupId>
                <artifactId>knife4j-spring-boot-starter</artifactId>
                <version>${knife4j.version}</version>
            </dependency>
            <dependency>
                <groupId>org.aspectj</groupId>
                <artifactId>aspectjrt</artifactId>
                <version>${aspectj.version}</version>
            </dependency>
            <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt</artifactId>
                <version>${jjwt.version}</version>
            </dependency>
            <dependency>
                <groupId>com.aliyun.oss</groupId>
                <artifactId>aliyun-sdk-oss</artifactId>
                <version>${aliyun.oss.version}</version>
            </dependency>
            <dependency>
                <groupId>javax.xml.bind</groupId>
                <artifactId>jaxb-api</artifactId>
                <version>${jaxb.version}</version>
            </dependency>
            <dependency>
                <groupId>org.apache.poi</groupId>
                <artifactId>poi</artifactId>
                <version>${poi.version}</version>
            </dependency>
            <dependency>
                <groupId>org.apache.poi</groupId>
                <artifactId>poi-ooxml</artifactId>
                <version>${poi.version}</version>
            </dependency>
            <dependency>
                <groupId>com.github.wechatpay-apiv3</groupId>
                <artifactId>wechatpay-apache-httpclient</artifactId>
                <version>${wechatpay.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>


</project>

	sky-common模块下的pom
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <artifactId>sky-common</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <!--    继承sky父项目-->
    <parent>
        <artifactId>sky</artifactId>
        <groupId>com.tang</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <!--    导入依赖,版本号在父项目中统一管理,本模块独有的依赖要写版本号-->
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-json</artifactId>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
        </dependency>
        <!--支持配置属性类,yml文件中可以提示配置项-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.aliyun.oss</groupId>
            <artifactId>aliyun-sdk-oss</artifactId>
        </dependency>
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
        </dependency>
        <!--微信支付-->
        <dependency>
            <groupId>com.github.wechatpay-apiv3</groupId>
            <artifactId>wechatpay-apache-httpclient</artifactId>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>1.9.7</version>
        </dependency>
    </dependencies>
</project>

sky-pojo模块下的pom

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <artifactId>sky-pojo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
<!--    导入sky父项目依赖坐标-->
    <parent>
        <artifactId>sky</artifactId>
        <groupId>com.tang</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
<!--        帮助将Java对象与JSON数据进行相互转换-->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-spring-boot-starter</artifactId>
        </dependency>
    </dependencies>

</project>

sky-server模块下的pom

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <artifactId>sky-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <!--    导入父工程依赖坐标-->
    <parent>
        <artifactId>sky</artifactId>
        <groupId>com.tang</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <dependencies>
        <!--        导入兄弟工程依赖坐标,这样就可以继承兄弟工程独有的依赖-->
        <dependency>
            <groupId>com.tang</groupId>
            <artifactId>sky-common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.tang</groupId>
            <artifactId>sky-pojo</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>

        <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>
        <!--        数据库连接: 这个依赖项的主要功能是提供与 MySQL 数据库的连接能力-->
        <!--        。它允许 Java 应用程序在运行时与 MySQL 数据库进行交互,执行查询、插入、更新等操作。-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

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

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

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

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
        </dependency>

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
        </dependency>

        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-spring-boot-starter</artifactId>
        </dependency>

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

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

        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
        </dependency>

        <!-- poi -->
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
        </dependency>
    </dependencies>

    <!--    这段代码的主要功能是配置 Maven 构建过程中使用的 Spring Boot 插件。Spring Boot 插件提供了许多有用的功能,-->
    <!--    如将项目打包为可执行的 JAR 文件、运行 Spring Boot 应用程序等。通过配置这个插件,开发者可以更方便地使用 Maven-->
    <!--    构建和运行 Spring Boot 项目。-->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

4.创建如图所示的建构目录

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

5.yml文件,作用:配置项目启动的端口,数据库信息等
application.yml:起作用的yml,可以通过 profiles: active: dev来控制使用哪个版本的yml
application-dev.yml:开发版本的yml
在这里插入图片描述

配置数据库信息

sky:(项目名)
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver(固定写法)
    host: localhost(本机)
    port: 3306(固定端口)
    database: sky_take_out(数据库名字)
    username: root(写自己的数据库账号名)
    password: ****(写自己的数据库密码)

application.yml:

server:
  port: 8080

spring:
#  由于SpringBoot整合Swagger时,SpringBoot版本与 springfox版本不兼容
#  ant_path_matcher 是一个具体的值,表示使用 Ant 风格的路径匹配策略。
#  Ant 风格的路径匹配策略允许使用通配符(如 ***)来匹配 URL 路径。
  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher
#      Spring Boot支持多个配置文件,例如dev(开发环境)、test(测试环境)、prod(生产环境)等。
#      每个配置文件可以包含不同的配置设置,例如数据库连接、日志级别、缓存配置等。
  profiles:
    active: dev
#    默认情况下,Spring容器是不允许循环依赖的,因为这可能导致无限递归和内存溢出等问题。
#    将 allow-circular-references 设置为 true 表示允许Spring容器在创建Bean时处理循环依赖。
#    免由于循环依赖导致的应用程序启动失败
  main:
    allow-circular-references: true
#    配置数据库连接信息,属性值从application-dev.yml中读取。
  datasource:
    druid:
      driver-class-name: ${sky.datasource.driver-class-name}
      url: jdbc:mysql://${sky.datasource.host}:${sky.datasource.port}/${sky.datasource.database}
      username: ${sky.datasource.username}
      password: ${sky.datasource.password}
mybatis:
#  指定了MyBatisMapper XML文件的位置,即在类路径下的mapper目录中查找所有的XML文件
  mapper-locations: classpath*:mapper/*.xml
  指定了类型别名包为com:
    sky:
#      entity,这样在该包中的类可以使用简短的类名作为别名:
  type-aliases-package: com.sky.entity
#  驼峰命名自动映射:开启了驼峰命名自动映射功能,使得数据库中的下划线命名(如user_name)
#  可以自动映射到Java对象的驼峰命名属性(如userName)。
  configuration:
    map-underscore-to-camel-case: true
#    这段代码的主要功能是配置Spring Boot应用程序的日志记录级别。具体来说,它为 com.sky 包下的不同子包
#    (controller、service、mapper)设置了不同的日志级别:
#
#    com.sky.controller 和 com.sky.service 包中的类将记录 info 级别及以上的日志信息。
#    com.sky.mapper 包中的类将记录 debug 级别及以上的日志信息。
#    通过这种方式,开发者可以根据不同的包和类来控制日志的详细程度,从而更好地调试和监控应用程序的运行状态。
logging:
  level:
    com:
      sky:
        controller: info
        service: info
        mapper: debug
#        配置jwt令牌的密钥和有效期,并指定令牌的名称。
sky:
  jwt:
    admin-secret-key: tang
    admin-ttl: 72000000
    admin-token-name: token

配置文件:
在这里插入图片描述
JwtUtil工具类:作用,生成jwt令牌和解析jwt令牌。

package com.sky.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Map;

public class JwtUtil {

    /**
     * 生成JWT
     *
     * @param secretKey
     * @param ttlMillis
     * @param claims
     * @return
     * @throws UnsupportedEncodingException
     */
    public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) throws UnsupportedEncodingException {
        //1.指定签名算法
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
//2.JWT过期时间=现在时间+ttlMillis
        long expMillis = System.currentTimeMillis() + ttlMillis;
        //转换为Date类型
        Date exp = new Date(expMillis);
//3.生成JWT,claim是自定义的键值对
        String compact = Jwts.builder()
                .setClaims(claims)
                .setExpiration(exp)
                .signWith(signatureAlgorithm, secretKey.getBytes("UTF-8"))
                .compact();
        return compact;

    }

    /**
     * 解析JWT
     *
     * @param secretKey
     * @param token
     * @return
     */
    public static Claims parseJwt(String secretKey, String token) {
        //1.给出秘钥和要解析的token
        Claims claims = Jwts.parser()
                .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
                .parseClaimsJws(token).getBody();
        return claims;
    }
}

interception拦截器:作用,每次请求后端都要拦截下来校验是否放行。并且需要在webMvcConfiguration文件中注册才能生效。

package com.sky.interception;

import com.sky.properties.JwtProperties;
import com.sky.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
@Slf4j
// 该拦截器用于验证管理员JWT的有效性
public class JwtAdminInterception implements HandlerInterceptor {

    private JwtProperties jwtProperties;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //如果不是controller方法,放行
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        //从前端的请求头中获取token
        String token = request.getHeader(jwtProperties.getAdminTokenName());

        try {
            //解析token,如果解析失败,则返回401
            Claims claims = JwtUtil.parseJwt(jwtProperties.getAdminSecretKey(), token);
            return true;
        } catch (Exception e) {
            response.setStatus(401);
            return false;
        }
    }
}

webMvcConfiguration配置文件:作用:WebMvcConfiguration 是 Spring MVC 框架中的一个重要配置类,主要用于定制和配置 Spring MVC 的行为。通过这个配置类,开发者可以自定义 Spring MVC 的各种组件和行为,例如视图解析器、拦截器、消息转换器等。

package com.sky.config;

import com.sky.interception.JwtAdminInterception;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;

@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
    @Autowired
    private JwtAdminInterception jwtAdminInterception;

    // 注册拦截器
    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtAdminInterception)
                //拦截/admin下的所有请求
                .addPathPatterns("/admin/**")
                //排除/admin/employee/login请求,即登录请求
                .excludePathPatterns("/admin/employee/login");
    }

    /**
     * 生成接口文档,两步:new ApiInfo描述文档的基本信息
     * 2.new Docket描述文档的详细信息,包括要扫描的包、标题、描述、版本等信息
     * @return
     */
    public Docket docket1() {
        log.info("準備生成接口文檔...");
        ApiInfo apiInfo = new ApiInfoBuilder()
                .title("蒼穹外賣管理系統API")
                .description("蒼穹外賣管理系統API")
                .version("1.0")
                .build();

        Docket docket = new Docket(DocumentationType.SWAGGER_2)
                .groupName("管理端")
                .apiInfo(apiInfo)
                .select()
                //指定要扫描的包
                .apis(RequestHandlerSelectors.basePackage("com.sky.controller.admin"))
                .build();
        return docket;
    }

    /**
     *   静态资源映射,访问地址:http://localhost:8080/doc.html会自动映射到Swagger2帮我们生成的API文档页面,
     *   固定在classpath:/META-INF/resources/webjars包下
     */
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
    }
}

Result:作用:固定的返回给前端的对象

package com.sky.result;

import lombok.Data;

import java.io.Serializable;

@Data
public class Result<T> implements Serializable {
    private Integer code;// 1:成功 0:失败
    private String msg;// 错误信息
    private T data;// 返回数据

    public static <T> Result success() {
        Result<T> result = new Result<T>();
        result.code = 1;
        return result;
    }

    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<T>();
        result.code = 1;
        result.data = data;
        return result;
    }

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

contant:自定义的一些常量

package com.sky.constant;

public class MessageConstant {


    public static final String PASSWORD_ERROR = "密码错误";
    public static final String ACCOUNT_NOT_FOUND = "账号不存在";
    public static final String ACCOUNT_LOCKED = "账号被锁定";
    public static final String UNKNOWN_ERROR = "未知错误";
}

package com.sky.constant;

public class JwtClaimsConstant {
    public static final String EMP_ID = "empId";
    public static final String USER_ID = "userId";
   public static final String PHONE = "phone";
   public static final String USERNAME = "username";
   public static final String NAME = "name";
}

exception:异常类,定义各种异常
BaseException:父异常

package com.sky.exception;

public class BaseException extends RuntimeException{
    public BaseException() {
    }
    public BaseException(String message) {
        super(message);
    }
}

子异常继承BaseException
在这里插入图片描述

最后一步,全局异常处理器,比如throw AccountNotFoundException,她就捕获它,并以 Result对象给
前端返回数据(e.getMessage)),至此,后端基础项目搭建完成,可以开始完成第一个接口–》用户登录

package com.sky.handler;

import com.sky.result.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler
    public Result exceptionHandler(Exception e) {
        log.error("全局异常处理", e);
        return Result.error(e.getMessage());
    }
}

管理端模块

新增用户

1.controller:

   @ApiOperation("用戶新增")
    @PostMapping
    public Result save(@RequestBody EmployeeDTO employeeDTO){
        employeeService.save(employeeDTO);
        return Result.success();
    }

2.service接口:

 void save(EmployeeDTO employeeDTO);

3.serviceImpl:

    @Override
    public void save(EmployeeDTO employeeDTO) {
        Employee employee = new Employee();
        //把前端传来的dto复制到employee对象中,
        // 所以employee对象中还有password,和status属性,create_time,update_time,update_user和update_User属性还赋值值
        //password和status属性赋值默认值,create_time,update_time,update_user和update_User属性用切面来自动填充
        BeanUtils.copyProperties(employeeDTO,employee);
        //password加密并赋值给employee对象
        String password = DigestUtils.md5DigestAsHex(PasswordConstant.PASSWORD_DEFAULT.getBytes());
        employee.setPassword(password);
        //status赋值给employee对象
        employee.setStatus(StatusConstant.ENABLE);
        employeeMapper.save(employee);
    }

4.mapper接口:

    void save(EmployeeDTO employeeDTO);

5.mapper.xml:

  <insert id="save">
        insert into 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})
    </insert>

附:
1.枚举类:

package com.sky.enumeration;

public enum OperationType {
    UPDATE,
    INSERT
}

2.注解类:

package com.sky.annotation;

import com.sky.enumeration.OperationType;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

//表示用在方法上(Method)添加注解
@Target(ElementType.METHOD)
//表示注解的生命周期,在运行时期
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
    OperationType value();
}

3.线程常量类:

package com.sky.context;

public class BaseContext {
    // 线程变量,用于保存当前请求的id,不同用户开启一条线程来区别不同用户
    public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    // 设置当前请求用户的id
    public static void setCurrentId(Long id) {
        threadLocal.set(id);
    }

    // 获取当前请求用户的id
    public static Long getCurrentId() {
        return threadLocal.get();
    }

    // 清除当前请求用户的id
    public static void removeCurrentId() {
        threadLocal.remove();
    }
}

4.在拦截器拦截用户请求时把用户id(empId)放入线程中

package com.sky.interception;

import com.sky.constant.JwtClaimsConstant;
import com.sky.context.BaseContext;
import com.sky.properties.JwtProperties;
import com.sky.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
@Slf4j
// 该拦截器用于验证管理员JWT的有效性
public class JwtAdminInterception implements HandlerInterceptor {

    @Autowired
    private JwtProperties jwtProperties;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //如果不是controller方法,放行
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        //从前端的请求头中获取token
        String token = request.getHeader(jwtProperties.getAdminTokenName());

        try {
            //解析token,如果解析失败,则返回401
            Claims claims = JwtUtil.parseJwt(jwtProperties.getAdminSecretKey(), token);
            Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
           log.info("当前登录用户的empId为:{}", empId);
           BaseContext.setCurrentId(empId);
            return true;
        } catch (Exception e) {
            response.setStatus(401);
            return false;
        }
    }
}

5.切面类aspect:

package com.sky.aspect;

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

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

@Aspect
@Component
@Slf4j
public class AutoFillAspect {

    //切入點,即哪些地方要執行自動填充
    @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
    public void autoFillPointCut() {
    }

    //前置通知,在原來的方法運行前,先執行以下代碼
    @Before("autoFillPointCut()")
    public void autoFill(JoinPoint joinPoint) {
        log.info("開始自動填寫");
        //獲取簽名對象,例如:com.sky.mapper.EmployeeMapper.save(Employee employee)
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        //獲取簽名對象的方法(save)的上面注解
        AutoFill annotation = signature.getMethod().getAnnotation(AutoFill.class);
        //獲取操作類型
        OperationType operationType = annotation.value();


        Object[] args = joinPoint.getArgs();
        if (args == null || args.length == 0) {
            return;
        }
        //獲取操作實體對象(例如:Employee)
        Object entity = args[0];
        //準備填寫時間和操作人
        LocalDateTime now = LocalDateTime.now();
        //從當前綫程中獲取操作人id
        Long currentId = BaseContext.getCurrentId();

        //通過反射,將時間和操作人填入實體對象
        if (operationType == OperationType.INSERT) {
            try {
                //獲取實體對象的createTime、updateTime、createUser、updateUser方法
                Method createTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
                Method updateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                Method createUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
                Method updateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);

                //通過反射,將時間和操作人填入實體對象
                createTime.invoke(entity, now);
                updateTime.invoke(entity, now);
                createUser.invoke(entity, currentId);
                updateUser.invoke(entity, currentId);
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else if (operationType == OperationType.UPDATE) {
            try {
                Method updateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                Method updateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);

                updateTime.invoke(entity, now);
                updateUser.invoke(entity, currentId);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

添加消息转换器

在这里插入图片描述

package com.sky.json;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class JacksonObjectMapper extends ObjectMapper {

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

    public JacksonObjectMapper() {
     super();
        //收到未知屬性時不報異常,**否則在用@RequestBody接受前端數據報錯**
        this.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        //反序列時,屬性不存在的兼容處理
        this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
        //这里创建了一个 SimpleModule 的实例,SimpleModule 是 Jackson 提供的一个模块化功能,
        // 允许你添加自定义的序列化器(Serializer)和反序列化器(Deserializer)。
        SimpleModule simpleModule = new SimpleModule()
                .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
        //注册功能模块 例如,可以添加自定义序列化器和反序列化器
        this.registerModule(simpleModule);
    }
}

在这里插入图片描述

    /**
     * ,这个类扩展了 Spring MVC 的消息转换器
     *
     * @param converters
     */
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        //这里创建了一个 MappingJackson2HttpMessageConverter 的实例。这个转换器是 Spring 提供的,
        // 主要用于将 Java 对象转换为 JSON 格式,并将 JSON 数据转换为 Java 对象。
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        //将一个自定义的 JacksonObjectMapper 设置为转换器的 ObjectMapper。
        // 这允许开发者定制 JSON 的序列化和反序列化行为,比如处理日期格式、字段命名策略等
        converter.setObjectMapper(new JacksonObjectMapper());
        //将刚创建并配置好的转换器添加到 converters 列表的第一个位置。这样做会确保在进行消息转换时优先使用这个自定义的转换器。
        converters.add(0, converter);
    }

阿里云文件上传5步走

1.yml文件配置阿里云信息(要配置自己阿里云Oss的信息)
sky:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    host: localhost
    port: 3306
    database: sky_take_out
    username: root
    password: 1234
  alioss:
    endpoint: oss-cn-beijing.aliyuncs.com
    access-key-id: LTAI5tBLYppXGW46Lx*****
    access-key-secret: KVuiJrPcHrp58qbKZkt******
    bucket-name: sky-itcast-kaka

2.properties包

作用:把yml件中的oss配置转换为java对象并交给bean容器管理

package com.sky.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Data
@Component
//提取yml中的sky.alioss开头的配置
@ConfigurationProperties(prefix="sky.alioss")
public class AliOssProperties {

    private String endpoint;
    private String accessKeyId;
    private String accessKeySecret;
    private String bucketName;
}

3.查看阿里云官方给的上传文件的示例代码并改造一下
package com.sky.utils;

import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;

import java.io.ByteArrayInputStream;

@Data
@AllArgsConstructor
@Slf4j
public class AliOssUtils {
    private String endpoint;
    private String accessKeyId;
    private String accessKeySecret;
    private String bucketName;

    public String upload(byte[] bytes, String fileName) {
//        OSSClientBuilder是一个构建OSS客户端的工具类。
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
        //上传对象
        ossClient.putObject(bucketName, fileName, new ByteArrayInputStream(bytes));
        //关闭OSSClient。
        ossClient.shutdown();
        //文件访问路径规则 https://BucketName.Endpoint/ObjectName
        StringBuilder stringBuilder = new StringBuilder("https://");
        stringBuilder.append(bucketName)
                .append(".")
                .append(endpoint)
                .append("/")
                .append(fileName);
        //返回文件访问路径
        return stringBuilder.toString();
    }
}

4.config包

作用:初始化AliOssUtils类,即以有参构造new 一个AliOssUtils对象交给bean管理,方便自动装配

package com.sky.config;

import com.sky.properties.AliOssProperties;
import com.sky.utils.AliOssUtils;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AliOssConfig {

    @Bean
    //这个注解是 Spring 提供的条件注解,表示仅在 Spring 容器中不存在相同类型的 Bean 时,
    // 才会创建并注册这个 Bean。这样做的目的可以防止 Bean 的重复定义。
    @ConditionalOnMissingBean
    public AliOssUtils aliOssUtils(AliOssProperties aliOssProperties) {
        return new AliOssUtils(aliOssProperties.getEndpoint(), aliOssProperties.getAccessKeyId(),
                aliOssProperties.getAccessKeySecret(), aliOssProperties.getBucketName());
    }
}

5.controller类
package com.sky.controller.admin;

import com.sky.constant.MessageConstant;
import com.sky.exception.NoNullException;
import com.sky.result.Result;
import com.sky.utils.AliOssUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.UUID;

@RestController
@Api(tags = "公共接口")
@Slf4j
@RequestMapping("/admin/common/upload")
public class CommonController {

    @Autowired
    private AliOssUtils aliOssUtils;

    @PostMapping
    @ApiOperation("文件上传")
    public Result<String> upload(MultipartFile file) {
        String originalFilename = file.getOriginalFilename();
        //截取最後一個.的後面的字串
        String suffix = originalFilename.substring(originalFilename.lastIndexOf(".") + 1);
        //UUID生成随机字符串+文件后缀作爲新的文件名,防止文件名重复
        String uuid = UUID.randomUUID().toString();
        String newFilename = uuid + "." + suffix;
        String filename = null;
        try {
            filename = aliOssUtils.upload(file.getBytes(), newFilename);
        } catch (IOException e) {
            throw new NoNullException(MessageConstant.FILE_UPLOAD_ERROR);
        }
        return Result.success(filename);
    }
}

设置店铺营业状态
1.导入依赖坐标
  <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
2.配置yml文件
  redis:
    host: localhost
    port: 6379
    password: 123456
    database: 10
3.配置config(非必要),把序列化器指定为String类型的,方便我们看key
package com.sky.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
@Slf4j
public class RedisConfiguration {

    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate redisTemplate = new RedisTemplate();
        // 配置连接工厂
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        // 设置value的序列化方式
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        return redisTemplate;
    }
}

4.controller,注意,RestController一定要其花名,否知会Bean重复
package com.sky.controller.user;

import com.sky.result.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController("userShopController")
@Slf4j
@Api(tags = "用戶端店鋪接口")
@RequestMapping("/user/shop")
public class ShopController {

    private static final String SHOP_STATUS = "SHOP_STATUS";

    @Autowired
    private RedisTemplate redisTemplate;

    @GetMapping("/status")
    @ApiOperation("獲取營業狀態")
    public Result<Integer> getStatus() {
        ValueOperations valueOperations = redisTemplate.opsForValue();
        Integer status = (Integer) valueOperations.get(SHOP_STATUS);
        log.info("獲取店鋪狀態: {}", status == 1 ? "營業中" : "打烊中");
        return Result.success(status);
    }
}

用戶端模块

用户登录

1.yml配置小程序信息并提取成bean
  wechat:
    appid: wxb2439a7******
    secret: c2906d5c0ca244c*******
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; //小程序的秘钥
}

2.controller
package com.sky.controller.user;

import com.sky.constant.JwtClaimsConstant;
import com.sky.dao.UserLoginDTO;
import com.sky.entity.User;
import com.sky.properties.JwtProperties;
import com.sky.result.Result;
import com.sky.service.UserService;
import com.sky.utils.JwtUtil;
import com.sky.vo.UserLoginVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.UnsupportedEncodingException;
import java.util.HashMap;

@RestController
@Slf4j
@Api(tags = "用户管理")
@RequestMapping("/user/user")
public class UserController {
    @Autowired
    private UserService userService;
    @Autowired
    private JwtProperties jwtProperties;

    @PostMapping("/login")
    @ApiOperation("登錄")
    public Result<UserLoginVO> wxLogin(@RequestBody UserLoginDTO userLoginDTO) throws UnsupportedEncodingException {
        User user = userService.login(userLoginDTO);
        HashMap<String, Object> claims = new HashMap<>();
        claims.put(JwtClaimsConstant.USER_ID, user.getId());
        String token = JwtUtil.createJWT(jwtProperties.getUserSecretKey(), jwtProperties.getUserTtl(), claims);
        UserLoginVO userLoginVO = UserLoginVO.builder()
                .id(user.getId())
                .openId(user.getOpenid())
                .token(token)
                .build();

        return Result.success(userLoginVO);
    }
}

3.serviceImpl,按照微信官方提供的步骤获取openId

在这里插入图片描述

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

package com.sky.service.Impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.sky.constant.MessageConstant;
import com.sky.dao.UserLoginDTO;
import com.sky.entity.User;
import com.sky.exception.LoginFailedException;
import com.sky.mapper.UserMapper;
import com.sky.properties.WeChatProperties;
import com.sky.service.UserService;
import com.sky.vo.UserLoginVO;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.net.URI;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

@Service
public class UserServiceImpl implements UserService {
    //微信服务接口地址
    public static final String WX_LOGIN = "https://api.weixin.qq.com/sns/jscode2session";

    @Autowired
    private WeChatProperties weChatProperties;
    @Autowired
    private UserMapper userMapper;

    public User login(UserLoginDTO userLoginDTO) {
        String openId = getOpenId(userLoginDTO.getCode());
        if (openId == null) {
            throw new LoginFailedException(MessageConstant.LOGIN_FAILED);
        }

        User user = userMapper.getByOpenId(openId);

        if (user == null) {
            user = User.builder()
                    .openid(openId)
                    .createTime(LocalDateTime.now())
                    .build();
            userMapper.insert(user);
        }
        return user;
    }

    private String getOpenId(String code) {
        //调用微信接口服务,获得当前微信用户的openid
        Map<String, String> map = new HashMap<>();
        map.put("appid", weChatProperties.getAppid());
        map.put("secret",weChatProperties.getSecret());
        map.put("js_code", code);
        map.put("grant_type", "authorization_code");
        // 创建Httpclient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();

        String result = "";
        CloseableHttpResponse response = null;

        try {
            URIBuilder builder = new URIBuilder(WX_LOGIN);
            if (map != null) {
                for (String key : map.keySet()) {
                    builder.addParameter(key, map.get(key));
                }
            }
            URI uri = builder.build();

            //创建GET请求
            HttpGet httpGet = new HttpGet(uri);

            //发送请求
            response = httpClient.execute(httpGet);

            //判断响应状态
            if (response.getStatusLine().getStatusCode() == 200) {
                result = EntityUtils.toString(response.getEntity(), "UTF-8");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                response.close();
                httpClient.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        JSONObject jsonObject = JSON.parseObject(result);
        String openid = jsonObject.getString("openid");
        return openid;
    }
}

缓存数据

1.spring cache起步依赖

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

2.启动类加注解:@EnableCaching

3.常见注解

在这里插入图片描述
说明: key的写法如下

#user.id : #user指的是方法形参的名称, id指的是user的id属性 , 也就是使用user的id属性作为key ;

#result.id : #result代表方法返回值,该表达式 代表以返回对象的id属性作为key ;

#p0.id:#p0指的是方法中的第一个参数,id指的是第一个参数的id属性,也就是使用第一个参数的id属性作为key ;

#a0.id:#a0指的是方法中的第一个参数,id指的是第一个参数的id属性,也就是使用第一个参数的id属性作为key ;

#root.args[0].id:#root.args[0]指的是方法中的第一个参数,id指的是第一个参数的id属性,也就是使用第一个参数

的id属性作为key ;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值