定时任务(三)动态配置定时任务

第二篇我们主要介绍了Quartz引入Spring框架的基本配置,这一篇直接开始对定时任务进行动态操控

一、 创建相关实体类

(1)定时任务参数在这里插入图片描述
(2)存储TriggerName 的类,主要用来防重
在这里插入图片描述
(3)定时任务参数类的请求与返回
在这里插入图片描述
(4)调用任务实体类

package com.test.pojo.job;

import java.lang.reflect.Method;

import org.quartz.DisallowConcurrentExecution;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.quartz.QuartzJobBean;

/**
 * 功能说明:任务实体类<br>
 * 注意事项:<br>
 * 系统版本:version 1.0<br>
 * 开发人员:renzw26782<br>
 * 开发时间:2020年8月3日<br>
 */
@DisallowConcurrentExecution
public class QuartzJob extends QuartzJobBean {

	private static final Logger logger = LoggerFactory.getLogger(QuartzJob.class);

	private UseJobDto useJobDto;

	protected void executeInternal(JobExecutionContext context) throws JobExecutionException {

		// 调用余额对账定时服务
		this.execute(useJobDto.getFunctionId());

	}

	public UseJobDto getUseJobDto() {
		return useJobDto;
	}

	public void setUseJobDto(UseJobDto useJobDto) {
		this.useJobDto = useJobDto;
	}

	private void execute(String url) {
		try {
			if (logger.isDebugEnabled()) {
				logger.debug("调用服务[" + url + "]调用开始!");
			}
			String[] arr = url.split("#");
			String useJobMethod = arr[1].trim();
			// 通过反射调用服务
			String useJob = arr[0].trim();
			String useJobName = useJob.substring(0, 1).toUpperCase() + useJob.substring(1);
			@SuppressWarnings("rawtypes")
			Class c = Class.forName("com.test.job." + useJobName);
			Object obj = c.newInstance();
			Method jobMethod = obj.getClass().getMethod(useJobMethod);
			jobMethod.invoke(obj);
			System.out.println("执行成功");
		} catch (Exception e) {
			logger.debug("调用服务[" + url + "]失败", e);
			System.out.println("执行失败");
		} finally {
			if (logger.isDebugEnabled()) {
				logger.debug("调用服务[" + url + "]调用结束!");
			}
		}

	}

}

二、 各处理层逻辑处理

直接上代码吧
(1)controller控制层

package com.test.controller.job;

import java.util.List;

import org.apache.maven.surefire.shade.org.apache.maven.shared.utils.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import com.test.enums.UseJobExecutePathEnum;
import com.test.enums.UseJobStatusEnum;
import com.test.pojo.job.BaseResp;
import com.test.pojo.job.UseJob;
import com.test.pojo.job.UseJobReq;
import com.test.pojo.job.UseJobResp;
import com.test.service.common.CommonService;
import com.test.service.job.UseJobService;

@Controller
@RequestMapping(value = "/useJop")
public class UseJobController {

	@ModelAttribute
	protected void initData(Model model) {
		// 1、定时任务状态
		model.addAttribute("useJobStatusMap", UseJobStatusEnum.toMap());
		model.addAttribute("useJobExecutePathMap", UseJobExecutePathEnum.toMap());
		model.addAttribute("aaa", "111");
	}

	@RequestMapping(value = "/toJobPage")
	public String totoJobPage() {
		return "WEB-INF/job/jobPage";
	}

	@RequestMapping(value = "/addJobPage")
	public String toAddJobPage(UseJob useJob, Model model) {
		if (useJob != null && StringUtils.isNotBlank(useJob.getJobId())) {
			UseJobReq req = new UseJobReq(useJob);
			UseJobResp resp = useJobService.getUseJodInfoById(req);
			model.addAttribute("job", resp.getItem());
		}
		return "WEB-INF/job/addJobPage";
	}

	@Autowired
	private UseJobService useJobService;

	@Autowired
	CommonService commonService;

