SFTP-SpringBean-EasyExcel-tools

1.spring注入

1.静态注入(SpringContextHolder)

//日志(slf4j)
private static final Logger logger = LoggerFactory.getLogger(EasyExcelBodyListener.class);
/*
	有一个工具类, 需要使用到Redis, 把这个工具类放入到Spring中(@Component), 只要加上注解就会报错
	所以使用ApplicationContextAware来进行注入
*/
private static RedisCache redisCache = SpringContextHolder.getBean(RedisCache.class);

@Component
public class SpringContextHolder implements ApplicationContextAware, DisposableBean {
    private static Logger logger = LoggerFactory.getLogger(SpringContextHolder.class);

    private static ApplicationContext applicationContext = null;

    /**
     * 取得存储在静态变量中的ApplicationContext.
     */
    public static ApplicationContext getApplicationContext() {
        assertContextInjected();
        return applicationContext;
    }

    /**
     * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
     */
    public static <T> T getBean(String name) {
        assertContextInjected();
        return (T) applicationContext.getBean(name);
    }

    /**
     * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
     */
    public static <T> T getBean(Class<T> requiredType) {
        assertContextInjected();
        return applicationContext.getBean(requiredType);
    }

    /**
     * 检查ApplicationContext不为空.
     */
    private static void assertContextInjected() {
        if (applicationContext == null) {
            throw new IllegalStateException("applicaitonContext属性未注入, 请在applicationContext" +
                    ".xml中定义SpringContextHolder或在SpringBoot启动类中注册SpringContextHolder.");
        }
    }

    /**
     * 清除SpringContextHolder中的ApplicationContext为Null.
     */
    public static void clearHolder() {
        logger.debug("清除SpringContextHolder中的ApplicationContext:"
                + applicationContext);
        applicationContext = null;
    }

    @Override
    public void destroy() throws Exception {
        SpringContextHolder.clearHolder();
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        if (SpringContextHolder.applicationContext != null) {
            logger.warn("SpringContextHolder中的ApplicationContext被覆盖, 原有ApplicationContext为:" + SpringContextHolder.applicationContext);
        }
        SpringContextHolder.applicationContext = applicationContext;
    }
}

2.构造器注入

//不能被spring管理,要每次读取都要new,然后里面用到spring可以构造方法传进去
    private DynamicFileContent dynamicFileContent;
    
//初始化
    public EasyExcelBodyListener(DynamicFileContent dynamicFileContent) {
        this.dynamicFileContent = dynamicFileContent;
    }

3.若依工具(BeanFactoryPostProcessor)

package com.ruoyi.common.utils.spring;

import org.springframework.aop.framework.AopContext;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.stereotype.Component;

/**
 * spring工具类 方便在非spring管理环境中获取bean
 * 
 * @author ruoyi
 */
@Component
public final class SpringUtils implements BeanFactoryPostProcessor
{
    /** Spring应用上下文环境 */
    private static ConfigurableListableBeanFactory beanFactory;

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException
    {
        SpringUtils.beanFactory = beanFactory;
    }

    /**
     * 获取对象
     *
     * @param name
     * @return Object 一个以所给名字注册的bean的实例
     * @throws org.springframework.beans.BeansException
     *
     */
    @SuppressWarnings("unchecked")
    public static <T> T getBean(String name) throws BeansException
    {
        return (T) beanFactory.getBean(name);
    }

    /**
     * 获取类型为requiredType的对象
     *
     * @param clz
     * @return
     * @throws org.springframework.beans.BeansException
     *
     */
    public static <T> T getBean(Class<T> clz) throws BeansException
    {
        T result = (T) beanFactory.getBean(clz);
        return result;
    }

    /**
     * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true
     *
     * @param name
     * @return boolean
     */
    public static boolean containsBean(String name)
    {
        return beanFactory.containsBean(name);
    }

    /**
     * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException)
     *
     * @param name
     * @return boolean
     * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
     *
     */
    public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException
    {
        return beanFactory.isSingleton(name);
    }

    /**
     * @param name
     * @return Class 注册对象的类型
     * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
     *
     */
    public static Class<?> getType(String name) throws NoSuchBeanDefinitionException
    {
        return beanFactory.getType(name);
    }

    /**
     * 如果给定的bean名字在bean定义中有别名,则返回这些别名
     *
     * @param name
     * @return
     * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
     *
     */
    public static String[] getAliases(String name) throws NoSuchBeanDefinitionException
    {
        return beanFactory.getAliases(name);
    }

