苍穹外卖复习

jwt的登录验证

1.JwtProperties(jwt的基本配置项):

@Component
@ConfigurationProperties(prefix = "sky.jwt")
@Data
public class JwtProperties {

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

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

}

在配置文件中设置好

sky:
  jwt:
    # 设置jwt签名加密时使用的秘钥
    admin-secret-key: itcast
    # 设置jwt过期时间
    admin-ttl: 7200000
    # 设置前端传递过来的令牌名称
    admin-token-name: token
    user-secret-key: itheima
    user-ttl: 7200000
    user-token-name: authentication

2.JwtTokenInterceptor(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 {

        System.out.println("当前线程的id"+ Thread.currentThread().getId());
        //判断当前拦截到的是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);
            BaseContext.setCurrentId(empId);
            //3、通过,放行
            return true;
        } catch (Exception ex) {
            //4、不通过,响应401状态码
            response.setStatus(401);
            return false;
        }
    }
}

image-20251219185810255

JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);

这行代码的作用是验证并解析JWT令牌

  • JwtUtil.parseJWT():调用JWT工具类的解析方法,该方法会验证令牌的签名有效性并解码内容 。
  • jwtProperties.getAdminSecretKey():获取用于验证签名的密钥,这个密钥必须与生成令牌时使用的密钥一致 。
  • token:待解析的JWT令牌字符串,通常从HTTP请求头中获取。
  • Claims claims:解析成功后返回的声明对象,包含了JWT负载(Payload)中的所有数据 。Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
  • 这行代码从解析后的声明中提取具体的用户标识信息
    • claims.get(JwtClaimsConstant.EMP_ID):从Claims对象中获取键为EMP_ID的声明值,这通常是在用户登录生成JWT时存入的员工ID 。
    • .toString():将获取到的对象转换为字符串,确保类型安全。
    • Long.valueOf():把字符串转换为Long类型的员工ID,便于后续在业务中使用。

其中的parseJWT是token解密:

