目录 1、前言 1.1 运用场景 1.2 什么是定时任务 1.2.1 Java 实现定时任务三大方式 1.2.2 常见开源分布式任务框架 1.2.3 传通定时任务的不足 ·2、Xxl-job 分布式定时任务 2.1 Xxl-job
目录
3.11 xxl-job 任务配置之运行模式GLUE【Java】
3.12 xxl-job 任务配置之运行模式GLUE【Python】
3.13 xxl-job 任务配置之运行模式GLUE【Shell】
3.14 xxl-job 任务配置之运行模式GLUE【PHP】
3.15 xxl-job 任务配置之运行模式GLUE【Node.js】
3.16 xxl-job 任务配置之运行模式GLUE【PowerShell】
定时任务是指基于给定的时间点,间隔或者给定执行次数自动的执行程序。任务调度室系统的重要组成部分,而对于实时的系统,任务调度直接影响着系统的实时性。任务调度涉及多线程并发、运行时间规则定制与解析、线程池的维护等多项工作。
JDK 自带的定时器实现
public static void main(String[] args) { for (int i = 0; i < 10; ++i) { new Timer("timer - " + i).schedule(new TimerTask() { @Override public void run() { println(Thread.currentThread().getName() + " run "); } }, 1000); } }out :timer - 2 run timer - 1 run timer - 0 run timer - 3 run timer - 9 run timer - 4 run timer - 8 run timer - 5 run timer - 6 run timer - 7 run
public static void main(String[] args) { for (int i = 0; i < 10; ++i) { new Timer("timer - " + i).schedule(new TimerTask() { @Override public void run() { println(Thread.currentThread().getName() + " run "); } }, new Date(System.currentTimeMillis() + 2000)); } }out:timer - 0 run timer - 7 run timer - 6 run timer - 8 run timer - 3 run timer - 5 run timer - 2 run timer - 1 run timer - 4 run timer - 9 run
public static void main(String[] args) { for (int i = 0; i < 10; ++i) { new Timer("timer - " + i).schedule(new TimerTask() { @Override public void run() { println(Thread.currentThread().getName() + " run "); } }, 2000 , 3000); } }out:timer - 0 run timer - 5 run timer - 4 run timer - 8 run timer - 3 run timer - 2 run timer - 1 run timer - 7 run timer - 9 run timer - 6 run timer - 3 run timer - 7 run timer - 5 run timer - 4 run timer - 8 run
JDK ScheduledExecutorService 接口实现类
ScheduledExecutorService 是JAVA 1.5 后新增的定时任务接口,主要有以下几个方法。
- ScheduledFuture> schedule(Runnable command,long delay, TimeUnit unit);- ScheduledFuture schedule(Callable callable,long delay, TimeUnit unit);- ScheduledFuture> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnitunit);- ScheduledFuture> scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnitunit);
默认实现为ScheduledThreadPoolExecutor 继承了ThreadPoolExecutor 的线程池特性,配合future特性,比Timer更强大。
示例功能代码:
public static void main(String[] args) throws SchedulerException { ScheduledThreadPoolExecutor executor = (ScheduledThreadPoolExecutor)Executors.newScheduledThreadPool(10); for (int i = 0; i < 10; ++i) { executor.schedule(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName() + " run "); } } , 2 , TimeUnit.SECONDS); } executor.shutdown(); }out:pool-1-thread-2 run pool-1-thread-5 run pool-1-thread-4 run pool-1-thread-3 run pool-1-thread-8 run pool-1-thread-5 run pool-1-thread-7 run pool-1-thread-2 run pool-1-thread-1 run pool-1-thread-6 run
温馨提示:spring Task内部实现也是基于ScheduledExecutorService 接口。
Quartz 定时器
Quartz是一个完全由Java编写的开源作业调度框架,为在Java应用程序中进行作业调度提供了简单却强大的机制。Quartz允许开发人员根据时间间隔来调度作业。它实现了作业和触发器的多对多的关系,还能把多个作业与不同的触发器关联。可以动态的添加删除定时任务,另外很好的支撑集群调度
。
示例功能代码:
org.quartz-scheduler quartz 2.3.0
创建Quartz 定时器Job类
public class TestJob implements Job{ @Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { println(Thread.currentThread().getName() + " test job begin " + DateUtil.getCurrentTimeStr()); }}
代码测试
public static void main(String[] args) throws InterruptedException, SchedulerException { Scheduler scheduler = new StdSchedulerFactory().getScheduler(); // 开始 scheduler.start(); // job 唯一标识 test.test-1 JobKey jobKey = new JobKey("test" , "test-1"); JobDetail jobDetail = JobBuilder.newJob(TestJob.class).withIdentity(jobKey).build(); Trigger trigger = TriggerBuilder.newTrigger() .withIdentity("test" , "test") // 延迟一秒执行 .startAt(new Date(System.currentTimeMillis() + 1000)) // 每隔一秒执行 并一直重复 .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(1).repeatForever()) .build(); scheduler.scheduleJob(jobDetail , trigger); Thread.sleep(5000); // 删除job scheduler.deleteJob(jobKey); }out :DefaultQuartzScheduler_Worker-1test job begin 2017-06-03 14:30:33DefaultQuartzScheduler_Worker-2test job begin 2017-06-03 14:30:34DefaultQuartzScheduler_Worker-3test job begin 2017-06-03 14:30:35DefaultQuartzScheduler_Worker-4test job begin 2017-06-03 14:30:36DefaultQuartzScheduler_Worker-5test job begin 2017-06-03 14:30:37
Quartz 核心部分
Job:是一个接口,只有一个方法void execute(JobExecutionContext context),开发者实现该接口定义运行任务,JobExecutionContext类提供了调度上下文的各种信息。Job运行时的信息保存在JobDataMap实例中;JobDetail:Quartz在每次执行Job时,都重新创建一个Job实例,所以它不直接接受一个Job的实例,相反它接收一个Job实现类,以便运行时通过newInstance()的反射机制实例化Job。因此需要通过一个类来描述Job的实现类及其它相关的静态信息,如Job名字、描述、关联监听器等信息,JobDetail承担了这一角色。Trigger:是一个类,描述触发Job执行的时间触发规则。主要有SimpleTrigger和CronTrigger这两个子类。当仅需触发一次或者以固定时间间隔周期执行,SimpleTrigger是最适合的选择;而CronTrigger则可以通过Cron表达式定义出各种复杂时间规则的调度方案:如每早晨9:00执行,周一、周三、周五下午5:00执行等;Calendar:org.quartz.Calendar和java.util.Calendar不同,它是一些日历特定时间点的集合(可以简单地将org.quartz.Calendar看作java.util.Calendar的集合——java.util.Calendar代表一个日历时间点,无特殊说明后面的Calendar即指org.quartz.Calendar)。一个Trigger可以和多个Calendar关联,以便排除或包含某些时间点。假设,我们安排每周星期一早上10:00执行任务,但是如果碰到法定的节日,任务则不执行,这时就需要在Trigger触发机制的基础上使用Calendar进行定点排除。Scheduler:代表一个Quartz的独立运行容器,Trigger和JobDetail可以注册到Scheduler中,两者在Scheduler中拥有各自的组及名称,组及名称是Scheduler查找定位容器中某一对象的依据,Trigger的组及名称必须唯一,JobDetail的组和名称也必须唯一(但可以和Trigger的组和名称相同,因为它们是不同类型的)。Scheduler定义了多个接口方法,允许外部通过组及名称访问和控制容器中Trigger和JobDetail。Scheduler可以将Trigger绑定到某一JobDetail中,这样当Trigger触发时,对应的Job就被执行。一个Job可以对应多个Trigger,但一个Trigger只能对应一个Job。可以通过SchedulerFactory创建一个Scheduler实例。Scheduler拥有一个SchedulerContext,它类似于ServletContext,保存着Scheduler上下文信息,Job和Trigger都可以访问SchedulerContext内的信息。SchedulerContext内部通过一个Map,以键值对的方式维护这些上下文数据,SchedulerContext为保存和获取数据提供了多个put()和getXxx()的方法。可以通过Scheduler#getContext()获取对应的SchedulerContext实例;ThreadPool:Scheduler使用一个线程池作为任务运行的基础设施,任务通过共享线程池中的线程提高运行效率。
Spring Task
Spring Task 是 Spring 提供的轻量级定时任务工具,也就意味着不需要再添加第三方依赖了,相比其他第三方类库更加方便易用。
第一步:新建配置类 SpringTaskConfig,并添加 @EnableScheduling注解开启 Spring Task。
@Configuration@EnableSchedulingpublic class SpringTaskConfig {}
可以不新建这个配置类,直接在主类上添加 @EnableScheduling 注解。
@SpringBootApplication@EnableSchedulingpublic class codingmoreSpringtaskApplication {public static void main(String[] args) {SpringApplication.run(CodingmoreSpringtaskApplication.class, args);}}
第二步,新建定时任务类 CronTask,使用 @Scheduled 注解注册 Cron 表达式执行定时任务。
@Slf4j@Componentpublic class CronTask { @Scheduled(cron = "0/1 * * ? * ?") public void cron() { log.info("定时执行,时间{}", DateUtil.now()); }}
启动服务器端,发现每隔一秒钟会打印一次日志,证明 Spring Task 的 cron 表达式形式已经起效了。
默认情况下,@Scheduled 创建的线程池大小为 1,如果想增加线程池大小的话,可以让 SpringTaskConfig 类实现 SchedulinGConfigurer 接口,通过 setPoolSize 增加线程池大小。
@Configuration@EnableSchedulingpublic class SpringTaskConfig implements SchedulingConfigurer { @Override public void configureTasks(ScheduledTaskReGIStrar taskRegistrar) { ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); threadPoolTaskScheduler.setPoolSize(10); threadPoolTaskScheduler.setThreadNamePrefix("my-scheduled-task-pool-"); threadPoolTaskScheduler.initialize(); taskRegistrar.setTaskScheduler(threadPoolTaskScheduler); }}
知识拓展:Spring Task 除了支持 Cron 表达式,还有 fixedRate(固定速率执行)、fixedDelay(固定延迟执行)、initialDelay(初始延迟)三种用法。
@Scheduled(fixedRate = 5000)public void reportCurrentTimeWithFixedRate() { log.info("Current Thread : {}", Thread.currentThread().getName()); log.info("Fixed Rate Task : The time is now {}", DateUtil.now());}@Scheduled(fixedDelay = 2000)public void reportCurrentTimeWithFixedDelay() { try { TimeUnit.SECONDS.sleep(3); log.info("Fixed Delay Task : The time is now {}",DateUtil.now()); } catch (InterruptedException e) { e.printStackTrace(); }}@Scheduled(initialDelay = 5000, fixedRate = 5000)public void reportCurrentTimeWithInitialDelay() { log.info("Fixed Rate Task with Initial Delay : The time is now {}", DateUtil.now());}
Xxl-job是一个开源的,具有丰富的任务管理功能以及高性能,高可用等特点的轻量级分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展、开箱即用!!!
1、将调度行为抽象形成“调度中心”公共平台,而平台自身并不承担业务逻辑,
“调度中心”负责发起调度请求。
2、将任务抽象成分散的JobHandler,交由执行器统一管理,执行器负责接受调度请求
并执行对应的JobHandler中的业务逻辑。
目的: “调度”和“执行任务”可以互相解耦,提供系统整体的稳定性和拓展性。
简单:支持通过WEB页面对任务进行CRUD操作,操作简单,一分钟上手;
动态:支持动态修改任务状态、启动/停止任务,以及终止运行中任务,即时生效;
路由策略:执行器集群部署时提供丰富的路由策略,包括:第一个、最后一个、轮询、随机、一致性HASH、最不经常使用、最近最久未使用、故障转移、忙碌转移等;
故障转移:任务路由策略选择”故障转移”情况下,如果执行器集群中某一台机器故障,将会自动Failover切换到一台正常的执行器发送调度请求。
任务超时控制:支持自定义任务超时时间,任务运行超时将会主动中断任务
一致性:“调度中心”通过DB锁保证集群分布式调度的一致性, 一次任务调度只会触发一次执行
邮件报警:任务失败时支持邮件报警,支持配置多邮件地址群发报警邮件;
任务进度监控:支持实时监控任务进度.
下载项目源码并解压,获取“数据库初始化sql脚本”并执行即可。
git地址:https://github.com/xuxueli/xxl-job
码云地址:https://github.com/xuxueli/xxl-job
官方文档地址:https://www.xuxueli.com/xxl-job/
将数据库脚本导入数据库。
application.properties 配置文件详解
### web 服务端口及其项目名称server.port=8080server.servlet.context-path=/xxl-job-admin### actuator 健康检查management.server.servlet.context-path=/actuatORManagement.health.mail.enabled=false### resources 静态资源配置spring.mvc.servlet.load-on-startup=0spring.mvc.static-path-pattern=/static}
package com.xxl.job.actuator;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplicationpublic class XxlJobActuatorApplication { public static void main(String[] args) { SpringApplication.run(XxlJobActuatorApplication.class, args); }}
第四步:启动执行器项目,并向调度中心注册此执行器,执行器名称为:xxl-job-actuator-sample
打开Xxl-job 调度中心管理端。选择执行器管理/新增
在执行器项目:xxl-job-actuator-boot, 添加定时任务DemoJob
package com.xxl.job.actuator.job;import com.xxl.job.core.context.XxlJobHelper;import com.xxl.job.core.handler.annotation.XxlJob;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Component;import java.util.concurrent.TimeUnit;@Componentpublic class DemoJob { private static Logger logger = LoggerFactory.getLogger(DemoJob.class); @XxlJob("demoJobHandler") public void demoJobHandler() throws Exception { XxlJobHelper.log("XXL-JOB, 你好."); for (int i = 0; i < 5; i++) { XxlJobHelper.log("beat at:" + i); TimeUnit.SECONDS.sleep(2); } // default success }}
打开Xxl-job 调度中心管理端。选择任务管理/新增
点击操作按钮,选择启动,等待一段时间后,查看定时任务执行产生的相关日志信息。
新建任务,在高级配置中路由策略包含如下截图模式:
向DemoJob 定时任务,添加如下验证路由策略功能代码片段。
@XxlJob("firstTask") public ReturnT firstTask(){ XxlJobHelper.log("路由策略模式之:第一个"); return ReturnT.SUCCESS; } @XxlJob("lastTask") public ReturnT lastTask(){ XxlJobHelper.log("路由策略模式之:最后一个"); return ReturnT.SUCCESS; } @XxlJob("pollTask") public ReturnT pollTask(){ XxlJobHelper.log("路由策略模式之:轮训"); return ReturnT.SUCCESS; } @XxlJob("hashTask") public ReturnT hashTask(){ XxlJobHelper.log("路由策略模式之:Hash"); return ReturnT.SUCCESS; } @XxlJob("randomTask") public ReturnT randomTask(){ XxlJobHelper.log("路由策略模式之:随机"); return ReturnT.SUCCESS; }
为实现路由策略模式的代码测试,我们需要把xxl-job-actuator-boot执行器项目启动多个实例。多个实例分配如下:
项目名称 | 项目端口 | 内部通信端口 |
XxlJobActuatorApplication | 8081 | 9999 |
XxlJobActuatorApplication-8082 | 8082 | 9998 |
XxlJobActuatorApplication-8082 | 8083 | 9997 |
多个项目实例操作截图:
验证:同一个执行器是否包含多个服务实例。
高级配置中路由策略功能验证:
父子任务,是指父任务执行后自动调用子任务,完成一种类似于链式的调用。在使用是仅仅需要配置子任务的ID即可。
新建任务,在高级配置中子任务ID包含如下截图模式:
向DemoJob 定时任务,添加如下验证父子任务功能代码片段。
@XxlJob("fatherTask") public ReturnT fatherTask(){ logger.info("父任务执行"); return ReturnT.SUCCESS; } @XxlJob("childTask") public ReturnT childTask(){ logger.info("子任务执行"); return ReturnT.SUCCESS; }
打开Xxl-job 调度中心管理端。选择任务管理/新增父子任务。
温馨提示:子任务Id 为xxl-job 为每个任务生成的唯一Id
xxl-job 支持动态的接受参数进行任务调度,调度器可以传指定参数给具体的任务,
具体任务接受调度参数后,进行相应的业务逻辑处理。
向DemoJob 定时任务,添加如下验证动态参数任务功能代码片段。
@XxlJob("parameterTask") public ReturnT parameterTask(){ logger.info("动态参数任务执行"); // 通过XxlJobHelper.getJobParam() 获取参数 String param = XxlJobHelper.getJobParam(); logger.info("参数值为:" + param); return ReturnT.SUCCESS; }
打开Xxl-job 调度中心管理端。选择任务管理/新增动态参数任务。
执行操作,选择仅执行一次,在任务参数中传入当前日期"2022-10-23"
执行器控制台打印相关结果参数:
JobThread-10-1666540416032,10,main]
23:55:59.254 logback [xxl-job, EmbedServer bizThreadPool-1562433863] INFO c.x.job.core.executor.XxlJobExecutor - >>>>>>>>>>> xxl-job regist JobThread success, jobId:10, handler:com.xxl.job.core.handler.impl.MethodJobHandler@118102ee[class com.xxl.job.actuator.job.DemoJob#parameterTask]
23:55:59.255 logback [xxl-job, JobThread-10-1666540559254] INFO com.xxl.job.actuator.job.DemoJob - 动态参数任务执行
23:55:59.256 logback [xxl-job, JobThread-10-1666540559254] INFO com.xxl.job.actuator.job.DemoJob - 参数值为:2022-10-23
分片任务是指会对所有的执行器广播这个任务,所有的执行器都会接受调用请求,
每个执行器可以根据总分片数及当前执行器的缩影进行相关业务处理。
向DemoJob 定时任务,添加如下验证分片任务功能代码片段。
@XxlJob("shardTask") public ReturnT shardTask(){ logger.info("分片任务执行"); // 获取分片信息 //总分片数量 int shardTotal =XxlJobHelper.getShardTotal(); // 当前分片的索引 int shardIndex = XxlJobHelper.getShardIndex(); // 总数据量 int total = 10 *10000; // 分片平均数据量 int size = total / shardTotal // 每个 分片的起始值 int startIndex = shardIndex * size +1; int endIndex = (shardIndex + 1) *size; // 处理最后一个分片 if(shardIndex == (shardTotal -1)){ endIndex = total; } logger.info("总分片数:{}, 当前分片索引为:{}, 处理数据范围为:{}~{}", shardTotal, shardIndex, startIndex, endIndex); return ReturnT.SUCCESS; }
打开Xxl-job 调度中心管理端。选择任务管理/新增分片任务。
选择新增分片任务,高级配置/路由策略,选择分片广播。
执行器控制台打印相关结果参数:
00:20:56.301 logback [xxl-job, EmbedServer bizThreadPool-1472487530] INFO c.x.job.core.executor.XxlJobExecutor - >>>>>>>>>>> xxl-job regist JobThread success, jobId:11, handler:com.xxl.job.core.handler.impl.MethodJobHandler@56681eaf[class com.xxl.job.actuator.job.DemoJob#shardTask]
00:20:56.311 logback [xxl-job, JobThread-11-1666542056301] INFO com.xxl.job.actuator.job.DemoJob - 分片任务执行
00:20:56.311 logback [xxl-job, JobThread-11-1666542056301] INFO com.xxl.job.actuator.job.DemoJob - 总分片数:1, 当前分片索引为:0, 处理数据范围为:1~100000
日志回调是指执行器在执行任务时可以将执行日志传递给调度中心,即使任务没有执行完成,调度中心也可以看到回调的调度日志内容,便于开发者能够更细化的分析任务的执行情况。
向DemoJob 定时任务,添加如下验证任务日志回调功能代码片段。
@XxlJob(value = "logCallBackTask") public ReturnT logCallBackTask() throws InterruptedException { logger.info("任务日志回调执行"); XxlJobHelper.log("当前日志执行至{}行", 143); Thread.sleep(3000); XxlJobHelper.log("当前日志执行至{}行", 145); Thread.sleep(3000); XxlJobHelper.log("当前日志执行至{}行", 147); return ReturnT.SUCCESS; }
打开Xxl-job 调度中心管理端。选择任务管理/新增任务日志回调任务。
根据任务ID,将页面切换至调度日志,点击查询执行日志信息。
xxl-job 支持在任务调用时,第一次调用时先执行指定方法,然后在执行具体的任务,
当执行器停止时会执行指定方法,这就是xxl-job 任务的生命周期。
向DemoJob 定时任务,添加如下验证任务生命周期功能代码片段。
@XxlJob(value = "lifeCycleTask", init = "init", destroy = "destroy") public ReturnT lifeCycleTask(){ logger.info("任务生命周期执行"); return ReturnT.SUCCESS; } public void init(){ logger.info("任务初始化方法"); } public void destroy() { logger.info("任务销毁方法"); }
打开Xxl-job 调度中心管理端。选择任务管理/新增任务生命周期任务。
执行器控制台打印相关结果参数:
00:30:30.055 logback [xxl-job, JobThread-12-1666542630055] INFO com.xxl.job.actuator.job.DemoJob - 任务初始化方法
00:30:30.063 logback [xxl-job, JobThread-12-1666542630055] INFO com.xxl.job.actuator.job.DemoJob - 任务生命周期执行
00:32:03.274 logback [xxl-job, JobThread-12-1666542630055] INFO com.xxl.job.actuator.job.DemoJob - 任务销毁方法
任务以源码方式维护在调度中心,支持通过Web IDE在线更新,实时编译和生效,因此不需要指定JobHandler。
打开Xxl-job 调度中心管理端。选择任务管理/新增GLUE【Java】任务。
开发任务代码:
选中指定任务,点击该任务右侧“GLUE”按钮,将会前往GLUE任务的Web IDE界面,在该界面支持对任务代码进行开发(也可以在IDE中开发完成后,复制粘贴到编辑中)。
版本回溯功能(支持30个版本的版本回溯):在GLUE任务的Web IDE界面,选择右上角下拉框“版本回溯”,会列出该GLUE的更新历史,选择相应版本即可显示该版本代码,保存后GLUE代码即回退到对应的历史版本;
查看任务调度中心,查看GLUE【Java】日志记录
温馨提示:请先确认调度中心服务器是否安装Python环境,我本地调度中心服务器安装的是python3.
新建的任务进行参数配置,运行模式选中 “GLUE模式(Python)”;
开发任务Python代码:
选中指定任务,点击该任务右侧“GLUE”按钮,将会前往GLUE任务的Web IDE界面,在该界面支持对任务代码进行开发(也可以在IDE中开发完成后,复制粘贴到编辑中)
#!/usr/bin/python3import timeimport ioimport syssys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') print ("xxl-job: hello python")print ("脚本位置:", sys.argv[0])print ("任务参数:", sys.argv[1])print ("Good bye!")
查看任务调度中心,查看GLUE【Python】日志记录
新建的任务进行参数配置,运行模式选中 “GLUE模式(Shell)”;
开发任务Shell代码:
选中指定任务,点击该任务右侧“GLUE”按钮,将会前往GLUE任务的Web IDE界面,在该界面支持对任务代码进行开发(也可以在IDE中开发完成后,复制粘贴到编辑中)
#!/bin/bashecho "xxl-job: hello shell"echo "脚本位置:$0"echo "任务参数:$1"echo "分片序号 = $2"echo "分片总数 = $3"echo "Good bye!"exit 0
查看任务调度中心,查看GLUE【Shell】日志记录
产生上述错误的原因:调度中心服务器是windows 系统,Bash 命令是linux 系统,解决此问题的办法:第一种方法:将调度中心的系统切换为Linux 系统, 第二种方法:Windows系统中安装Cygwin.
由于本机缺失环境,此章节不做讲解
由于本机缺失环境,此章节不做讲解
由于本机缺失环境,此章节不做讲解
来源地址:https://blog.csdn.net/zhouzhiwengang/article/details/127463853
--结束END--
本文标题: Xxl-job 一文读懂
本文链接: https://lsjlt.com/news/390218.html(转载时请注明来源链接)
有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341
2024-04-01
2024-04-03
2024-04-03
2024-01-21
2024-01-21
2024-01-21
2024-01-21
2023-12-23
回答
回答
回答
回答
回答
回答
回答
回答
回答
回答
0