    /**
     * 获取aop代理对象
     * 
     * @param invoker
     * @return
     */
    @SuppressWarnings("unchecked")
    public static <T> T getAopProxy(T invoker)
    {
        return (T) AopContext.currentProxy();
    }
}

2.EasyExcel(使用Map来接收值, 用户自定义的Excel表头)

public class EasyExcelBodyListener extends AnalysisEventListener {

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

    //定义每多少条数据进行数据库保存
    private static final int BATCH_COUNT = 20;
    //用list集合保存解析到的结果
    private List<Map<Integer, Map<Integer, String>>> list;

    private PageData pd;

    //不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
    private DynamicFileContent dynamicFileContent;

    //初始化
    public EasyExcelBodyListener(DynamicFileContent dynamicFileContent, PageData pd) {
        this.dynamicFileContent = dynamicFileContent;
        this.pd = pd;
        this.list = new ArrayList<>();
    }
    
    @Override
    public void invokeHeadMap(Map headMap, AnalysisContext context) {
        logger.info("解析到一条头数据:{}, ", JSON.toJSONString(headMap));
        //保存头数据到数据库
        pd.put("head", JSON.toJSONString(headMap));
        saveHeadData(pd);
    }

    @Override
    public void invoke(Object data, AnalysisContext context) {
        logger.info("解析到一条数据:{}, currentRowIndex: {}----", JSON.toJSONString(data), context.readRowHolder().getRowIndex());
        Map<Integer, Map<Integer, String>> map = new HashMap<>();
        map.put(context.readRowHolder().getRowIndex(), (Map<Integer, String>) data);
        list.add(map);
        // 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
        if (list.size() >= BATCH_COUNT) {
            //保存动态文件内容
            saveBodyData(pd);
            // 存储完成清理 list
            list.clear();
        }
    }

    /**
     * 解析到最后会进入这个方法,需要重写这个doAfterAllAnalysed方法,然后里面调用自己定义好保存方法
     * @param context
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        // 这里也要保存数据,确保最后遗留的数据也存储到数据库
        saveBodyData(pd);
        logger.info("所有数据解析完成!");
    }

    /**
     * 加上存储数据库
     */
    private void saveBodyData(PageData pd) {
        logger.info("{}条数据,开始存储数据库!", list.size());
//        dynamicFileContent.saveBodyData(pd);
    }
}

3.SFTP工具类

public class MySFTP {

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

//    private String username = "root";//登录sftp的用户名
//    private String password = "123456o-0";//登录sftp的密码
//    private String host = "192.168.0.201";//主机ip
//    private Integer port = 22;//主机端口

    Map<String, Object> map = null;