/**
 * 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;
}

生成jwt:

public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {
    // 指定签名的时候使用的签名算法,也就是header那部分
    SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

    // 生成JWT的时间
    long expMillis = System.currentTimeMillis() + ttlMillis;
    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();
}

3.在代码中使用jwt:

@PostMapping("/login")
@ApiOperation(value = "员工登录")
public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) {
    log.info("员工登录:{}", employeeLoginDTO);

    Employee employee = employeeService.login(employeeLoginDTO);

    //登录成功后,生成jwt令牌
    Map<String, Object> claims = new HashMap<>();
    claims.put(JwtClaimsConstant.EMP_ID, employee.getId());
    String token = JwtUtil.createJWT(
            jwtProperties.getAdminSecretKey(),
            jwtProperties.getAdminTtl(),
            claims);

    EmployeeLoginVO employeeLoginVO = EmployeeLoginVO.builder()
            .id(employee.getId())
            .userName(employee.getUsername())
            .name(employee.getName())
            .token(token)
            .build();

    return Result.success(employeeLoginVO);
}

在webConfiguration中注册自定义拦截器:

/**
 * 注册自定义拦截器
 *
 * @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");
}

新增菜品

在新增菜品中用到了集合遍历,下面这三种遍历效果一样

传统 for 循环

for (DishFlavor flavor : flavors) { flavor.setDishId(dishId); }

语法冗长,但逻辑清晰

Stream API

flavors.stream().peek(flavor -> flavor.setDishId(dishId)).collect(...)

适合链式操作,但需注意副作用(如修改原集合)

peek的作用
对流中的每个 flavor对象执行 setDishId(dishId)操作,为每个口味对象设置关联的菜品 ID。
关键点:peek是中间操作,不会终止流,需配合 collect等终止操作触发执行。
链式操作流程

流生成:flavors.stream()将集合转为流。

副作用操作:peek修改元素属性(设置 dishId)。

终止操作:collect(...)收集结果(如转 List或 Map)。

方法引用

flavors.forEach(flavor -> flavor.setDishId(dishId));

与 Lambda 类似,但更简洁

在新增菜品的时候的xml语句

1.插入数据,并且设置自增键

核心机制

  1. 数据库自动生成主键 当数据库表的主键字段设置为 自增(AUTO_INCREMENT) 时(如 MySQL 的 id INT AUTO_INCREMENT),执行 INSERT语句时数据库会自动生成唯一的主键值。 无需手动插入:你不需要在 SQL 中显式为 id字段赋值(如 id=#{id}),数据库会自动填充。
  2. MyBatis 回填主键到实体类 useGeneratedKeys="true":启用 MyBatis 的 主键回填功能,通过 JDBC 的 getGeneratedKeys()方法获取数据库生成的主键。 keyProperty="id":将获取到的主键值赋给 Java 对象的 id属性(需与实体类属性名一致)。

<!-- useGeneratedKeys:true 表示获取主键值

keyProperty="id" 表示将主键值赋给id属性 -->

insert into dish
(status, name, category_id, price, image, description, create_time, update_time, create_user,update_user) values
(#{status}, #{name}, #{categoryId}, #{price}, #{image}, #{description}, #{createTime}, #{updateTime},#{createUser}, #{updateUser})

2:批量插入数据

collection="flavors"
功能:指定要迭代的集合来源

item="dishFlavor"
功能:定义集合中每个元素的别名。

separator=","
功能:指定元素之间的分隔符。

insert into dish_flavor(dish_id, name, value) values

​ (#{dishFlavor.dishId},#{dishFlavor.name},#{dishFlavor.value})

菜品的分页查询

除了DTO和VO外,分页查询还需要的是一个PageResult来获取一个总的记录数和当前页的数据集合:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageResult implements Serializable {

    private long total; //总记录数

    private List records; //当前页数据集合

}
@GetMapping("/page")
@ApiOperation("菜品分页查询")
public Result<PageResult>page (DishPageQueryDTO dishPageQueryDTO) {
    log.info("菜品分页查询");
    PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);
    return Result.success(pageResult) ;
}

实际实现分页查询:

PageHelper.startPage:在分页查询中使用到了PageHelper.startPage来设置分页参数并启动分页拦截器。

分页参数即当前页码和当前要返回多少数据,这是用前端给我们的DTO中获取的

Page:这是内置的类型,用于分页,含有方法来帮助获得总记录数和数据集合。

public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {
    PageHelper.startPage(dishPageQueryDTO.getPage(),dishPageQueryDTO.getPageSize());
    Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO);
    return new PageResult(page.getTotal(),page.getResult());
}

查询的xml语句:

作用

  • 查询 dish表所有字段(d.*)和 category表的 name字段(别名 categoryName)。
  • 左外连接:即使 dish表的 category_idcategory表中无匹配记录,仍会返回 dish数据(categoryNameNULL)。
<select id="pageQuery" resultType="com.sky.vo.DishVO">
    select d.* , c.name as categoryName from dish d left outer join category c on d.category_id = c.id
    <where>
        <if test="name != null">
            and d.name like concat('%',#{name},'%')

        </if>
        <if test="categoryId != null">
            and d.category_id = #{categoryId}
        </if>
        <if test="status != null">
            and d.status = #{status}
        </if>
    </where>
    order by d.create_time desc
</select>

删除菜品

测功能较为简单

单个删除:

@Delete("delete from dish where id = #{id}")

批量删除:

open和close是在最后的结果上加上左括号和右括号

<delete id="deleteByDishIds">
    delete from dish_flavor where dish_id in
        <foreach collection="dishIds" item="dishids" separator="," open="(" close=")">
            #{dishids}
        </foreach>
</delete>

修改菜品

正常的修改菜品

<update id="update">
    update dish
    <set>
        <if test="name != null">name = #{name}, </if>
        <if test="categoryId != null">category_id = #{categoryId}, </if>
        <if test="price != null">price = #{price}, </if>
        <if test="image != null">image = #{image}, </if>
        <if test="description != null">description = #{description}, </if>
        <if test="status != null">status = #{status}, </if>
        <if test="updateTime != null">update_time = #{updateTime}, </if>
        <if test="updateUser != null">update_user = #{updateUser}, </if>
    </set>
    where id = #{id}
</update>

在修改对应的口味的时候,执行删除和插入口味的操作

Redis

在java中使用redis

1.导入Spring Data Redis 的maven坐标
2.配置Redis数据源
3.编写配置类,创建RedisTemplate对象
4.通过RedisTemplate对象操作Redis

利用RedisTemplate对象模板来进行编写redis:

@Configuration
public class RedisConfiguration {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        
        // 设置Key的序列化器
        template.setKeySerializer(new StringRedisSerializer());
        // 设置Value的序列化器为JSON序列化器
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        // 设置Hash Key的序列化器
        template.setHashKeySerializer(new StringRedisSerializer());
        // 设置Hash Value的序列化器
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        
        return template;
    }
}

为什么要设置序列化器?

答:

RedisTemplate默认使用 JDK 序列化,这会导致存储在 Redis 里的 key 和 value 是二进制的、不可读的格式(类似 \xac\xed\x00\x05t\x00\x03city)。

通过设置 KeySerializerStringRedisSerializer,确保了所有 Redis 的 key 都会以可读的字符串形式存储(例如,直接就是 "city"),这在命令行调试或可视化工具中查看时非常清晰。

操作几种类型的数据:

ValueOperations valueOperations = redisTemplate.opsForValue();
HashOperations hashOperations = redisTemplate.opsForHash();
ListOperations listOperations = redisTemplate.opsForList();
SetOperations setOperations = redisTemplate.opsForSet();
ZSetOperations zSetOperations = redisTemplate.opsForZSet();
/**
 * 操作字符串类型数据
 */