	@RequestMapping(value = "/queryJobList.do")
	@ResponseBody
	public List<UseJob> queryUseJobList(String jobName, String jobState) {
		UseJob job = new UseJob();

		if (StringUtils.isBlank(jobState)
				|| StringUtils.equals(jobState.trim(), UseJobStatusEnum.PLEASE_SELECT.getCode())) {
			job.setJobState("");
		} else {
			job.setJobState(jobState.trim());
		}
		if (StringUtils.isBlank(jobName)) {
			job.setJobName("");
		} else {
			job.setJobName(jobName.trim());
		}
		UseJobReq useJobReq = new UseJobReq(job);
		UseJobResp useJobResp = null;
		useJobResp = useJobService.queryUseJobList(useJobReq);
		return useJobResp.getItems();

	}

	@RequestMapping("/addUseJob.do")
	@ResponseBody
	public void addUseJob(UseJob useJob) {
		UseJobReq req = new UseJobReq(useJob);
		BaseResp baseResp = null;
		if (StringUtils.isBlank(useJob.getJobId())) {
			Long nowDate = commonService.getNowDate();
			Long nowTime = commonService.getNowTime();
			useJob.setJobId(nowDate.toString() + nowTime.toString());
			baseResp = useJobService.addUseJob(req);
		} else {
			baseResp = useJobService.updateUseJob(req);
		}

	}

	/**
	 * 启动定时任务
	 * 
	 * @param info
	 * @return
	 */
	@RequestMapping("/startJob.do")
	@ResponseBody
	public String startUseJob(String jobId, String functionId) {
		UseJob object = new UseJob();
		object.setFunctionId(functionId);
		object.setJobId(jobId);
		UseJobReq req = new UseJobReq(object);
		BaseResp resp = useJobService.startUseJob(req);
		String message = "启动任务成功";
		if (resp.getErrorNo() != 0) {
			message = "启动任务失败" + resp.getErrorInfo();
		}
		return message;
	}

	/**
	 * 启动定时任务
	 * 
	 * @param info
	 * @return
	 */
	@RequestMapping("/stopJob.do")
	@ResponseBody
	public String stopUseJob(String jobId, String functionId) {
		UseJob object = new UseJob();
		object.setFunctionId(functionId);
		object.setJobId(jobId);
		UseJobReq req = new UseJobReq(object);
		BaseResp resp = useJobService.stopUseJob(req);
		String message = "启动任务成功";
		if (resp.getErrorNo() != 0) {
			message = "启动任务失败" + resp.getErrorInfo();
		}
		return message;
	}

	/**
	 * 删除定时任务
	 * 
	 * @param info
	 * @return
	 */
	@RequestMapping("/delJob.do")
	@ResponseBody
	public String delUseJob(String jobId, String functionId) {
		UseJob object = new UseJob();
		object.setFunctionId(functionId);
		object.setJobId(jobId);
		UseJobReq req = new UseJobReq(object);
		BaseResp resp = useJobService.delUseJob(req);
		String message = "删除任务成功";
		if (resp.getErrorNo() != 0) {
			message = "删除任务失败" + resp.getErrorInfo();
		}
		return message;
	}

}

(2) service服务层

package com.test.service.job;

import java.util.List;

import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.test.logic.job.UseJobLogic;
import com.test.pojo.job.BaseResp;
import com.test.pojo.job.UseJob;
import com.test.pojo.job.UseJobDto;
import com.test.pojo.job.UseJobReq;
import com.test.pojo.job.UseJobResp;
import com.test.utils.BeanUtils;
import com.test.utils.StringUtils;

@Service("jobFactory")
public class UseJobServiceImpl implements UseJobService {
	private Logger logger = Logger.getLogger(UseJobServiceImpl.class);

	@Autowired
	private UseJobLogic useJobLogic;

	@Override
	public UseJobResp queryUseJobList(UseJobReq req) {

		UseJobResp resp = new UseJobResp();
		List<UseJob> ls = null;
		try {
			ls = useJobLogic.queryJobs(req.getObject());
		} catch (Exception e) {
			resp.setErrorId("999999");
			resp.setErrorInfo("查询定时任务失败,请联系相关技术人员!");
		}
		resp.setItems(ls);
		return resp;

	}