    /**
     * 上传文件
     * @param directory 上传的目录-相对于SFPT设置的用户访问目录,
                        为空则在SFTP设置的根目录进行创建文件(除设置了服务器全磁盘访问)
     * @param uploadFile 要上传的文件全路径
     */
    public void upload(String directory, String uploadFile) {
        try {
            //建立连接
            map = getConnect(username, password, host, port);
            ChannelSftp sftp = (ChannelSftp) map.get("sftp");
            try {
                sftp.cd(directory); //进入目录
            } catch (SftpException sException) {
                if (sftp.SSH_FX_NO_SUCH_FILE == sException.id) { //指定上传路径不存在
                    sftp.mkdir(directory);//创建目录
                    sftp.cd(directory);  //进入目录
                }
            }
            File file = new File(uploadFile);
            InputStream in = new FileInputStream(file);
            sftp.put(in, file.getName());
            in.close();
            logger.info("备份成功");
        } catch (Exception e) {
            logger.info("备份失败");
            e.printStackTrace();
        } finally {
            if (map != null) {
                try {
                    disConn((HashMap<String, Object>) map);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }


    /**
     * 下载文件
     * @param directory 下载目录 根据SFTP设置的根目录来进行传入
     * @param downloadFile  下载的文件
     * @param saveFile  存在本地的路径
     */
    public void download(String directory, String downloadFile,String saveFile) throws Exception {
        //建立连接
        map = getConnect(username, password, host, port);
        ChannelSftp sftp = (ChannelSftp) map.get("sftp");
        try {
            sftp.cd(directory); //进入目录
            File file = new File(saveFile);
            boolean bFile;
            bFile = false;
            bFile = file.exists();
            if (!bFile) {
                bFile = file.mkdirs();//创建目录
            }
            OutputStream out=new FileOutputStream(new File(saveFile,downloadFile));
            sftp.get(downloadFile, out);
            out.flush();
            out.close();
            logger.info("下载成功");
        } catch (Exception e) {
            logger.info("下载失败");
            throw new Exception(e.getMessage(),e);
        } finally {
            disConn((HashMap<String, Object>) map);
        }
    }


    /**
     * 连接服务器
     */
    public Map<String, Object> getConnect(String username, String password, String host, int port) throws Exception {
        Session session = null;
        Channel channel = null;
        ChannelSftp sftp = null;// sftp操作类
        JSch jsch = new JSch();//创建客户端
        session = jsch.getSession(username, host, port);//连接属性
        session.setPassword(password);//输入密码
        //设置
        Properties config = new Properties();
        config.put("StrictHostKeyChecking", "no"); // 不验证 HostKey
        //激活设置
        session.setConfig(config);
        //登录
        try {
            session.connect();
            logger.info("连接ftp成功");
        } catch (Exception e) {
            if (session.isConnected())
                session.disconnect();
            logger.error("连接服务器失败,请检查主机[" + host + "],端口[" + port
                    + "],用户名[" + username + "],密码[" + password
                    + "]是否正确,以上信息正确的情况下请检查网络连接是否正常或者请求被防火墙拒绝.");
        }
        channel = session.openChannel("sftp");
        //打开通道
        try {
            channel.connect();
        } catch (Exception e) {
            if (channel.isConnected())
                channel.disconnect();
            logger.error("连接服务器失败,请检查主机[" + host + "],端口[" + port
                    + "],用户名[" + username + "],密码是否正确,以上信息正确的情况下请检查网络连接是否正常或者请求被防火墙拒绝.");
        }
        //保存属性
        sftp = (ChannelSftp) channel;
        HashMap<String, Object> map = new HashMap<>();
        map.put("channel", channel);
        map.put("session", session);
        map.put("sftp", sftp);
        return map;
    }


    /**
     * 断开连接
     */
    public void disConn(HashMap<String, Object> map) throws Exception {
        Session session = (Session) map.get("session");
        Channel channel = (Channel) map.get("channel");
        ChannelSftp sftp = (ChannelSftp) map.get("sftp");
        if (null != sftp) {
            sftp.disconnect();
            sftp.exit();
        }
        if (null != channel) {
            channel.disconnect();
        }
        if (null != session) {
            session.disconnect();
        }
    }

}

1.定时

@Scheduled(cron = "0 0 19 * * ?")

4.AOP日志(若依)

@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WorkBenchBacklog {
    /**
     * 功能
     */
    public BusinessTypes businessType() default BusinessTypes.OTHER;


    /**
     * 是否保存请求的参数
     */
    public boolean isSaveRequestData() default true;
}

public enum BusinessTypes{
    /**
     * 其它
     */
    OTHER,

    /**
     * 新增
     */
    INSERT,

    /**
     * 修改
     */
    UPDATE,

    /**
     * 删除
     */
    DELETE,

}
package com.ruoyi.framework.aspectj;

import com.ruoyi.common.utils.PageData;
import com.ruoyi.framework.aspectj.lang.annotation.WorkBenchBacklog;
import com.ruoyi.framework.manager.AsyncManager;
import com.ruoyi.framework.manager.factory.AsyncFactory;
import com.ruoyi.project.module.workbench.domain.WorkbenchBacklogList;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Map;

/**
 * Create on 2021/7/9.
 *
 * @author Jaime
 * @Description:
 */
@Aspect
@Component
public class WorkBenchBacklogAspect {
    private static final Logger log = LoggerFactory.getLogger(WorkBenchBacklogAspect.class);

    // 配置织入点
    @Pointcut("@annotation(com.ruoyi.framework.aspectj.lang.annotation.WorkBenchBacklog)")
    public void workBenchBacklogPointCut()
    {
    }

    /**
     * 处理完请求后执行
     *
     * @param joinPoint 切点
     */
    @AfterReturning(pointcut = "workBenchBacklogPointCut()", returning = "jsonResult")
    public void doAfterReturning(JoinPoint joinPoint, Object jsonResult){
        //操作工作台待办处理
        workBenchTodo(joinPoint, null, jsonResult);
    }

    protected void workBenchTodo(final JoinPoint joinPoint, final Exception e, Object jsonResult){
        try{
            // 获得注解
            WorkBenchBacklog annotationBacklog = getAnnotationBacklog(joinPoint);

            if (annotationBacklog == null)
            {
                return;
            }
            PageData pageData = new PageData();
            pageData.putAll((Map)jsonResult);
            PageData pa = new PageData();
            PageData d = (PageData)pageData.get("data");
            if(d != null && d.size() != 0) {
                    pa.putAll(d);
            }else {
                return;
            }

            //声明实体
            WorkbenchBacklogList workbenchBacklogList = new WorkbenchBacklogList()
                    .setid(Integer.valueOf(pa.get("id") + ""))
                    .setName(pa.get("name") + "")
                    .setAnswer(pa.get("answer") + "")
                    .setType(pa.get("type") + "")
                    .setCaption(pa.get("caption") + "")
                    .setTopic(pa.get("topic") + "")
                    .setPid(Integer.valueOf(pa.get("pid") + ""))
                    .setBid(Integer.valueOf(pa.get("bid") + ""))
                    .setDynamictype(pa.get("dynamictype") + "")
                    .setCreatetime(pa.get("createtime") + "")
                    .setShowwho(pa.get("showwho") + "")
                    .setParentid(Integer.valueOf(pa.get("parentid") + ""))
                    .setIsread(pa.get("isread") + "")
                    .setWhichone(pa.get("whichone") + "")
                    .setAppend(pa.get("append") + "")
                    .setisshow(pa.get("isshow") + "")
                    .setStatus(pa.get("status") + "");

            // 保存数据库
            AsyncManager.me().execute(AsyncFactory.backlogOperd(workbenchBacklogList));

        }catch (Exception exp){
            // 记录本地异常日志
            log.error("==前置通知异常==");
            log.error("异常信息:{}", exp.getMessage());
            exp.printStackTrace();
        }
    }


    /**
     * 是否存在注解,如果存在就获取
     */
    private WorkBenchBacklog getAnnotationBacklog(JoinPoint joinPoint) throws Exception
    {
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();

        if (method != null)
        {
            return method.getAnnotation(WorkBenchBacklog.class);
        }
        return null;
    }
}

public class AsyncManager
{
    /**
     * 操作延迟10毫秒
     */
    private final int OPERATE_DELAY_TIME = 10;

    /**
     * 异步操作任务调度线程池
     */
    private ScheduledExecutorService executor = SpringUtils.getBean("scheduledExecutorService");

    /**
     * 单例模式
     */
    private AsyncManager(){}

    private static AsyncManager me = new AsyncManager();

    public static AsyncManager me()
    {
        return me;
    }

    /**
     * 执行任务
     * 
     * @param task 任务
     */
    public void execute(TimerTask task)
    {
        executor.schedule(task, OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS);
    }

    /**
     * 停止任务线程池
     */
    public void shutdown()
    {
        Threads.shutdownAndAwaitTermination(executor);
    }
}

package com.ruoyi.framework.manager.factory;

import java.util.TimerTask;

import com.ruoyi.project.module.workbench.domain.WorkbenchBacklogList;
import com.ruoyi.project.module.workbench.service.IWorkbenchBacklogListService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.utils.LogUtils;
import com.ruoyi.common.utils.ServletUtils;
import com.ruoyi.common.utils.ip.AddressUtils;
import com.ruoyi.common.utils.ip.IpUtils;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.project.monitor.domain.SysDynamicLog;
import com.ruoyi.project.monitor.domain.SysLogininfor;
import com.ruoyi.project.monitor.domain.SysOperLog;
import com.ruoyi.project.monitor.service.ISysLogininforService;
import com.ruoyi.project.monitor.service.ISysOperLogService;
import eu.bitwalker.useragentutils.UserAgent;

/**
 * 异步工厂(产生任务用)
 * 
 * @author ruoyi
 */
public class AsyncFactory
{
    private static final Logger sys_user_logger = LoggerFactory.getLogger("sys-user");

    /**
     * 记录登陆信息
     * 
     * @param username 用户名
     * @param status 状态
     * @param message 消息
     * @param args 列表
     * @return 任务task
     */
    public static TimerTask recordLogininfor(final String username, final String status, final String message,
            final Object... args)
    {
        final UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
        final String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
        return new TimerTask()
        {
            @Override
            public void run()
            {
                String address = AddressUtils.getRealAddressByIP(ip);
                StringBuilder s = new StringBuilder();
                s.append(LogUtils.getBlock(ip));
                s.append(address);
                s.append(LogUtils.getBlock(username));
                s.append(LogUtils.getBlock(status));
                s.append(LogUtils.getBlock(message));
                // 打印信息到日志
                sys_user_logger.info(s.toString(), args);
                // 获取客户端操作系统
                String os = userAgent.getOperatingSystem().getName();
                // 获取客户端浏览器
                String browser = userAgent.getBrowser().getName();
                // 封装对象
                SysLogininfor logininfor = new SysLogininfor();
                logininfor.setUserName(username);
                logininfor.setIpaddr(ip);
                logininfor.setLoginLocation(address);
                logininfor.setBrowser(browser);
                logininfor.setOs(os);
                logininfor.setMsg(message);
                // 日志状态
                if (Constants.LOGIN_SUCCESS.equals(status) || Constants.LOGOUT.equals(status))
                {
                    logininfor.setStatus(Constants.SUCCESS);
                }
                else if (Constants.LOGIN_FAIL.equals(status))
                {
                    logininfor.setStatus(Constants.FAIL);
                }
                // 插入数据
                SpringUtils.getBean(ISysLogininforService.class).insertLogininfor(logininfor);
            }
        };
    }

    /**
     * 操作日志记录
     * 
     * @param operLog 操作日志信息
     * @return 任务task
     */
    public static TimerTask recordOper(final SysOperLog operLog)
    {
        return new TimerTask()
        {
            @Override
            public void run()
            {
                // 远程查询操作地点
                operLog.setOperLocation(AddressUtils.getRealAddressByIP(operLog.getOperIp()));
                SpringUtils.getBean(ISysOperLogService.class).insertOperlog(operLog);
            }
        };
    }
    
    /**
     * 动态日志记录
     * 
     * @param dLog 动态日志信息
     * @return 任务task
     */
    public static TimerTask recordOperd(final SysDynamicLog dLog)
    {
        return new TimerTask()
        {
            @Override
            public void run()
            {
                SpringUtils.getBean(ISysOperLogService.class).insertOperlogd(dLog);
            }
        };
    }

    /**
     * 待办记录
     *
     * @param backlog 待办信息
     * @return 任务task
     */
    public static TimerTask backlogOperd(final WorkbenchBacklogList backlog)
    {
        return new TimerTask()
        {
            @Override
            public void run()
            {
                SpringUtils.getBean(IWorkbenchBacklogListService.class).insertWorkbenchBacklogList(backlog);
            }
        };
    }
}

//线程工具类
package com.ruoyi.common.utils;

import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 线程相关工具类.
 * 
 * @author ruoyi
 */
public class Threads
{
    private static final Logger logger = LoggerFactory.getLogger(Threads.class);

    /**
     * sleep等待,单位为毫秒
     */
    public static void sleep(long milliseconds)
    {
        try
        {
            Thread.sleep(milliseconds);
        }
        catch (InterruptedException e)
        {
            return;
        }
    }

    /**
     * 停止线程池
     * 先使用shutdown, 停止接收新任务并尝试完成所有已存在任务.
     * 如果超时, 则调用shutdownNow, 取消在workQueue中Pending的任务,并中断所有阻塞函数.
     * 如果仍人超時,則強制退出.
     * 另对在shutdown时线程本身被调用中断做了处理.
     */
    public static void shutdownAndAwaitTermination(ExecutorService pool)
    {
        if (pool != null && !pool.isShutdown())
        {
            pool.shutdown();
            try
            {
                if (!pool.awaitTermination(120, TimeUnit.SECONDS))
                {
                    pool.shutdownNow();
                    if (!pool.awaitTermination(120, TimeUnit.SECONDS))
                    {
                        logger.info("Pool did not terminate");
                    }
                }
            }
            catch (InterruptedException ie)
            {
                pool.shutdownNow();
                Thread.currentThread().interrupt();
            }
        }
    }

    /**
     * 打印线程异常信息
     */
    public static void printException(Runnable r, Throwable t)
    {
        if (t == null && r instanceof Future<?>)
        {
            try
            {
                Future<?> future = (Future<?>) r;
                if (future.isDone())
                {
                    future.get();
                }
            }
            catch (CancellationException ce)
            {
                t = ce;
            }
            catch (ExecutionException ee)
            {
                t = ee.getCause();
            }
            catch (InterruptedException ie)
            {
                Thread.currentThread().interrupt();
            }
        }
        if (t != null)
        {
            logger.error(t.getMessage(), t);
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值