@Test
public void testString() {
    //set
    redisTemplate.opsForValue().set("city", "BEIJING");
    //get
    String city = (String) redisTemplate.opsForValue().get("city");
    System.out.println(city);
    //setex
    redisTemplate.opsForValue().set("province", "shanxi", 2, TimeUnit.MINUTES);
    //setnx
    redisTemplate.opsForValue().setIfAbsent("look", "1");
    redisTemplate.opsForValue().setIfAbsent("look", "2");
}

/**
 * 操作哈希类型的数据
 */
@Test
public void testHash() {
    //hset het hdel hkeys hvals
    HashOperations hashOperations = redisTemplate.opsForHash();
    hashOperations.put("100", "name", "tom");
    hashOperations.put("100", "age", "20");
    String name = (String) hashOperations.get("100", "age");
    System.out.println(name);

    Set keys = hashOperations.keys("100");
    System.out.println(keys);

    List values = hashOperations.values("100");
    System.out.println(values);
    hashOperations.delete("100", "age");

}

/**
 * 操作列表类型数据
 */
//Lpush Lrange rpop Llen
@Test
public void testList() {
    ListOperations listOperations = redisTemplate.opsForList();

    listOperations.leftPushAll("mylist", "a", "b", "c");
    listOperations.leftPush("mylist", "d");

    List mylist = listOperations.range("mylist", 0, -1);
    System.out.println(mylist);

    listOperations.rightPop("mylist");

    Long size = listOperations.size("mylist");
    System.out.println(size);

}

@Test
public void testSet() {
    //sadd smebers scard sinter sunion srem
    SetOperations setOperations = redisTemplate.opsForSet();
    setOperations.add("set1", "a", "b", "c", "d");
    setOperations.add("set2", "b", "c", "d", "e");

    Set members = setOperations.members("set1");
    System.out.println(members);

    Long size = setOperations.size("set1");
    System.out.println(size);

    Set intersection = setOperations.intersect("set1", "set2");
    System.out.println(intersection);

    Set union = setOperations.union("set1", "set2");
    System.out.println(union);

    setOperations.remove("set1", "a", "b");
}