	@Override
	public BaseResp addUseJob(UseJobReq req) {
		BaseResp resp = new BaseResp();
		UseJobDto job = new UseJobDto();
		try {
			UseJob object = req.getObject();
			BeanUtils.copyProperties(job, object);
			// 1-参数校验
			resp = validateJobDto(job);
			if (resp.getErrorNo() > 0) {
				return resp;
			}

			// 2-校验任务是否存在

			if (useJobLogic.isExistJob(job)) {
				resp.setErrorNo(999999);
				resp.setErrorInfo("待新增的任务已存在");
				return resp;
			}

			// 2-添加任务
			useJobLogic.addJob(job);
			// 记录操作日志
			try {
			} catch (Exception e) {

			}
		} catch (Exception e) {
			resp.setErrorNo(999999);
			resp.setErrorInfo("新增定时任务失败,请联系相关技术人员!");
			logger.error(resp.getErrorInfo(), e);
		}
		return resp;

	}

	/**
	 * 描述:校验入参<br>
	 * 参数:@param job 参数:@return<br>
	 * 返回值:BaseResp<br>
	 * 
	 * @throws Exception
	 */
	private BaseResp validateJobDto(UseJobDto job) throws Exception {
		BaseResp resp = new BaseResp();
		if (job == null) {
			resp.setErrorNo(999999);
			resp.setErrorInfo("任务对象不能为空");
			return resp;
		}

		if (StringUtils.isBlank(job.getJobId())) {
			resp.setErrorNo(999999);
			resp.setErrorInfo("批次信息不能为空");
			return resp;
		}

		if (StringUtils.isBlank(job.getFunctionId())) {
			resp.setErrorNo(999999);
			resp.setErrorInfo("服务路径不能为空");
			return resp;
		}

		if (StringUtils.isBlank(job.getJobName())) {
			resp.setErrorNo(999999);
			resp.setErrorInfo("任务名称不能为空");
			return resp;
		}

		if (StringUtils.isBlank(job.getExpression())) {
			resp.setErrorNo(999999);
			resp.setErrorInfo("时间间隔表达式不能为空");
			return resp;
		}

		return resp;
	}

	@Override
	public BaseResp startUseJob(UseJobReq req) {

		BaseResp resp = new BaseResp();
		UseJobDto job = new UseJobDto();
		try {
			UseJob object = req.getObject();
			BeanUtils.copyProperties(job, object);

			// 1-校验任务是否存在
			if (!useJobLogic.isExistJob(job)) {
				resp.setErrorNo(999999);
				resp.setErrorInfo("该任务不存在");
				return resp;
			}

			if (!useJobLogic.isPausedJob(job)) {
				return resp;
			}
			useJobLogic.startUseJob(job);

		} catch (Exception e) {
			resp.setErrorNo(999999);
			resp.setErrorInfo("启动定时任务失败,请联系相关技术人员!");
			logger.error(resp.getErrorInfo(), e);
		}
		return resp;

	}

	@Override
	public BaseResp stopUseJob(UseJobReq req) {
		BaseResp resp = new BaseResp();
		UseJobDto job = new UseJobDto();
		try {
			UseJob object = req.getObject();
			BeanUtils.copyProperties(job, object);

			// 1-校验任务是否存在
			if (!useJobLogic.isExistJob(job)) {
				resp.setErrorNo(999999);
				resp.setErrorInfo("该任务不存在");
				return resp;
			}

			if (useJobLogic.isPausedJob(job)) {
				return resp;
			}
			useJobLogic.stopUseJob(job);
			// 记录操作日志

		} catch (Exception e) {
			resp.setErrorNo(999999);
			resp.setErrorInfo("暂停定时任务失败,请联系相关技术人员!");
			logger.error(resp.getErrorInfo(), e);
		}
		return resp;
	}

