Java萬級并發場景-實戰解決

今天我們來做一個典型的消費力度能達到萬級別的并發場景,老師點名-學生簽到

正常情況

正常情況來說是不同班級下的老師發布不同的點名--然后不同班級下的很多學生同一時間進行簽到,簽到成功就去修改數據庫,簽到失敗就返回,但是這樣的話 簽到的學生一多,數據庫修改每一行的內容,都會加上行鎖,那么改的多了,數據庫很可能出現卡頓的情況,導致學生明明在規定時間內簽到了,但是卻出現簽到結束的情況,或者說出現其他的冗余簽到的情況,這樣顯然是不希望我們看到的,也不希望學生看到

并發級處理

怎么解決前面的那種簽到錯誤的場景呢?

那么當然就是傳統級別的 面對并發情況下的重拳三連了哈哈哈

mysql-redis-rabbitMq

首先 我們這個業務需要怎么寫?

redis的key怎么選擇,學生的key怎么選都是一個問題,下面我們來一一的進行分析

MySQL表的業務數據關聯

因為我們是測試demo,所以我們只做出了關鍵的表結構關聯,像老師表我們是沒有做的

看上圖,首先我們最頂部有一個課程表,寫的有一個課程id和名稱,還有還有學生表,學生表和課程表之間有一個中間的表關聯,叫學生課程表(student-courses),然后我們老師點名的時候是屬于課堂活動表,里面記錄的課堂的活動,比如點名和提問,這個表(class_activities)與課程表關聯,最后的是每一個學生在該課程下的做出的課堂活動,也就是學生活動表(student-activities),她關聯了學生表,課堂活動表和課程表。

主要流程

老師發布點名,然后課堂互動表記錄一條會過期的課堂活動,狀態是進行中,然后學生簽到,簽到之后,找到該課程下的該簽到過的學生,像學生活動表中添加一條簽到過的數據

Redis業務

在redis方面,我們主要做的就是對學生簽到數據的存儲,對老師發布的簽到數據的存儲

我們知道 redis的string的數據類型是比較占用空間的,所以對于我們單個的老師發布的簽到數據,我們可以用string類型,對于不同班級下的多個學生的簽到情況,我們可以用hash結構 ,因為對于ihash結構,我們的數據一般是使用ziplist壓縮,更省空間

RabbitMQ業務

我們mq主要做的就是讀取redis中的簽到過的學生數據,然后把學生數據做一個異步寫入mysql,這樣減緩簽到高峰時段mysql的壓力

我們mq首先從redis中查到簽到過的學生數據,然后跟該課程下的學生數據做對比,如果該課程下學生有數據,redis中學生簽到無數據,那么該學生就是未簽到

如果簽到,就把簽到數據存入數據庫

總體代碼

老師點名-學生簽到