/**
 * 操作有序集合
 */
@Test
public void testZSet() {
    //zadd zrange zincrby zrem
    ZSetOperations zSetOperations = redisTemplate.opsForZSet();

    zSetOperations.add("zset1", "a", 10);
    zSetOperations.add("zset1", "b", 20);
    zSetOperations.add("zset1", "c", 30);

    Set zset1 = zSetOperations.range("zset1", 0, -1);
    System.out.println(zset1);

    zSetOperations.incrementScore("zset1", "c", 1);

    zSetOperations.remove("zset1", "a");
}

/**
 * 通用命令操作
 */
//keys exists type del
@Test
public void testCommon() {


Set keys = redisTemplate.keys("*");
System.out.println(keys);

Boolean name = redisTemplate.hasKey("name");
Boolean age = redisTemplate.hasKey("set1");

for (Object key : keys) {
    DataType type = redisTemplate.type(key);
    System.out.println(type.name());
}
redisTemplate.delete("mylist");
}

HttpClient

作用:1.发送http请求

​ 2.接受响应数据

代码解释:

CloseableHttpClient httpClient = HttpClients.createDefault();

创建HTTP客户端实例。这是发送请求的基础,类似于打开一个浏览器

HttpClients.createDefault()会创建一个具有默认配置的HTTP客户端,该客户端支持连接复用(连接池),能有效提升性能

HttpGet httpGet = new HttpGet("http://localhost:8080/...");

构建GET请求对象。指定了请求的URL(这里是一个本地服务的地址)和HTTP方法(GET)

HttpGet对象封装了请求的所有信息,包括URL、请求头、参数等。你可以通过 setHeader方法为其添加请求头

CloseableHttpResponse response = httpClient.execute(httpGet);

发送请求并获取响应。这是执行网络调用的核心步骤

httpClient.execute(...)方法会同步地发送请求,并返回一个 CloseableHttpResponse对象,该对象包含了服务器返回的全部信息

int statusCode = response.getStatusLine().getStatusCode();

从响应中获取HTTP状态码(如200表示成功,404表示未找到等)

状态码是HTTP协议规定的一部分,位于响应行的第一行,通过 StatusLine对象获取

HttpEntity entity = response.getEntity();

String body = EntityUtils.toString(entity);

从响应中提取响应体内容。响应体是服务器返回的实际数据(如JSON、HTML等)

HttpEntity代表了HTTP消息的实体内容(如请求体或响应体)。EntityUtils.toString(...)是一个工具方法,将实体内容流转换为字符串

response.close();

httpClient.close();

关闭连接,释放资源。这非常重要,可以防止资源泄漏

GET方式请求:

@Test
public void testGet() throws IOException {
    //创建httpclient对象
    CloseableHttpClient httpClient = HttpClients.createDefault();
    //创建请求对象
    HttpGet httpGet = new HttpGet("http://localhost:8080/user/shop/status");
    //发送请求并接收响应结果
      CloseableHttpResponse response =  httpClient.execute(httpGet);
    //获取服务端返回的状态码
    int statusCode = response.getStatusLine().getStatusCode();
    System.out.println(statusCode);

   HttpEntity entity =  response.getEntity();
    String body = EntityUtils.toString(entity);
    System.out.println("服务端返回的数据是"+body);

    //关闭资源
    response.close();
    httpClient.close();
}

1. 接收原始数据

CloseableHttpResponse

代表完整的HTTP响应,其内部通过 socket 连接接收来自服务器的原始字节流。这是最底层的数据形式。

优点:完整保留了服务器发送的原始信息。
缺点:对开发者不友好,无法直接读取和操作。

2. 协议层封装

HttpEntity entity = response.getEntity();

HttpEntity是一个封装对象,它包裹了原始的字节流 (InputStream),并提供了访问响应体(内容字节)、内容类型 (Content-Type)内容长度 (Content-Length) 等元数据的方法。它将杂乱的字节流包装成一个结构化的对象。