	@Override
	public BaseResp delUseJob(UseJobReq req) {
		BaseResp resp = new BaseResp();
		UseJobDto job = new UseJobDto();
		try {
			UseJob object = req.getObject();
			BeanUtils.copyProperties(job, object);
			// 1-校验任务是否存在
			if (!useJobLogic.isExistJob(job)) {
				resp.setErrorNo(999999);
				resp.setErrorInfo("所删除的任务不存在");
				return resp;
			}

			// 2-校验当前任务状态是否是暂停状态
			if (!useJobLogic.isPausedJob(job)) {
				resp.setErrorNo(999999);
				resp.setErrorInfo("所删除的任务必须处于暂停状态");
				return resp;
			}

			// 3-删除任务
			useJobLogic.deleteJob(job);
			// 记录操作日志

		} catch (Exception e) {
			resp.setErrorNo(999999);
			resp.setErrorInfo("删除定时任务失败,请联系相关技术人员!");
			logger.error(resp.getErrorInfo(), e);
		}
		return resp;
	}

	@Override
	public UseJobResp getUseJodInfoById(UseJobReq req) {

		UseJobResp resp = new UseJobResp();
		UseJobDto job = new UseJobDto();
		try {
			UseJob object = req.getObject();
			BeanUtils.copyProperties(job, object);
			// 1-校验任务是否存在
			if (!useJobLogic.isExistJob(job)) {
				resp.setErrorNo(999999);
				resp.setErrorInfo("所查看的任务不存在");
				return resp;
			}
			// 2-添加任务
			UseJob item = useJobLogic.getUseJodInfo(job);
			resp.setItem(item);
		} catch (Exception e) {
			resp.setErrorNo(999999);
			resp.setErrorInfo("查看定时任务失败,请联系相关技术人员!");
			logger.error(resp.getErrorInfo(), e);
		}

		return resp;

	}

	@Override
	public BaseResp updateUseJob(UseJobReq req) {
		BaseResp resp = new BaseResp();
		UseJobDto job = new UseJobDto();
		try {
			UseJob object = req.getObject();
			BeanUtils.copyProperties(job, object);
			// 1-参数校验
			resp = validateJobDto(job);
			if (resp.getErrorNo() > 0) {
				return resp;
			}

			// 2-校验任务是否存在
			if (!useJobLogic.isExistJob(job)) {
				resp.setErrorNo(999999);
				resp.setErrorInfo("所修改的任务不存在");
				return resp;
			}

			// 3-校验当前任务状态是否是暂停状态
			if (!useJobLogic.isPausedJob(job)) {
				resp.setErrorNo(999999);
				resp.setErrorInfo("所修改的任务必须处于暂停状态");
				return resp;
			}

			// 4-修改任务
			useJobLogic.updateJob(job);
			// 记录操作日志

		} catch (Exception e) {
			resp.setErrorNo(999999);
			resp.setErrorInfo("新增定时任务失败,请联系相关技术人员!");
			logger.error(resp.getErrorInfo(), e);
		}

		return resp;
	}

}

(3)logic逻辑层

package com.test.logic.job;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.Predicate;
import org.quartz.CronScheduleBuilder;
import org.quartz.CronTrigger;
import org.quartz.JobBuilder;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.Trigger.TriggerState;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerKey;
import org.quartz.impl.matchers.GroupMatcher;
import org.quartz.utils.Key;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.stereotype.Component;

import com.chinabond.core.exception.BusinessException;
import com.test.pojo.job.QuartzJob;
import com.test.pojo.job.UseJob;
import com.test.pojo.job.UseJobDto;
import com.test.utils.StringUtils;

@Component
public class UseJobLogic {

	private static final String GROUP_NAME_DEFAULT = Key.DEFAULT_GROUP;
	@Autowired(required = false)
	private SchedulerFactoryBean schedulerFactory;

	/**
	 * 获取线程调度器
	 * 
	 * @return
	 */
	private Scheduler getScheduler() {

		Scheduler scheduler = schedulerFactory.getScheduler();
		if (scheduler == null) {
			throw new BusinessException("999999", "定时任务调度器为空,请确认是否开启定时任务功能!");
		}
		return scheduler;
	}