package com.example.tabledemo.controller;import cn.hutool.core.date.DateTime;
import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.example.tabledemo.config.RabbitConfig;
import com.example.tabledemo.generator.service.ClassActivitiesService;
import com.example.tabledemo.generator.service.CourseService;
import com.example.tabledemo.pojo.Result;
import com.example.tabledemo.pojo.entity.ClassActivitiesEntity;
import com.example.tabledemo.pojo.entity.CourseEntity;
import com.example.tabledemo.pojo.request.ClassActivitiesRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
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.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Objects;import static cn.hutool.core.date.DateTime.now;/*** @Author: wyz* @Date: 2025-04-08-16:17* @Description:課堂活動*/
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/class/activities")
public class ClassActivitiesController {private final ClassActivitiesService classActivitiesService;private final CourseService courseService;private final StringRedisTemplate redisTemplate;private final RabbitTemplate rabbitTemplate;/*** 老師點名*/@PostMapping("/teacher/rollCall")public Result teacherRollCall(@RequestBody ClassActivitiesRequest.TeacherRollCall teacherRollCall) {//判斷是否有課程CourseEntity course = courseService.getById(teacherRollCall.getCourseId());if (Objects.isNull(course)) {return Result.fail("沒有該課程");}//查看該課程下是否有點名活動LambdaQueryWrapper<ClassActivitiesEntity> eq = Wrappers.lambdaQuery(ClassActivitiesEntity.class).eq(ClassActivitiesEntity::getCourseId, teacherRollCall.getCourseId()).eq(ClassActivitiesEntity::getActiveType, 1).eq(ClassActivitiesEntity::getActiveStatus, 0);ClassActivitiesEntity one = classActivitiesService.getOne(eq);if(!Objects.isNull(one)){return Result.fail("該課程已存在點名,請勿重復點名");}//生成簽到碼//// String signCode = RandomUtil.randomNumbers(4);String signCode = "1234";ClassActivitiesEntity classActivitiesEntity = new ClassActivitiesEntity();classActivitiesEntity.setCourseId(teacherRollCall.getCourseId());// 獲取當前時間DateTime now = now();classActivitiesEntity.setStartTime(now);// 使用Calendar計算未來時間Calendar calendar = Calendar.getInstance();calendar.setTime(now);calendar.add(Calendar.SECOND, teacherRollCall.getSignSeconds());Date endTime = calendar.getTime();classActivitiesEntity.setEndTime(endTime);classActivitiesEntity.setActiveType(1);classActivitiesEntity.setActiveStatus(0);//課堂活動存入數據庫boolean save = classActivitiesService.save(classActivitiesEntity);//redis中生成簽到碼的keyString signCodeKey = "sign_" + teacherRollCall.getCourseId() + "_" + signCode;redisTemplate.opsForValue().set(signCodeKey, signCode);//發給rabbitmq 延遲隊列 讓延遲隊列處理 最終的簽到情況//1. 學生查看課堂的活動的信息 應該在 課堂活動表中查看//2. 延遲隊列處理 簽到結束后的情況HashMap<Object, Object> map = new HashMap<>();map.put("course_id", teacherRollCall.getCourseId());map.put("class_activities_id", classActivitiesEntity.getId());map.put("sign_code", signCode);rabbitTemplate.convertAndSend(RabbitConfig.ROLL_CALL_DEAD_EXCHANGE, RabbitConfig.ROLL_CALL_DEAD_ROUTING_KEY, map, new MessagePostProcessor() {@Overridepublic Message postProcessMessage(Message message) throws AmqpException {message.getMessageProperties().setDelay(teacherRollCall.getSignSeconds()*1000);return message;}});return Result.success("發布簽到成功",signCode);}/*** 學生簽到*/@PostMapping("/student/sign")public Result studentSign(@RequestBody ClassActivitiesRequest.StudentSign studentSign) {//判斷該學生是否在班級當中//這里我們不判斷 知道就行String signCodeKey = "sign_" + studentSign.getCourseId() + "_" + studentSign.getSignCode();//不為空 證明有該簽到String signCode = redisTemplate.opsForValue().get(signCodeKey);if (!Objects.isNull(signCode)) {if (!signCode.equals(studentSign.getSignCode())) {return Result.fail("簽到碼錯誤,簽到失敗");}//學生簽到keyString studentSignKey="student_sign_"+studentSign.getStudentId();if(redisTemplate.opsForHash().hasKey("h"+signCodeKey,studentSignKey)){return  Result.fail("您已經簽到成功,請勿重復簽到");}//value正常應該是 簽到時間  我們換成簽到碼redisTemplate.opsForHash().put("h"+signCodeKey,studentSignKey,signCode);return Result.success("簽到成功");} else {return Result.fail("簽到已過期或已被刪除");}}
}

mq配置

package com.example.tabledemo.config;import org.springframework.amqp.core.*;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** @Author: wyz* @Date: 2025-04-08-17:19* @Description:*/
@Configuration
public class RabbitConfig {@Beanpublic MessageConverter messageConverter() {// 定義消息轉換器Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter();return jackson2JsonMessageConverter;}//    //點名延遲交換機
//    public static final String ROLL_CALL_EXCHANGE = "roll_call_exchange";
//    //點名延遲隊列
//    public static final String ROLL_CALL_QUEUE = "roll_call_queue";//點名死信交換機public static final String ROLL_CALL_DEAD_EXCHANGE = "roll_call_dead_exchange";//點名死信隊列public static final String ROLL_CALL_DEAD_QUEUE = "roll_call_dead_queue";public static final String ROLL_CALL_DEAD_ROUTING_KEY = "roll_call";/*** 綁定 點名消息隊列 -> 點名私信交換機->點名私信隊列** @return*/
//    @Bean
//    public Queue bindMsgDeadQueue() {
//        return QueueBuilder.durable(ROLL_CALL_QUEUE)
//                .deadLetterExchange(ROLL_CALL_DEAD_EXCHANGE)
//                .deadLetterRoutingKey(ROLL_CALL_DEAD_ROUTING_KEY)
//
//                .build();
//    }
//
//
//
//
//    /**
//     * 聲明點名交換機
//     */
//    @Bean
//    Exchange rollCallExchange() {
//        return ExchangeBuilder.directExchange(ROLL_CALL_EXCHANGE)
//                .durable(true)
//                .build();
//    }
//
//    /**
//     * 綁定 點名 交換機隊列
//     */
//    @Bean
//    Binding bingingRollCallExchangeQueue() {
//        return BindingBuilder.bind(bindMsgDeadQueue())
//                .to(rollCallExchange())
//                .with(ROLL_CALL_DEAD_ROUTING_KEY).noargs();
//    }/*** 聲明點名死信隊列*/@BeanQueue rollCallDeadQueue() {return QueueBuilder.durable(ROLL_CALL_DEAD_QUEUE).build();}/*** 聲明點名 死信交換機*/@BeanExchange rollCallDeadExchange() {return ExchangeBuilder.directExchange(ROLL_CALL_DEAD_EXCHANGE).delayed().durable(true).build();}/*** 綁定點名 私信交換機隊列*/@BeanBinding bindingRollCallExchangeQueue() {return BindingBuilder.bind(rollCallDeadQueue()).to(rollCallDeadExchange()).with(ROLL_CALL_DEAD_ROUTING_KEY).noargs();}}

消費者配置

package com.example.tabledemo.consumer;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.example.tabledemo.config.RabbitConfig;
import com.example.tabledemo.generator.service.ClassActivitiesService;
import com.example.tabledemo.generator.service.StudentActivitiesService;
import com.example.tabledemo.pojo.entity.ClassActivitiesEntity;
import com.example.tabledemo.pojo.entity.StudentActivitiesEntity;
import com.example.tabledemo.student.StudentCoursesEntity;
import com.example.tabledemo.student.service.StudentCoursesService;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.GetResponse;
import lombok.extern.slf4j.Slf4j;import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.*;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;import java.io.IOException;import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;import static java.time.LocalTime.now;/*** @Author: wyz* @Date: 2025-04-08-20:40* @Description:處理學生簽到的消費者*/
@Component
@Slf4j
public class SignConsumer {@Autowiredprivate StringRedisTemplate redisTemplate;@Autowiredprivate StudentCoursesService studentCoursesService;@Autowiredprivate ClassActivitiesService classActivitiesService;@Autowiredprivate StudentActivitiesService studentActivitiesService;@RabbitListener(queues = RabbitConfig.ROLL_CALL_DEAD_QUEUE)@RabbitHandler// 直接引用隊列名public void studentSignConsumer(HashMap<Object, Object> map, Channel channel, Message message) throws IOException {try {log.info(now() + "----------老師點名延遲消息處理開始----------");//解析消息Integer courseId = (Integer) map.get("course_id");Integer classActivitiesId = (Integer) map.get("class_activities_id");String signCode = (String) map.get("sign_code");//業務冪等性判斷ClassActivitiesEntity byId = classActivitiesService.getById(classActivitiesId);//證明已經消費過了 本來是額外存的這里 只用狀態判斷if(byId.getActiveStatus()==1){return;}//拿到redis中的學生簽到數據String signCodeKey = "sign_" + courseId + "_" + signCode;Map<Object, Object> studentSignMap = redisTemplate.opsForHash().entries("h" + signCodeKey);//課堂活動狀態改為已經結束LambdaUpdateWrapper<ClassActivitiesEntity> eq1 = Wrappers.lambdaUpdate(ClassActivitiesEntity.class).set(ClassActivitiesEntity::getActiveStatus, 1).eq(ClassActivitiesEntity::getId, classActivitiesId);classActivitiesService.update(eq1);//學生簽到key//String studentSignKey="student_sign_"+studentSign.getStudentId();List<Integer> studentSignIdList = studentSignMap.entrySet().stream().map(i -> {String studentSignKey = (String) i.getKey();log.info("學生信息為{}", studentSignKey);Integer studentId = Integer.valueOf(studentSignKey.split("_")[2]);log.info("學生id為{}", studentId);return studentId;}).collect(Collectors.toList());//查出該課程下 的所有學生idLambdaQueryWrapper<StudentCoursesEntity> eq = Wrappers.lambdaQuery(StudentCoursesEntity.class).eq(StudentCoursesEntity::getCourseId, courseId);List<StudentCoursesEntity> list = studentCoursesService.list(eq);List<Integer> studentIds = list.stream().map(i -> i.getStudentId()).collect(Collectors.toList());//正常是 會有課程狀態 課程結課什么的 ,這里我們模擬 不做處理ArrayList<StudentActivitiesEntity> studentActivitiesEntities = new ArrayList<>();studentIds.stream().forEach(studentId -> {StudentActivitiesEntity studentActivitiesEntity = new StudentActivitiesEntity();studentActivitiesEntity.setStudentId(studentId);studentActivitiesEntity.setClassActivitiesId(classActivitiesId);studentActivitiesEntity.setCourseId(courseId);studentActivitiesEntity.setStudentActivitiesStatus(0);if (studentSignIdList.contains(studentId)) {log.info("有學生簽到了");studentActivitiesEntity.setStudentActivitiesStatus(1);}studentActivitiesEntities.add(studentActivitiesEntity);});//構建學生活動表的數據studentActivitiesService.saveBatch(studentActivitiesEntities);//刪除redis數據redisTemplate.delete(signCodeKey);redisTemplate.delete("h" + signCodeKey);//true 和false 代表著 是否 確認該條消息之前的  true 是確認  false 不確認// 假設隊列中有消息 deliveryTag=5,6,7  現在是6// 結果:僅消息6被確認刪除,消息5和7仍在隊列中channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);log.info(now() + "----------老師點名延遲消息處理結束----------");} catch (Exception e) {Boolean redelivered = message.getMessageProperties().getRedelivered();if (redelivered) {log.info(now() + "----------老師點名延遲消息處理異常,已被重新投遞,丟棄消息----------");channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);} else {log.info(now() + "----------老師點名延遲消息處理異常,消息重新投遞----------");channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);}throw e;}}
}

測試流程

接口測試

jmeter 壓測

數據庫數據查看?

可見 已經測試成功了?

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/76851.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/76851.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/76851.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

openGauss新特性 | 自動參數化執行計劃緩存

目錄 自動化參數執行計劃緩存簡介 SQL參數化及約束條件 一般常量參數化示例 總結 自動化參數執行計劃緩存簡介 執行計劃緩存用于減少執行計劃的生成次數。openGauss數據庫會緩存之前生成的執行計劃&#xff0c;以便在下次執行該SQL時直接使用&#xff0c;可…

計算機操作系統——存儲器管理

系列文章目錄 1.存儲器的層次結構 2.程序的裝入和鏈接 3.連續分配存儲管理方式&#xff08;內存夠用&#xff09; 4.對換&#xff08;Swapping&#xff09;(內存不夠用) 5.分頁存儲管理方式 6.分段存儲管理方式 文章目錄 系列文章目錄前言一、存儲器的存儲結構寄存器&…

KF V.S. GM-PHD

在計算機視覺的多目標跟蹤&#xff08;MOT&#xff09;任務中&#xff0c;卡爾曼濾波&#xff08;KF&#xff09;和高斯混合概率假設密度&#xff08;GM-PHD&#xff09;濾波器是兩種經典的狀態估計方法&#xff0c;但它們的原理和應用場景存在顯著差異。以下是兩者的核心機制和…

車載通信架構 --- DOIP系統機制初入門

我是穿拖鞋的漢子,魔都中堅持長期主義的汽車電子工程師。 老規矩,分享一段喜歡的文字,避免自己成為高知識低文化的工程師: 周末洗了一個澡,換了一身衣服,出了門卻不知道去哪兒,不知道去找誰,漫無目的走著,大概這就是成年人最深的孤獨吧! 舊人不知我近況,新人不知我過…

C++對象池設計:從高頻`new/delete`到性能飛躍的工業級解決方案

一、new/delete的性能之殤&#xff1a;一個真實的生產事故 2023年某證券交易系統在峰值時段出現請求堆積&#xff0c;事后定位發現&#xff1a;每秒40萬次的訂單對象創建/銷毀&#xff0c;導致&#xff1a; 內存碎片率高達37%&#xff08;jemalloc統計&#xff09;malloc調用…

【C/C++】深入理解整型截斷與提升:原理、應用與區別

文章目錄 1. 整形截斷&#xff08;Integer Truncation&#xff09;1.1 整形截斷的例子1.2 整形截斷的細節 2. 整形提升&#xff08;Integer Promotion&#xff09;2.1 整形提升的規則2.2 整形提升的示例2.3 整形提升的實際應用2.4 整型提升與標準操作符 3. 整型截斷與提升的區別…

python藍橋杯備賽常用算法模板

一、python基礎 &#xff08;一&#xff09;集合操作 s1 {1,2,3} s2{3,4,5} print(s1|s2)#求并集 print(s1&s2)#求交集 #結果 #{1, 2, 3, 4, 5} #{3}&#xff08;二&#xff09;對多維列表排序 1.新建列表 list1[[1,2,3],[2,3,4],[0,3,2]] #提取每個小列表的下標為2的…

【模塊化拆解與多視角信息3】教育背景:學歷通脹時代的生存法則

教育背景:學歷通脹時代的生存法則 寫在最前 作為一個中古程序猿,我有很多自己想做的事情,比如埋頭苦干手搓一個低代碼數據庫設計平臺(目前只針對寫java的朋友),比如很喜歡幫身邊的朋友看看簡歷,講講面試技巧,畢竟工作這么多年,也做到過高管,有很多面人經歷,意見還算…

uniapp實現H5頁面麥克風權限獲取與錄音功能

1.權限配置 在uni-app開發H5頁面時&#xff0c;需要在manifest.json文件中添加錄音權限的配置。具體如下&#xff1a; {"h5": {"permissions": {"scope.record": {"desc": "請授權使用錄音功能"}}} }這段配置代碼是用于向…

功能豐富的PDF處理免費軟件推薦

軟件介紹 今天給大家介紹一款超棒的PDF工具箱&#xff0c;它處理PDF文檔的能力超強&#xff0c;而且是完全免費使用的&#xff0c;沒有任何限制。 TinyTools&#xff08;PC&#xff09;這款軟件&#xff0c;下載完成后即可直接打開使用。在使用過程中&#xff0c;操作完畢后&a…

鴻蒙開發-ArkUi控件使用

2.0控件-按鈕 2.1.控件-文本框 Text(this.message).fontSize(40) // 設置文本的文字大小.fontWeight(FontWeight.Bolder) // 設置文本的粗細.fontColor(Color.Red) // 設置文本的顏色------------------------------------------------------------------------- //設置邊框Tex…

深入理解 ResponseBodyAdvice 及其應用

ResponseBodyAdvice 是 Spring MVC 提供的一個強大接口&#xff0c;允許你在響應體被寫入 HTTP 響應之前對其進行全局處理。 下面我將全面介紹它的工作原理、使用場景和最佳實踐。 基本概念 接口定義 public interface ResponseBodyAdvice<T> {boolean supports(Metho…

深度解析Redis過期字段清理機制:從源碼到集群化實踐 (一)

深度解析Redis過期字段清理機制&#xff1a;從源碼到集群化實踐 一、問題本質與架構設計 1.1 過期數據管理的核心挑戰 Redis連接池時序圖技術方案 ??設計規范&#xff1a;? #mermaid-svg-Yr9fBwszePgHNnEQ {font-family:"trebuchet ms",verdana,arial,sans-se…

數據庫ocm有什么用

專業能力的權威象征 。技術水平的高度認可&#xff1a;OCM 是 Oracle 認證體系中的最高級別&#xff0c;代表著持證人在 Oracle 數據庫領域具備深厚的專業知識和卓越的實踐技能。它證明持證人能夠熟練掌握數據庫的安裝、配置、管理、優化、備份恢復等核心技術&#xff0c;并且能…

無人船 | 圖解基于視線引導(LOS)的無人艇制導算法

目錄 1 視線引導法介紹2 LOS制導原理推導3 Lyapunov穩定性分析4 LOS制導效果 1 視線引導法介紹 視線引導法&#xff08;Line of Sight, LOS&#xff09;作為無人水面艇&#xff08;USV&#xff09;自主導航領域的核心技術&#xff0c;通過幾何制導與動態控制深度融合的機制&am…

Swift觀察機制新突破:如何用AsyncSequence實現原子化數據監聽?

網羅開發 &#xff08;小紅書、快手、視頻號同名&#xff09; 大家好&#xff0c;我是 展菲&#xff0c;目前在上市企業從事人工智能項目研發管理工作&#xff0c;平時熱衷于分享各種編程領域的軟硬技能知識以及前沿技術&#xff0c;包括iOS、前端、Harmony OS、Java、Python等…

【KWDB創作者計劃】_KWDB部署與使用詳細版本

KWDB發展歷程 介紹KWDB前&#xff0c;先介紹下KaiwuDB&#xff0c; KaiwuDB 是浪潮控股的數據庫企業&#xff0c;該企業提供的KaiwuDB數據庫是一款分布式多模數據庫產品&#xff0c;主要面向工業物聯網、數字能源、車聯網、智慧產業等行業領域。 在2024年7月&#xff0c; Kai…

Go:接口

接口既約定 Go 語言中接口是抽象類型 &#xff0c;與具體類型不同 &#xff0c;不暴露數據布局、內部結構及基本操作 &#xff0c;僅提供一些方法 &#xff0c;拿到接口類型的值 &#xff0c;只能知道它能做什么 &#xff0c;即提供了哪些方法 。 func Fprintf(w io.Writer, …

一、Appium環境安裝

找了一圈操作手機的工具或軟件&#xff0c;踩了好多坑&#xff0c;最后決定用這個工具(影刀RPA手機用的也是這個)&#xff0c;目前最新的版本是v2.17.1&#xff0c;是基于nodejs環境的&#xff0c;有兩種方式&#xff0c;我只試了第一種方式&#xff0c;第二種方式應該是比較簡…

【玩轉全棧】—— Django 連接 vue3 保姆級教程,前后端分離式項目2025年4月最新!!!

本文基于之前的一個旅游網站&#xff0c;實現 Django 連接 vue3&#xff0c;使 vue3 能攜帶 CSRF Token 發送 axios 請求給后端&#xff0c;后端再響應數據給前端。想要源碼直接滑倒底部。 目錄 實現效果 解決跨域 獲取 csrf-token 什么是 csrf-token &#xff1f; CSRF攻擊的…