对象与xml互相转换、通过xml报文形式发送请求

本文提供两种方式:
方式一:解析固定格式xml报文更方便,但未找到使用泛型时,解析的方式;
方式二:可根据不同接口,报文不同,改变实体(更复杂,功能更强大);
方式三:可使用泛型;

方法一

所需依赖:

    <dependency>
        <groupId>dom4j</groupId>
        <artifactId>dom4j</artifactId>
        <version>1.6.1</version>
    </dependency>

    <!--解析xml报文-->
    <dependency>
        <groupId>com.thoughtworks.xstream</groupId>
        <artifactId>xstream</artifactId>
        <version>1.4.10</version>
    </dependency>

发送xml报文请求:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;


public class HttpPostXml {

    //获取数据超时时间60秒
    private final int OVERTIME = 60000;

    /*@Value("${sysHead.userLang}")
    private String userLang;*/

    /**
     * 创建连接,发送xml请求报文,获取响应报文
     *
     * @param urlStr  请求路径
     * @param bodyStr body标签数据字符串
     * @return
     */
    public String httpRequestAndTransData(InputSysHead sysHead, InputAppHead appHead, String urlStr, String bodyStr) {
        String line = "";
        StringBuffer resXmlStr = new StringBuffer();
        try {
            //1.声明URL
            URL url = new URL(urlStr);
            //2.创建链接
            URLConnection con = url.openConnection();
            //3.封装报文传输进行传输
            //调用getXmlInfo()进行报文封装
            String xmlInfo = getXmlInfo(sysHead, appHead, bodyStr);
            byte[] xmlData = xmlInfo.getBytes();
            con.setDoOutput(true);
            con.setDoInput(true);
            con.setUseCaches(false);
            con.setRequestProperty("Pragma:", "no-cache"); //指示请求或响应消息不能缓存
            /*
             * Cache-Control 指定请求和响应遵循的缓存机制
             *  在请求消息或响应消息中设置Cache-Control并不会修改另一个消息处理过程中的缓存处理过程
             *   请求时的缓存指令包括no-cache、no-store、max-age、max-stale、min-fresh、only-if-cached
             *   响应消息中的指令包括public、private、no-cache、no-store、no-transform、must-revalidate、proxy-revalidate、max-age
             *  Public指示响应可被任何缓存区缓存。
             *  Private指示对于单个用户的整个或部分响应消息,不能被共享缓存处理。这允许服务器仅仅描述当用户的部分响应消息,此响应消息对于其他用户的请求无效。
             *  no-cache指示请求或响应消息不能缓存
             *  no-store用于防止重要的信息被无意的发布。在请求消息中发送将使得请求和响应消息都不使用缓存。
             *  max-age指示客户机可以接收生存期不大于指定时间(以秒为单位)的响应。
             *  min-fresh指示客户机可以接收响应时间小于当前时间加上指定时间的响应。
             *  max-stale指示客户机可以接收超出超时期间的响应消息。如果指定max-stale消息的值,那么客户机可以接收超出超时期指定值之内的响应消息。
             */
            con.setRequestProperty("Cache-Control", "no-cache");
            con.setRequestProperty("Content-Type", "text/xml");
            //设置超时时间60s
            con.setReadTimeout(OVERTIME);
            con.setRequestProperty("Content-length", String.valueOf(xmlData.length));
            OutputStreamWriter out = new OutputStreamWriter(con.getOutputStream());
            out.write(new String(xmlInfo.getBytes("ISO-8859-1")));
            out.flush();
            out.close();
            //4.获取响应报文
            BufferedReader br = new BufferedReader(new InputStreamReader(
                    con.getInputStream()));
            //返回响应报文
            for (line = br.readLine(); line != null; line = br.readLine()) {
                resXmlStr.append(line);
            }
            return resXmlStr.toString();
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return resXmlStr.toString();
    }

    /**
     * 接收数据,拼接生成请求报文。需要根据需求自行添加body标签
     *
     * @param sysHead  输入系统头
     * @param appHead  输出应用头
     * @param bodyStr  body标签数据字符串
     * @return
     */
    private static String getXmlInfo(InputSysHead sysHead, InputAppHead appHead, String bodyStr) {

        // 系统头和应用头标签是死的.数据是活的,根据接口不同变化,body标签、数据是活的 (读配置文件)
        // 系统头和应用头
        StringBuilder sb = new StringBuilder();
        sb.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
        sb.append("<service>");
        sb.append("    <SYS_HEAD>");
        sb.append("        <SvcCd>" + sysHead.getSvcCd() + "</SvcCd>");
        sb.append("        <SvcScn>" + sysHead.getSvcScn() + "</SvcScn>");
        sb.append("        <CnsmSysId>" + sysHead.getCnsmSysId() + "</CnsmSysId>");
        sb.append("        <ChnlTp>" + sysHead.getChnlTp() + "</ChnlTp>");
        sb.append("        <SrcSysId>" + sysHead.getSrcSysId() + "</SrcSysId>");
        sb.append("        <SrcSysSeqNo>" + sysHead.getSrcSysSeqNo() + "</SrcSysSeqNo>");
        sb.append("        <CnsmSysSeqNo>" + sysHead.getCnsmSysSeqNo() + "</CnsmSysSeqNo>");
        sb.append("        <Mac/>");
        sb.append("        <TranMD>" + sysHead.getTranDt() + "</TranMD>");
        sb.append("        <TranDt>" + sysHead.getTranDt() + "</TranDt>");
        sb.append("        <TranTm>" + sysHead.getTranTm() + "</TranTm>");
        sb.append("        <TmnlNo>" + sysHead.getTmnlNo() + "</TmnlNo>");
        sb.append("        <SrcSysTmnlNo/>");
        sb.append("        <CnsmSysSvrId/>");
        sb.append("        <SrcSysSvrId/>");
        sb.append("    </SYS_HEAD>");
        sb.append("    <APP_HEAD>");
        sb.append("        <TlrNo>" + appHead.getTlrNo() + "</TlrNo>");
        sb.append("        <BranchId>" + appHead.getBranchId() + "</BranchId>");
        sb.append("        <TlrPswd/>");
        sb.append("        <TlrLvl/>");
        sb.append("        <TlrTp/>");
        sb.append("        <AprvFlg/>");
        sb.append("        <AuthFlg/>");
        sb.append("    </APP_HEAD>");
		/*sb.append("    <BODY>");
		sb.append("        <AuthTlrStts>" + authTlrStts + "</AuthTlrStts>");
		sb.append("        <InfoNo>" + infoNo + "</InfoNo>");
		sb.append("    </BODY>");*/

//		sb.append("</service>");
        return sb.toString() + bodyStr;
    }
}

解析响应报文:

public class XmlToBean {

