BIM安装到解析、以及完整项目总结

由于项目中用到各类单位建筑消防BIM图,所以研究了近一个月,稍有心得,现在公布出来方便其他人少走弯路;

1、由于BIMserver最新版本一直在更新,所以有很多线解串器插件更不上,我的BIMserver版本为1.5.88,这个版本较为稳定,下载地址:https://github.com/opensourceBIM/BIMserver/releases

2、这里面包含war包和jar包,我是war包;各自根据需要自行下载;

一、BIMserver的安装

第一步:安装BIMserver

运行环境:Linux centOS7、JavaJDK1.8以上、Tomcat8

1、把下载的1.5.88版本war直接复制到服务器webApp中,由于上传的BIM文件超过了tomcat默认内存128M,所以需要进行修改,修改方式分以下几种:

第一种方法:Windows下,在文件/bin/catalina.bat,Unix下,在文件/bin/catalina.sh的前面,增加如下设置:JAVA_OPTS='-Xms【初始化内存大小】 -Xmx【可以使用的最大内存】'需要把这个两个参数值调大。例如:JAVA_OPTS='-Xms256m -Xmx512m'表示初始化内存为256MB,可以使用的最大内存为512MB。

第二种方法: 环境变量中设变量名:JAVA_OPTS     变量值:-Xms512m   -Xmx512m

第三种方法:前两种方法针对的是bin目录下有catalina.bat的情况(比如直接解压的Tomcat等),但是有些安装版的Tomcat下没有catalina.bat,这个时候可以采用如下方法,当然这个方法也是最通用的方法:打开tomcatHome//bin//tomcat5w.exe,点击Java选项卡,然后将会发现其中有这么两项:Initial memory pool和Maximum memory pool.Initial memory pool这个就是初始化设置的内存的大小。Maximum memory pool这个是最大内存的大小 设置完了就按确定然后再重启TOMCAT你就会发现tomcat中jvm可用的内存改变了。

我自己是第一种办法设置的(截图为2048M):

 

 

 

然后就是启动了tomca了,启动花费时间较长请耐心等待。

 

第二步:开始安装解串器各种插件

访问:http://192.168.199.148:8080/bimserver88 这是我服务器+项目名称

(1)依次为:BIM服务器地址 、服务名称、服务说明、服务器图片,(0~0!这个图片是晚上其他地方截取的)点击下一步

(2)依次为:管理员名称、管理员账号、密码(记得账号必须为Email格式)

(3)由于我的服务器是内外,所以必须手动安装插件,点击红框就可以选择了,对应的插件版本为下图

自己到这里进行下载:https://oss.sonatype.org/content/groups/public/org/opensourcebim/

(4)等到所有插件安装完成;

 

第三步、登录BIMserver后台管理页面

访问:http://192.168.199.148:8080/bimserver88/

这样就会访问登录页面,安装就到这里.

二、后台代码实现

  整体项目构建为SpringMVC、mybaits、maven、jdk1.8编译;
    解释:由于整个项目是分布式的,所以这里面用到了ods-common包(为自己的公共util,本项目实际上只用到了一个OdsResult.java)、pagehelper分页包可以自己下载;
    由于整个项目的配置和类太多这里不方便贴出来,请到
    http://download.csdn.net/download/lustres/10229631
    这里下载完整项目,这里我只做简单介绍和OdsResult.java类贴出来;
OdsResult.java类
package com.cqprosper.ods.util;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * 自定义响应结构
 */
public class OdsResult implements Serializable{

    // 定义jackson对象
    private static final ObjectMapper MAPPER = new ObjectMapper();

    // 响应业务状态
    private String state;

    // 响应消息
    private String message;

    // 响应中的数据
    private Object data;

    public static Map buildMap(String name,Object object){
        Map map = new HashMap();
        map.put("name",name);
        map.put("value",object);
        return map;
    }

    public static Map resultMap(String key,Object object){
        Map map = new HashMap();
        map.put(key,object);
        return map;
    }

    public static OdsResult build(String state, String message, Object data) {
        return new OdsResult(state, message, data);
    }