	public List<UseJob> queryJobs(UseJob useJob) throws SchedulerException {
		List<UseJob> ls = new ArrayList<UseJob>();

		Set<TriggerKey> triggerKeys = getScheduler()
				.getTriggerKeys(GroupMatcher.triggerGroupEquals(GROUP_NAME_DEFAULT));
		for (TriggerKey triggerKey : triggerKeys) {
			Trigger trigger = getScheduler().getTrigger(triggerKey);
			TriggerState triggerState = getScheduler().getTriggerState(triggerKey);

			UseJobDto useJobDto = (UseJobDto) getScheduler().getJobDetail(trigger.getJobKey()).getJobDataMap()
					.get("useJobDto");
			if (useJobDto == null) {
				// 只处理新增的
				continue;
			}
			useJobDto.setJobState(triggerState.toString());
			useJobDto.setNextFireTime(trigger.getNextFireTime());
			ls.add(useJobDto);

		}

		// 条件查询
		CollectionUtils.filter(ls, new Predicate() {
			@Override
			public boolean evaluate(Object object) {
				if (object == null || !(object instanceof UseJob)) {
					return false;
				}
				UseJob temp = (UseJob) object;

				if (StringUtils.isNotBlank(useJob.getJobName()) && !temp.getJobName().contains(useJob.getJobName())) {
					return false;
				}

				if (StringUtils.isNotBlank(useJob.getJobState())
						&& !StringUtils.equals(useJob.getJobState(), temp.getJobState())) {
					return false;
				}

				return true;
			}
		});
		return ls;
	}

	/**
	 * 判断任务是否存在
	 * 
	 * @param job
	 * @return
	 * @throws SchedulerException
	 */
	public boolean isExistJob(UseJobDto job) throws SchedulerException {
		TriggerKey triggerKey = TriggerKey.triggerKey(job.getTriggerKey(), GROUP_NAME_DEFAULT);
		CronTrigger trigger = (CronTrigger) getScheduler().getTrigger(triggerKey);
		return trigger != null;
	}

	/**
	 * <pre>
	 * addJob(这里用一句话描述这个方法的作用)   
	 * 创建人:任智伟
	 * 创建时间:2020年9月18日 上午9:51:36    
	 * 修改人:任智伟      
	 * 修改时间:2020年9月18日 上午9:51:36    
	 * 修改备注: 
	 * &#64;param job
	 * &#64;throws SchedulerException
	 * </pre>
	 */
	public void addJob(UseJobDto job) throws SchedulerException {
		TriggerKey triggerKey = TriggerKey.triggerKey(job.getTriggerKey(), GROUP_NAME_DEFAULT);
		CronTrigger trigger = (CronTrigger) getScheduler().getTrigger(triggerKey);
		if (trigger == null) {
			/** Trigger不存在,则新建 */

			JobDataMap jobDataMap = new JobDataMap();
			job.setJobState(TriggerState.NORMAL.toString());
			jobDataMap.put("useJobDto", job);

			// 新建任务
			JobKey jobKey = new JobKey(job.getJobDetailKey(), GROUP_NAME_DEFAULT);
			JobDetail jobDetail = JobBuilder.newJob(QuartzJob.class).setJobData(jobDataMap).withIdentity(jobKey)
					.requestRecovery(true).storeDurably(true).build();

			// 表达式调度构建器
			CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(job.getExpression());

			// 构建触发器
			trigger = TriggerBuilder.newTrigger().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();

			// 装配任务
			System.out.println("暂停ing-------------");
			getScheduler().scheduleJob(jobDetail, trigger);
			getScheduler().pauseTrigger(triggerKey);

		}

	}

	/**
	 * <pre>
	 * isPausedJob(判断任务状态是否已执行)   
	 * 创建人:任智伟
	 * 创建时间:2020年9月24日 上午10:31:01    
	 * 修改人:任智伟      
	 * 修改时间:2020年9月24日 上午10:31:01    
	 * 修改备注: 
	 * &#64;param job  定时任务
	 * &#64;return 暂停 :true ,其他 :false
	 * </pre>
	 * 
	 * @throws SchedulerException
	 */
	public boolean isPausedJob(UseJobDto job) throws SchedulerException {
		TriggerState triggerState = getScheduler().getTriggerState(TriggerKey.triggerKey(job.getTriggerKey()));
		return triggerState.toString().equals(TriggerState.PAUSED.toString());
	}