优点:提供了一个标准的接口来访问响应内容和元数据,屏蔽了底层字节流的复杂性。
缺点:内容本身仍是字节,需要进一步处理。

3. 应用层解码

String body = EntityUtils.toString(entity);

这是最关键的一步!EntityUtils.toString()方法会做两件事:

  1. HttpEntity中读取字节流。
    2. 根据元数据(如Content-Type头中的charset=UTF-8)或默认设置,将字节流解码成字符串。这一步将机器理解的字节转换成了人类和程序容易处理的文本。

post方式请求:

@Test
public void testPost() throws IOException, JSONException {
    //创建httpClient对象
    CloseableHttpClient httpClient = HttpClients.createDefault();
    //创建请求对象
    HttpPost httpPost = new HttpPost("http://localhost:8080/admin/employee/login");

    JSONObject jsonObject = new JSONObject();
    jsonObject.put("username","admin");
    jsonObject.put("password","123456");

    StringEntity stringEntity = new StringEntity(jsonObject.toString());
    //指定请求编码方式
    stringEntity.setContentEncoding("UTF-8");
    stringEntity.setContentType("application/json");
    httpPost.setEntity(stringEntity);


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

   //解析返回结果
    int statusCode = response.getStatusLine().getStatusCode();
    System.out.println("响应码为:"+statusCode);
    HttpEntity entity =  response.getEntity();
    String body = EntityUtils.toString(entity);
    System.out.println("响应数据为"+body);
    //关闭资源
    response.close();
    httpClient.close();
}

代码解析:

CloseableHttpClient httpClient = HttpClients.createDefault();
创建HTTP客户端实例。这是发送所有请求的基础,类似于打开一个浏览器
HttpPost httpPost = new HttpPost("http://localhost...");
创建POST请求对象,并设定请求的目标URL(这里是一个本地登录接口)
JSONObject jsonObject = new JSONObject();
jsonObject.put("username","admin");...
构建JSON格式的请求参数。这里将一个包含用户名(admin)和密码(123456)的Java对象转换为JSON字符串,作为请求体发送
StringEntity stringEntity = new StringEntity(jsonObject.toString());
stringEntity.setContentEncoding("UTF-8");
stringEntity.setContentType("application/json");
httpPost.setEntity(stringEntity);
设置请求体。将JSON字符串包装成StringEntity,并明确指定其内容编码为UTF-8,内容类型为application/json,然后将其放入POST请求中
CloseableHttpResponse response = httpClient.execute(httpPost);
执行请求。客户端将携带参数的请求发送至服务器,并获取一个包含所有返回信息的响应对象
int statusCode = response.getStatusLine().getStatusCode();
String body = EntityUtils.toString(entity);
解析响应。从响应中提取HTTP状态码(如200成功)和响应体(服务器返回的实际数据,如登录结果)
response.close();
httpClient.close();
关闭连接,释放资源。这是一个好习惯,可以防止资源泄漏

