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


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),確保字符正確傳輸

微信登錄

配置微信登陸的設置項:


配置為微信用户生成jwt令牌時使用的配置項:


DTO:


VO:


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登錄驗證一起統一學習)

緩存菜品


例如:在查詢菜品的時候開始是否緩存?

/**
     * 根據分類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);
}

利用註解來進行緩存:


@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可避免方法執行異常導致緩存未清除。

微信支付:





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:


實現步驟:

直接使用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 >#{begin}
        </if>
        <if test="end != null">
            and order_time <#{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);
    }




}