	/**
	 * <pre>
	 * startUseJob(启动定时任务)   
	 * 创建人:任智伟
	 * 创建时间:2020年9月24日 上午10:36:42    
	 * 修改人:任智伟      
	 * 修改时间:2020年9月24日 上午10:36:42    
	 * 修改备注: 
	 * &#64;param job 定时任务
	 * </pre>
	 * 
	 * @throws SchedulerException
	 */
	public void startUseJob(UseJobDto job) throws SchedulerException {
		getScheduler().resumeTrigger(TriggerKey.triggerKey(job.getTriggerKey()));
		getScheduler().resumeJob(JobKey.jobKey(job.getJobDetailKey()));
	}

	/**
	 * <pre>
	 * stopUseJob(停止定时任务)   
	 * 创建人:任智伟
	 * 创建时间:2020年9月24日 上午11:10:47    
	 * 修改人:任智伟      
	 * 修改时间:2020年9月24日 上午11:10:47    
	 * 修改备注: 
	 * &#64;param job
	 * </pre>
	 * 
	 * @throws SchedulerException
	 */
	public void stopUseJob(UseJobDto job) throws SchedulerException {

		getScheduler().pauseJob(JobKey.jobKey(job.getJobDetailKey()));
		getScheduler().pauseTrigger(TriggerKey.triggerKey(job.getTriggerKey()));

	}

	/**
	 * <pre>
	 * deleteJob(删除定时任务)   
	 * 创建人:任智伟
	 * 创建时间:2020年9月24日 下午4:54:57    
	 * 修改人:任智伟      
	 * 修改时间:2020年9月24日 下午4:54:57    
	 * 修改备注: 
	 * &#64;param job
	 * &#64;throws SchedulerException
	 * </pre>
	 */
	public void deleteJob(UseJobDto job) throws SchedulerException {
		if (getScheduler().checkExists(JobKey.jobKey(job.getJobDetailKey()))) {
			getScheduler().deleteJob(JobKey.jobKey(job.getJobDetailKey()));
		}

	}

	/**
	 * <pre>
	 * getUseJodInfo(修改回显查询数据)   
	 * 创建人:任智伟
	 * 创建时间:2020年9月29日 下午2:49:09    
	 * 修改人:任智伟      
	 * 修改时间:2020年9月29日 下午2:49:09    
	 * 修改备注: 
	 * &#64;param job
	 * &#64;return
	 * </pre>
	 * 
	 * @throws SchedulerException
	 */
	public UseJob getUseJodInfo(UseJobDto job) throws SchedulerException {
		TriggerKey triggerKey = TriggerKey.triggerKey(job.getTriggerKey());
		Trigger trigger = getScheduler().getTrigger(triggerKey);
		TriggerState triggerState = getScheduler().getTriggerState(triggerKey);

		UseJobDto jobDto = (UseJobDto) getScheduler().getJobDetail(JobKey.jobKey(job.getJobDetailKey())).getJobDataMap()
				.get("useJobDto");
		jobDto.setJobState(triggerState.toString());
		jobDto.setNextFireTime(trigger.getNextFireTime());
		return jobDto;
	}

	/**
	 * <pre>
	 * updateJob(修改定时任务)   
	 * 创建人:任智伟
	 * 创建时间:2020年10月12日 上午9:44:29    
	 * 修改人:任智伟      
	 * 修改时间:2020年10月12日 上午9:44:29    
	 * 修改备注: 
	 * &#64;param job
	 * </pre>
	 * 
	 * @throws SchedulerException
	 */
	public void updateJob(UseJobDto job) throws SchedulerException {
		deleteJob(job);
		addJob(job);
	}

}

三、完成后效果

在这里插入图片描述
在这里插入图片描述
关联时间表达式的js代码逻辑较为复杂,这里就不展示了,有需要的小伙伴可以私我

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值