(为什么将其包装成StringEnity?

答:因为HTTP请求体(Body)传输的是字节流(byte stream),而不是Java对象。直接将JSON对象(如JSONObject)放进去是不可能的,必须有一个“转换器”将它变成符合HTTP协议标准的格式。StringEntity就是这个关键的转换器。

下面这个表格清晰地对比了两种方式的本质区别:

特性 直接放入JSON对象 (不可行) 使用StringEntity (标准做法)
HTTP协议要求 请求体必须是字节流 将字符串转换为字节流,符合协议要求
数据格式 Java内存中的对象,无法直接传输 字符串(String),是字节流的直接来源
内容类型(Content-Type) 无法自动设置,服务器无法识别数据格式 可明确设置(如application/json),告诉服务器如何解析
字符编码 无法控制,可能导致乱码 可指定编码(如UTF-8),确保字符正确传输

微信登录

配置微信登陆的设置项:

image-20251218214538533

配置为微信用户生成jwt令牌时使用的配置项:

image-20251218214640844

DTO:

image-20251218214704472

VO:

image-20251218214723588

Controller:

@RestController
@RequestMapping("/user/user")
@Slf4j
@Api(tags = "C端用户接口")
public class UserController {
    @Autowired
    private  UserService userService;
    @Autowired
    private JwtProperties jwtProperties;
    /**
     * 微信登陆
     * @param userLoginDTO
     * @return
     */
    @PostMapping("/login")
    @ApiOperation("微信登录")
    public Result<UserLoginVO> login(@RequestBody UserLoginDTO userLoginDTO) {
        log.info("微信用户登录:{}",userLoginDTO.getCode());
        //微信登录
        User user = userService.wxlogin(userLoginDTO);

        //微信用户生成jwt令牌
        Map<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);
    }
}

Service:

@Service
@Slf4j
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;
    @Autowired
    private UserController userController;

    /**
     * 微信登录
     * @param userLoginDTO
     * @return
     */
    @Override
    public User wxlogin(UserLoginDTO userLoginDTO) {
//        Map<String, String> map = new HashMap();
//
//        map.put("appid",weChatProperties.getAppid());
//        map.put("secret",weChatProperties.getSecret());
//        map.put("js_code",userLoginDTO.getCode());
//        map.put("grant_type","authorization_code");
//        //调用微信接口服务,获取用户的openid
//       String json =  HttpClientUtil.doGet(WX_LOGIN,map);
//
//
//        JSONObject jsonObject = JSON.parseObject(json);
//        String openid = jsonObject.getString("openid");
        String openid =getOpenid(userLoginDTO.getCode());
        //判断openid是否为空?抛出异常:合法
        if (openid ==null){
            throw new LoginFailedException(MessageConstant.LOGIN_FAILED);
        }
        //判断微信用户是否为外卖系统的新用户,是的话进行注册并保存openid
        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) {
        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");
        //调用微信接口服务,获取用户的openid
        String json =  HttpClientUtil.doGet(WX_LOGIN,map);


        JSONObject jsonObject = JSON.parseObject(json);
        String openid = jsonObject.getString("openid");
        return openid;
    }
}

还有jwt的拦截器编写和注册拦截器...(和jwt登录验证一起统一学习)

缓存菜品

image-20251219153336055

例如:在查询菜品的时候开始是否缓存?

 /**
     * 根据分类id查询菜品
     *
     * @param categoryId     * @return
     */
    @GetMapping("/list")
    @ApiOperation("根据分类id查询菜品")
    public Result<List<DishVO>> list(Long categoryId) {

        //构造redis中的key,规则:dish_分类id
        String key = "dish_"+categoryId;

        //查询redis中是否存在菜品数据
        List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key);
        if (list != null && list.size()>0) {
                //如果存在,直接返回,无需查询数据库
                return Result.success(list);
        }



        Dish dish = new Dish();
        dish.setCategoryId(categoryId);
        dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品
//如果不存在,查询数据库,将查询到的数据放入redis中
        list = dishService.listWithFlavor(dish);
        redisTemplate.opsForValue().set(key,list);
        return Result.success(list);
    }

删除缓存操作(像在进行对菜品修改的时候,对菜品删除的时候,它的缓存数据就需要删除):

private void claeanCache(String pattern){
    Set keys  = redisTemplate.keys(pattern);
    redisTemplate.delete(keys);
}

利用注解来进行缓存:

image-20251219154218321

@Cacheable

  • 作用:适用于查询方法。遵循“先查缓存,没有再执行方法”的流程。
  • 用法:标注在方法上。主要指定两个属性: value/ cacheNames:缓存名称(必填),可指定多个,如 @Cacheable("users")@Cacheable({"users", "admins"})key:缓存键(可选)。支持 SpEL 表达式,默认为方法参数组合。如 @Cacheable(value="user", key="#id")condition:条件缓存(可选)。满足条件才缓存,如 @Cacheable(..., condition="#id > 10")