    public ServiceXml readStringXml(String xmlStr) {

        XStream xs = new XStream();
        // 设置默认安全性。解决Security framework of XStream not initialized, XStream is probably vulnerable.
        XStream.setupDefaultSecurity(xs);
        // 允许类型
        xs.allowTypes(new Class[]{ServiceXml.class});
        // 转换类型
        xs.processAnnotations(new Class[]{ServiceXml.class});
        //把xml转成对象,强转成ServiceXml对象
        ServiceXml obj = (ServiceXml) xs.fromXML(xmlStr);
        return obj;
    }
}

实体:

ServiceXml 类:
@XStreamAlias("service")
public class ServiceXml<T> {

    @XStreamAlias("SYS_HEAD")
    private OutSysHead sysHead;

    @XStreamAlias("APP_HEAD")
    private OutAppHead appHead;

    @XStreamAlias("BODY")
    private Body body;
    // private T body;

}
OutAppHead 类:
@XStreamAlias("APP_HEAD")
public class OutAppHead {

    // 机构Id
    @XStreamAlias("BranchId")
    private String branchId;
    
}
OutSysHead 类:
@XStreamAlias("SYS_HEAD")
public class OutSysHead {

    // 服务代码
    @XStreamAlias("SvcCd")
    private String svcCd;

    // 服务场景
    @XStreamAlias("SvcScn")
    private String svcScn;

	// 系统头中的array标签
    @XStreamAlias("array")
    private OutSysHeadArray sysHeadArray;
	
}
OutSysHeadArray 类:
/**
 * 系统头中的array标签
 */
@XStreamAlias("array")
public class OutSysHeadArray {

    // 交易返回信息数组
    @XStreamImplicit(itemFieldName = "RetInf")
    private List<OutSysRetInfo> retInfos;

}
OutSysRetInfo 类:
/**
 * 系统头交易返回数组中元素
 */
@XStreamAlias("RetInfo")
public class OutSysRetInfo {

    // 交易返回代码
    @XStreamAlias("RetCode")
    private String retCd;