    public static OdsResult ok(Object data) {
        return new OdsResult(data);
    }

    public static OdsResult ok() {
        return new OdsResult(new ArrayList<>());
    }

    public OdsResult() {

    }

    public static OdsResult build(String state, String message) {
        return new OdsResult(state, message, null);
    }

    public OdsResult(String state, String message, Object data) {
        this.state = state;
        this.message = message;
        this.data = data;
    }

    public OdsResult(Object data) {
        this.state = "200";
        this.message = "OK";
        this.data = data;
    }

//    public Boolean isOK() {
//        return this.state == 200;
//    }

    public String getState() {
        return state;
    }

    public void setState(String state) {
        this.state = state;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    /**
     * 将json结果集转化为esult对象
     * 
     * @param jsonData json数据
     * @param clazz OdsResult中的object类型
     * @return
     */
    public static OdsResult formatToPojo(String jsonData, Class<?> clazz) {
        try {
            if (clazz == null) {
                return MAPPER.readValue(jsonData, OdsResult.class);
            }
            JsonNode jsonNode = MAPPER.readTree(jsonData);
            JsonNode data = jsonNode.get("data");
            Object obj = null;
            if (clazz != null) {
                if (data.isObject()) {
                    obj = MAPPER.readValue(data.traverse(), clazz);
                } else if (data.isTextual()) {
                    obj = MAPPER.readValue(data.asText(), clazz);
                }
            }
            return build(jsonNode.get("state").toString(), jsonNode.get("message").asText(), obj);
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * 没有object对象的转化
     * 
     * @param json
     * @return
     */
    public static OdsResult format(String json) {
        try {
            return MAPPER.readValue(json, OdsResult.class);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * Object是集合转化
     * 
     * @param jsonData json数据
     * @param clazz 集合中的类型
     * @return
     */
    public static OdsResult formatToList(String jsonData, Class<?> clazz) {
        try {
            JsonNode jsonNode = MAPPER.readTree(jsonData);
            JsonNode data = jsonNode.get("data");
            Object obj = null;
            if (data.isArray() && data.size() > 0) {
                obj = MAPPER.readValue(data.traverse(),
                        MAPPER.getTypeFactory().constructCollectionType(List.class, clazz));
            }
            return build(jsonNode.get("state").toString(), jsonNode.get("message").asText(), obj);
        } catch (Exception e) {
            return null;
        }
    }

}
 
 
这个项目的核心方法主要是BimUtil.java类
package com.cqprosper.ods.bim.util;


import com.alibaba.druid.util.StringUtils;
import org.bimserver.client.BimServerClient;
import org.bimserver.client.json.JsonBimServerClientFactory;
import org.bimserver.interfaces.objects.SDeserializerPluginConfiguration;
import org.bimserver.interfaces.objects.SProject;
import org.bimserver.plugins.services.Flow;
import org.bimserver.shared.UsernamePasswordAuthenticationInfo;
import java.io.InputStream;

/**
 * @author mc
 * @version V1.0
 * @description:BIM图工具类
 * @company: www.cqprosper.com
 * @date 2018-01-18 下午 15:42
 */
public class BimUtil {
    private static BimUtil bimUtilConfigManager;
    private static BimServerClient client;
    private BimUtil(String servers, String userName, String passWord){
        try {
            JsonBimServerClientFactory factory = new JsonBimServerClientFactory(servers);
            client = factory.create(new UsernamePasswordAuthenticationInfo(userName, passWord));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    /**
     * 单列BIM服务器连接
     * @param servers
     * @param userName
     * @param passWord
     * @return
     */
    public static BimUtil bimUtilConfigManager(String servers, String userName, String passWord){
        if (bimUtilConfigManager == null){
            bimUtilConfigManager = new BimUtil(servers,userName,passWord);
        }
        return bimUtilConfigManager;
    }

    /**
     * 与BIM服务器建立连接
     * @return
     */
    public BimServerClient createConnect(){
        return client;
    }

    /**
     * 创建工程
     * @param client
     * @param projectName
     * @param schema
     * @return
     */
    public static SProject addProject(BimServerClient client , String projectName, String schema){
        SProject newProject = null;
        try {
            newProject = client.getServiceInterface().addProject(projectName,schema);
        } catch (org.bimserver.shared.exceptions.ServerException e) {
            e.printStackTrace();
        } catch (org.bimserver.shared.exceptions.UserException e) {
            e.printStackTrace();
        }
        System.out.println("在BIM服务器上创建工程成功~!");
        return newProject;
    }

    /**
     * 上传IFC文件至BIM服务器
     * @param client 客户端
     * @param poid 工程唯一标示
     * @param comment 备注
     * @param fileSize 文件大小
     * @param filename 文件名称
     * @param inputStream 文件流
     * @param ifc 默认为ifc,可以指定特定解串器
     * @return
     */
    public static long doSomethingWithClient(BimServerClient client, long poid, String comment, long fileSize, String filename, InputStream inputStream,String ifc){
        if (StringUtils.isEmpty(ifc)){
            ifc ="ifc";
        }
        long start=0L;
        try {
            SDeserializerPluginConfiguration deserializer = client.getServiceInterface().getSuggestedDeserializerForExtension(ifc, poid);
            // Here we actually checkin the IFC file. Flow.SYNC indicates that we only want to continue the code-flow after the checkin has been completed
            start = client.checkin(poid, comment, deserializer.getOid(), false, Flow.SYNC, fileSize,filename,inputStream);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("上传文件返回值为:"+start);
        return start;
    }
}

三、前端BIMsuffer实现

完整demo请到这里下载: https://download.csdn.net/download/lustres/10229631
如果你是后端的话,下面的代码可以找前端实现,我列出一个Dome,请注意中文注释
<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8"/>
    <title>BIMsurfer demo</title>
    <link href='https://fonts.googleapis.com/css?family=Open+Sans:300,600' rel='stylesheet' type='text/css' />
    
    <link rel="stylesheet" href="css/demo.css"/>
    <link rel="stylesheet" href="css/apiref.css"/>
    <link rel="stylesheet" href="../css/tree.css"/>
    <link rel="stylesheet" href="../css/metadata.css"/>
    
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css"/>
    
    <script type="text/javascript" src="js/utils.js"></script>
	
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
    <script>
        var address = QueryString.address;//这里为BIMserver服务端地址
        var token = QueryString.token;//这里为后台传过来token
          var windowBimSurfer;
		  
        /* Generating a new version based on the current time, this way resources are never cached
         When building a version of BIMsurfer V2 this should be replaced by an actual version number in order
         To facilitate proper caching
        */
        var version = new Date().getTime();

        // This has been moved to bimserverapi, can be removed in a day
        String.prototype.firstUpper = function () {
            return this.charAt(0).toUpperCase() + this.slice(1);
        }
        
        // Because the demo is in a subfolder compared to the BIMsurfer API, we tell require JS to use the "../" baseUrl
        var require = {
            baseUrl: "../",
            //urlArgs: "bust=" + version
        };
    </script>

    <script type="text/javascript" src="../bimsurfer/lib/require.js"></script>
    <script type="text/javascript">

        // Loads a model from BIMServer, builds an explorer tree UI.
        // Clicking on a tree node fits the view to its scene object.

        loadScripts("http://192.168.199.148:8080/bimserver88/apps/bimserverjavascriptapi/js/", [
             "bimserverclient.js",
             "model.js",
             "bimserverapiwebsocket.js",
             "bimserverapipromise.js",
			 "geometry.js",
              "ifc2x3tc1.js",
             "ifc4.js",
             "translations_en.js",
        ], function(){
            require(["bimsurfer/src/BimSurfer","bimsurfer/src/BimServerModelLoader","bimsurfer/src/StaticTreeRenderer","bimsurfer/src/MetaDataRenderer","bimsurfer/lib/domReady!"],
	            function (BimSurfer, BimServerModelLoader, StaticTreeRenderer, MetaDataRenderer) {
            	
            		function processBimSurferModel(bimSurferModel) {
						
            			bimSurferModel.getTree().then(function (tree) {
	                        // Build a tree view of the elements in the model. The fact that it
	                        // is 'static' refers to the fact that all branches are loaded and
	                        // rendered immediately.
							//alert(1);
	                        var domtree = new StaticTreeRenderer({
	                            domNode: 'treeContainer'
	                        });
	                        domtree.addModel({name: "", id:QueryString.roid, tree:tree});
	                        domtree.build();
	                        console.log(QueryString.roid);
							//console.log(tree);
	                        // Add a widget that displays metadata (IfcPropertySet and instance
	                        // attributes) of the selected element.
	                        var metadata = new MetaDataRenderer({
	                            domNode: 'dataContainer'
	                        });
							
							
	                        metadata.addModel({name: "", id:QueryString.roid, model:bimSurferModel});
	                         windowBimSurfer = bimSurfer;
							 
	                        bimSurfer.on("selection-changed", function(selected) {
	                            domtree.setSelected(selected, domtree.SELECT_EXCLUSIVE);
	                            metadata.setSelected(selected);
	                        });
	                        
	                        domtree.on("click", function (oid, selected) {
	                            // Clicking an explorer node fits the view to its object and selects
                                if (selected.length) {
                                    bimSurfer.viewFit({
                                        ids: selected,
                                        animate: true
                                    });
                                }
                                bimSurfer.setSelection({
                                    ids:selected,
                                    clear:true,
                                    selected:true
                                });
	                        });
	
	                        
	                        // Write API ref
	                                
	                        var flatten = function(n) {
	                            var li = []
	                            var f = function(n) {
	                                li.push(n.id);
	                                (n.children || []).forEach(f);
	                            }
	                            f(n);
	                            return li;
	                        };
	                        
	                        var oids = flatten(tree);
	                        _.shuffle(oids);
	                        oids.splice(10);
	                        var guids = bimSurfer.toGuid(oids);
	                        
	                        oids = "["+oids.join(", ")+"]";
	                        guids = "["+guids.map(function(s) {return '"'+s+'"';}).join(", ")+"]";
							
	                        console.log(81);
							
	                        var METHODS = [
	                            {name:'setVisibility',     args:[{name: "ids", value: oids}, {name: "visible", value:false}]},
	                            {name:'setVisibility',     args:[{name: "types", value: '["IfcWallStandardCase"]'}, {name: "visible", value:false}]},
	                            {name:'setSelectionState', args:[{name: "ids", value: oids}, {name: "selected", value:true}]},
	                            {name:'getSelected', args:[], hasResult: true},
	                            {name:'toId', args:[guids], hasResult: true},
	                            {name:'toGuid', args:[oids], hasResult: true},
	                            {name:'setColor', args:[{name: "ids", value: oids}, {name: "color", value:"{r:1, g:0, b:0, a:1}"}]},
	                            {name:'viewFit', args:[{name: "ids", value: oids}, {name: "animate", value:500}]},
	                            {name:'setCamera', args:[{name: "type", value: "'ortho'"}]},
	                            {name:'getCamera', args:[], hasResult: true},
	                            {name:'reset', args:[{name: "cameraPosition", value: true}]},
	                        ];
	
	                        var n = document.getElementById('apirefContainer');
							
							
							//console.log(METHODS.toString());
	                        METHODS.forEach(function(m, i) {
								 
	                            n.innerHTML += "<h2>"+m.name+"()</h2>";
	                            
	                            var hasNamedArgs = false;
	                            var args = m.args.map(function(a) {
	                                if (a.name) {
	                                    hasNamedArgs = true;
	                                    return a.name + ":" + a.value;
	                                } else {
	                                    return a;
	                                }
	                            }).join(", ");
	                            
	                            if (hasNamedArgs) {
	                                args = "{"+args+"}";
	                            }
	                            
	                            var cmd = "bimSurfer."+m.name+"("+args+");";
	                            n.innerHTML += "<textarea rows=3 id='code"+i+"' spellcheck=false>"+cmd+"\n</textarea>";
	                            exec_statement = "eval(document.getElementById(\"code"+i+"\").value)"
	                            if (m.hasResult) {
	                                exec_statement = "document.getElementById(\"result"+i+"\").innerHTML = JSON.stringify(" + exec_statement + ").replace(/,/g, \", \")";
	                            } else {
	                                exec_statement += "; window.scrollTo(0,0)"
	                            }
	                            n.innerHTML += "<button onclick='"+exec_statement+"'>run</button>";
	                            if (m.hasResult) {
	                                n.innerHTML += "<pre id='result"+i+"' />";
	                            }
								//console.log(n.innerHTML);
	                        });
	                    });
            		}
            	
	                var bimSurfer = new BimSurfer({
	                    domNode: "viewerContainer"
	                });
	                
				
	                bimSurfer.on("loading-finished", function(){
	                	document.getElementById("status").innerHTML = "Loading finished";
                        var domNode = document.getElementById("typeSelector");
                        domNode.innerHTML = "";
                        bimSurfer.getTypes().forEach(function(ifc_type) {
                            var on = ifc_type.visible;
                            var d = document.createElement("div");
                            var t = document.createTextNode(ifc_type.name);
                            var setClass = function() {
                                d.className = "fa fa-eye " + ["inactive", "active"][on*1];
                            };
                            setClass();
                            d.appendChild(t);
                            domNode.appendChild(d);
                            d.onclick = function() {
                                on = !on;
                                setClass();
                                bimSurfer.setVisibility({types:[ifc_type.name], visible:on});
                            };
                        });
	                });
	                bimSurfer.on("loading-started", function(){
	                	document.getElementById("status").innerHTML = "Loading...";
	                });

	                // Lets us play with the Surfer in the console
	                window.bimSurfer = bimSurfer;

	                // Create a BIMserver API, we only need one of those for every server we connect to
	                var bimServerClient = new BimServerClient(address, null);
	                bimServerClient.init(function(){
						
						bimServerClient.setToken(token, function() {
			                // Create a model loader, this one is able to load models from a BIMserver and therefore can have BIMserver specific calls
			                var modelLoader = new BimServerModelLoader(bimServerClient, bimSurfer);
			                
			                var models = {}; // roid -> Model
			                
			                // For this example, we'll fetch all the latest revisions of all the subprojects of the main project
			                var poid = QueryString.poid;//这里为后端传过来的项目poid
			                
			                var nrProjects;
			                
			                function loadModels(models, totalBounds) {
			                	var center = [
			                		(totalBounds.min[0] + totalBounds.max[0]) / 2,
			                		(totalBounds.min[1] + totalBounds.max[1]) / 2,
			                		(totalBounds.min[2] + totalBounds.max[2]) / 2
			                	];
			                		
			                	var globalTransformationMatrix = [
			                		1, 0, 0, 0,
			                		0, 1, 0, 0,
			                		0, 0, 1, 0,
			                		-center[0], -center[1], -center[2], 1
			                	];
			                	console.log(totalBounds);
								//console.log(models.toString());
								//alert(models[0].toString());
								//for (var i = 0; i < models.length - 1; i++) {
			                	for (var roid in models) {
			                		var model = models[roid];
									console.log(model.toString());
									console.log(890);
				                	// Example 1: Load a full model
				                	modelLoader.setGlobalTransformationMatrix(globalTransformationMatrix);
					                modelLoader.loadFullModel(model).then(function(bimSurferModel){
					                    processBimSurferModel(bimSurferModel);
					                });
				                	
				                	// Example 2: Load a list of objects (all walls and subtypes)
//				                	var objects = [];
//				                	model.getAllOfType("IfcWall", true, function(wall){
//				                		objects.push(wall);
//				                	}).done(function(){
//						                modelLoader.loadObjects(model, objects).then(function(bimSurferModel){
//						                    processBimSurferModel(bimSurferModel);
//						                });
//				                	});
				                	
				                	// Example 3: Load the results of a query
//				                	var objects = [];
//				                	var query = {
//				                		types: ["IfcDoor", "IfcWindow"]
//				                	};
//				                	model.query(query, function(object){
//				                		objects.push(object);
//				                	}).done(function(){
//						                modelLoader.loadObjects(model, objects).then(function(bimSurferModel){
//						                    processBimSurferModel(bimSurferModel);
//						                });
//				                	});
			                	}
			                }
			                
			                bimServerClient.call("ServiceInterface", "getAllRelatedProjects", {poid: poid}, function(projects){
			                	nrProjects = projects.length;
			                	console.log(nrProjects);
			                	var totalBounds = {
			                		min: [Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE],
			                		max: [-Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE]
			                	};
			                	
			                	projects.forEach(function(project){
								
			                		//if (project.lastRevisionId != -1 && project.oid != poid) {
									if (project.lastRevisionId != -1) {
										
			                			bimServerClient.getModel(project.oid, project.lastRevisionId, project.schema, false, function (model) {
										    console.log(project.lastRevisionId);
											console.log(model);
						                	models[project.lastRevisionId] = model;
						                	
						                	bimServerClient.call("ServiceInterface", "getModelMinBounds", {roid: project.lastRevisionId}, function(minBounds){
							                	bimServerClient.call("ServiceInterface", "getModelMaxBounds", {roid: project.lastRevisionId}, function(maxBounds){
							                		if (minBounds.x < totalBounds.min[0]) {
							                			totalBounds.min[0] = minBounds.x;
							                		}
							                		if (minBounds.y < totalBounds.min[1]) {
							                			totalBounds.min[1] = minBounds.y;
							                		}
							                		if (minBounds.z < totalBounds.min[2]) {
							                			totalBounds.min[2] = minBounds.z;
							                		}
							                		if (maxBounds.x > totalBounds.max[0]) {
							                			totalBounds.max[0] = maxBounds.x;
							                		}
							                		if (maxBounds.y > totalBounds.max[1]) {
							                			totalBounds.max[1] = maxBounds.y;
							                		}
							                		if (maxBounds.z > totalBounds.max[2]) {
							                			totalBounds.max[2] = maxBounds.z;
							                		}
							                		nrProjects--;
							                		if (nrProjects == 0) {
														console.log(6);
							                			loadModels(models, totalBounds);
							                		}
							                	});
						                	});
						                });
			                		} else {
			                			nrProjects--;
				                		if (nrProjects == 0) {
											console.log(5);
				                			loadModels(models, totalBounds);
				                		}
			                		}
			                	});
			                });							
			            });
	                });
	            });
        });
		
		
		  function finish() {
            document.getElementById("status").innerHTML = "Loading finished";
            var domNode = document.getElementById("typeSelector");
            domNode.innerHTML = "";
            windowBimSurfer.getTypes().forEach(function (ifc_type) {
                var on = ifc_type.visible;
                var d = document.createElement("div");
                var t = document.createTextNode(ifc_type.name);
                var setClass = function () {
                    d.className = "fa fa-eye " + ["inactive", "active"][on * 1];
                };
                setClass();
                d.appendChild(t);
                domNode.appendChild(d);
                d.onclick = function () {
                    on = !on;
                    setClass();
                    windowBimSurfer.setVisibility({ types: [ifc_type.name], visible: on });
                };
            });
        }

    </script>
</head>
<body>
<div id="maincontainer">
    <div id="topsection">
        <h1>BIMsurfer demo</h1>
		  <input type="button" value="finished" onclick="finish()"/>
        <div id="typeSelector">
            <div> </div>
        </div>
    </div>
    <div id="contentwrapper">
        <div id="colmid">
            <div id="colright">
                <div id="col1wrap">
                    <div id="col1pad">
                        <div id="viewerContainer">
                        </div>
                    </div>
                </div>
                <div id="treeContainer" class="bimsurfer-static-tree">
                </div>
                <div id="dataContainer" class="bimsurfer-metadata">
                </div>
            </div>
        </div>
    </div>
    
</div>
<div id="status"></div>

<div id='apirefContainer'>
    <h1>API Reference</h1>
</div>

</body>
</html>

 
就是这样了,我也是刚刚研究没有多久,如有不正确的请指出
 
 
 
 
  • 6
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 22
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 22
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值