@CachePut

  • 作用:适用于更新/新增方法。总是会执行方法,并将方法返回的结果更新到缓存中。
  • 用法:通常用于更新数据库后同步更新缓存。必须和 @Cacheable使用相同的 valuekey,以确保更新的是同一份数据。
  • 注意:与 @Cacheable最大区别在于,它从不跳过方法执行。

@CacheEvict

  • 作用:适用于删除方法。用于从缓存中移除数据。
  • 用法:标注在方法上。关键属性: value/ cacheNames:指定要清除的缓存名称。 key:指定要清除的缓存键。 allEntries:是否清空整个缓存区域(可选,默认为false)。若为 true,则忽略 key,清除 value下的所有缓存。 beforeInvocation:清除操作在方法调用前还是调用后执行(可选,默认为false即方法调用后执行)。设置为 true可避免方法执行异常导致缓存未清除。

微信支付:

image-20251219155810107

image-20251219160424277

image-20251219160437767

image-20251219160537197

Spring Task定时任务编写:

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

利用此注解进行定时任务触发。

Websocket:

image-20251219164339437

实现步骤:

直接使用websocket.html页面作为WebSocket客户端
导入WebSocket的maven坐标
导入WebSocket服务端组件WebSocketServer,用于和客户端通信
导入配置类WebSocketConfiguration,注册WebSocket的服务端组件
导入定时任务类WebSocketTask,定时向客户端推送数据

WebSocketServer:

/**
 * 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();
            }
        }
    }

}

APch@ServerEndpoint("/ws/{sid}")
将该类声明为一个WebSocket服务端点,客户端通过 ws://地址:端口/ws/客户端唯一标识连接
@Component
表明这是一个Spring管理的Bean,通常需要配合ServerEndpointExporter配置才能生效
sessionMap
一个静态的Map,核心设计,用于全局保存所有已连接的客户端会话(Session)及其标识(sid)
@OnOpen
标注客户端连接成功时调用的方法,用于初始化操作
@OnMessage
标注收到客户端消息时调用的方法,用于处理业务逻辑
@OnClose
标注连接关闭时调用的方法,用于清理资源
@PathParam("sid")
用于从连接路径中提取变量sid的值,作为客户端的唯一标识
sendToAllClient
一个自定义的群发方法,用于向所有连接的客户端推送消息

代码片段

角色与作用

通俗理解

session

代表一次独立的 WebSocket 连接

好比是和一个朋友建立的专属电话线路

.getBasicRemote()

获取一个同步的消息发送对象

选择用普通电话模式交流(说完一句等对方回应)

.sendText(message)

同步地发送文本消息

在电话里说出一句话,并等待对方听到

WebSocketConfiguration

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

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

}
测试任务:

WebSocketTask

@Component
public class WebSocketTask {
    @Autowired
    private WebSocketServer webSocketServer;

    /**
     * 通过WebSocket每隔5秒向客户端发送消息
     */
    @Scheduled(cron = "0/5 * * * * ?")
    public void sendMessageToClient() {
        webSocketServer.sendToAllClient("这是来自服务端的消息:" + DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalDateTime.now()));
    }
}

Apache ECharts

/**
 * 订单统计
 * @param begin
 * @param end
 * @return
 */