    // 交易返回信息
    @XStreamAlias("RetMsg")
    private String retMsg;

}
响应报文:
String xmlStr = "<?xml version="1.0" encoding="utf-8"?><service><SYS_HEAD><SvcCd>123</SvcCd>" +
                "<SvcScn>22</SvcScn><array><RetInf><RetMsg>交易成功</RetMsg></RetInf></array></SYS_HEAD>" +
                "<APP_HEAD><BranchId>0099</BranchId></APP_HEAD><BODY><InfoNo>0</InfoNo></BODY></service>";
问题:

以上方法,解析响应报文时,ServiceXml 使用泛型,会报错,暂未找到解决方法

方法二:

所需依赖:

    <dependency>
        <groupId>com.squareup.retrofit2</groupId>
        <artifactId>retrofit</artifactId>
        <version>2.6.1</version>
    </dependency>
    <dependency>
        <groupId>com.squareup.retrofit2</groupId>
        <artifactId>adapter-rxjava2</artifactId>
        <version>2.6.1</version>
    </dependency>
    <dependency>
        <groupId>io.reactivex.rxjava2</groupId>
        <artifactId>rxjava</artifactId>
        <version>2.2.11</version>
    </dependency>
    <dependency>
        <groupId>javax.xml.bind</groupId>
        <artifactId>jaxb-api</artifactId>
        <version>2.3.1</version>
    </dependency>
    <dependency>
        <groupId>com.squareup.okhttp3</groupId>
        <artifactId>okhttp</artifactId>
        <version>3.14.2</version>
    </dependency>
    <dependency>
        <groupId>com.squareup.okhttp3</groupId>
        <artifactId>logging-interceptor</artifactId>
        <version>3.14.2</version>
    </dependency>

application.yml

third: 
	url: http://locahost:9091/	# 结尾必须有/

Config类:从config类入

import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import retrofit2.Retrofit;
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@Configuration
public class Config {


    private static Map<String, Object> services = new HashMap<String, Object>();
    private static Logger logger = LoggerFactory.getLogger(Config.class);

    @Autowired
    private ThirdRequestUrl thirdRequestUrl;

    protected HttpLoggingInterceptor loggingInterceptor() {
        //Log相关
        HttpLoggingInterceptor logging = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
            @Override
            public void log(String s) {
                logger.info(s);
            }
        });
        logging.setLevel(HttpLoggingInterceptor.Level.BODY);
        return logging;
    }

    /**
     * 缺省OKHttp配置
     *
     * @return
     */
    private OkHttpClient.Builder getdefOkhttp() {
        OkHttpClient.Builder okHttpClient = new OkHttpClient.Builder();
        okHttpClient.connectTimeout(60, TimeUnit.SECONDS); //连接超时时间
        okHttpClient.readTimeout(60, TimeUnit.SECONDS); //读取超时时间
        okHttpClient.writeTimeout(60, TimeUnit.SECONDS); //写超时
        okHttpClient.addInterceptor(loggingInterceptor()); //写日志拦截器
        okHttpClient.addInterceptor(customHttpLogInterceptor()); //自定义日志拦截器
        //失败重连
        okHttpClient.retryOnConnectionFailure(true);
        return okHttpClient;
    }

    // 拦截器的加载在springcontext之前,所以自动注入的mapper是null,需要添加拦截器之前用@bean注解将拦截器注入工厂,接着添加拦截器
    @Bean
    public CustomHttpLogInterceptor customHttpLogInterceptor(){
        return new CustomHttpLogInterceptor();
    }

    protected <T> T createService(Class<T> serviceClass, String baseUrl) {
        OkHttpClient.Builder okHttpClient = getdefOkhttp();
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(baseUrl)
                //设置OKHttpClient
                .client(okHttpClient.build())
                .addConverterFactory(JaxbConverterFactory.create())
//                .addConverterFactory(JacksonConverterFactory.create()) // json 格式转换
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())//
                .build();
        T service = retrofit.create(serviceClass);
        return service;
    }

	//  入口
	@Bean
    public CardServiceAPI cardServiceAPI() {
        return this.createService(CardServiceAPI.class, thirdRequestUrl.getCards());
    }
    
}

CardServiceAPI接口:

import org.springframework.stereotype.Repository;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.POST;

@Repository
public interface CardServiceAPI {

    @POST("testCard") // 发送post请求,必须要有路径。即"testCard"不可少
    Call<CardMessage> selectCard(@Body CardMessage message);

}
说明:

1、service层注入此接口;
2、若同一URL(ip + 端口)下,仅具体的“testCard”不同,可写在同一个XxxServiceAPI接口中,改变“testCard”即可。
3、“testCard”不为“/testCard”

JaxbRequestConverter类:请求转换器:请求实体转换为xml

import com.sun.xml.internal.bind.marshaller.CharacterEscapeHandler;
import okhttp3.RequestBody;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import retrofit2.Converter;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.*;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;

final class JaxbRequestConverter<T> implements Converter<T, RequestBody> {


    private static Logger logger = LoggerFactory.getLogger(JaxbRequestConverter.class);
    final JAXBContext context;
    final Class<T> type;

    JaxbRequestConverter(JAXBContext context, Class<T> type) {
        this.context = context;
        this.type = type;
    }

    public RequestBody convert(T value) throws IOException {
        StringBuilder data = new StringBuilder("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
        try {
            StringWriter original = new StringWriter();
            Marshaller marshaller = this.context.createMarshaller();
            marshaller.setProperty(Marshaller.JAXB_ENCODING, JaxbConverterFactory.XML.charset().name());// //编码格式
            marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, false);// 是否格式化生成的xml串
            // 不进行转义字符的处理
            marshaller.setProperty(CharacterEscapeHandler.class.getName(), new CharacterEscapeHandler() {
                public void escape(char[] ch, int start,int length, boolean isAttVal, Writer writer) throws IOException {
                    writer.write(ch, start, length);
                }
            });
            marshaller.setProperty(Marshaller.JAXB_FRAGMENT, true);// 是否省略xm头声明信息
            marshaller.marshal(value, original);
            logger.debug(original.toString());
            data.append(original.toString());
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
            throw new RuntimeException(e);
        }
        return RequestBody.create(JaxbConverterFactory.XML, data.toString());
    }

}

JaxbResponseConverter类:响应转换器:响应报文xml转为实体

import com.oneconnect.sg.hub.client.utils.AesCbcPkcs5Base64Utils;
import okhttp3.ResponseBody;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import retrofit2.Converter;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import java.io.IOException;
import java.io.StringReader;

final class JaxbResponseConverter<T> implements Converter<ResponseBody, T> {

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

    final XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance();
    final JAXBContext context;
    final Class<T> type;

    JaxbResponseConverter(JAXBContext context, Class<T> type) {
        this.context = context;
        this.type = type;
    }