@GetMapping("/ordersStatistics")
@ApiOperation("订单数据统计")
public Result<OrderReportVO> ordersStatistics(@DateTimeFormat(pattern = "yyyy-MM-dd")LocalDate begin,
                                              @DateTimeFormat(pattern = "yyyy-MM-dd")LocalDate end)
{
    log.info("订单数据统计:{},{}",begin,end);
    return Result.success(reportService.getOrderStatistics(begin,end));
}
@Override
public OrderReportVO getOrderStatistics(LocalDate begin, LocalDate end) {
     List<LocalDate> dateList = new ArrayList<>();
     dateList.add(begin);
  while(!begin.equals(end)){
      begin = begin.plusDays(1);
      dateList.add(begin);
  }
  List<Integer> orderCountList = new ArrayList<>();
  List<Integer> ValidOrderCountList = new ArrayList<>();
  Integer TotalOrderCount = 0;
  Integer ValidOrderCount = 0;
  for (LocalDate date : dateList) {
      LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);
      LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);
      Map map = new HashMap();
      map.put("begin", beginTime);
      map.put("end", endTime);
      map.put("status", Orders.COMPLETED);
      Integer totalOrder = userMapper.countOrderByMap(map);
      TotalOrderCount = TotalOrderCount + totalOrder;
      orderCountList.add(totalOrder);
      Integer validOrder = userMapper.countOrderValidByMap(map);
      ValidOrderCount = ValidOrderCount + validOrder;
      ValidOrderCountList.add(validOrder);
  }
  Double OrderCompletionRate =  ValidOrderCount.doubleValue()/TotalOrderCount.doubleValue();
  return OrderReportVO.builder()
          .dateList(StringUtils.join(dateList, ","))
          .orderCountList(StringUtils.join(orderCountList, ","))
          .validOrderCountList(StringUtils.join(ValidOrderCountList, ","))
          .totalOrderCount(TotalOrderCount)
          .validOrderCount(ValidOrderCount)
          .orderCompletionRate(OrderCompletionRate)
         .build();
}
<select id="countOrderByMap" resultType="java.lang.Integer">
    select count(id) from orders
    <where>
        <if test="begin != null">
            and order_time &gt;#{begin}
        </if>
        <if test="end != null">
            and order_time &lt;#{end}
        </if>
    </where>
</select>

Apache POI:

@Override
public void exportBusinessData(HttpServletResponse response) {
    //1.查询数据库,获取营业数据————查询销量30天的运营数据
   LocalDate dateBegin = LocalDate.now().minusDays(30);
    LocalDate dateEnd = LocalDate.now().minusDays(1);
    LocalDateTime beginTime = LocalDateTime.of(dateBegin, LocalTime.MIN);
    LocalDateTime endTime = LocalDateTime.of(dateEnd, LocalTime.MAX);
   BusinessDataVO businessDataVO = workspaceService.getBusinessData(beginTime, endTime);
    //2.通过poi将数据写入excel文件
    InputStream in = this.getClass().getClassLoader().getResourceAsStream("template/运营数据报表模板.xlsx");
    try {
        //基于模板文件创建一个心的excel文件
        XSSFWorkbook excel = new XSSFWorkbook(in);

        XSSFSheet sheet = excel.getSheet("Sheet1");
        //填充数据--时间
        sheet.getRow(1).getCell(1).setCellValue("时间:"+dateBegin+"至:"+dateEnd);
        sheet.getRow(3).getCell(2).setCellValue(businessDataVO.getTurnover());
        sheet.getRow(3).getCell(4).setCellValue(businessDataVO.getOrderCompletionRate());
        sheet.getRow(3).getCell(6).setCellValue(businessDataVO.getNewUsers());
        //获得第五行
        XSSFRow 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);
            //查询某一天的营业数据
            BusinessDataVO businessData = 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(businessData.getTurnover());
            row.getCell(3).setCellValue(businessData.getValidOrderCount());
            row.getCell(4).setCellValue(businessData.getOrderCompletionRate());
            row.getCell(5).setCellValue(businessData.getUnitPrice());
            row.getCell(6).setCellValue(businessData.getNewUsers());

        }
        //3.通过输出流将excel文件下载到客户端浏览器
        ServletOutputStream out = response.getOutputStream();
        excel.write(out);
        //关闭资源
        out.close();
        excel.close();
    } catch (IOException e) {
        throw new RuntimeException(e);
    }




}
posted @ 2025-12-22 21:29  KaKaWlW  阅读(0)  评论(0)    收藏  举报