    public T convert(ResponseBody value) throws IOException {
        try {
            StringBuilder data = new StringBuilder(value.string());
            //String key = SpringUtils.getProperty("app.owsdl.encrypt.key");

            String key = "qyxjjb74cidnc16b";  // 加密秘钥
            String iv = "xf073abfru46awwt";  // 解密秘钥
           // String iv = SpringUtils.getProperty("app.owsdl.encrypt.iv");
            String cKey = key + "|" + iv;
            if (data.indexOf("<body>") > -1) {
                int start = data.indexOf("<body>") + 6;
                int end = data.indexOf("</body>");
                String body = data.substring(start, end);
                body = body.trim();
                String de = AesCbcPkcs5Base64Utils.getDecryString(body, cKey);
                data.replace(start, end, de);
            }
            logger.debug(data.toString());
            Unmarshaller unmarshaller = this.context.createUnmarshaller();
            XMLStreamReader streamReader = this.xmlInputFactory.createXMLStreamReader(new StringReader(data.toString()));
            return unmarshaller.unmarshal(streamReader, this.type).getValue();
        } catch (XMLStreamException | JAXBException var4) {
            throw new RuntimeException(var4);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

JaxbConverterFactory类:转换器工厂

import io.reactivex.annotations.Nullable;
import okhttp3.MediaType;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
import retrofit2.Converter;
import retrofit2.Retrofit;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.annotation.XmlRootElement;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;

public class JaxbConverterFactory extends Converter.Factory {

    static final MediaType XML = MediaType.parse("application/xml; charset=utf-8");
    @Nullable
    private final JAXBContext context;

    public static JaxbConverterFactory create() {
        return new JaxbConverterFactory(null);
    }

    public static JaxbConverterFactory create(JAXBContext context) {
        if (context == null) {
            throw new NullPointerException("context == null");
        } else {
            return new JaxbConverterFactory(context);
        }
    }

    private JaxbConverterFactory(@Nullable JAXBContext context) {
        this.context = context;
    }

    public Converter<?, RequestBody> requestBodyConverter(Type type, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
        return type instanceof Class && ((Class)type).isAnnotationPresent(XmlRootElement.class) ? new JaxbRequestConverter(this.contextForType((Class)type), (Class)type) : null;
    }

    public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
        return type instanceof Class && ((Class)type).isAnnotationPresent(XmlRootElement.class) ? new JaxbResponseConverter(this.contextForType((Class)type), (Class)type) : null;
    }

    private JAXBContext contextForType(Class<?> type) {
        try {
            return this.context != null ? this.context : JAXBContext.newInstance(type);
        } catch (JAXBException var3) {
            throw new IllegalArgumentException(var3);
        }
    }
}

实体:

Message类:
import javax.xml.bind.annotation.*;

@XmlAccessorType(XmlAccessType.NONE)
//@XmlRootElement(name = "service")
//@XmlSeeAlso({String.class, CustomerBody.class, CardBody.class,})
public class Message {

    @XmlElement(name = "SYS_HEAD")
    private SysHead sysHead;

    @XmlElement(name = "APP_HEAD")
    private AppHead appHead;

//    @XmlAnyElement(lax = true)
//    private T body;

    public SysHead getSysHead() {
        return sysHead;
    }

    public void setSysHead(SysHead sysHead) {
        this.sysHead = sysHead;
    }

    public AppHead getAppHead() {
        return appHead;
    }

    public void setAppHead(AppHead appHead) {
        this.appHead = appHead;
    }

//    public T getBody() {
//        return body;
//    }
//
//    public void setBody(T body) {
//        this.body = body;
//    }
}
SysHead类:固定的(所有请求报文xml、响应报文xml都有的标签、参数)
@XmlAccessorType(XmlAccessType.NONE) //所有属性都不映射为xml的元素
@Data
public class SysHead {

    // 服务代码
    @XmlElement(name = "SvcCd")
    private String svcCd;

    // 服务场景
    @XmlElement(name = "SvcScn")
    private String svcScn;
}
AppHead类:固定的(所有请求报文xml、响应报文xml都有的标签、参数)
@XmlAccessorType(XmlAccessType.NONE) //所有属性都不映射为xml的元素
@Data
public class AppHead {

    // T号
    @XmlElement(name = "TlrNo")
    private String tlrNo;

    // 机构Id
    @XmlElement(name = "BranchId")
    private String branchId;
}
CardMessage类:可变的(根据请求不同而改变。请求报文xml、响应报文xml中BODY标签中的参数不同)
import javax.xml.bind.annotation.*;

@XmlAccessorType(XmlAccessType.FIELD)
@XmlSeeAlso({CardBody.class})
@XmlRootElement(name = "service") // 标签名
public class CardMessage extends Message {

    @XmlElement(name = "BODY")
    private CardBody body;

    public CardBody getBody() {
        return body;
    }

    public void setBody(CardBody body) {
        this.body = body;
    }
}
CardBody类:可变的(根据请求不同而改变。请求报文xml、响应报文xml中BODY标签中的参数不同)
@XmlAccessorType(XmlAccessType.NONE)
//@XmlRootElement(name = "BODY") //标签名 在CardMessage中有@XmlElement(name = "BODY")后,可不写此注解
@ApiModel("卡信息")
@Data
public class CardBody {

    /**
     * 卡号
     */
    @ApiModelProperty("卡号")
    @XmlElement(name = "CstNo") //标签名
    private  String cstNo;

    /**
     * 客户名
     */
    @ApiModelProperty("客户名")
    @XmlElement(name = "CstNm")
    private String cstNm;
    
}

方法三:

使用泛型。

import com.alibaba.druid.util.StringUtils;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import java.io.StringReader;
import java.io.StringWriter;

public class XmlToEntity {


    public static <T> T xmlToBean(String xmlStr, Class<T> body) throws JAXBException {
        if (null == body || StringUtils.isEmpty(xmlStr)) {
            return null;
        }
        JAXBContext context = JAXBContext.newInstance(body);
        Unmarshaller unmarshaller = context.createUnmarshaller();
        StringReader stringReader = new StringReader(xmlStr);
        Object unmarshal = unmarshaller.unmarshal(stringReader);
        return (T) unmarshal;
    }

    public static <T> String beanToXml(Class<T> body) throws JAXBException {
        JAXBContext context = JAXBContext.newInstance(body);
        Marshaller marshaller = context.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8");
        //去掉请求头
        marshaller.setProperty(Marshaller.JAXB_FRAGMENT, true);
        StringWriter stringWriter = new StringWriter();
        stringWriter.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
        marshaller.marshal(body, stringWriter);
        return stringWriter.toString();
    }
}

说明:

1、根据Body的不同,新建不同XxxBody,同时需要新建XxxMessage类继承Message类;
2、SysHead类、AppHead类为报文中固定的标签。CardBody类根据请求不同,报文中的BODY标签不同,而对应改变;
3、使用泛型;


————以上,两种方式,欢迎交流————


  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值